/**
* This file Copyright (c) 2005-2007 Aptana, Inc. This program is
* dual-licensed under both the Aptana Public License and the GNU General
* Public license. You may elect to use one or the other of these licenses.
*
* This program is distributed in the hope that it will be useful, but
* AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or
* NONINFRINGEMENT. Redistribution, except as permitted by whichever of
* the GPL or APL you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or modify this
* program under the terms of the GNU General Public License,
* Version 3, as published by the Free Software Foundation. You should
* have received a copy of the GNU General Public License, Version 3 along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Aptana provides a special exception to allow redistribution of this file
* with certain Eclipse Public Licensed code and certain additional terms
* pursuant to Section 7 of the GPL. You may view the exception and these
* terms on the web at http://www.aptana.com/legal/gpl/.
*
* 2. For the Aptana Public License (APL), this program and the
* accompanying materials are made available under the terms of the APL
* v1.0 which accompanies this distribution, and is available at
* http://www.aptana.com/legal/apl/.
*
* You may view the GPL, Aptana's exception and additional terms, and the
* APL in the file titled license.html at the root of the corresponding
* plugin containing this source file.
*
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.ide.editor.js.parsing;
import java.text.ParseException;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.Platform;
import com.aptana.ide.core.IdeLog;
import com.aptana.ide.editor.js.JSPlugin;
import com.aptana.ide.editor.js.lexing.JSTokenTypes;
import com.aptana.ide.editor.jscomment.parsing.JSCommentMimeType;
import com.aptana.ide.editor.scriptdoc.parsing.ScriptDocMimeType;
import com.aptana.ide.editors.UnifiedEditorsPlugin;
import com.aptana.ide.editors.unified.folding.GenericCommentNode;
import com.aptana.ide.editors.unified.parsing.UnifiedParser;
import com.aptana.ide.lexer.ILexer;
import com.aptana.ide.lexer.Lexeme;
import com.aptana.ide.lexer.LexemeList;
import com.aptana.ide.lexer.LexerException;
import com.aptana.ide.lexer.Range;
import com.aptana.ide.lexer.TokenCategories;
import com.aptana.ide.parsing.IParseState;
import com.aptana.ide.parsing.IParser;
import com.aptana.ide.parsing.ParserInitializationException;
import com.aptana.ide.parsing.nodes.IParseNode;
/**
* @author Kevin Lindsey
*/
public abstract class JSParserBase extends UnifiedParser
{
private static final String NESTED_LANGUAGE_ID = "nested_languages"; //$NON-NLS-1$
private static final String DOCUMENTATION_DELIMITER_GROUP = "documentation-delimiter"; //$NON-NLS-1$
private static final String LINE_DELIMITER_GROUP = "line-delimiter"; //$NON-NLS-1$
private static final String DEFAULT_GROUP = "default"; //$NON-NLS-1$
/**
* current parent AST node
*/
protected IParseNode _currentParentNode;
/**
* language registry
*/
protected JSLanguageRegistry _languageRegistry;
/**
* processing instruction language MIME type
*/
protected String _piLanguage;
private boolean _fastScan;
/**
* JSParserBase
*
* @throws ParserInitializationException
*/
public JSParserBase() throws ParserInitializationException
{
this(JSMimeType.MimeType);
}
/**
* JSParserBase
* @param language
* @throws ParserInitializationException
*/
public JSParserBase(String language) throws ParserInitializationException
{
super(language);
this._fastScan = UnifiedEditorsPlugin.getDefault().useFastScan();
}
/**
* @see com.aptana.ide.parsing.AbstractParser#addChildParsers()
*/
protected void addChildParsers() throws ParserInitializationException
{
super.addChildParsers();
if (this._languageRegistry == null)
{
this._languageRegistry = new JSLanguageRegistry();
IExtensionRegistry registry = Platform.getExtensionRegistry();
if (registry != null)
{
IExtensionPoint extensionPoint = registry.getExtensionPoint(JSPlugin.ID, NESTED_LANGUAGE_ID);
IExtension[] extensions = extensionPoint.getExtensions();
for (IExtension extension : extensions)
{
IParser[] parsers;
if (this.isScanner())
{
parsers = this._languageRegistry.loadScannersFromExtension(extension);
}
else
{
parsers = this._languageRegistry.loadParsersFromExtension(extension);
}
for (IParser parser : parsers)
{
this.addChildParser(parser);
}
}
}
}
}
/**
* Advance to the next lexeme in the lexeme stream
*
* @throws LexerException
*/
protected void advance() throws LexerException
{
ILexer lexer = this.getLexer();
Lexeme currentLexeme = EOS;
if (this._currentParentNode != null && this.currentLexeme != null && this.currentLexeme != EOS)
{
this._currentParentNode.includeLexemeInRange(this.currentLexeme);
}
if (lexer.isEOS() == false)
{
boolean inWhitespace = true;
boolean lastWasEOL = false;
while (inWhitespace)
{
if (lexer.isEOS() == false)
{
currentLexeme = this.getNextLexemeInLanguage();
if (currentLexeme != null && currentLexeme != EOS)
{
if (currentLexeme.typeIndex != JSTokenTypes.LINE_TERMINATOR)
{
// add all non-EOL lexemes to our final list for
// display purposes
this.addLexeme(currentLexeme);
}
// determine if token is in the WHITESPACE category
if (currentLexeme.getCategoryIndex() == TokenCategories.WHITESPACE)
{
if (currentLexeme.typeIndex == JSTokenTypes.CDC || currentLexeme.typeIndex == JSTokenTypes.CDO)
{
GenericCommentNode node = new GenericCommentNode(
currentLexeme.getStartingOffset(),
currentLexeme.getEndingOffset(), "HTMLCOMMENT", JSMimeType.MimeType); //$NON-NLS-1$
this.getParseState().addCommentRegion(node);
}
lastWasEOL = (currentLexeme.typeIndex == JSTokenTypes.LINE_TERMINATOR);
}
else
{
inWhitespace = false;
if (lastWasEOL)
{
currentLexeme.setAfterEOL();
}
}
}
else
{
// couldn't recover from error, so mark as end of stream
// NOTE: We may want to throw an exception here since we
// should be able to return at least an ERROR token
currentLexeme = EOS;
inWhitespace = false;
}
}
else
{
// we've reached the end of the source text
currentLexeme = EOS;
inWhitespace = false;
}
}
}
this.currentLexeme = currentLexeme;
}
/**
* checkForLanguageTransition
*
* @param lexeme
* @return Lexeme
* @throws LexerException
*/
private Lexeme checkForLanguageTransition(Lexeme lexeme) throws LexerException
{
Lexeme result = lexeme;
if (lexeme != null && lexeme != EOS)
{
String terminator = null;
String mimeType = null;
switch (lexeme.typeIndex)
{
case JSTokenTypes.PI_OPEN:
this.onPIOpen();
this.advance();
break;
case JSTokenTypes.PI_CLOSE:
this.advance();
break;
case JSTokenTypes.START_DOCUMENTATION:
terminator = DOCUMENTATION_DELIMITER_GROUP;
mimeType = ScriptDocMimeType.MimeType;
break;
case JSTokenTypes.START_MULTILINE_COMMENT:
terminator = DOCUMENTATION_DELIMITER_GROUP;
mimeType = JSCommentMimeType.MimeType;
break;
case JSTokenTypes.COMMENT:
terminator = LINE_DELIMITER_GROUP;
mimeType = JSCommentMimeType.MimeType;
break;
default:
break;
}
if (terminator != null && mimeType != null)
{
// language will change, logic is below
ILexer lexer = this.getLexer();
// backup to the start of the lexeme
lexer.setCurrentOffset(lexeme.offset);
// find offset
Range range = lexer.find(terminator);
// include the delimiter in the doc or comment
int offset = (lexeme.typeIndex == JSTokenTypes.COMMENT) ? range.getStartingOffset() : range.getEndingOffset();
if (range.isEmpty())
{
offset = lexer.getSourceLength();
}
try
{
this.changeLanguage(mimeType, offset, this._currentParentNode);
}
catch (LexerException e)
{
}
catch (ParseException e)
{
}
// advance over end of comment
this.advance();
result = this.currentLexeme;
}
}
return result;
}
/**
* @see com.aptana.ide.parsing.AbstractParser#createParseState(com.aptana.ide.parsing.IParseState)
*/
public IParseState createParseState(IParseState parent)
{
IParseState result;
IParseState root = parent;
if (parent == null)
{
result = new JSParseState();
root = result.getRoot();
}
else
{
result = new JSParseState(root);
}
// get nested parsers
IParser jsCommentParser = this.getParserForMimeType(JSCommentMimeType.MimeType);
IParser scriptDocParser = this.getParserForMimeType(ScriptDocMimeType.MimeType);
// add their parser states, if they exist
if (jsCommentParser != null)
{
result.addChildState(jsCommentParser.createParseState(root));
}
if (scriptDocParser != null)
{
result.addChildState(scriptDocParser.createParseState(root));
}
return result;
}
/**
* @see com.aptana.ide.parsing.AbstractParser#getNextLexemeInLanguage()
*/
protected Lexeme getNextLexemeInLanguage() throws LexerException
{
if (this._fastScan == false)
{
Lexeme result = super.getNextLexemeInLanguage();
// always need to look for language change
result = this.checkForLanguageTransition(result);
return result;
}
ILexer lexer = this.getLexer();
Lexeme result = null;
while (result == null && lexer.isEOS() == false)
{
result = lexer.getNextLexeme();
if (result != null && result != EOS)
{
String terminator = null;
String mimeType = null;
String text = result.getText();
int textLength = text.length();
int offset = 0;
if (textLength >= 2)
{
char[] source;
int eof;
switch (text.charAt(0))
{
case '/':
switch (text.charAt(1))
{
// "/**" or "/*"
case '*':
if (textLength == 3 && text.charAt(2) == '*')
{
// "/**"
terminator = DOCUMENTATION_DELIMITER_GROUP;
mimeType = ScriptDocMimeType.MimeType;
}
else
{
// "/*"
terminator = DOCUMENTATION_DELIMITER_GROUP;
mimeType = JSCommentMimeType.MimeType;
}
source = lexer.getSourceUnsafe();
eof = lexer.getEOFOffset();
for (offset = result.getEndingOffset(); offset < eof; offset++)
{
if (source[offset] == '*')
{
if (offset + 1 < eof && source[offset + 1] == '/')
{
offset += 2;
break;
}
}
}
break;
// "//"
case '/':
terminator = LINE_DELIMITER_GROUP;
mimeType = JSCommentMimeType.MimeType;
source = lexer.getSourceUnsafe();
eof = lexer.getEOFOffset();
for (offset = result.getEndingOffset(); offset < eof; offset++)
{
char current = source[offset];
if (current == '\r' || current == '\n')
{
break;
}
}
break;
}
break;
case '<':
if (text.charAt(1) == '?')
{
// "<?"
this.onPIOpen();
this.advance();
}
break;
case '?':
if (text.charAt(1) == '>')
{
// "?>"
this.advance();
}
break;
}
}
if (terminator != null && mimeType != null)
{
// backup to the start of the lexeme
lexer.setCurrentOffset(result.offset);
try
{
this.changeLanguage(mimeType, offset, this._currentParentNode);
}
catch (LexerException e)
{
}
catch (ParseException e)
{
}
// advance over end of comment
this.advance();
result = this.currentLexeme;
}
else if (result.getLanguage().equals(this.getLanguage()) == false)
{
LexemeList lexemes = this.getLexemeList();
lexemes.getAffectedRegion().includeInRange(result);
this.removeLexeme(result);
lexer.setCurrentOffset(result.offset);
result = lexer.getNextLexeme();
}
}
if (result == null && lexer.isEOS() == false)
{
// if we're already in the error group, then abort
if ("error".equals(lexer.getGroup())) //$NON-NLS-1$
{
break;
}
// Switch to error group.
lexer.setGroup("error"); //$NON-NLS-1$
// get error lexeme
result = lexer.getNextLexeme();
// if we failed to get a new lexeme and we're still in the error state,
// then we need to abort to prevent an infinite loop
if (result == null && "error".equals(lexer.getGroup())) //$NON-NLS-1$
{
break;
}
}
}
return result;
}
/**
* @see com.aptana.ide.parsing.AbstractParser#initializeLexer()
*/
public void initializeLexer() throws LexerException
{
// get lexer
ILexer lexer = this.getLexer();
String language = this.getLanguage();
// ignore whitespace
lexer.setIgnoreSet(language, new int[] { JSTokenTypes.WHITESPACE });
lexer.setLanguageAndGroup(language, DEFAULT_GROUP);
}
/**
* isScanner
*
* @return
*/
public boolean isScanner()
{
return false;
}
/**
* let's handle PI OPEN lexeme
*/
protected void onPIOpen()
{
try
{
if (this._piLanguage != null)
{
this.changeLanguage(this._piLanguage, Integer.MAX_VALUE, this._currentParentNode);
}
}
catch (LexerException e)
{
IdeLog.logError(JSPlugin.getDefault(), "Lexing exception", e); //$NON-NLS-1$
}
catch (ParseException e)
{
IdeLog.logError(JSPlugin.getDefault(), "Parsing exception", e); //$NON-NLS-1$
}
}
}