// Copyright 2012 Google Inc. All Rights Reserved. // // 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.google.collide.client.code.autocomplete.html; import static com.google.collide.codemirror2.TokenType.ATTRIBUTE; import static com.google.collide.codemirror2.TokenType.TAG; import com.google.collide.client.code.autocomplete.CodeAnalyzer; import com.google.collide.client.util.collections.StringMultiset; import com.google.collide.codemirror2.CodeMirror2; import com.google.collide.codemirror2.Token; import com.google.collide.codemirror2.TokenType; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.TaggableLine; import com.google.collide.shared.util.JsonCollections; import javax.annotation.Nonnull; /** * Analyzes token stream and builds or updates {@link HtmlTagWithAttributes}. * * <p>For each line we hold:<ol> * <li> {@link HtmlTagWithAttributes} unfinished at the beginning of line * <li> {@link HtmlTagWithAttributes} unfinished at the end of line * <li> list of attributes added to (1) in this line * </ol> * * <p>When line is reparsed:<ol> * <li> remove attributes from the list from appropriate tag and clean list * <li> set unfinished beginning tag equal to the ending tag of previous line * <li> add attributes to list until tag closes * <li> build and set unfinished tag at the end of line * </ol> * * <p> That way, during completion we have 2 cases:<ul> * <li> for tag that starts and ends in this line we need to parse it's content * <li> for tag that is unfinished (starts in previous lines or end in the * following lines) we already have parsed tag information. * </ul> * */ public class XmlCodeAnalyzer implements CodeAnalyzer { static final String TAG_START_TAG = HtmlAutocompleter.class.getName() + ".startTag"; static final String TAG_END_TAG = HtmlAutocompleter.class.getName() + ".endTag"; private static final String TAG_ATTRIBUTES = HtmlAutocompleter.class.getName() + ".attributes"; @Override public void onBeforeParse() { // Do nothing. } @Override public void onParseLine( TaggableLine previousLine, TaggableLine line, @Nonnull JsonArray<Token> tokens) { processLine(previousLine, line, tokens); } static void processLine( TaggableLine previousLine, TaggableLine line, @Nonnull JsonArray<Token> tokens) { // Ignore case always, as HTML == XmlCodeAnalyzer for now. final boolean ignoreCase = true; clearLine(line); HtmlTagWithAttributes tag = previousLine.getTag(TAG_END_TAG); line.putTag(TAG_START_TAG, tag); int index = 0; int size = tokens.size(); boolean inTag = false; int lastTagTokenIndex = -1; if (tag != null) { inTag = true; boolean newAttributes = false; JsonArray<String> attributes = line.getTag(TAG_ATTRIBUTES); if (attributes == null) { newAttributes = true; attributes = JsonCollections.createArray(); } StringMultiset tagAttributes = tag.getAttributes(); while (index < size) { Token token = tokens.get(index); index++; TokenType tokenType = token.getType(); if (ATTRIBUTE == tokenType) { String attribute = token.getValue(); attribute = ignoreCase ? attribute.toLowerCase() : attribute; attributes.add(attribute); tagAttributes.add(attribute); } else if (TAG == tokenType) { // Tag closing token tag.setDirty(false); inTag = false; break; } } if (newAttributes && attributes.size() != 0) { line.putTag(TAG_ATTRIBUTES, attributes); } else if (!newAttributes && attributes.size() == 0) { line.putTag(TAG_ATTRIBUTES, null); } } else { line.putTag(TAG_ATTRIBUTES, null); } while (index < size) { Token token = tokens.get(index); index++; TokenType tokenType = token.getType(); if (TAG == tokenType) { if (inTag) { if (">".equals(token.getValue()) || "/>".equals(token.getValue())) { // If type is "tag" and content is ">", this is HTML token. inTag = false; } } else { // Check that we are in html mode. if (CodeMirror2.HTML.equals(token.getMode())) { lastTagTokenIndex = index - 1; inTag = true; } } } } if (inTag) { if (lastTagTokenIndex != -1) { index = lastTagTokenIndex; Token token = tokens.get(index); index++; String tagName = token.getValue().substring(1).trim(); tag = new HtmlTagWithAttributes(tagName); StringMultiset tagAttributes = tag.getAttributes(); while (index < size) { token = tokens.get(index); index++; TokenType tokenType = token.getType(); if (ATTRIBUTE == tokenType) { String attribute = token.getValue(); tagAttributes.add(ignoreCase ? attribute.toLowerCase() : attribute); } } } // In case when document ends, but last tag is not closed we state that // tag content is "complete" - i.e. it will not be updated further. if (line.isLastLine()) { tag.setDirty(false); } line.putTag(TAG_END_TAG, tag); } else { line.putTag(TAG_END_TAG, null); } } @Override public void onAfterParse() { // Do nothing. } @Override public void onLinesDeleted(JsonArray<TaggableLine> deletedLines) { for (TaggableLine line : deletedLines.asIterable()) { clearLine(line); } } private static void clearLine(TaggableLine line) { HtmlTagWithAttributes tag = line.getTag(TAG_START_TAG); if (tag == null) { return; } tag.setDirty(true); JsonArray<String> attributes = line.getTag(TAG_ATTRIBUTES); if (attributes == null) { return; } tag.getAttributes().removeAll(attributes); attributes.clear(); } }