/* * Copyright (c) 2014, the Dart project authors. * * Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html * * 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.dart.engine.internal.html.polymer; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.dart.engine.context.AnalysisException; import com.google.dart.engine.element.ClassElement; import com.google.dart.engine.element.Element; import com.google.dart.engine.element.ElementAnnotation; import com.google.dart.engine.element.ExternalHtmlScriptElement; import com.google.dart.engine.element.FieldElement; import com.google.dart.engine.element.HtmlScriptElement; import com.google.dart.engine.element.LibraryElement; import com.google.dart.engine.element.polymer.PolymerAttributeElement; import com.google.dart.engine.element.polymer.PolymerTagDartElement; import com.google.dart.engine.element.polymer.PolymerTagHtmlElement; import com.google.dart.engine.element.visitor.RecursiveElementVisitor; import com.google.dart.engine.error.AnalysisError; import com.google.dart.engine.error.AnalysisErrorListener; import com.google.dart.engine.error.ErrorCode; import com.google.dart.engine.error.PolymerCode; import com.google.dart.engine.html.ast.HtmlScriptTagNode; import com.google.dart.engine.html.ast.HtmlUnit; import com.google.dart.engine.html.ast.XmlAttributeNode; import com.google.dart.engine.html.ast.XmlTagNode; import com.google.dart.engine.html.ast.visitor.RecursiveXmlVisitor; import com.google.dart.engine.html.scanner.Token; import com.google.dart.engine.internal.context.InternalAnalysisContext; import com.google.dart.engine.internal.element.HtmlElementImpl; import com.google.dart.engine.internal.element.polymer.PolymerAttributeElementImpl; import com.google.dart.engine.internal.element.polymer.PolymerTagDartElementImpl; import com.google.dart.engine.internal.element.polymer.PolymerTagHtmlElementImpl; import com.google.dart.engine.internal.resolver.TypeProvider; import com.google.dart.engine.source.Source; import com.google.dart.engine.utilities.source.LineInfo; import java.util.List; import java.util.Set; /** * Instances of the class {@link PolymerHtmlUnitBuilder} build Polymer specific elements. */ public class PolymerHtmlUnitBuilder extends RecursiveXmlVisitor<Void> { private static class FoundTagDartElementError extends Error { private final PolymerTagDartElementImpl result; public FoundTagDartElementError(PolymerTagDartElementImpl result) { this.result = result; } } private class NameToken { private final int offset; private final String value; public NameToken(int offset, String value) { this.offset = offset; this.value = value; } } /** * These names are forbidden to use as a custom tag name. * <p> * http://w3c.github.io/webcomponents/spec/custom/#concepts */ private static final Set<String> FORBIDDEN_TAG_NAMES = Sets.newHashSet(new String[] { "annotation-xml", "color-profile", "font-face", "font-face-src", "font-face-uri", "font-face-format", "font-face-name", "missing-glyph",}); @VisibleForTesting public static boolean isValidAttributeName(String name) { // cannot be empty if (name.isEmpty()) { return false; } // check characters int length = name.length(); for (int i = 0; i < length; i++) { char c = name.charAt(i); if (i == 0) { if (!Character.isLetter(c)) { return false; } } else { if (!(Character.isLetterOrDigit(c) || c == '_')) { return false; } } } return true; } @VisibleForTesting public static boolean isValidTagName(String name) { // cannot be empty if (name.isEmpty()) { return false; } // check for forbidden name if (FORBIDDEN_TAG_NAMES.contains(name)) { return false; } // check characters int length = name.length(); boolean hasDash = false; for (int i = 0; i < length; i++) { char c = name.charAt(i); // check for '-' if (c == '-') { hasDash = true; } // check character if (i == 0) { if (hasDash) { return false; } if (!Character.isLetter(c)) { return false; } } else { if (!(Character.isLetterOrDigit(c) || c == '-' || c == '_')) { return false; } } } if (!hasDash) { return false; } return true; } private final InternalAnalysisContext context; private final TypeProvider typeProvider; private final AnalysisErrorListener errorListener; private final Source source; private final LineInfo lineInfo; private final HtmlUnit unit; private final List<PolymerTagHtmlElement> tagHtmlElements = Lists.newArrayList(); private XmlTagNode elementNode; private String elementName; private PolymerTagHtmlElementImpl htmlElement; private PolymerTagDartElementImpl dartElement; public PolymerHtmlUnitBuilder(InternalAnalysisContext context, AnalysisErrorListener errorListener, Source source, LineInfo lineInfo, HtmlUnit unit) throws AnalysisException { this.context = context; this.typeProvider = context.getTypeProvider(); this.errorListener = errorListener; this.source = source; this.lineInfo = lineInfo; this.unit = unit; } /** * Builds Polymer specific HTML elements. */ public void build() throws AnalysisException { unit.accept(this); // set Polymer tags HtmlElementImpl unitElement = (HtmlElementImpl) unit.getElement(); unitElement.setPolymerTags(tagHtmlElements.toArray(new PolymerTagHtmlElement[tagHtmlElements.size()])); } @Override public Void visitXmlTagNode(XmlTagNode node) { if (node.getTag().equals("polymer-element")) { createTagHtmlElement(node); } // visit children return super.visitXmlTagNode(node); } private void createAttributeElements() { // prepare "attributes" attribute XmlAttributeNode attributesAttribute = elementNode.getAttribute("attributes"); if (attributesAttribute == null) { return; } // check if there is a Dart part to resolve against it if (dartElement == null) { // TODO(scheglov) maybe report error (if it is allowed at all to have element without Dart part) return; } // prepare value of the "attributes" attribute String attributesText = attributesAttribute.getText(); if (attributesText.trim().isEmpty()) { reportErrorForAttribute(attributesAttribute, PolymerCode.EMPTY_ATTRIBUTES); return; } // prepare attribute name tokens List<NameToken> nameTokens = Lists.newArrayList(); { int index = 0; int textOffset = attributesAttribute.getTextOffset(); int nameOffset = -1; StringBuilder nameBuilder = new StringBuilder(); while (index < attributesText.length()) { char c = attributesText.charAt(index++); if (Character.isWhitespace(c)) { if (nameOffset != -1) { nameTokens.add(new NameToken(nameOffset, nameBuilder.toString())); nameBuilder = new StringBuilder(); nameOffset = -1; } continue; } if (nameOffset == -1) { nameOffset = textOffset + index - 1; } nameBuilder.append(c); } if (nameOffset != -1) { nameTokens.add(new NameToken(nameOffset, nameBuilder.toString())); nameBuilder = new StringBuilder(); } } // create attributes for name tokens List<PolymerAttributeElement> attributes = Lists.newArrayList(); Set<String> definedNames = Sets.newHashSet(); ClassElement classElement = dartElement.getClassElement(); for (NameToken nameToken : nameTokens) { int offset = nameToken.offset; // prepare name String name = nameToken.value; if (!isValidAttributeName(name)) { reportErrorForNameToken(nameToken, PolymerCode.INVALID_ATTRIBUTE_NAME, name); continue; } if (!definedNames.add(name)) { reportErrorForNameToken(nameToken, PolymerCode.DUPLICATE_ATTRIBUTE_DEFINITION, name); continue; } // create attribute PolymerAttributeElementImpl attribute = new PolymerAttributeElementImpl(name, offset); attributes.add(attribute); // resolve field FieldElement field = classElement.getField(name); if (field == null) { reportErrorForNameToken( nameToken, PolymerCode.UNDEFINED_ATTRIBUTE_FIELD, name, classElement.getDisplayName()); continue; } if (!isPublishedField(field)) { reportErrorForNameToken( nameToken, PolymerCode.ATTRIBUTE_FIELD_NOT_PUBLISHED, name, classElement.getDisplayName()); } attribute.setField(field); } htmlElement.setAttributes(attributes.toArray(new PolymerAttributeElement[attributes.size()])); } private void createTagHtmlElement(XmlTagNode node) { this.elementNode = node; this.elementName = null; this.htmlElement = null; this.dartElement = null; // prepare 'name' attribute XmlAttributeNode nameAttribute = node.getAttribute("name"); if (nameAttribute == null) { reportErrorForToken(node.getTagToken(), PolymerCode.MISSING_TAG_NAME); return; } // prepare name elementName = nameAttribute.getText(); if (!isValidTagName(elementName)) { reportErrorForAttributeValue(nameAttribute, PolymerCode.INVALID_TAG_NAME, elementName); return; } // TODO(scheglov) Maybe check that at least one of "template" or "script" children. // TODO(scheglov) Maybe check if more than one top-level "template". // create HTML element int nameOffset = nameAttribute.getTextOffset(); htmlElement = new PolymerTagHtmlElementImpl(elementName, nameOffset); // bind to the corresponding Dart element dartElement = findTagDartElement(); if (dartElement != null) { htmlElement.setDartElement(dartElement); dartElement.setHtmlElement(htmlElement); } // TODO(scheglov) create attributes createAttributeElements(); // done tagHtmlElements.add(htmlElement); } /** * Returns the {@link PolymerTagDartElement} that corresponds to the Polymer custom tag declared * by the given {@link XmlTagNode}. */ private PolymerTagDartElementImpl findTagDartElement() { LibraryElement dartLibraryElement = getDartUnitElement(); if (dartLibraryElement == null) { return null; } return findTagDartElement_inLibrary(dartLibraryElement); } /** * Returns the {@link PolymerTagDartElementImpl} declared in the given {@link LibraryElement} with * the {@link #elementName}. Maybe {@code null}. */ private PolymerTagDartElementImpl findTagDartElement_inLibrary(LibraryElement library) { try { library.accept(new RecursiveElementVisitor<Void>() { @Override public Void visitPolymerTagDartElement(PolymerTagDartElement element) { if (element.getName().equals(elementName)) { throw new FoundTagDartElementError((PolymerTagDartElementImpl) element); } return null; } }); } catch (FoundTagDartElementError e) { return e.result; } return null; } /** * Returns the only {@link LibraryElement} referenced by a direct {@code script} child. Maybe * {@code null} if none. */ private LibraryElement getDartUnitElement() { // TODO(scheglov) Maybe check if more than one "script". for (XmlTagNode child : elementNode.getTagNodes()) { if (child instanceof HtmlScriptTagNode) { HtmlScriptElement scriptElement = ((HtmlScriptTagNode) child).getScriptElement(); if (scriptElement instanceof ExternalHtmlScriptElement) { Source scriptSource = ((ExternalHtmlScriptElement) scriptElement).getScriptSource(); if (scriptSource != null) { return context.getLibraryElement(scriptSource); } } } } return null; } private boolean isPublishedAnnotation(ElementAnnotation annotation) { Element element = annotation.getElement(); if (element != null && element.getName().equals("published")) { return true; } return false; } private boolean isPublishedField(FieldElement field) { ElementAnnotation[] annotations = field.getMetadata(); for (ElementAnnotation annotation : annotations) { if (isPublishedAnnotation(annotation)) { return true; } } return false; } /** * Reports an error on the attribute's value, or (if absent) on the attribute's name. */ private void reportErrorForAttribute(XmlAttributeNode node, ErrorCode errorCode, Object... arguments) { reportErrorForOffset(node.getOffset(), node.getLength(), errorCode, arguments); } /** * Reports an error on the attribute's value, or (if absent) on the attribute's name. */ private void reportErrorForAttributeValue(XmlAttributeNode node, ErrorCode errorCode, Object... arguments) { Token valueToken = node.getValueToken(); if (valueToken == null || valueToken.isSynthetic()) { reportErrorForAttribute(node, errorCode, arguments); } else { reportErrorForToken(valueToken, errorCode, arguments); } } private void reportErrorForNameToken(NameToken token, ErrorCode errorCode, Object... arguments) { int offset = token.offset; int length = token.value.length(); reportErrorForOffset(offset, length, errorCode, arguments); } private void reportErrorForOffset(int offset, int length, ErrorCode errorCode, Object... arguments) { errorListener.onError(new AnalysisError(source, offset, length, errorCode, arguments)); } private void reportErrorForToken(Token token, ErrorCode errorCode, Object... arguments) { int offset = token.getOffset(); int length = token.getLength(); reportErrorForOffset(offset, length, errorCode, arguments); } }