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 part of csslib.visitor; | |
6 | |
7 /** | |
8 * Visitor that produces a formatted string representation of the CSS tree. | |
9 */ | |
10 class CssPrinter extends Visitor { | |
11 StringBuffer _buff = new StringBuffer(); | |
12 bool prettyPrint = true; | |
13 | |
14 /** | |
15 * Walk the [tree] Stylesheet. [pretty] if true emits line breaks, extra | |
16 * spaces, friendly property values, etc., if false emits compacted output. | |
17 */ | |
18 void visitTree(StyleSheet tree, {bool pretty: false}) { | |
19 prettyPrint = pretty; | |
20 _buff = new StringBuffer(); | |
21 visitStyleSheet(tree); | |
22 } | |
23 | |
24 /** Appends [str] to the output buffer. */ | |
25 void emit(String str) { | |
26 _buff.write(str); | |
27 } | |
28 | |
29 /** Returns the output buffer. */ | |
30 String toString() => _buff.toString().trim(); | |
31 | |
32 String get _newLine => prettyPrint ? '\n' : ' '; | |
33 String get _sp => prettyPrint ? ' ' : ''; | |
34 | |
35 // TODO(terry): When adding obfuscation we'll need isOptimized (compact w/ | |
36 // obufuscation) and have isTesting (compact no obfuscation) and | |
37 // isCompact would be !prettyPrint. We'll need another boolean | |
38 // flag for obfuscation. | |
39 bool get _isTesting => !prettyPrint; | |
40 | |
41 void visitCssComment(CssComment node) { | |
42 emit('/* ${node.comment} */'); | |
43 } | |
44 | |
45 void visitCommentDefinition(CommentDefinition node) { | |
46 emit('<!-- ${node.comment} -->'); | |
47 } | |
48 | |
49 void visitMediaExpression(MediaExpression node) { | |
50 emit(node.andOperator ? ' AND ' : ' '); | |
51 emit('(${node.mediaFeature}:'); | |
52 visitExpressions(node.exprs); | |
53 emit(')'); | |
54 } | |
55 | |
56 void visitMediaQuery(MediaQuery query) { | |
57 var unary = query.hasUnary ? ' ${query.unary}' : ''; | |
58 var mediaType = query.hasMediaType ? ' ${query.mediaType}' : ''; | |
59 emit('$unary$mediaType'); | |
60 for (var expression in query.expressions) { | |
61 visitMediaExpression(expression); | |
62 } | |
63 } | |
64 | |
65 void emitMediaQueries(queries) { | |
66 var queriesLen = queries.length; | |
67 for (var i = 0; i < queriesLen; i++) { | |
68 var query = queries[i]; | |
69 if (query.hasMediaType && i > 0) emit(','); | |
70 visitMediaQuery(query); | |
71 } | |
72 } | |
73 | |
74 void visitMediaDirective(MediaDirective node) { | |
75 emit(' @media'); | |
76 emitMediaQueries(node.mediaQueries); | |
77 emit(' {'); | |
78 for (var ruleset in node.rulesets) { | |
79 ruleset.visit(this); | |
80 } | |
81 emit('$_newLine\}'); | |
82 } | |
83 | |
84 void visitHostDirective(HostDirective node) { | |
85 emit('\n@host {'); | |
86 for (var ruleset in node.rulesets) { | |
87 ruleset.visit(this); | |
88 } | |
89 emit('$_newLine\}'); | |
90 } | |
91 | |
92 /** | |
93 * @page : pseudoPage { | |
94 * decls | |
95 * } | |
96 */ | |
97 void visitPageDirective(PageDirective node) { | |
98 emit('$_newLine@page'); | |
99 if (node.hasIdent || node.hasPseudoPage) { | |
100 if (node.hasIdent) emit(' '); | |
101 emit(node._ident); | |
102 emit(node.hasPseudoPage ? ':${node._pseudoPage}' : ''); | |
103 } | |
104 emit(' '); | |
105 | |
106 var declsMargin = node._declsMargin; | |
107 var declsMarginLength = declsMargin.length; | |
108 for (var i = 0; i < declsMarginLength; i++) { | |
109 if (i > 0) emit(_newLine); | |
110 emit('{$_newLine'); | |
111 declsMargin[i].visit(this); | |
112 emit('}'); | |
113 } | |
114 } | |
115 | |
116 /** @charset "charset encoding" */ | |
117 void visitCharsetDirective(CharsetDirective node) { | |
118 emit('$_newLine@charset "${node.charEncoding}";'); | |
119 } | |
120 | |
121 void visitImportDirective(ImportDirective node) { | |
122 bool isStartingQuote(String ch) => ('\'"'.indexOf(ch[0]) >= 0); | |
123 | |
124 if (_isTesting) { | |
125 // Emit assuming url() was parsed; most suite tests use url function. | |
126 emit(' @import url(${node.import})'); | |
127 } else if (isStartingQuote(node.import)) { | |
128 emit(' @import ${node.import}'); | |
129 } else { | |
130 // url(...) isn't needed only a URI can follow an @import directive; emit | |
131 // url as a string. | |
132 emit(' @import "${node.import}"'); | |
133 } | |
134 emitMediaQueries(node.mediaQueries); | |
135 emit(';'); | |
136 } | |
137 | |
138 void visitKeyFrameDirective(KeyFrameDirective node) { | |
139 emit('$_newLine${node.keyFrameName} '); | |
140 node.name.visit(this); | |
141 emit('$_sp{$_newLine'); | |
142 for (final block in node._blocks) { | |
143 block.visit(this); | |
144 } | |
145 emit('}'); | |
146 } | |
147 | |
148 void visitFontFaceDirective(FontFaceDirective node) { | |
149 emit('$_newLine@font-face '); | |
150 emit('$_sp{$_newLine'); | |
151 node._declarations.visit(this); | |
152 emit('}'); | |
153 } | |
154 | |
155 void visitKeyFrameBlock(KeyFrameBlock node) { | |
156 emit('$_sp$_sp'); | |
157 node._blockSelectors.visit(this); | |
158 emit('$_sp{$_newLine'); | |
159 node._declarations.visit(this); | |
160 emit('$_sp$_sp}$_newLine'); | |
161 } | |
162 | |
163 void visitStyletDirective(StyletDirective node) { | |
164 emit('/* @stylet export as ${node.dartClassName} */\n'); | |
165 } | |
166 | |
167 void visitNamespaceDirective(NamespaceDirective node) { | |
168 bool isStartingQuote(String ch) => ('\'"'.indexOf(ch) >= 0); | |
169 | |
170 if (isStartingQuote(node._uri)) { | |
171 emit(' @namespace ${node.prefix}"${node._uri}"'); | |
172 } else { | |
173 if (_isTesting) { | |
174 // Emit exactly was we parsed. | |
175 emit(' @namespace ${node.prefix}url(${node._uri})'); | |
176 } else { | |
177 // url(...) isn't needed only a URI can follow a: | |
178 // @namespace prefix directive. | |
179 emit(' @namespace ${node.prefix}${node._uri}'); | |
180 } | |
181 } | |
182 emit(';'); | |
183 } | |
184 | |
185 void visitVarDefinitionDirective(VarDefinitionDirective node) { | |
186 visitVarDefinition(node.def); | |
187 emit(';$_newLine'); | |
188 } | |
189 | |
190 void visitMixinRulesetDirective(MixinRulesetDirective node) { | |
191 emit('@mixin ${node.name} {'); | |
192 for (var ruleset in node.rulesets) { | |
193 ruleset.visit(this); | |
194 } | |
195 emit('}'); | |
196 } | |
197 | |
198 void visitMixinDeclarationDirective(MixinDeclarationDirective node) { | |
199 emit('@mixin ${node.name} {\n'); | |
200 visitDeclarationGroup(node.declarations); | |
201 emit('}'); | |
202 } | |
203 | |
204 /** | |
205 * Added optional newLine for handling @include at top-level vs/ inside of | |
206 * a declaration group. | |
207 */ | |
208 void visitIncludeDirective(IncludeDirective node, [bool topLevel = true]) { | |
209 if (topLevel) emit(_newLine); | |
210 emit('@include ${node.name}'); | |
211 emit(';'); | |
212 } | |
213 | |
214 void visitContentDirective(ContentDirective node) { | |
215 // TODO(terry): TBD | |
216 } | |
217 | |
218 void visitRuleSet(RuleSet node) { | |
219 emit("$_newLine"); | |
220 node._selectorGroup.visit(this); | |
221 emit(" {$_newLine"); | |
222 node._declarationGroup.visit(this); | |
223 emit("}"); | |
224 } | |
225 | |
226 void visitDeclarationGroup(DeclarationGroup node) { | |
227 var declarations = node.declarations; | |
228 var declarationsLength = declarations.length; | |
229 for (var i = 0; i < declarationsLength; i++) { | |
230 if (i > 0) emit(_newLine); | |
231 emit("$_sp$_sp"); | |
232 declarations[i].visit(this); | |
233 emit(";"); | |
234 } | |
235 if (declarationsLength > 0) emit(_newLine); | |
236 } | |
237 | |
238 void visitMarginGroup(MarginGroup node) { | |
239 var margin_sym_name = | |
240 TokenKind.idToValue(TokenKind.MARGIN_DIRECTIVES, node.margin_sym); | |
241 | |
242 emit("@$margin_sym_name {$_newLine"); | |
243 | |
244 visitDeclarationGroup(node); | |
245 | |
246 emit("}$_newLine"); | |
247 } | |
248 | |
249 void visitDeclaration(Declaration node) { | |
250 String importantAsString() => node.important ? '$_sp!important' : ''; | |
251 | |
252 emit("${node.property}: "); | |
253 node._expression.visit(this); | |
254 | |
255 emit("${importantAsString()}"); | |
256 } | |
257 | |
258 void visitVarDefinition(VarDefinition node) { | |
259 emit("var-${node.definedName}: "); | |
260 node._expression.visit(this); | |
261 } | |
262 | |
263 void visitIncludeMixinAtDeclaration(IncludeMixinAtDeclaration node) { | |
264 // Don't emit a new line we're inside of a declaration group. | |
265 visitIncludeDirective(node.include, false); | |
266 } | |
267 | |
268 void visitExtendDeclaration(ExtendDeclaration node) { | |
269 emit("@extend "); | |
270 for (var selector in node.selectors) { | |
271 selector.visit(this); | |
272 } | |
273 } | |
274 | |
275 void visitSelectorGroup(SelectorGroup node) { | |
276 var selectors = node.selectors; | |
277 var selectorsLength = selectors.length; | |
278 for (var i = 0; i < selectorsLength; i++) { | |
279 if (i > 0) emit(',$_sp'); | |
280 selectors[i].visit(this); | |
281 } | |
282 } | |
283 | |
284 void visitSimpleSelectorSequence(SimpleSelectorSequence node) { | |
285 emit('${node._combinatorToString}'); | |
286 node.simpleSelector.visit(this); | |
287 } | |
288 | |
289 void visitSimpleSelector(SimpleSelector node) { | |
290 emit(node.name); | |
291 } | |
292 | |
293 void visitNamespaceSelector(NamespaceSelector node) { | |
294 emit(node.toString()); | |
295 } | |
296 | |
297 void visitElementSelector(ElementSelector node) { | |
298 emit(node.toString()); | |
299 } | |
300 | |
301 void visitAttributeSelector(AttributeSelector node) { | |
302 emit(node.toString()); | |
303 } | |
304 | |
305 void visitIdSelector(IdSelector node) { | |
306 emit(node.toString()); | |
307 } | |
308 | |
309 void visitClassSelector(ClassSelector node) { | |
310 emit(node.toString()); | |
311 } | |
312 | |
313 void visitPseudoClassSelector(PseudoClassSelector node) { | |
314 emit(node.toString()); | |
315 } | |
316 | |
317 void visitPseudoElementSelector(PseudoElementSelector node) { | |
318 emit(node.toString()); | |
319 } | |
320 | |
321 void visitPseudoClassFunctionSelector(PseudoClassFunctionSelector node) { | |
322 emit(":${node.name}("); | |
323 node.expression.visit(this); | |
324 emit(')'); | |
325 } | |
326 | |
327 void visitPseudoElementFunctionSelector(PseudoElementFunctionSelector node) { | |
328 emit("::${node.name}("); | |
329 node.expression.visit(this); | |
330 emit(')'); | |
331 } | |
332 | |
333 void visitNegationSelector(NegationSelector node) { | |
334 emit(':not('); | |
335 node.negationArg.visit(this); | |
336 emit(')'); | |
337 } | |
338 | |
339 void visitSelectorExpression(SelectorExpression node) { | |
340 var expressions = node.expressions; | |
341 var expressionsLength = expressions.length; | |
342 for (var i = 0; i < expressionsLength; i++) { | |
343 // Add space seperator between terms without an operator. | |
344 var expression = expressions[i]; | |
345 expression.visit(this); | |
346 } | |
347 } | |
348 | |
349 void visitUnicodeRangeTerm(UnicodeRangeTerm node) { | |
350 if (node.hasSecond) { | |
351 emit("U+${node.first}-${node.second}"); | |
352 } else { | |
353 emit("U+${node.first}"); | |
354 } | |
355 } | |
356 | |
357 void visitLiteralTerm(LiteralTerm node) { | |
358 emit(node.text); | |
359 } | |
360 | |
361 void visitHexColorTerm(HexColorTerm node) { | |
362 var mappedName; | |
363 if (_isTesting && (node.value is! BAD_HEX_VALUE)) { | |
364 mappedName = TokenKind.hexToColorName(node.value); | |
365 } | |
366 if (mappedName == null) { | |
367 mappedName = '#${node.text}'; | |
368 } | |
369 | |
370 emit(mappedName); | |
371 } | |
372 | |
373 void visitNumberTerm(NumberTerm node) { | |
374 visitLiteralTerm(node); | |
375 } | |
376 | |
377 void visitUnitTerm(UnitTerm node) { | |
378 emit(node.toString()); | |
379 } | |
380 | |
381 void visitLengthTerm(LengthTerm node) { | |
382 emit(node.toString()); | |
383 } | |
384 | |
385 void visitPercentageTerm(PercentageTerm node) { | |
386 emit('${node.text}%'); | |
387 } | |
388 | |
389 void visitEmTerm(EmTerm node) { | |
390 emit('${node.text}em'); | |
391 } | |
392 | |
393 void visitExTerm(ExTerm node) { | |
394 emit('${node.text}ex'); | |
395 } | |
396 | |
397 void visitAngleTerm(AngleTerm node) { | |
398 emit(node.toString()); | |
399 } | |
400 | |
401 void visitTimeTerm(TimeTerm node) { | |
402 emit(node.toString()); | |
403 } | |
404 | |
405 void visitFreqTerm(FreqTerm node) { | |
406 emit(node.toString()); | |
407 } | |
408 | |
409 void visitFractionTerm(FractionTerm node) { | |
410 emit('${node.text}fr'); | |
411 } | |
412 | |
413 void visitUriTerm(UriTerm node) { | |
414 emit('url("${node.text}")'); | |
415 } | |
416 | |
417 void visitResolutionTerm(ResolutionTerm node) { | |
418 emit(node.toString()); | |
419 } | |
420 | |
421 void visitViewportTerm(ViewportTerm node) { | |
422 emit(node.toString()); | |
423 } | |
424 | |
425 void visitFunctionTerm(FunctionTerm node) { | |
426 // TODO(terry): Optimize rgb to a hexcolor. | |
427 emit('${node.text}('); | |
428 node._params.visit(this); | |
429 emit(')'); | |
430 } | |
431 | |
432 void visitGroupTerm(GroupTerm node) { | |
433 emit('('); | |
434 var terms = node._terms; | |
435 var termsLength = terms.length; | |
436 for (var i = 0; i < termsLength; i++) { | |
437 if (i > 0) emit('$_sp'); | |
438 terms[i].visit(this); | |
439 } | |
440 emit(')'); | |
441 } | |
442 | |
443 void visitItemTerm(ItemTerm node) { | |
444 emit('[${node.text}]'); | |
445 } | |
446 | |
447 void visitIE8Term(IE8Term node) { | |
448 visitLiteralTerm(node); | |
449 } | |
450 | |
451 void visitOperatorSlash(OperatorSlash node) { | |
452 emit('/'); | |
453 } | |
454 | |
455 void visitOperatorComma(OperatorComma node) { | |
456 emit(','); | |
457 } | |
458 | |
459 void visitOperatorPlus(OperatorPlus node) { | |
460 emit('+'); | |
461 } | |
462 | |
463 void visitOperatorMinus(OperatorMinus node) { | |
464 emit('-'); | |
465 } | |
466 | |
467 void visitVarUsage(VarUsage node) { | |
468 emit('var(${node.name}'); | |
469 if (!node.defaultValues.isEmpty) { | |
470 emit(','); | |
471 for (var defaultValue in node.defaultValues) { | |
472 emit(' '); | |
473 defaultValue.visit(this); | |
474 } | |
475 } | |
476 emit(')'); | |
477 } | |
478 | |
479 void visitExpressions(Expressions node) { | |
480 var expressions = node.expressions; | |
481 var expressionsLength = expressions.length; | |
482 for (var i = 0; i < expressionsLength; i++) { | |
483 // Add space seperator between terms without an operator. | |
484 // TODO(terry): Should have a BinaryExpression to solve this problem. | |
485 var expression = expressions[i]; | |
486 if (i > 0 && | |
487 !(expression is OperatorComma || expression is OperatorSlash)) { | |
488 emit(' '); | |
489 } | |
490 expression.visit(this); | |
491 } | |
492 } | |
493 | |
494 void visitBinaryExpression(BinaryExpression node) { | |
495 // TODO(terry): TBD | |
496 throw UnimplementedError; | |
497 } | |
498 | |
499 void visitUnaryExpression(UnaryExpression node) { | |
500 // TODO(terry): TBD | |
501 throw UnimplementedError; | |
502 } | |
503 | |
504 void visitIdentifier(Identifier node) { | |
505 emit(node.name); | |
506 } | |
507 | |
508 void visitWildcard(Wildcard node) { | |
509 emit('*'); | |
510 } | |
511 | |
512 void visitDartStyleExpression(DartStyleExpression node) { | |
513 // TODO(terry): TBD | |
514 throw UnimplementedError; | |
515 } | |
516 } | |
OLD | NEW |