OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library html_css_fixup; | |
6 | |
7 import 'dart:json' as json; | |
8 | |
9 import 'package:csslib/parser.dart' as css; | |
10 import 'package:csslib/visitor.dart'; | |
11 import 'package:html5lib/dom.dart'; | |
12 import 'package:html5lib/dom_parsing.dart'; | |
13 | |
14 import 'compiler.dart'; | |
15 import 'emitters.dart'; | |
16 import 'info.dart'; | |
17 import 'messages.dart'; | |
18 import 'compiler_options.dart'; | |
19 import 'paths.dart'; | |
20 import 'utils.dart'; | |
21 | |
22 /** Enum for type of polyfills supported. */ | |
23 class CssPolyfillKind { | |
24 final _index; | |
25 const CssPolyfillKind._internal(this._index); | |
26 | |
27 /** Emit CSS selectors as seen (no polyfill). */ | |
28 static const NO_POLYFILL = const CssPolyfillKind._internal(0); | |
29 | |
30 /** Emit CSS selectors scoped to the "is" attribute of the component. */ | |
31 static const SCOPED_POLYFILL = const CssPolyfillKind._internal(1); | |
32 | |
33 /** Emit CSS selectors mangled. */ | |
34 static const MANGLED_POLYFILL = const CssPolyfillKind._internal(2); | |
35 | |
36 static CssPolyfillKind of(CompilerOptions options, ComponentInfo component) { | |
37 if (!options.processCss || !component.scoped) return NO_POLYFILL; | |
38 if (options.mangleCss) return MANGLED_POLYFILL; | |
39 if (!component.hasAuthorStyles && !options.hasCssReset) { | |
40 return MANGLED_POLYFILL; | |
41 } | |
42 return SCOPED_POLYFILL; | |
43 } | |
44 } | |
45 | |
46 | |
47 /** | |
48 * If processCss is enabled, prefix any component's HTML attributes for id or | |
49 * class to reference the mangled CSS class name or id. | |
50 */ | |
51 void fixupHtmlCss(FileInfo fileInfo, CompilerOptions options) { | |
52 // Walk the HTML tree looking for class names or id that are in our parsed | |
53 // stylesheet selectors and making those CSS classes and ids unique to that | |
54 // component. | |
55 if (options.verbose) { | |
56 print(" CSS fixup ${path.basename(fileInfo.inputUrl.resolvedPath)}"); | |
57 } | |
58 for (var component in fileInfo.declaredComponents) { | |
59 // Mangle class names and element ids in the HTML to match the stylesheet. | |
60 // TODO(terry): Allow more than one style sheet per component. | |
61 if (component.styleSheets.length == 1) { | |
62 // For components only 1 stylesheet allowed. | |
63 var styleSheet = component.styleSheets[0]; | |
64 var prefix = CssPolyfillKind.of(options, component) == | |
65 CssPolyfillKind.MANGLED_POLYFILL ? component.tagName : null; | |
66 | |
67 // List of referenced #id and .class in CSS. | |
68 var knownCss = new IdClassVisitor()..visitTree(styleSheet); | |
69 // Prefix all id and class refs in CSS selectors and HTML attributes. | |
70 new _ScopedStyleRenamer(knownCss, prefix, options.debugCss) | |
71 .visit(component.element); | |
72 } | |
73 } | |
74 } | |
75 | |
76 /** Build list of every CSS class name and id selector in a stylesheet. */ | |
77 class IdClassVisitor extends Visitor { | |
78 final Set<String> classes = new Set(); | |
79 final Set<String> ids = new Set(); | |
80 | |
81 void visitClassSelector(ClassSelector node) { | |
82 classes.add(node.name); | |
83 } | |
84 | |
85 void visitIdSelector(IdSelector node) { | |
86 ids.add(node.name); | |
87 } | |
88 } | |
89 | |
90 /** Build the Dart map of managled class/id names and component tag name. */ | |
91 Map _createCssSimpleSelectors(IdClassVisitor visitedCss, ComponentInfo info, | |
92 CssPolyfillKind kind) { | |
93 bool mangleNames = kind == CssPolyfillKind.MANGLED_POLYFILL; | |
94 Map selectors = {}; | |
95 if (visitedCss != null) { | |
96 for (var cssClass in visitedCss.classes) { | |
97 selectors['.$cssClass'] = | |
98 mangleNames ? '${info.tagName}_$cssClass' : cssClass; | |
99 } | |
100 for (var id in visitedCss.ids) { | |
101 selectors['#$id'] = mangleNames ? '${info.tagName}_$id' : id; | |
102 } | |
103 } | |
104 | |
105 // Add tag name selector x-comp == [is="x-comp"]. | |
106 var componentName = info.tagName; | |
107 selectors['$componentName'] = '[is="$componentName"]'; | |
108 | |
109 return selectors; | |
110 } | |
111 | |
112 /** | |
113 * Return a map of simple CSS selectors (class and id selectors) as a Dart map | |
114 * definition. | |
115 */ | |
116 String createCssSelectorsExpression(ComponentInfo info, CssPolyfillKind kind) { | |
117 var cssVisited = new IdClassVisitor(); | |
118 | |
119 // For components only 1 stylesheet allowed. | |
120 if (!info.styleSheets.isEmpty && info.styleSheets.length == 1) { | |
121 var styleSheet = info.styleSheets[0]; | |
122 cssVisited..visitTree(styleSheet); | |
123 } | |
124 | |
125 return json.stringify(_createCssSimpleSelectors(cssVisited, info, kind)); | |
126 } | |
127 | |
128 // TODO(terry): Need to handle other selectors than IDs/classes like tag name | |
129 // e.g., DIV { color: red; } | |
130 // TODO(terry): Would be nice if we didn't need to mangle names; requires users | |
131 // to be careful in their code and makes it more than a "polyfill". | |
132 // Maybe mechanism that generates CSS class name for scoping. This | |
133 // would solve tag name selectors (see above TODO). | |
134 /** | |
135 * Fix a component's HTML to implement scoped stylesheets. | |
136 * | |
137 * We do this by renaming all element class and id attributes to be globally | |
138 * unique to a component. | |
139 */ | |
140 class _ScopedStyleRenamer extends TreeVisitor { | |
141 final bool _debugCss; | |
142 | |
143 /** Set of classes and ids defined for this component. */ | |
144 final IdClassVisitor _knownCss; | |
145 | |
146 /** Prefix to apply to each class/id reference. */ | |
147 final String _prefix; | |
148 | |
149 _ScopedStyleRenamer(this._knownCss, this._prefix, this._debugCss); | |
150 | |
151 void visitElement(Element node) { | |
152 // Walk the HTML elements mangling any references to id or class attributes. | |
153 _mangleClassAttribute(node, _knownCss.classes, _prefix); | |
154 _mangleIdAttribute(node, _knownCss.ids, _prefix); | |
155 | |
156 super.visitElement(node); | |
157 } | |
158 | |
159 /** | |
160 * Mangles HTML class reference that matches a CSS class name defined in the | |
161 * component's style sheet. | |
162 */ | |
163 void _mangleClassAttribute(Node node, Set<String> classes, String prefix) { | |
164 if (node.attributes.containsKey('class')) { | |
165 var refClasses = node.attributes['class'].trim().split(" "); | |
166 | |
167 bool changed = false; | |
168 var len = refClasses.length; | |
169 for (var i = 0; i < len; i++) { | |
170 var refClass = refClasses[i]; | |
171 if (classes.contains(refClass)) { | |
172 if (prefix != null) { | |
173 refClasses[i] = '${prefix}_$refClass'; | |
174 changed = true; | |
175 } | |
176 } | |
177 } | |
178 | |
179 if (changed) { | |
180 StringBuffer newClasses = new StringBuffer(); | |
181 refClasses.forEach((String className) { | |
182 newClasses.write("${(newClasses.length > 0) ? ' ' : ''}$className"); | |
183 }); | |
184 var mangledClasses = newClasses.toString(); | |
185 if (_debugCss) { | |
186 print(" class = ${node.attributes['class'].trim()} => " | |
187 "$mangledClasses"); | |
188 } | |
189 node.attributes['class'] = mangledClasses; | |
190 } | |
191 } | |
192 } | |
193 | |
194 /** | |
195 * Mangles an HTML id reference that matches a CSS id selector name defined | |
196 * in the component's style sheet. | |
197 */ | |
198 void _mangleIdAttribute(Node node, Set<String> ids, String prefix) { | |
199 if (prefix != null) { | |
200 var id = node.attributes['id']; | |
201 if (id != null && ids.contains(id)) { | |
202 var mangledName = '${prefix}_$id'; | |
203 if (_debugCss) { | |
204 print(" id = ${node.attributes['id'].toString()} => $mangledName"); | |
205 } | |
206 node.attributes['id'] = mangledName; | |
207 } | |
208 } | |
209 } | |
210 } | |
211 | |
212 | |
213 /** | |
214 * Find var- definitions in a style sheet. | |
215 * [found] list of known definitions. | |
216 */ | |
217 class VarDefinitions extends Visitor { | |
218 final Map<String, VarDefinition> found = new Map(); | |
219 | |
220 void visitTree(StyleSheet tree) { | |
221 visitStyleSheet(tree); | |
222 } | |
223 | |
224 visitVarDefinition(VarDefinition node) { | |
225 // Replace with latest variable definition. | |
226 found[node.definedName] = node; | |
227 super.visitVarDefinition(node); | |
228 } | |
229 | |
230 void visitVarDefinitionDirective(VarDefinitionDirective node) { | |
231 visitVarDefinition(node.def); | |
232 } | |
233 } | |
234 | |
235 /** | |
236 * Resolve any CSS expression which contains a var() usage to the ultimate real | |
237 * CSS expression value e.g., | |
238 * | |
239 * var-one: var(two); | |
240 * var-two: #ff00ff; | |
241 * | |
242 * .test { | |
243 * color: var(one); | |
244 * } | |
245 * | |
246 * then .test's color would be #ff00ff | |
247 */ | |
248 class ResolveVarUsages extends Visitor { | |
249 final Map<String, VarDefinition> varDefs; | |
250 bool inVarDefinition = false; | |
251 bool inUsage = false; | |
252 Expressions currentExpressions; | |
253 | |
254 ResolveVarUsages(this.varDefs); | |
255 | |
256 void visitTree(StyleSheet tree) { | |
257 visitStyleSheet(tree); | |
258 } | |
259 | |
260 void visitVarDefinition(VarDefinition varDef) { | |
261 inVarDefinition = true; | |
262 super.visitVarDefinition(varDef); | |
263 inVarDefinition = false; | |
264 } | |
265 | |
266 void visitExpressions(Expressions node) { | |
267 currentExpressions = node; | |
268 super.visitExpressions(node); | |
269 currentExpressions = null; | |
270 } | |
271 | |
272 void visitVarUsage(VarUsage node) { | |
273 // Don't process other var() inside of a varUsage. That implies that the | |
274 // default is a var() too. Also, don't process any var() inside of a | |
275 // varDefinition (they're just place holders until we've resolved all real | |
276 // usages. | |
277 if (!inUsage && !inVarDefinition && currentExpressions != null) { | |
278 var expressions = currentExpressions.expressions; | |
279 var index = expressions.indexOf(node); | |
280 assert(index >= 0); | |
281 var def = varDefs[node.name]; | |
282 if (def != null) { | |
283 // Found a VarDefinition use it. | |
284 _resolveVarUsage(currentExpressions.expressions, index, def); | |
285 } else if (node.defaultValues.any((e) => e is VarUsage)) { | |
286 // Don't have a VarDefinition need to use default values resolve all | |
287 // default values. | |
288 var terminalDefaults = []; | |
289 for (var defaultValue in node.defaultValues) { | |
290 terminalDefaults.addAll(resolveUsageTerminal(defaultValue)); | |
291 } | |
292 expressions.replaceRange(index, index + 1, terminalDefaults); | |
293 } else { | |
294 // No VarDefinition but default value is a terminal expression; use it. | |
295 expressions.replaceRange(index, index + 1, node.defaultValues); | |
296 } | |
297 } | |
298 | |
299 inUsage = true; | |
300 super.visitVarUsage(node); | |
301 inUsage = false; | |
302 } | |
303 | |
304 List<Expression> resolveUsageTerminal(VarUsage usage) { | |
305 var result = []; | |
306 | |
307 var varDef = varDefs[usage.name]; | |
308 var expressions; | |
309 if (varDef == null) { | |
310 // VarDefinition not found try the defaultValues. | |
311 expressions = usage.defaultValues; | |
312 } else { | |
313 // Use the VarDefinition found. | |
314 expressions = (varDef.expression as Expressions).expressions; | |
315 } | |
316 | |
317 for (var expr in expressions) { | |
318 if (expr is VarUsage) { | |
319 // Get terminal value. | |
320 result.addAll(resolveUsageTerminal(expr)); | |
321 } | |
322 } | |
323 | |
324 // We're at a terminal just return the VarDefinition expression. | |
325 if (result.isEmpty && varDef != null) { | |
326 result = (varDef.expression as Expressions).expressions; | |
327 } | |
328 | |
329 return result; | |
330 } | |
331 | |
332 _resolveVarUsage(List<Expressions> expressions, int index, | |
333 VarDefinition def) { | |
334 var defExpressions = (def.expression as Expressions).expressions; | |
335 expressions.replaceRange(index, index + 1, defExpressions); | |
336 } | |
337 } | |
338 | |
339 /** Remove all var definitions. */ | |
340 class RemoveVarDefinitions extends Visitor { | |
341 void visitTree(StyleSheet tree) { | |
342 visitStyleSheet(tree); | |
343 } | |
344 | |
345 void visitStyleSheet(StyleSheet ss) { | |
346 ss.topLevels.removeWhere((e) => e is VarDefinitionDirective); | |
347 super.visitStyleSheet(ss); | |
348 } | |
349 | |
350 void visitDeclarationGroup(DeclarationGroup node) { | |
351 node.declarations.removeWhere((e) => e is VarDefinition); | |
352 super.visitDeclarationGroup(node); | |
353 } | |
354 } | |
355 | |
356 /** | |
357 * Process all selectors looking for a pseudo-element in a selector. If the | |
358 * name is found in our list of known pseudo-elements. Known pseudo-elements | |
359 * are built when parsing a component looking for an attribute named "pseudo". | |
360 * The value of the pseudo attribute is the name of the custom pseudo-element. | |
361 * The name is mangled so Dart/JS can't directly access the pseudo-element only | |
362 * CSS can access a custom pseudo-element (and see issue #510, querying needs | |
363 * access to custom pseudo-elements). | |
364 * | |
365 * Change the custom pseudo-element to be a child of the pseudo attribute's | |
366 * mangled custom pseudo element name. e.g, | |
367 * | |
368 * .test::x-box | |
369 * | |
370 * would become: | |
371 * | |
372 * .test > *[pseudo="x-box_2"] | |
373 */ | |
374 class PseudoElementExpander extends Visitor { | |
375 final Map<String, String> _pseudoElements; | |
376 | |
377 PseudoElementExpander(this._pseudoElements); | |
378 | |
379 void visitTree(StyleSheet tree) => visitStyleSheet(tree); | |
380 | |
381 visitSelector(Selector node) { | |
382 var selectors = node.simpleSelectorSequences; | |
383 for (var index = 0; index < selectors.length; index++) { | |
384 var selector = selectors[index].simpleSelector; | |
385 if (selector is PseudoElementSelector) { | |
386 if (_pseudoElements.containsKey(selector.name)) { | |
387 // Pseudo Element is a custom element. | |
388 var mangledName = _pseudoElements[selector.name]; | |
389 | |
390 var span = selectors[index].span; | |
391 | |
392 var attrSelector = new AttributeSelector( | |
393 new Identifier('pseudo', span), css.TokenKind.EQUALS, | |
394 mangledName, span); | |
395 // The wildcard * namespace selector. | |
396 var wildCard = new ElementSelector(new Wildcard(span), span); | |
397 selectors[index] = new SimpleSelectorSequence(wildCard, span, | |
398 css.TokenKind.COMBINATOR_GREATER); | |
399 selectors.insert(++index, | |
400 new SimpleSelectorSequence(attrSelector, span)); | |
401 } | |
402 } | |
403 } | |
404 } | |
405 } | |
406 | |
407 /** Compute each CSS URI resource relative from the generated CSS file. */ | |
408 class UriVisitor extends Visitor { | |
409 /** | |
410 * Relative path from the output css file to the location of the original | |
411 * css file that contained the URI to each resource. | |
412 */ | |
413 final String _pathToOriginalCss; | |
414 | |
415 factory UriVisitor(PathMapper pathMapper, String cssPath, bool rewriteUrl) { | |
416 var cssDir = path.dirname(cssPath); | |
417 var outCssDir = rewriteUrl ? pathMapper.outputDirPath(cssPath) | |
418 : path.dirname(cssPath); | |
419 return new UriVisitor._internal(path.relative(cssDir, from: outCssDir)); | |
420 } | |
421 | |
422 UriVisitor._internal(this._pathToOriginalCss); | |
423 | |
424 void visitUriTerm(UriTerm node) { | |
425 // Don't touch URIs that have any scheme (http, etc.). | |
426 var uri = Uri.parse(node.text); | |
427 if (uri.host != '') return; | |
428 if (uri.scheme != '' && uri.scheme != 'package') return; | |
429 | |
430 node.text = pathToUrl( | |
431 path.normalize(path.join(_pathToOriginalCss, node.text))); | |
432 } | |
433 } | |
434 | |
435 List<UrlInfo> findImportsInStyleSheet(StyleSheet styleSheet, | |
436 String packageRoot, UrlInfo inputUrl, Messages messages) { | |
437 var visitor = new CssImports(packageRoot, inputUrl, messages); | |
438 visitor.visitTree(styleSheet); | |
439 return visitor.urlInfos; | |
440 } | |
441 | |
442 /** | |
443 * Find any imports in the style sheet; normalize the style sheet href and | |
444 * return a list of all fully qualified CSS files. | |
445 */ | |
446 class CssImports extends Visitor { | |
447 final String packageRoot; | |
448 | |
449 /** Input url of the css file, used to normalize relative import urls. */ | |
450 final UrlInfo inputUrl; | |
451 | |
452 /** List of all imported style sheets. */ | |
453 final List<UrlInfo> urlInfos = []; | |
454 | |
455 final Messages _messages; | |
456 | |
457 CssImports(this.packageRoot, this.inputUrl, this._messages); | |
458 | |
459 void visitTree(StyleSheet tree) { | |
460 visitStyleSheet(tree); | |
461 } | |
462 | |
463 void visitImportDirective(ImportDirective node) { | |
464 var urlInfo = UrlInfo.resolve(node.import, inputUrl, | |
465 node.span, packageRoot, _messages, ignoreAbsolute: true); | |
466 if (urlInfo == null) return; | |
467 urlInfos.add(urlInfo); | |
468 } | |
469 } | |
470 | |
471 StyleSheet parseCss(String content, Messages messages, | |
472 CompilerOptions options) { | |
473 if (content.trim().isEmpty) return null; | |
474 | |
475 var errors = []; | |
476 | |
477 // TODO(terry): Add --checked when fully implemented and error handling. | |
478 var stylesheet = css.parse(content, errors: errors, options: | |
479 [options.warningsAsErrors ? '--warnings_as_errors' : '', 'memory']); | |
480 | |
481 // Note: errors aren't fatal in HTML (unless strict mode is on). | |
482 // So just print them as warnings. | |
483 for (var e in errors) { | |
484 messages.warning(e.message, e.span); | |
485 } | |
486 | |
487 return stylesheet; | |
488 } | |
489 | |
490 /** Find terminal definition (non VarUsage implies real CSS value). */ | |
491 VarDefinition findTerminalVarDefinition(Map<String, VarDefinition> varDefs, | |
492 VarDefinition varDef) { | |
493 var expressions = varDef.expression as Expressions; | |
494 for (var expr in expressions.expressions) { | |
495 if (expr is VarUsage) { | |
496 var usageName = (expr as VarUsage).name; | |
497 var foundDef = varDefs[usageName]; | |
498 | |
499 // If foundDef is unknown check if defaultValues; if it exist then resolve | |
500 // to terminal value. | |
501 if (foundDef == null) { | |
502 // We're either a VarUsage or terminal definition if in varDefs; | |
503 // either way replace VarUsage with it's default value because the | |
504 // VarDefinition isn't found. | |
505 var defaultValues = (expr as VarUsage).defaultValues; | |
506 var replaceExprs = expressions.expressions; | |
507 assert(replaceExprs.length == 1); | |
508 replaceExprs.replaceRange(0, 1, defaultValues); | |
509 return varDef; | |
510 } | |
511 if (foundDef is VarDefinition) { | |
512 return findTerminalVarDefinition(varDefs, foundDef); | |
513 } | |
514 } else { | |
515 // Return real CSS property. | |
516 return varDef; | |
517 } | |
518 } | |
519 | |
520 // Didn't point to a var definition that existed. | |
521 return varDef; | |
522 } | |
523 | |
524 /** | |
525 * Find urls imported inside style tags under [info]. If [info] is a FileInfo | |
526 * then process only style tags in the body (don't process any style tags in a | |
527 * component). If [info] is a ComponentInfo only process style tags inside of | |
528 * the element are processed. For an [info] of type FileInfo [node] is the | |
529 * file's document and for an [info] of type ComponentInfo then [node] is the | |
530 * component's element tag. | |
531 */ | |
532 List<UrlInfo> findUrlsImported(LibraryInfo info, UrlInfo inputUrl, | |
533 String packageRoot, Node node, Messages messages, CompilerOptions options) { | |
534 // Process any @imports inside of the <style> tag. | |
535 var styleProcessor = | |
536 new CssStyleTag(packageRoot, info, inputUrl, messages, options); | |
537 styleProcessor.visit(node); | |
538 return styleProcessor.imports; | |
539 } | |
540 | |
541 /* Process CSS inside of a style tag. */ | |
542 class CssStyleTag extends TreeVisitor { | |
543 final String _packageRoot; | |
544 | |
545 /** Either a FileInfo or ComponentInfo. */ | |
546 final LibraryInfo _info; | |
547 final Messages _messages; | |
548 final CompilerOptions _options; | |
549 | |
550 /** | |
551 * Path of the declaring file, for a [_info] of type FileInfo it's the file's | |
552 * path for a type ComponentInfo it's the declaring file path. | |
553 */ | |
554 final UrlInfo _inputUrl; | |
555 | |
556 /** List of @imports found. */ | |
557 List<UrlInfo> imports = []; | |
558 | |
559 CssStyleTag(this._packageRoot, this._info, this._inputUrl, this._messages, | |
560 this._options); | |
561 | |
562 void visitElement(Element node) { | |
563 // Don't process any style tags inside of element if we're processing a | |
564 // FileInfo. The style tags inside of a component defintion will be | |
565 // processed when _info is a ComponentInfo. | |
566 if (node.tagName == 'polymer-element' && _info is FileInfo) return; | |
567 if (node.tagName == 'style') { | |
568 // Parse the contents of the scoped style tag. | |
569 var styleSheet = parseCss(node.nodes.single.value, _messages, _options); | |
570 if (styleSheet != null) { | |
571 _info.styleSheets.add(styleSheet); | |
572 | |
573 // TODO(terry): Check on scoped attribute there's a rumor that styles | |
574 // might always be scoped in a component. | |
575 // TODO(terry): May need to handle multiple style tags some with scoped | |
576 // and some without for now first style tag determines how | |
577 // CSS is emitted. | |
578 if (node.attributes.containsKey('scoped') && _info is ComponentInfo) { | |
579 (_info as ComponentInfo).scoped = true; | |
580 } | |
581 | |
582 // Find all imports return list of @imports in this style tag. | |
583 var urlInfos = findImportsInStyleSheet(styleSheet, _packageRoot, | |
584 _inputUrl, _messages); | |
585 imports.addAll(urlInfos); | |
586 } | |
587 } | |
588 super.visitElement(node); | |
589 } | |
590 } | |
OLD | NEW |