Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /// Transfomer that inlines polymer-element definitions from html imports. | 5 /// Transfomer that inlines polymer-element definitions from html imports. |
| 6 library polymer.src.build.import_inliner; | 6 library polymer.src.build.import_inliner; |
| 7 | 7 |
| 8 import 'dart:async'; | 8 import 'dart:async'; |
| 9 import 'dart:convert'; | 9 import 'dart:convert'; |
| 10 | 10 |
| 11 import 'package:analyzer/src/generated/ast.dart'; | |
| 11 import 'package:barback/barback.dart'; | 12 import 'package:barback/barback.dart'; |
| 12 import 'package:path/path.dart' as path; | 13 import 'package:path/path.dart' as path; |
| 13 import 'package:html5lib/dom.dart' show | 14 import 'package:html5lib/dom.dart' show |
| 14 Document, DocumentFragment, Element, Node; | 15 Document, DocumentFragment, Element, Node; |
| 15 import 'package:html5lib/dom_parsing.dart' show TreeVisitor; | 16 import 'package:html5lib/dom_parsing.dart' show TreeVisitor; |
| 17 import 'package:source_maps/refactor.dart' show TextEditTransaction; | |
| 16 import 'package:source_maps/span.dart'; | 18 import 'package:source_maps/span.dart'; |
| 17 | 19 |
| 18 import 'code_extractor.dart'; // import just for documentation. | |
| 19 import 'common.dart'; | 20 import 'common.dart'; |
| 20 | 21 |
| 21 class _HtmlInliner extends PolymerTransformer { | 22 class _HtmlInliner extends PolymerTransformer { |
| 22 final TransformOptions options; | 23 final TransformOptions options; |
| 23 final Transform transform; | 24 final Transform transform; |
| 24 final TransformLogger logger; | 25 final TransformLogger logger; |
| 25 final AssetId docId; | 26 final AssetId docId; |
| 26 final seen = new Set<AssetId>(); | 27 final seen = new Set<AssetId>(); |
| 27 final scriptIds = <AssetId>[]; | 28 final scriptIds = <AssetId>[]; |
| 28 | 29 |
| 29 static const TYPE_DART = 'application/dart'; | |
| 30 static const TYPE_JS = 'text/javascript'; | |
| 31 | |
| 32 _HtmlInliner(this.options, Transform transform) | 30 _HtmlInliner(this.options, Transform transform) |
| 33 : transform = transform, | 31 : transform = transform, |
| 34 logger = transform.logger, | 32 logger = transform.logger, |
| 35 docId = transform.primaryInput.id; | 33 docId = transform.primaryInput.id; |
| 36 | 34 |
| 37 Future apply() { | 35 Future apply() { |
| 38 seen.add(docId); | 36 seen.add(docId); |
| 39 | 37 |
| 40 Document document; | 38 Document document; |
| 39 bool changed; | |
| 41 | 40 |
| 42 return readPrimaryAsHtml(transform).then((document) => | 41 return readPrimaryAsHtml(transform).then((doc) { |
| 43 _visitImports(document, docId).then((importsFound) { | 42 document = doc; |
| 43 // Add the main script's ID, or null if none is present. | |
| 44 // This will be used by ScriptCompactor. | |
| 45 changed = _extractScripts(document.querySelectorAll('script')); | |
| 46 return _visitImports(document); | |
| 47 }).then((importsFound) { | |
| 48 changed = changed || importsFound; | |
| 44 | 49 |
| 45 var output = transform.primaryInput; | 50 var output = transform.primaryInput; |
| 46 if (importsFound) { | 51 if (changed) output = new Asset.fromString(docId, document.outerHtml); |
| 47 output = new Asset.fromString(docId, document.outerHtml); | |
| 48 } | |
| 49 transform.addOutput(output); | 52 transform.addOutput(output); |
| 50 | 53 |
| 51 // We produce a secondary asset with extra information for later phases. | 54 // We produce a secondary asset with extra information for later phases. |
| 52 transform.addOutput(new Asset.fromString( | 55 transform.addOutput(new Asset.fromString( |
| 53 docId.addExtension('.scriptUrls'), | 56 docId.addExtension('.scriptUrls'), |
| 54 JSON.encode(scriptIds, toEncodable: (id) => id.serialize()))); | 57 JSON.encode(scriptIds, toEncodable: (id) => id.serialize()))); |
| 55 })); | 58 }); |
| 56 } | 59 } |
| 57 | 60 |
| 58 /// Visits imports in [document] and add the imported documents to documents. | 61 /// Visits imports in [document] and add the imported documents to documents. |
| 59 /// Documents are added in the order they appear, transitive imports are added | 62 /// Documents are added in the order they appear, transitive imports are added |
| 60 /// first. | 63 /// first. |
| 61 /// | 64 /// |
| 62 /// Returns `true` if and only if the document was changed and should be | 65 /// Returns `true` if and only if the document was changed and should be |
| 63 /// written out. | 66 /// written out. |
| 64 Future<bool> _visitImports(Document document, AssetId sourceId) { | 67 Future<bool> _visitImports(Document document) { |
| 65 bool changed = false; | 68 bool changed = false; |
| 66 | 69 |
| 67 _moveHeadToBody(document); | 70 _moveHeadToBody(document); |
| 68 | 71 |
| 69 // Note: we need to preserve the import order in the generated output. | 72 // Note: we need to preserve the import order in the generated output. |
| 70 return Future.forEach(document.querySelectorAll('link'), (Element tag) { | 73 return Future.forEach(document.querySelectorAll('link'), (Element tag) { |
| 71 var rel = tag.attributes['rel']; | 74 var rel = tag.attributes['rel']; |
| 72 if (rel != 'import' && rel != 'stylesheet') return null; | 75 if (rel != 'import' && rel != 'stylesheet') return null; |
| 73 | 76 |
| 77 // Note: URL has already been normalized so use docId. | |
| 74 var href = tag.attributes['href']; | 78 var href = tag.attributes['href']; |
| 75 var id = resolve(sourceId, href, transform.logger, tag.sourceSpan, | 79 var id = resolve(docId, href, transform.logger, tag.sourceSpan, |
| 76 allowAbsolute: rel == 'stylesheet'); | 80 allowAbsolute: rel == 'stylesheet'); |
| 77 | 81 |
| 78 if (rel == 'import') { | 82 if (rel == 'import') { |
| 79 changed = true; | 83 changed = true; |
| 80 if (id == null || !seen.add(id)) { | 84 if (id == null || !seen.add(id)) { |
| 81 tag.remove(); | 85 tag.remove(); |
| 82 return null; | 86 return null; |
| 83 } | 87 } |
| 84 return _inlineImport(id, tag); | 88 return _inlineImport(id, tag); |
| 85 | 89 |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 100 /// respect to eachother, because stylesheets can be pulled in transitively | 104 /// respect to eachother, because stylesheets can be pulled in transitively |
| 101 /// from imports. | 105 /// from imports. |
| 102 // TODO(jmesserly): vulcanizer doesn't need this because they inline JS | 106 // TODO(jmesserly): vulcanizer doesn't need this because they inline JS |
| 103 // scripts, causing them to be naturally moved as part of the inlining. | 107 // scripts, causing them to be naturally moved as part of the inlining. |
| 104 // Should we do the same? Alternatively could we inline head into head and | 108 // Should we do the same? Alternatively could we inline head into head and |
| 105 // body into body and avoid this whole thing? | 109 // body into body and avoid this whole thing? |
| 106 void _moveHeadToBody(Document doc) { | 110 void _moveHeadToBody(Document doc) { |
| 107 var insertionPoint = doc.body.firstChild; | 111 var insertionPoint = doc.body.firstChild; |
| 108 for (var node in doc.head.nodes.toList(growable: false)) { | 112 for (var node in doc.head.nodes.toList(growable: false)) { |
| 109 if (node is! Element) continue; | 113 if (node is! Element) continue; |
| 110 var tag = node.tagName; | 114 var tag = node.localName; |
| 111 var type = node.attributes['type']; | 115 var type = node.attributes['type']; |
| 112 var rel = node.attributes['rel']; | 116 var rel = node.attributes['rel']; |
| 113 if (tag == 'style' || tag == 'script' && | 117 if (tag == 'style' || tag == 'script' && |
| 114 (type == null || type == TYPE_JS || type == TYPE_DART) || | 118 (type == null || type == TYPE_JS || type == TYPE_DART) || |
| 115 tag == 'link' && (rel == 'stylesheet' || rel == 'import')) { | 119 tag == 'link' && (rel == 'stylesheet' || rel == 'import')) { |
| 116 // Move the node into the body, where its contents will be placed. | 120 // Move the node into the body, where its contents will be placed. |
| 117 doc.body.insertBefore(node, insertionPoint); | 121 doc.body.insertBefore(node, insertionPoint); |
| 118 } | 122 } |
| 119 } | 123 } |
| 120 } | 124 } |
| 121 | 125 |
| 122 // Loads an asset identified by [id], visits its imports and collects its | 126 /// Loads an asset identified by [id], visits its imports and collects its |
| 123 // html imports. Then inlines it into the main document. | 127 /// html imports. Then inlines it into the main document. |
| 124 Future _inlineImport(AssetId id, Element link) => | 128 Future _inlineImport(AssetId id, Element link) { |
| 125 readAsHtml(id, transform).then((doc) => _visitImports(doc, id).then((_) { | 129 return readAsHtml(id, transform).then((doc) { |
| 130 new _UrlNormalizer(transform, id).visit(doc); | |
| 131 return _visitImports(doc).then((_) { | |
| 132 var scripts = doc.querySelectorAll('script'); | |
| 133 _extractScripts(scripts); | |
| 134 _removeScripts(scripts); | |
| 126 | 135 |
| 127 new _UrlNormalizer(transform, id).visit(doc); | 136 // TODO(jmesserly): figure out how this is working in vulcanizer. |
| 128 _extractScripts(doc); | 137 // Do they produce a <body> tag with a <head> and <body> inside? |
| 129 | 138 var imported = new DocumentFragment(); |
| 130 // TODO(jmesserly): figure out how this is working in vulcanizer. | 139 imported.nodes..addAll(doc.head.nodes)..addAll(doc.body.nodes); |
| 131 // Do they produce a <body> tag with a <head> and <body> inside? | 140 link.replaceWith(imported); |
| 132 var imported = new DocumentFragment(); | 141 }); |
| 133 imported.nodes..addAll(doc.head.nodes)..addAll(doc.body.nodes); | 142 }); |
| 134 link.replaceWith(imported); | 143 } |
| 135 })); | |
| 136 | 144 |
| 137 Future _inlineStylesheet(AssetId id, Element link) { | 145 Future _inlineStylesheet(AssetId id, Element link) { |
| 138 return transform.readInputAsString(id).then((css) { | 146 return transform.readInputAsString(id).then((css) { |
| 139 var url = spanUrlFor(id, transform); | 147 css = new _UrlNormalizer(transform, id).visitCss(css); |
| 140 css = new _UrlNormalizer(transform, id).visitCss(css, url); | |
| 141 link.replaceWith(new Element.tag('style')..text = css); | 148 link.replaceWith(new Element.tag('style')..text = css); |
| 142 }); | 149 }); |
| 143 } | 150 } |
| 144 | 151 |
| 145 /// Split Dart script tags from all the other elements. Now that Dartium | 152 /// Remove scripts from HTML imports, and remember their [AssetId]s for later |
| 146 /// only allows a single script tag per page, we can't inline script | 153 /// use. |
| 147 /// tags. Instead, we collect the urls of each script tag so we import | 154 /// |
| 148 /// them directly from the Dart bootstrap code. | 155 /// Dartium only allows a single script tag per page, so we can't inline |
| 149 void _extractScripts(Document document) { | 156 /// the script tags. Instead we remove them entirely. |
| 150 bool first = true; | 157 void _removeScripts(List<Element> scripts) { |
| 151 for (var script in document.querySelectorAll('script')) { | 158 for (var script in scripts) { |
| 152 if (script.attributes['type'] == TYPE_DART) { | 159 if (script.attributes['type'] == TYPE_DART) { |
| 153 script.remove(); | 160 script.remove(); |
| 161 var src = script.attributes['src']; | |
| 162 scriptIds.add(resolve(docId, src, logger, script.sourceSpan)); | |
| 154 | 163 |
| 155 // only one Dart script per document is supported in Dartium. | 164 // only the first script needs to be added. |
| 156 if (first) { | 165 // others are already removed by _extractScripts |
| 157 first = false; | 166 return; |
| 158 | |
| 159 var src = script.attributes['src']; | |
| 160 if (src == null) { | |
| 161 logger.warning('unexpected script without a src url. The ' | |
| 162 'ImportInliner transformer should run after running the ' | |
| 163 'InlineCodeExtractor', span: script.sourceSpan); | |
| 164 continue; | |
| 165 } | |
| 166 scriptIds.add(resolve(docId, src, logger, script.sourceSpan)); | |
| 167 | |
| 168 } else { | |
| 169 // TODO(jmesserly): remove this when we are running linter. | |
| 170 logger.warning('more than one Dart script per HTML ' | |
| 171 'document is not supported. Script will be ignored.', | |
| 172 span: script.sourceSpan); | |
| 173 } | |
| 174 } | 167 } |
| 175 } | 168 } |
| 176 } | 169 } |
| 170 | |
| 171 /// Split inline scripts into their own files. We need to do this for dart2js | |
| 172 /// to be able to compile them. | |
| 173 /// | |
| 174 /// This also validates that there weren't any duplicate scripts. | |
| 175 bool _extractScripts(List<Element> scripts) { | |
| 176 bool changed = false; | |
| 177 bool first = true; | |
| 178 for (var script in scripts) { | |
| 179 if (script.attributes['type'] != TYPE_DART) continue; | |
| 180 | |
| 181 // only one Dart script per document is supported in Dartium. | |
| 182 if (!first) { | |
| 183 // Remove the script. It's invalid to have more than one in Dartium. | |
| 184 script.remove(); | |
| 185 changed = true; | |
| 186 | |
| 187 // TODO(jmesserly): remove this when we are running linter. | |
| 188 logger.warning('more than one Dart script per HTML ' | |
| 189 'document is not supported. Script will be ignored.', | |
| 190 span: script.sourceSpan); | |
| 191 continue; | |
| 192 } | |
| 193 | |
| 194 first = false; | |
| 195 | |
| 196 var src = script.attributes['src']; | |
| 197 if (src != null) continue; | |
| 198 | |
| 199 var filename = path.url.basename(docId.path); | |
| 200 var code = script.text; | |
| 201 // TODO(sigmund): ensure this path is unique (dartbug.com/12618). | |
| 202 script.attributes['src'] = src = '$filename.dart'; | |
| 203 script.text = ''; | |
| 204 changed = true; | |
| 205 | |
| 206 var newId = docId.addExtension('.dart'); | |
| 207 if (!_hasLibraryDirective(code)) { | |
| 208 // TODO(jmesserly): consolidate this with our other parsing. | |
| 209 var libname = path.withoutExtension(newId.path) | |
| 210 .replaceAll(new RegExp('[-./]'), '_'); | |
| 211 code = "library $libname;\n$code"; | |
| 212 } | |
| 213 transform.addOutput(new Asset.fromString(newId, code)); | |
| 214 } | |
| 215 return changed; | |
| 216 } | |
| 177 } | 217 } |
| 178 | 218 |
| 219 /// Parse [code] and determine whether it has a library directive. | |
| 220 bool _hasLibraryDirective(String code) => | |
| 221 parseCompilationUnit(code).directives.any((d) => d is LibraryDirective); | |
| 222 | |
| 223 | |
| 179 /// Recursively inlines the contents of HTML imports. Produces as output a | 224 /// Recursively inlines the contents of HTML imports. Produces as output a |
| 180 /// single HTML file that inlines the polymer-element definitions, and a text | 225 /// single HTML file that inlines the polymer-element definitions, and a text |
| 181 /// file that contains, in order, the URIs to each library that sourced in a | 226 /// file that contains, in order, the URIs to each library that sourced in a |
| 182 /// script tag. | 227 /// script tag. |
| 183 /// | 228 /// |
| 184 /// This transformer assumes that all script tags point to external files. To | 229 /// This transformer assumes that all script tags point to external files. To |
| 185 /// support script tags with inlined code, use this transformer after running | 230 /// support script tags with inlined code, use this transformer after running |
| 186 /// [InlineCodeExtractor] on an earlier phase. | 231 /// [InlineCodeExtractor] on an earlier phase. |
| 187 class ImportInliner extends Transformer { | 232 class ImportInliner extends Transformer { |
| 188 final TransformOptions options; | 233 final TransformOptions options; |
| 189 | 234 |
| 190 ImportInliner(this.options); | 235 ImportInliner(this.options); |
| 191 | 236 |
| 192 /// Only run on entry point .html files. | 237 /// Only run on entry point .html files. |
| 193 Future<bool> isPrimary(Asset input) => | 238 Future<bool> isPrimary(Asset input) => |
| 194 new Future.value(options.isHtmlEntryPoint(input.id)); | 239 new Future.value(options.isHtmlEntryPoint(input.id)); |
| 195 | 240 |
| 196 Future apply(Transform transform) => | 241 Future apply(Transform transform) => |
| 197 new _HtmlInliner(options, transform).apply(); | 242 new _HtmlInliner(options, transform).apply(); |
| 198 } | 243 } |
| 199 | 244 |
| 245 const TYPE_DART = 'application/dart'; | |
| 246 const TYPE_JS = 'text/javascript'; | |
| 200 | 247 |
| 201 /// Internally adjusts urls in the html that we are about to inline. | 248 /// Internally adjusts urls in the html that we are about to inline. |
| 202 class _UrlNormalizer extends TreeVisitor { | 249 class _UrlNormalizer extends TreeVisitor { |
| 203 final Transform transform; | 250 final Transform transform; |
| 204 | 251 |
| 205 /// Asset where the original content (and original url) was found. | 252 /// Asset where the original content (and original url) was found. |
| 206 final AssetId sourceId; | 253 final AssetId sourceId; |
| 207 | 254 |
| 208 _UrlNormalizer(this.transform, this.sourceId); | 255 _UrlNormalizer(this.transform, this.sourceId); |
| 209 | 256 |
| 210 visitElement(Element node) { | 257 visitElement(Element node) { |
| 211 for (var key in node.attributes.keys) { | 258 node.attributes.forEach((name, value) { |
| 212 if (_urlAttributes.contains(key)) { | 259 if (_urlAttributes.contains(name)) { |
| 213 var url = node.attributes[key]; | 260 if (value != '' && !value.trim().startsWith('{{')) { |
| 214 if (url != null && url != '' && !url.startsWith('{{')) { | 261 node.attributes[name] = _newUrl(value, node.sourceSpan); |
| 215 node.attributes[key] = _newUrl(url, node.sourceSpan); | |
| 216 } | 262 } |
| 217 } | 263 } |
| 264 }); | |
| 265 if (node.localName == 'style') { | |
| 266 node.text = visitCss(node.text); | |
| 267 } else if (node.localName == 'script' && | |
| 268 node.attributes['type'] == TYPE_DART) { | |
| 269 // TODO(jmesserly): we might need to visit JS too to handle ES Harmony | |
| 270 // modules. | |
| 271 node.text = visitInlineDart(node.text); | |
| 218 } | 272 } |
| 219 super.visitElement(node); | 273 super.visitElement(node); |
| 220 } | 274 } |
| 221 | 275 |
| 222 static final _URL = new RegExp(r'url\(([^)]*)\)', multiLine: true); | 276 static final _URL = new RegExp(r'url\(([^)]*)\)', multiLine: true); |
| 223 static final _QUOTE = new RegExp('["\']', multiLine: true); | 277 static final _QUOTE = new RegExp('["\']', multiLine: true); |
| 224 | 278 |
| 225 /// Visit the CSS text and replace any relative URLs so we can inline it. | 279 /// Visit the CSS text and replace any relative URLs so we can inline it. |
| 226 // Ported from: | 280 // Ported from: |
| 227 // https://github.com/Polymer/vulcanize/blob/c14f63696797cda18dc3d372b78aa3378 acc691f/lib/vulcan.js#L149 | 281 // https://github.com/Polymer/vulcanize/blob/c14f63696797cda18dc3d372b78aa3378 acc691f/lib/vulcan.js#L149 |
| 228 // TODO(jmesserly): use csslib here instead? Parsing with RegEx is sadness. | 282 // TODO(jmesserly): use csslib here instead? Parsing with RegEx is sadness. |
| 229 // Maybe it's reliable enough for finding URLs in CSS? I'm not sure. | 283 // Maybe it's reliable enough for finding URLs in CSS? I'm not sure. |
| 230 String visitCss(String cssText, String url) { | 284 String visitCss(String cssText) { |
| 285 var url = spanUrlFor(sourceId, transform); | |
| 231 var src = new SourceFile.text(url, cssText); | 286 var src = new SourceFile.text(url, cssText); |
| 232 return cssText.replaceAllMapped(_URL, (match) { | 287 return cssText.replaceAllMapped(_URL, (match) { |
| 233 // Extract the URL, without any surrounding quotes. | 288 // Extract the URL, without any surrounding quotes. |
| 234 var span = src.span(match.start, match.end); | 289 var span = src.span(match.start, match.end); |
| 235 var href = match[1].replaceAll(_QUOTE, ''); | 290 var href = match[1].replaceAll(_QUOTE, ''); |
| 236 href = _newUrl(href, span); | 291 href = _newUrl(href, span); |
| 237 return 'url($href)'; | 292 return 'url($href)'; |
| 238 }); | 293 }); |
| 239 } | 294 } |
| 240 | 295 |
| 241 _newUrl(String href, Span span) { | 296 String visitInlineDart(String code) { |
| 297 var unit = parseCompilationUnit(code); | |
| 298 var file = new SourceFile.text(spanUrlFor(sourceId, transform), code); | |
| 299 var output = new TextEditTransaction(code, file); | |
| 300 | |
| 301 for (Directive directive in unit.directives) { | |
| 302 if (directive is UriBasedDirective) { | |
| 303 var uri = directive.uri.stringValue; | |
| 304 var span = _getSpan(file, directive.uri); | |
| 305 | |
| 306 var id = resolve(sourceId, uri, transform.logger, span); | |
| 307 if (id == null) continue; | |
| 308 | |
| 309 var primaryId = transform.primaryInput.id; | |
|
Jennifer Messerly
2014/02/27 22:32:19
fyi -- this was the fix for URLs from pkgs
| |
| 310 var newUri = assetUrlFor(id, primaryId, transform.logger); | |
| 311 if (newUri != uri) { | |
| 312 output.edit(span.start.offset, span.end.offset, "'$newUri'"); | |
| 313 } | |
| 314 } | |
| 315 } | |
| 316 | |
| 317 if (!output.hasEdits) return code; | |
| 318 | |
| 319 // TODO(sigmund): emit source maps when barback supports it (see | |
| 320 // dartbug.com/12340) | |
| 321 return (output.commit()..build(file.url)).text; | |
| 322 } | |
| 323 | |
| 324 String _newUrl(String href, Span span) { | |
| 242 var uri = Uri.parse(href); | 325 var uri = Uri.parse(href); |
| 243 if (uri.isAbsolute) return href; | 326 if (uri.isAbsolute) return href; |
| 244 if (!uri.scheme.isEmpty) return href; | 327 if (!uri.scheme.isEmpty) return href; |
| 245 if (!uri.host.isEmpty) return href; | 328 if (!uri.host.isEmpty) return href; |
| 246 if (uri.path.isEmpty) return href; // Implies standalone ? or # in URI. | 329 if (uri.path.isEmpty) return href; // Implies standalone ? or # in URI. |
| 247 if (path.isAbsolute(href)) return href; | 330 if (path.isAbsolute(href)) return href; |
| 248 | 331 |
| 249 var id = resolve(sourceId, href, transform.logger, span); | 332 var id = resolve(sourceId, href, transform.logger, span); |
| 250 if (id == null) return href; | 333 if (id == null) return href; |
| 251 var primaryId = transform.primaryInput.id; | 334 var primaryId = transform.primaryInput.id; |
| (...skipping 30 matching lines...) Expand all Loading... | |
| 282 'cite', // in blockquote, del, ins, q | 365 'cite', // in blockquote, del, ins, q |
| 283 'data', // in object | 366 'data', // in object |
| 284 'formaction', // in button, input | 367 'formaction', // in button, input |
| 285 'href', // in a, area, link, base, command | 368 'href', // in a, area, link, base, command |
| 286 'icon', // in command | 369 'icon', // in command |
| 287 'manifest', // in html | 370 'manifest', // in html |
| 288 'poster', // in video | 371 'poster', // in video |
| 289 'src', // in audio, embed, iframe, img, input, script, source, track, | 372 'src', // in audio, embed, iframe, img, input, script, source, track, |
| 290 // video | 373 // video |
| 291 ]; | 374 ]; |
| 375 | |
| 376 _getSpan(SourceFile file, ASTNode node) => file.span(node.offset, node.end); | |
| OLD | NEW |