| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/error/error.dart'; |
| import 'package:analyzer/error/listener.dart'; |
| import 'package:analyzer/src/generated/parser.dart'; |
| import 'package:analyzer/dart/ast/token.dart'; |
| import 'package:analyzer/src/dart/ast/token.dart' hide SimpleToken; |
| import 'package:analyzer/src/dart/scanner/reader.dart'; |
| import 'package:analyzer/src/dart/scanner/scanner.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:angular_ast/angular_ast.dart'; |
| import 'package:angular_analyzer_plugin/ast.dart'; |
| import 'package:angular_analyzer_plugin/src/ng_expr_parser.dart'; |
| import 'package:angular_analyzer_plugin/src/ignoring_error_listener.dart'; |
| import 'package:angular_analyzer_plugin/src/strings.dart'; |
| import 'package:angular_analyzer_plugin/tasks.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:tuple/tuple.dart'; |
| import 'package:source_span/source_span.dart'; |
| |
| class HtmlTreeConverter { |
| final EmbeddedDartParser dartParser; |
| final Source templateSource; |
| final AnalysisErrorListener errorListener; |
| |
| // Following Angular2 Logic: |
| // https://github.com/dart-lang/angular2/blob/8220ba3a693aff51eed33cd1ec9542bde9017423/lib/src/compiler/schema/dom_element_schema_registry.dart#L199 |
| static const attrToPropMap = const { |
| 'class': 'className', |
| 'innerHtml': 'innerHTML', |
| 'readonly': 'readOnly', |
| 'tabindex': 'tabIndex', |
| }; |
| |
| HtmlTreeConverter(this.dartParser, this.templateSource, this.errorListener); |
| |
| DocumentInfo convertFromAstList(List<StandaloneTemplateAst> asts) { |
| final root = new DocumentInfo(); |
| if (asts.isEmpty) { |
| root.childNodes.add(new TextInfo(0, '', root, [])); |
| } |
| for (final node in asts) { |
| final convertedNode = convert(node, parent: root); |
| if (convertedNode != null) { |
| root.childNodes.add(convertedNode); |
| } |
| } |
| return root; |
| } |
| |
| NodeInfo convert( |
| StandaloneTemplateAst node, { |
| @required ElementInfo parent, |
| }) { |
| if (node is ElementAst) { |
| final localName = node.name; |
| final attributes = _convertAttributes( |
| attributes: node.attributes, |
| bananas: node.bananas, |
| events: node.events, |
| properties: node.properties, |
| references: node.references, |
| stars: node.stars, |
| )..sort((a, b) => a.offset.compareTo(b.offset)); |
| final closeComponent = node.closeComplement; |
| SourceRange openingSpan; |
| SourceRange openingNameSpan; |
| SourceRange closingSpan; |
| SourceRange closingNameSpan; |
| |
| if (node.isSynthetic) { |
| openingSpan = _toSourceRange(closeComponent.beginToken.offset, 0); |
| openingNameSpan = openingSpan; |
| } else { |
| openingSpan = _toSourceRange( |
| node.beginToken.offset, node.endToken.end - node.beginToken.offset); |
| openingNameSpan = new SourceRange( |
| (node as ParsedElementAst).identifierToken.offset, |
| (node as ParsedElementAst).identifierToken.lexeme.length); |
| } |
| // Check for void element cases (has closing complement) |
| // If closeComponent is synthetic, handle it after child nodes are found. |
| if (closeComponent != null && !closeComponent.isSynthetic) { |
| closingSpan = _toSourceRange(closeComponent.beginToken.offset, |
| closeComponent.endToken.end - closeComponent.beginToken.offset); |
| closingNameSpan = |
| new SourceRange(closingSpan.offset + '</'.length, localName.length); |
| } |
| |
| final element = new ElementInfo( |
| localName, |
| openingSpan, |
| closingSpan, |
| openingNameSpan, |
| closingNameSpan, |
| attributes, |
| findTemplateAttribute(attributes), |
| parent, |
| isTemplate: false, |
| ); |
| |
| for (final attribute in attributes) { |
| attribute.parent = element; |
| } |
| |
| final children = _convertChildren(node, element); |
| element.childNodes.addAll(children); |
| |
| if (!element.isSynthetic && |
| element.openingSpanIsClosed && |
| closingSpan != null && |
| (openingSpan.offset + openingSpan.length) == closingSpan.offset) { |
| element.childNodes.add(new TextInfo( |
| openingSpan.offset + openingSpan.length, '', element, [], |
| synthetic: true)); |
| } |
| |
| return element; |
| } |
| if (node is EmbeddedContentAst) { |
| final localName = 'ng-content'; |
| final attributes = <AttributeInfo>[]; |
| final closeComplement = node.closeComplement; |
| SourceRange openingSpan; |
| SourceRange openingNameSpan; |
| SourceRange closingSpan; |
| SourceRange closingNameSpan; |
| |
| if (node.isSynthetic) { |
| openingSpan = _toSourceRange(closeComplement.beginToken.offset, 0); |
| openingNameSpan = openingSpan; |
| } else { |
| openingSpan = _toSourceRange( |
| node.beginToken.offset, node.endToken.end - node.beginToken.offset); |
| openingNameSpan = |
| new SourceRange(openingSpan.offset + '<'.length, localName.length); |
| final pnode = node as ParsedEmbeddedContentAst; |
| final valueToken = pnode.selectorValueToken; |
| if (pnode.selectToken != null) { |
| attributes.add(new TextAttribute( |
| 'select', |
| pnode.selectToken.offset, |
| valueToken?.innerValue?.lexeme, |
| valueToken?.innerValue?.offset, |
| [], |
| )); |
| } |
| } |
| |
| if (closeComplement.isSynthetic) { |
| closingSpan = _toSourceRange(node.endToken.end, 0); |
| closingNameSpan = closingSpan; |
| } else { |
| closingSpan = _toSourceRange(closeComplement.beginToken.offset, |
| closeComplement.endToken.end - closeComplement.beginToken.offset); |
| closingNameSpan = |
| new SourceRange(closingSpan.offset + '</'.length, localName.length); |
| } |
| |
| final ngContent = new ElementInfo( |
| localName, |
| openingSpan, |
| closingSpan, |
| openingNameSpan, |
| closingNameSpan, |
| attributes, |
| null, |
| parent, |
| isTemplate: false, |
| ); |
| |
| for (final attribute in attributes) { |
| attribute.parent = ngContent; |
| } |
| |
| return ngContent; |
| } |
| if (node is EmbeddedTemplateAst) { |
| final localName = 'template'; |
| final attributes = _convertAttributes( |
| attributes: node.attributes, |
| events: node.events, |
| properties: node.properties, |
| references: node.references, |
| ); |
| final closeComponent = node.closeComplement; |
| SourceRange openingSpan; |
| SourceRange openingNameSpan; |
| SourceRange closingSpan; |
| SourceRange closingNameSpan; |
| |
| if (node.isSynthetic) { |
| openingSpan = _toSourceRange(closeComponent.beginToken.offset, 0); |
| openingNameSpan = openingSpan; |
| } else { |
| openingSpan = _toSourceRange( |
| node.beginToken.offset, node.endToken.end - node.beginToken.offset); |
| openingNameSpan = |
| new SourceRange(openingSpan.offset + '<'.length, localName.length); |
| } |
| // Check for void element cases (has closing complement) |
| if (closeComponent != null) { |
| if (closeComponent.isSynthetic) { |
| closingSpan = _toSourceRange(node.endToken.end, 0); |
| closingNameSpan = closingSpan; |
| } else { |
| closingSpan = _toSourceRange(closeComponent.beginToken.offset, |
| closeComponent.endToken.end - closeComponent.beginToken.offset); |
| closingNameSpan = new SourceRange( |
| closingSpan.offset + '</'.length, localName.length); |
| } |
| } |
| |
| final element = new ElementInfo( |
| localName, |
| openingSpan, |
| closingSpan, |
| openingNameSpan, |
| closingNameSpan, |
| attributes, |
| findTemplateAttribute(attributes), |
| parent, |
| isTemplate: true, |
| ); |
| |
| for (final attribute in attributes) { |
| attribute.parent = element; |
| } |
| |
| final children = _convertChildren(node, element); |
| element.childNodes.addAll(children); |
| |
| if (!element.isSynthetic && |
| element.openingSpanIsClosed && |
| closingSpan != null && |
| (openingSpan.offset + openingSpan.length) == closingSpan.offset) { |
| element.childNodes.add(new TextInfo( |
| openingSpan.offset + openingSpan.length, '', element, [], |
| synthetic: true)); |
| } |
| |
| return element; |
| } |
| if (node is TextAst) { |
| final offset = node.sourceSpan.start.offset; |
| final text = node.value; |
| return new TextInfo( |
| offset, text, parent, dartParser.findMustaches(text, offset)); |
| } |
| if (node is InterpolationAst) { |
| final offset = node.sourceSpan.start.offset; |
| final text = '{{${node.value}}}'; |
| return new TextInfo( |
| offset, text, parent, dartParser.findMustaches(text, offset)); |
| } |
| return null; |
| } |
| |
| List<AttributeInfo> _convertAttributes({ |
| List<ParsedAttributeAst> attributes: const [], |
| List<ParsedBananaAst> bananas: const [], |
| List<ParsedEventAst> events: const [], |
| List<ParsedPropertyAst> properties: const [], |
| List<ParsedReferenceAst> references: const [], |
| List<ParsedStarAst> stars: const [], |
| }) { |
| final returnAttributes = <AttributeInfo>[]; |
| |
| for (final attribute in attributes) { |
| if (attribute.name == 'template') { |
| returnAttributes.add(_convertTemplateAttribute(attribute)); |
| } else { |
| String value; |
| int valueOffset; |
| if (attribute.valueToken != null) { |
| value = attribute.valueToken.innerValue.lexeme; |
| valueOffset = attribute.valueToken.innerValue.offset; |
| } |
| returnAttributes.add(new TextAttribute( |
| attribute.name, |
| attribute.nameOffset, |
| value, |
| valueOffset, |
| dartParser.findMustaches(value, valueOffset), |
| )); |
| } |
| } |
| |
| bananas.map(_convertExpressionBoundAttribute).forEach(returnAttributes.add); |
| events.map(_convertStatementsBoundAttribute).forEach(returnAttributes.add); |
| properties |
| .map(_convertExpressionBoundAttribute) |
| .forEach(returnAttributes.add); |
| |
| for (final reference in references) { |
| String value; |
| int valueOffset; |
| if (reference.valueToken != null) { |
| value = reference.valueToken.innerValue.lexeme; |
| valueOffset = reference.valueToken.innerValue.offset; |
| } |
| returnAttributes.add(new TextAttribute( |
| '${reference.prefixToken.lexeme}${reference.nameToken.lexeme}', |
| reference.prefixToken.offset, |
| value, |
| valueOffset, |
| dartParser.findMustaches(value, valueOffset))); |
| } |
| |
| stars.map(_convertTemplateAttribute).forEach(returnAttributes.add); |
| |
| return returnAttributes; |
| } |
| |
| TemplateAttribute _convertTemplateAttribute(TemplateAst ast) { |
| String name; |
| String prefix; |
| int nameOffset; |
| |
| String value; |
| int valueOffset; |
| |
| String origName; |
| int origNameOffset; |
| |
| var virtualAttributes = []; |
| |
| if (ast is ParsedStarAst) { |
| value = ast.value; |
| valueOffset = ast.valueOffset; |
| |
| origName = '${ast.prefixToken.lexeme}${ast.nameToken.lexeme}'; |
| origNameOffset = ast.prefixToken.offset; |
| |
| name = ast.nameToken.lexeme; |
| nameOffset = ast.nameToken.offset; |
| |
| String fullAstName; |
| if (value != null) { |
| final whitespacePad = |
| ' ' * (ast.valueToken.innerValue.offset - ast.nameToken.end); |
| fullAstName = "${ast.name}$whitespacePad${value ?? ''}"; |
| } else { |
| fullAstName = '${ast.name} '; |
| } |
| |
| final tuple = |
| dartParser.parseTemplateVirtualAttributes(nameOffset, fullAstName); |
| virtualAttributes = tuple.item2; |
| prefix = tuple.item1; |
| } |
| if (ast is ParsedAttributeAst) { |
| value = ast.value; |
| valueOffset = ast.valueOffset; |
| |
| origName = ast.name; |
| origNameOffset = ast.nameOffset; |
| |
| name = origName; |
| nameOffset = origNameOffset; |
| |
| if (value == null || value.isEmpty) { |
| errorListener.onError(new AnalysisError(templateSource, origNameOffset, |
| origName.length, AngularWarningCode.EMPTY_BINDING, [origName])); |
| } else { |
| virtualAttributes = |
| dartParser.parseTemplateVirtualAttributes(valueOffset, value).item2; |
| } |
| } |
| |
| final templateAttribute = new TemplateAttribute(name, nameOffset, value, |
| valueOffset, origName, origNameOffset, virtualAttributes, |
| prefix: prefix); |
| |
| for (final virtualAttribute in virtualAttributes) { |
| virtualAttribute.parent = templateAttribute; |
| } |
| |
| return templateAttribute; |
| } |
| |
| StatementsBoundAttribute _convertStatementsBoundAttribute( |
| ParsedEventAst ast) { |
| final prefixComponent = |
| (ast.prefixToken.errorSynthetic ? '' : ast.prefixToken.lexeme); |
| final suffixComponent = |
| ((ast.suffixToken == null) || ast.suffixToken.errorSynthetic) |
| ? '' |
| : ast.suffixToken.lexeme; |
| final origName = '$prefixComponent${ast.name}$suffixComponent'; |
| final origNameOffset = ast.prefixToken.offset; |
| |
| final value = ast.value; |
| if ((value == null || value.isEmpty) && |
| !ast.prefixToken.errorSynthetic && |
| !ast.suffixToken.errorSynthetic) { |
| errorListener.onError(new AnalysisError(templateSource, origNameOffset, |
| origName.length, AngularWarningCode.EMPTY_BINDING, [ast.name])); |
| } |
| final valueOffset = ast.valueOffset; |
| |
| final propName = ast.nameToken.lexeme; |
| final propNameOffset = ast.nameToken.offset; |
| |
| return new StatementsBoundAttribute( |
| propName, |
| propNameOffset, |
| value, |
| valueOffset, |
| origName, |
| origNameOffset, |
| dartParser.parseDartStatements(valueOffset, value)); |
| } |
| |
| ExpressionBoundAttribute _convertExpressionBoundAttribute(TemplateAst ast) { |
| // Default starting. |
| var bound = ExpressionBoundType.input; |
| |
| final parsed = ast as ParsedDecoratorAst; |
| String origName; |
| { |
| final _prefix = |
| parsed.prefixToken.errorSynthetic ? '' : parsed.prefixToken.lexeme; |
| final _suffix = |
| (parsed.suffixToken == null || parsed.suffixToken.errorSynthetic) |
| ? '' |
| : parsed.suffixToken.lexeme; |
| origName = '$_prefix${parsed.nameToken.lexeme}$_suffix'; |
| } |
| final origNameOffset = parsed.prefixToken.offset; |
| |
| var propName = parsed.nameToken.lexeme; |
| var propNameOffset = parsed.nameToken.offset; |
| |
| if (ast is ParsedPropertyAst) { |
| final name = ast.name; |
| if (ast.postfix != null) { |
| var replacePropName = false; |
| if (name == 'class') { |
| bound = ExpressionBoundType.clazz; |
| replacePropName = true; |
| } else if (name == 'attr') { |
| bound = ExpressionBoundType.attr; |
| replacePropName = true; |
| } else if (name == 'style') { |
| bound = ExpressionBoundType.style; |
| replacePropName = true; |
| } |
| if (replacePropName) { |
| final _unitName = ast.unit == null ? '' : '.${ast.unit}'; |
| propName = '${ast.postfix}$_unitName'; |
| propNameOffset = parsed.nameToken.offset + name.length + '.'.length; |
| } |
| } |
| } else { |
| bound = ExpressionBoundType.twoWay; |
| } |
| |
| final value = parsed.valueToken?.innerValue?.lexeme; |
| if ((value == null || value.isEmpty) && |
| !parsed.prefixToken.errorSynthetic && |
| !parsed.suffixToken.errorSynthetic) { |
| errorListener.onError(new AnalysisError( |
| templateSource, |
| origNameOffset, |
| origName.length, |
| AngularWarningCode.EMPTY_BINDING, |
| [origName], |
| )); |
| } |
| final valueOffset = parsed.valueToken?.innerValue?.offset; |
| |
| propName = attrToPropMap[propName] ?? propName; |
| |
| return new ExpressionBoundAttribute( |
| propName, |
| propNameOffset, |
| value, |
| valueOffset, |
| origName, |
| origNameOffset, |
| dartParser.parseDartExpression(valueOffset, value, |
| detectTrailing: true), |
| bound); |
| } |
| |
| List<NodeInfo> _convertChildren( |
| StandaloneTemplateAst node, ElementInfo parent) { |
| final children = <NodeInfo>[]; |
| for (final child in node.childNodes) { |
| final childNode = convert(child, parent: parent); |
| if (childNode != null) { |
| children.add(childNode); |
| if (childNode is ElementInfo) { |
| parent.childNodesMaxEnd = childNode.childNodesMaxEnd; |
| } else { |
| parent.childNodesMaxEnd = childNode.offset + childNode.length; |
| } |
| } |
| } |
| return children; |
| } |
| |
| TemplateAttribute findTemplateAttribute(List<AttributeInfo> attributes) { |
| for (final attribute in attributes) { |
| if (attribute is TemplateAttribute) { |
| return attribute; |
| } |
| } |
| return null; |
| } |
| |
| SourceRange _toSourceRange(int offset, int length) => |
| new SourceRange(offset, length); |
| } |
| |
| class EmbeddedDartParser { |
| final Source templateSource; |
| final AnalysisErrorListener errorListener; |
| final ErrorReporter errorReporter; |
| |
| EmbeddedDartParser( |
| this.templateSource, this.errorListener, this.errorReporter); |
| |
| /// Parse the given Dart [code] that starts at [offset]. |
| Expression parseDartExpression(int offset, String code, |
| {@required bool detectTrailing}) { |
| if (code == null) { |
| return null; |
| } |
| |
| final token = _scanDartCode(offset, code); |
| Expression expression; |
| |
| // suppress errors for this. But still parse it so we can analyze it and stuff |
| if (code.trim().isEmpty) { |
| expression = _parseDartExpressionAtToken(token, |
| errorListener: new IgnoringAnalysisErrorListener()); |
| } else { |
| expression = _parseDartExpressionAtToken(token); |
| } |
| |
| if (detectTrailing && expression.endToken.next.type != TokenType.EOF) { |
| final trailingExpressionBegin = expression.endToken.next.offset; |
| errorListener.onError(new AnalysisError( |
| templateSource, |
| trailingExpressionBegin, |
| offset + code.length - trailingExpressionBegin, |
| AngularWarningCode.TRAILING_EXPRESSION)); |
| } |
| |
| return expression; |
| } |
| |
| /// Parse the given Dart [code] that starts ot [offset]. |
| /// Also removes and reports dangling closing brackets. |
| List<Statement> parseDartStatements(int offset, String code) { |
| final allStatements = <Statement>[]; |
| if (code == null) { |
| return allStatements; |
| } |
| |
| // ignore: parameter_assignments, prefer_interpolation_to_compose_strings |
| code = code + ';'; |
| |
| var token = _scanDartCode(offset, code); |
| |
| while (token.type != TokenType.EOF) { |
| final currentStatements = _parseDartStatementsAtToken(token); |
| |
| if (currentStatements.isNotEmpty) { |
| allStatements.addAll(currentStatements); |
| token = currentStatements.last.endToken.next; |
| } |
| if (token.type == TokenType.EOF) { |
| break; |
| } |
| if (token.type == TokenType.CLOSE_CURLY_BRACKET) { |
| final startCloseBracket = token.offset; |
| while (token.type == TokenType.CLOSE_CURLY_BRACKET) { |
| token = token.next; |
| } |
| final length = token.offset - startCloseBracket; |
| errorListener.onError(new AnalysisError( |
| templateSource, |
| startCloseBracket, |
| length, |
| ParserErrorCode.UNEXPECTED_TOKEN, |
| ["}"])); |
| continue; |
| } else { |
| //Nothing should trigger here, but just in case to prevent infinite loop |
| token = token.next; |
| } |
| } |
| return allStatements; |
| } |
| |
| /// Parse the Dart expression starting at the given [token]. |
| Expression _parseDartExpressionAtToken(Token token, |
| {AnalysisErrorListener errorListener}) { |
| errorListener ??= this.errorListener; |
| final parser = new NgExprParser(templateSource, errorListener); |
| return parser.parseExpression(token); |
| } |
| |
| /// Parse the Dart statement starting at the given [token]. |
| List<Statement> _parseDartStatementsAtToken(Token token) { |
| final parser = new Parser(templateSource, errorListener); |
| return parser.parseStatements(token); |
| } |
| |
| /// Scan the given Dart [code] that starts at [offset]. |
| Token _scanDartCode(int offset, String code) { |
| // ignore: prefer_interpolation_to_compose_strings |
| final text = ' ' * offset + code; |
| final reader = new CharSequenceReader(text); |
| final scanner = new Scanner(templateSource, reader, errorListener); |
| return scanner.tokenize(); |
| } |
| |
| /// Scan the given [text] staring at the given [offset] and resolve all of |
| /// its embedded expressions. |
| List<Mustache> findMustaches(String text, int fileOffset) { |
| final mustaches = <Mustache>[]; |
| if (text == null || text.length < 2) { |
| return mustaches; |
| } |
| |
| var textOffset = 0; |
| while (true) { |
| // begin |
| final begin = text.indexOf('{{', textOffset); |
| final nextBegin = text.indexOf('{{', begin + 2); |
| final end = text.indexOf('}}', textOffset); |
| |
| int exprBegin, exprEnd; |
| var detectTrailing = false; |
| |
| // Absolutely no mustaches - simple text. |
| if (begin == -1 && end == -1) { |
| break; |
| } |
| |
| if (end == -1) { |
| // Begin mustache exists, but no end mustache. |
| errorListener.onError(new AnalysisError(templateSource, |
| fileOffset + begin, 2, AngularWarningCode.UNTERMINATED_MUSTACHE)); |
| // Move the cursor ahead and keep looking for more unmatched mustaches. |
| textOffset = begin + 2; |
| exprBegin = textOffset; |
| exprEnd = _startsWithWhitespace(text.substring(exprBegin)) |
| ? exprBegin |
| : text.length; |
| } else if (begin == -1 || end < begin) { |
| // Both exists, but there is an end before a begin. |
| // Example: blah }} {{ mustache ... |
| errorListener.onError(new AnalysisError(templateSource, |
| fileOffset + end, 2, AngularWarningCode.UNOPENED_MUSTACHE)); |
| // Move the cursor ahead and keep looking for more unmatched mustaches. |
| textOffset = end + 2; |
| continue; |
| } else if (nextBegin != -1 && nextBegin < end) { |
| // Two open mustaches, but both opens are in sequence before an end. |
| // Example: {{ blah {{ mustache }} |
| errorListener.onError(new AnalysisError(templateSource, |
| fileOffset + begin, 2, AngularWarningCode.UNTERMINATED_MUSTACHE)); |
| // Skip this open mustache, check the next open we found |
| textOffset = begin + 2; |
| exprBegin = textOffset; |
| exprEnd = nextBegin; |
| } else { |
| // Proper open and close mustache exists and in correct order. |
| exprBegin = begin + 2; |
| exprEnd = end; |
| textOffset = end + 2; |
| detectTrailing = true; |
| } |
| // resolve |
| final code = text.substring(exprBegin, exprEnd); |
| final expression = parseDartExpression(fileOffset + exprBegin, code, |
| detectTrailing: detectTrailing); |
| |
| final offset = fileOffset + begin; |
| |
| int length; |
| if (end == -1) { |
| length = expression.offset + expression.length - offset; |
| } else { |
| length = end + 2 - begin; |
| } |
| |
| mustaches.add(new Mustache( |
| offset, |
| length, |
| expression, |
| fileOffset + exprBegin, |
| fileOffset + exprEnd, |
| )); |
| } |
| return mustaches; |
| } |
| |
| bool _startsWithWhitespace(String string) => |
| // trim returns the original string when no changes were made |
| !identical(string.trimLeft(), string); |
| |
| /// Desugar a template="" or *blah="" attribute into its list of virtual |
| /// [AttributeInfo]s |
| Tuple2<String, List<AttributeInfo>> parseTemplateVirtualAttributes( |
| int offset, String code) { |
| final attributes = <AttributeInfo>[]; |
| |
| var token = _scanDartCode(offset, code); |
| String prefix; |
| while (token.type != TokenType.EOF) { |
| // skip optional comma or semicolons |
| if (_isDelimiter(token)) { |
| token = token.next; |
| continue; |
| } |
| // maybe a local variable |
| if (_isTemplateVarBeginToken(token)) { |
| final originalVarOffset = token.offset; |
| if (token.type == TokenType.HASH) { |
| errorReporter.reportErrorForToken( |
| AngularWarningCode.UNEXPECTED_HASH_IN_TEMPLATE, token); |
| } |
| |
| var originalName = token.lexeme; |
| |
| // get the local variable name |
| token = token.next; |
| var localVarName = ""; |
| var localVarOffset = token.offset; |
| if (!_tokenMatchesIdentifier(token)) { |
| errorReporter.reportErrorForToken( |
| AngularWarningCode.EXPECTED_IDENTIFIER, token); |
| } else { |
| localVarOffset = token.offset; |
| localVarName = token.lexeme; |
| // ignore: prefer_interpolation_to_compose_strings |
| originalName += |
| ' ' * (token.offset - originalVarOffset - 'let'.length) + |
| localVarName; |
| token = token.next; |
| } |
| |
| // get an optional internal variable |
| int internalVarOffset; |
| String internalVarName; |
| if (token.type == TokenType.EQ) { |
| token = token.next; |
| // get the internal variable |
| if (!_tokenMatchesIdentifier(token)) { |
| errorReporter.reportErrorForToken( |
| AngularWarningCode.EXPECTED_IDENTIFIER, token); |
| break; |
| } |
| internalVarOffset = token.offset; |
| internalVarName = token.lexeme; |
| token = token.next; |
| } |
| // declare the local variable |
| // Note the care that the varname's offset is preserved in place. |
| attributes.add(new TextAttribute.synthetic( |
| 'let-$localVarName', |
| localVarOffset - 'let-'.length, |
| internalVarName, |
| internalVarOffset, |
| originalName, |
| originalVarOffset, [])); |
| continue; |
| } |
| |
| // key |
| String key; |
| final keyBuffer = new StringBuffer(); |
| final keyOffset = token.offset; |
| String originalName; |
| final originalNameOffset = keyOffset; |
| if (_tokenMatchesIdentifier(token)) { |
| // scan for a full attribute name |
| var lastEnd = token.offset; |
| while (token.offset == lastEnd && |
| (_tokenMatchesIdentifier(token) || token.type == TokenType.MINUS)) { |
| keyBuffer.write(token.lexeme); |
| lastEnd = token.end; |
| token = token.next; |
| } |
| |
| originalName = keyBuffer.toString(); |
| |
| // add the prefix |
| if (prefix == null) { |
| prefix = keyBuffer.toString(); |
| key = keyBuffer.toString(); |
| } else { |
| // ignore: prefer_interpolation_to_compose_strings |
| key = prefix + capitalize(keyBuffer.toString()); |
| } |
| } else { |
| errorReporter.reportErrorForToken( |
| AngularWarningCode.EXPECTED_IDENTIFIER, token); |
| break; |
| } |
| // skip optional ':' or '=' |
| if (token.type == TokenType.COLON || token.type == TokenType.EQ) { |
| token = token.next; |
| } |
| // expression |
| if (!_isTemplateVarBeginToken(token) && |
| !_isDelimiter(token) && |
| token.type != TokenType.EOF) { |
| final expression = _parseDartExpressionAtToken(token); |
| final start = token.offset - offset; |
| |
| token = expression.endToken.next; |
| final end = token.offset - offset; |
| final exprCode = code.substring(start, end); |
| attributes.add(new ExpressionBoundAttribute( |
| key, |
| keyOffset, |
| exprCode, |
| expression.offset, |
| originalName, |
| originalNameOffset, |
| expression, |
| ExpressionBoundType.input)); |
| } else { |
| attributes.add(new TextAttribute.synthetic( |
| key, keyOffset, null, null, originalName, originalNameOffset, [])); |
| } |
| } |
| |
| return new Tuple2(prefix, attributes); |
| } |
| |
| static bool _isDelimiter(Token token) => |
| token.type == TokenType.COMMA || token.type == TokenType.SEMICOLON; |
| |
| static bool _isTemplateVarBeginToken(Token token) => |
| token is KeywordToken && token.keyword == Keyword.VAR || |
| (token.type == TokenType.IDENTIFIER && token.lexeme == 'let') || |
| token.type == TokenType.HASH; |
| |
| static bool _tokenMatchesBuiltInIdentifier(Token token) => |
| token is KeywordToken && token.keyword.isBuiltInOrPseudo; |
| |
| static bool _tokenMatchesIdentifier(Token token) => |
| token.type == TokenType.IDENTIFIER || |
| _tokenMatchesBuiltInIdentifier(token); |
| } |
| |
| class IgnorableHtmlInternalException implements Exception { |
| String msg; |
| IgnorableHtmlInternalException(this.msg); |
| |
| @override |
| String toString() => "IgnorableHtmlInternalException: $msg"; |
| } |