dart-sass/lib/src/visitor/serialize.dart

411 lines
11 KiB
Dart
Raw Normal View History

2016-06-03 22:36:20 +02:00
// Copyright 2016 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
2016-08-15 03:49:51 +02:00
import 'dart:math' as math;
2016-06-03 22:36:20 +02:00
import 'package:charcode/charcode.dart';
2016-08-15 03:49:51 +02:00
import 'package:string_scanner/string_scanner.dart';
2016-06-03 22:36:20 +02:00
2016-08-28 01:12:17 +02:00
import '../ast/css.dart';
import '../ast/selector.dart';
2016-08-15 08:51:29 +02:00
import '../util/character.dart';
import '../value.dart';
import 'interface/css.dart';
import 'interface/selector.dart';
import 'interface/value.dart';
2016-06-03 22:36:20 +02:00
2016-06-04 01:34:26 +02:00
String toCss(CssNode node) {
2016-06-03 22:36:20 +02:00
var visitor = new _SerializeCssVisitor();
node.accept(visitor);
var result = visitor._buffer.toString();
if (result.codeUnits.any((codeUnit) => codeUnit > 0x7F)) {
result = '@charset "UTF-8";\n$result';
}
// TODO(nweiz): Do this in a way that's not O(n), maybe using a custom buffer
// that's not append-only.
return result.trim();
}
2016-08-15 08:24:53 +02:00
String valueToCss(Value value) {
var visitor = new _SerializeCssVisitor();
value.accept(visitor);
return visitor._buffer.toString();
}
String selectorToCss(Selector selector) {
var visitor = new _SerializeCssVisitor();
selector.accept(visitor);
return visitor._buffer.toString();
}
class _SerializeCssVisitor
implements CssVisitor, ValueVisitor, SelectorVisitor {
2016-06-03 22:36:20 +02:00
final _buffer = new StringBuffer();
var _indentation = 0;
void visitStylesheet(CssStylesheet node) {
for (var child in node.children) {
child.accept(this);
_buffer.writeln();
}
}
void visitComment(CssComment node) {
2016-08-15 07:51:43 +02:00
var minimumIndentation = _minimumIndentation(node.text);
if (minimumIndentation == null) {
_buffer.writeln(node.text);
return;
}
if (node.span != null) {
minimumIndentation = math.min(minimumIndentation, node.span.start.column);
}
_writeIndentation();
_writeWithIndent(node.text, minimumIndentation);
2016-06-03 22:36:20 +02:00
}
2016-06-10 01:37:54 +02:00
void visitAtRule(CssAtRule node) {
_writeIndentation();
_buffer.writeCharCode($at);
_buffer.write(node.name);
if (node.value != null) {
_buffer.writeCharCode($space);
_buffer.write(node.value.value);
}
2016-08-27 07:49:09 +02:00
if (node.isChildless) {
2016-06-10 01:37:54 +02:00
_buffer.writeCharCode($semicolon);
} else {
_buffer.writeCharCode($space);
_visitChildren(node.children);
}
}
void visitMediaRule(CssMediaRule node) {
_writeIndentation();
_buffer.write("@media ");
for (var query in node.queries) {
visitMediaQuery(query);
}
_buffer.writeCharCode($space);
_visitChildren(node.children);
}
2016-08-29 02:12:03 +02:00
void visitImport(CssImport node) {
_writeIndentation();
_buffer.write("@import ");
_visitString(node.url.toString());
_buffer.writeCharCode($semicolon);
}
void visitMediaQuery(CssMediaQuery query) {
if (query.modifier != null) {
_buffer.write(query.modifier.value);
_buffer.writeCharCode($space);
}
if (query.type != null) {
_buffer.write(query.type.value);
if (query.features.isNotEmpty) _buffer.write(" and ");
}
_writeBetween(query.features, " and ", _buffer.write);
}
2016-06-03 22:36:20 +02:00
void visitStyleRule(CssStyleRule node) {
_writeIndentation();
_buffer.write(node.selector.value);
_buffer.writeCharCode($space);
2016-06-10 01:37:54 +02:00
_visitChildren(node.children);
2016-06-03 22:36:20 +02:00
// TODO: only add an extra newline if this is a group end
_buffer.writeln();
}
void visitDeclaration(CssDeclaration node) {
_writeIndentation();
_buffer.write(node.name.value);
_buffer.writeCharCode($colon);
2016-08-15 03:49:51 +02:00
if (node.isCustomProperty) {
_writeCustomPropertyValue(node);
} else {
_buffer.writeCharCode($space);
node.value.value.accept(this);
}
2016-06-03 22:36:20 +02:00
_buffer.writeCharCode($semicolon);
}
2016-08-15 03:49:51 +02:00
void _writeCustomPropertyValue(CssDeclaration node) {
var value = (node.value.value as SassIdentifier).text;
var minimumIndentation = _minimumIndentation(value);
if (minimumIndentation == null) {
_buffer.write(value);
return;
}
if (node.value.span != null) {
minimumIndentation =
math.min(minimumIndentation, node.name.span.start.column);
}
2016-08-15 07:51:43 +02:00
_writeWithIndent(value, minimumIndentation);
2016-08-15 03:49:51 +02:00
}
int _minimumIndentation(String text) {
var scanner = new LineScanner(text);
while (!scanner.isDone && scanner.readChar() != $lf) {}
if (scanner.isDone) return null;
var min = null;
while (!scanner.isDone) {
while (!scanner.isDone && scanner.scanChar($space)) {}
if (scanner.isDone || scanner.scanChar($lf)) continue;
min = min == null ? scanner.column : math.min(min, scanner.column);
while (!scanner.isDone && scanner.readChar() != $lf) {}
}
return min;
}
2016-08-15 07:51:43 +02:00
void _writeWithIndent(String text, int minimumIndentation) {
var scanner = new LineScanner(text);
while (!scanner.isDone && scanner.peekChar() != $lf) {
_buffer.writeCharCode(scanner.readChar());
}
while (!scanner.isDone) {
_buffer.writeCharCode(scanner.readChar());
for (var i = 0; i < minimumIndentation; i++) scanner.readChar();
_writeIndentation();
while (!scanner.isDone && scanner.peekChar() != $lf) {
_buffer.writeCharCode(scanner.readChar());
}
}
}
2016-08-19 23:33:47 +02:00
// ## Expressions
2016-08-29 00:04:48 +02:00
void visitBoolean(SassBoolean value) => _buffer.write(value.value.toString());
2016-06-03 22:36:20 +02:00
2016-06-04 03:01:16 +02:00
// TODO(nweiz): Use color names for named colors.
void visitColor(SassColor value) => _buffer.write(value.toString());
2016-06-04 01:31:29 +02:00
void visitIdentifier(SassIdentifier value) =>
2016-06-04 01:34:26 +02:00
_buffer.write(value.text.replaceAll("\n", " "));
2016-06-03 22:36:20 +02:00
void visitList(SassList value) {
2016-08-15 00:49:50 +02:00
if (value.isBracketed) {
_buffer.writeCharCode($lbracket);
} else if (value.contents.isEmpty) {
throw "() isn't a valid CSS value";
}
2016-06-03 22:36:20 +02:00
_writeBetween(
value.contents.where((element) => !element.isBlank),
value.separator == ListSeparator.space ? " " : ", ",
(element) => element.accept(this));
2016-08-15 00:49:50 +02:00
if (value.isBracketed) _buffer.writeCharCode($rbracket);
2016-06-03 22:36:20 +02:00
}
2016-08-15 07:32:00 +02:00
void visitMap(SassMap map) {
throw "$map isn't a valid CSS value.";
}
2016-06-03 22:36:20 +02:00
// TODO(nweiz): Support precision and don't support exponent notation.
2016-06-04 01:31:29 +02:00
void visitNumber(SassNumber value) {
2016-06-03 22:36:20 +02:00
_buffer.write(value.value.toString());
}
2016-06-04 01:27:41 +02:00
void visitString(SassString string) =>
_buffer.write(_visitString(string.text));
2016-06-03 22:36:20 +02:00
String _visitString(String string, {bool forceDoubleQuote: false}) {
var includesSingleQuote = false;
var includesDoubleQuote = false;
var buffer = new StringBuffer();
for (var i = 0; i < string.length; i++) {
var char = string.codeUnitAt(i);
switch (char) {
case $single_quote:
if (forceDoubleQuote) {
buffer.writeCharCode($single_quote);
} else if (includesDoubleQuote) {
return _visitString(string, forceDoubleQuote: true);
} else {
includesSingleQuote = true;
buffer.writeCharCode($single_quote);
}
break;
case $double_quote:
if (forceDoubleQuote) {
buffer.writeCharCode($backslash);
buffer.writeCharCode($double_quote);
} else if (includesSingleQuote) {
return _visitString(string, forceDoubleQuote: true);
} else {
includesDoubleQuote = true;
buffer.writeCharCode($double_quote);
}
break;
2016-06-04 01:27:41 +02:00
case $cr:
case $lf:
case $ff:
2016-06-03 22:36:20 +02:00
buffer.writeCharCode($backslash);
2016-06-04 01:27:41 +02:00
buffer.writeCharCode(hexCharFor(char));
if (string.length == i + 1) break;
var next = string.codeUnitAt(i + 1);
if (isHex(next) || next == $space || next == $tab) {
2016-06-03 22:36:20 +02:00
buffer.writeCharCode($space);
}
break;
case $backslash:
buffer.writeCharCode($backslash);
buffer.writeCharCode($backslash);
break;
default:
buffer.writeCharCode(char);
break;
}
}
var doubleQuote = forceDoubleQuote || !includesDoubleQuote;
return doubleQuote ? '"$buffer"' : "'$buffer'";
}
2016-08-19 23:33:47 +02:00
// ## Selectors
void visitAttributeSelector(AttributeSelector attribute) {
_buffer.writeCharCode($lbracket);
_buffer.write(attribute.name);
if (attribute.op == null) {
_buffer.write(attribute.op);
// TODO: quote the value if it's not an identifier
_buffer.write(attribute.value);
}
_buffer.writeCharCode($rbracket);
}
void visitClassSelector(ClassSelector klass) {
_buffer.writeCharCode($dot);
_buffer.write(klass.name);
}
void visitComplexSelector(ComplexSelector complex) {
_writeBetween(complex.components, " ", (component) {
if (component is CompoundSelector) {
visitCompoundSelector(component);
} else {
_buffer.write(component);
}
});
}
void visitCompoundSelector(CompoundSelector compound) {
for (var simple in compound.components) {
simple.accept(this);
}
}
void visitIDSelector(IDSelector id) {
_buffer.writeCharCode($hash);
_buffer.write(id.name);
}
void visitSelectorList(SelectorList list) {
2016-08-29 00:04:48 +02:00
_writeBetween(
list.components, ", ", (complex) => visitComplexSelector(complex));
}
void visitParentSelector(ParentSelector parent) {
_buffer.writeCharCode($and);
if (parent.suffix != null) _buffer.write(parent.suffix);
}
2016-08-16 09:16:40 +02:00
void visitPlaceholderSelector(PlaceholderSelector placeholder) {
_buffer.writeCharCode($percent);
_buffer.write(placeholder.name);
}
void visitPseudoSelector(PseudoSelector pseudo) {
2016-08-19 23:01:48 +02:00
_buffer.writeCharCode($colon);
if (pseudo.type == PseudoType.element) _buffer.writeCharCode($colon);
_buffer.write(pseudo.name);
if (pseudo.argument == null && pseudo.selector == null) return;
_buffer.writeCharCode($lparen);
if (pseudo.argument != null) {
_buffer.write(pseudo.argument);
if (pseudo.selector != null) _buffer.writeCharCode($space);
}
if (pseudo.selector != null) _buffer.write(pseudo.selector);
_buffer.writeCharCode($rparen);
}
void visitTypeSelector(TypeSelector type) {
_buffer.write(type.name);
}
void visitUniversalSelector(UniversalSelector universal) {
if (universal.namespace != null) {
_buffer.write(universal.namespace);
_buffer.writeCharCode($pipe);
}
_buffer.writeCharCode($asterisk);
}
2016-08-19 23:33:47 +02:00
// ## Utilities
2016-06-10 01:37:54 +02:00
void _visitChildren(Iterable<CssNode> children) {
_buffer.writeCharCode($lbrace);
_buffer.writeln();
_indent(() {
for (var child in children) {
child.accept(this);
_buffer.writeln();
}
});
_writeIndentation();
_buffer.writeCharCode($rbrace);
}
2016-06-03 22:36:20 +02:00
void _writeIndentation() {
for (var i = 0; i < _indentation; i++) {
_buffer.writeCharCode($space);
_buffer.writeCharCode($space);
}
}
2016-08-29 00:04:48 +02:00
void _writeBetween/*<T>*/(
Iterable/*<T>*/ iterable, String text, void callback(/*=T*/ value)) {
var first = true;
for (var value in iterable) {
if (first) {
first = false;
} else {
_buffer.write(text);
}
callback(value);
}
}
2016-06-03 22:36:20 +02:00
void _indent(void callback()) {
_indentation++;
callback();
_indentation--;
}
}