blob: 3e01e209c5eb06c8a0b51081a68b8f2d93eb9f75 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.lexer;
import com.intellij.codeInsight.completion.CompletionUtilCore;
import com.intellij.lang.HtmlScriptContentProvider;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageHtmlScriptContentProvider;
import com.intellij.lang.html.HTMLLanguage;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.impl.source.tree.TreeUtil;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.xml.XmlTokenType;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.xml.util.documentation.HtmlDescriptorsTable;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
/**
* @author Maxim.Mossienko
*/
abstract class BaseHtmlLexer extends DelegateLexer {
protected static final int BASE_STATE_MASK = 0x3F;
private static final int SEEN_STYLE = 0x40;
private static final int SEEN_TAG = 0x80;
private static final int SEEN_SCRIPT = 0x100;
private static final int SEEN_ATTRIBUTE = 0x200;
private static final int SEEN_CONTENT_TYPE = 0x400;
protected static final int BASE_STATE_SHIFT = 11;
@Nullable
protected static final Language ourDefaultLanguage = Language.findLanguageByID("JavaScript");
private boolean seenTag;
private boolean seenAttribute;
private boolean seenStyle;
private boolean seenScript;
@Nullable
protected String scriptType = null;
private final boolean caseInsensitive;
private boolean seenContentType;
private CharSequence cachedBufferSequence;
private Lexer lexerOfCacheBufferSequence;
static final TokenSet TOKENS_TO_MERGE = TokenSet.create(XmlTokenType.XML_COMMENT_CHARACTERS, XmlTokenType.XML_WHITE_SPACE, XmlTokenType.XML_REAL_WHITE_SPACE,
XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN, XmlTokenType.XML_DATA_CHARACTERS,
XmlTokenType.XML_TAG_CHARACTERS);
public interface TokenHandler {
void handleElement(Lexer lexer);
}
public class XmlNameHandler implements TokenHandler {
@NonNls private static final String TOKEN_SCRIPT = "script";
@NonNls private static final String TOKEN_STYLE = "style";
@NonNls private static final String TOKEN_ON = "on";
@Override
public void handleElement(Lexer lexer) {
final CharSequence buffer;
if (lexerOfCacheBufferSequence == lexer) {
buffer = cachedBufferSequence;
} else {
cachedBufferSequence = lexer.getBufferSequence();
buffer = cachedBufferSequence;
lexerOfCacheBufferSequence = lexer;
}
final char firstCh = buffer.charAt(lexer.getTokenStart());
if (seenScript && !seenTag) {
seenContentType = false;
if (((firstCh == 'l' || firstCh == 't') || (caseInsensitive && (firstCh == 'L' || firstCh == 'T')))) {
@NonNls String name = TreeUtil.getTokenText(lexer);
if (caseInsensitive) name = name.toLowerCase();
if ("language".equals(name) || "type".equals(name)) {
seenContentType = true;
}
}
return;
}
if (firstCh !='o' && firstCh !='s' &&
(!caseInsensitive || (firstCh !='S' && firstCh !='O') )
) {
return; // optimization
}
String name = TreeUtil.getTokenText(lexer);
if (caseInsensitive) name = name.toLowerCase();
final boolean style = name.equals(TOKEN_STYLE);
final int state = getState() & BASE_STATE_MASK;
final boolean script = name.equals(TOKEN_SCRIPT) ||
((name.startsWith(TOKEN_ON) && name.indexOf(':') == -1 && !isHtmlTagState(state) &&
HtmlDescriptorsTable.getAttributeDescriptor(name) != null));
if (style || script) {
// encountered tag name in end of tag
if (seenTag) {
if (isHtmlTagState(state)) {
seenTag = false;
}
return;
}
seenStyle = style;
seenScript = script;
if (!isHtmlTagState(state)) {
seenAttribute=true;
}
}
}
}
class XmlAttributeValueEndHandler implements TokenHandler {
@Override
public void handleElement(Lexer lexer) {
if (seenAttribute) {
seenStyle = false;
seenScript = false;
seenAttribute = false;
}
seenContentType = false;
}
}
class XmlAttributeValueHandler implements TokenHandler {
@Override
public void handleElement(Lexer lexer) {
if (seenContentType) {
if(!seenScript || seenAttribute) {
return; // something invalid
}
@NonNls String mimeType = TreeUtil.getTokenText(lexer);
if (caseInsensitive) mimeType = mimeType.toLowerCase();
scriptType = mimeType;
}
}
}
@Nullable
protected Language getScriptLanguage() {
Collection<Language> instancesByMimeType = Language.findInstancesByMimeType(scriptType != null ? scriptType.trim() : null);
return instancesByMimeType.isEmpty() ? null : instancesByMimeType.iterator().next();
}
@Nullable
protected IElementType getCurrentScriptElementType() {
HtmlScriptContentProvider scriptContentProvider = findScriptContentProvider(scriptType);
return scriptContentProvider == null ? null : scriptContentProvider.getScriptElementType();
}
@Nullable
protected static HtmlScriptContentProvider findScriptContentProvider(@Nullable String mimeType) {
if (StringUtil.isEmpty(mimeType)) {
return ourDefaultLanguage != null ? LanguageHtmlScriptContentProvider.getScriptContentProvider(ourDefaultLanguage) : null;
}
Collection<Language> instancesByMimeType = Language.findInstancesByMimeType(mimeType.trim());
if (instancesByMimeType.isEmpty() && mimeType.contains("template")) {
instancesByMimeType = Collections.<Language>singletonList(HTMLLanguage.INSTANCE);
}
for (Language language : instancesByMimeType) {
HtmlScriptContentProvider scriptContentProvider = LanguageHtmlScriptContentProvider.getScriptContentProvider(language);
if (scriptContentProvider != null) {
return scriptContentProvider;
}
}
return null;
}
class XmlTagClosedHandler implements TokenHandler {
@Override
public void handleElement(Lexer lexer) {
if (seenAttribute) {
seenScript=false;
seenStyle=false;
seenAttribute=false;
} else {
if (seenStyle || seenScript) {
seenTag=true;
}
}
}
}
class XmlTagEndHandler implements TokenHandler {
@Override
public void handleElement(Lexer lexer) {
seenStyle=false;
seenScript=false;
seenAttribute=false;
seenContentType=false;
scriptType = null;
}
}
private final HashMap<IElementType,TokenHandler> tokenHandlers = new HashMap<IElementType, TokenHandler>();
protected BaseHtmlLexer(Lexer _baseLexer, boolean _caseInsensitive) {
super(_baseLexer);
caseInsensitive = _caseInsensitive;
XmlNameHandler value = new XmlNameHandler();
tokenHandlers.put(XmlTokenType.XML_NAME,value);
tokenHandlers.put(XmlTokenType.XML_TAG_NAME,value);
tokenHandlers.put(XmlTokenType.XML_TAG_END,new XmlTagClosedHandler());
tokenHandlers.put(XmlTokenType.XML_END_TAG_START,new XmlTagEndHandler());
tokenHandlers.put(XmlTokenType.XML_EMPTY_ELEMENT_END,new XmlTagEndHandler());
tokenHandlers.put(XmlTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER,new XmlAttributeValueEndHandler());
tokenHandlers.put(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN,new XmlAttributeValueHandler());
}
protected void registerHandler(IElementType elementType, TokenHandler value) {
final TokenHandler tokenHandler = tokenHandlers.get(elementType);
if (tokenHandler != null) {
final TokenHandler newHandler = value;
value = new TokenHandler() {
@Override
public void handleElement(final Lexer lexer) {
tokenHandler.handleElement(lexer);
newHandler.handleElement(lexer);
}
};
}
tokenHandlers.put(elementType,value);
}
@Override
public void start(@NotNull final CharSequence buffer, final int startOffset, final int endOffset, final int initialState) {
initState(initialState);
super.start(buffer, startOffset, endOffset, initialState & BASE_STATE_MASK);
}
private void initState(final int initialState) {
seenScript = (initialState & SEEN_SCRIPT)!=0;
seenStyle = (initialState & SEEN_STYLE)!=0;
seenTag = (initialState & SEEN_TAG)!=0;
seenAttribute = (initialState & SEEN_ATTRIBUTE)!=0;
seenContentType = (initialState & SEEN_CONTENT_TYPE) != 0;
lexerOfCacheBufferSequence = null;
cachedBufferSequence = null;
}
protected int skipToTheEndOfTheEmbeddment() {
Lexer base = getDelegate();
int tokenEnd = base.getTokenEnd();
int lastState = 0;
int lastStart = 0;
final CharSequence buf = base.getBufferSequence();
final char[] bufArray = CharArrayUtil.fromSequenceWithoutCopying(buf);
if (seenTag) {
FoundEnd:
while(true) {
FoundEndOfTag:
while(base.getTokenType() != XmlTokenType.XML_END_TAG_START) {
if (base.getTokenType() == XmlTokenType.XML_COMMENT_CHARACTERS) {
// we should terminate on first occurence of </
final int end = base.getTokenEnd();
for(int i = base.getTokenStart(); i < end; ++i) {
if ((bufArray != null ? bufArray[i ]:buf.charAt(i)) == '<' &&
i + 1 < end &&
(bufArray != null ? bufArray[i+1]:buf.charAt(i+1)) == '/') {
tokenEnd = i;
lastStart = i - 1;
lastState = 0;
break FoundEndOfTag;
}
}
}
lastState = base.getState();
tokenEnd = base.getTokenEnd();
lastStart = base.getTokenStart();
if (tokenEnd == getBufferEnd()) break FoundEnd;
base.advance();
}
// check if next is script
if (base.getTokenType() != XmlTokenType.XML_END_TAG_START) { // we are inside comment
base.start(buf,lastStart+1,getBufferEnd(),lastState);
base.getTokenType();
base.advance();
} else {
base.advance();
}
while(XmlTokenType.WHITESPACES.contains(base.getTokenType())) {
base.advance();
}
if (base.getTokenType() == XmlTokenType.XML_NAME) {
String name = TreeUtil.getTokenText(base);
if (caseInsensitive) name = name.toLowerCase();
if((hasSeenScript() && XmlNameHandler.TOKEN_SCRIPT.equals(name)) ||
(hasSeenStyle() && XmlNameHandler.TOKEN_STYLE.equals(name)) ||
CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED.equalsIgnoreCase(name)) {
break; // really found end
}
}
}
base.start(buf,lastStart,getBufferEnd(),lastState);
base.getTokenType();
} else if (seenAttribute) {
while(true) {
if (!isValidAttributeValueTokenType(base.getTokenType())) break;
tokenEnd = base.getTokenEnd();
lastState = base.getState();
lastStart = base.getTokenStart();
if (tokenEnd == getBufferEnd()) break;
base.advance();
}
base.start(buf,lastStart,getBufferEnd(),lastState);
base.getTokenType();
}
return tokenEnd;
}
protected boolean isValidAttributeValueTokenType(final IElementType tokenType) {
return tokenType == XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN ||
tokenType == XmlTokenType.XML_ENTITY_REF_TOKEN ||
tokenType == XmlTokenType.XML_CHAR_ENTITY_REF;
}
@Override
public void advance() {
super.advance();
IElementType type = getDelegate().getTokenType();
TokenHandler tokenHandler = tokenHandlers.get(type);
if (tokenHandler!=null) tokenHandler.handleElement(this);
}
@Override
public int getState() {
int state = super.getState();
state |= ((seenScript)?SEEN_SCRIPT:0);
state |= ((seenTag)?SEEN_TAG:0);
state |= ((seenStyle)?SEEN_STYLE:0);
state |= ((seenAttribute)?SEEN_ATTRIBUTE:0);
state |= ((seenContentType)?SEEN_CONTENT_TYPE:0);
return state;
}
protected final boolean hasSeenStyle() {
return seenStyle;
}
protected final boolean hasSeenAttribute() {
return seenAttribute;
}
protected final boolean hasSeenTag() {
return seenTag;
}
protected boolean hasSeenScript() {
return seenScript;
}
protected abstract boolean isHtmlTagState(int state);
}