Split the parser up.

There are now separate parsers for selectors and at-root queries, since
those are parsed independently of the main stylesheet. The Parser class
contains utilities that are useful across different parsers.
This commit is contained in:
Natalie Weizenbaum 2016-09-10 14:29:01 -07:00 committed by Natalie Weizenbaum
parent 3329ba80b8
commit 3a2b7ca9df
8 changed files with 2346 additions and 2141 deletions

View File

@ -9,7 +9,7 @@ import 'package:stack_trace/stack_trace.dart';
import 'package:path/path.dart' as p;
import 'package:sass/src/exception.dart';
import 'package:sass/src/parser.dart';
import 'package:sass/src/parse.dart';
import 'package:sass/src/visitor/perform.dart';
import 'package:sass/src/visitor/serialize.dart';
@ -35,9 +35,9 @@ void main(List<String> args) {
try {
var file = options.rest.first;
var parser =
new Parser(new File(file).readAsStringSync(), url: p.toUri(file));
var cssTree = new PerformVisitor().visitStylesheet(parser.parse());
var sassTree =
parseScss(new File(file).readAsStringSync(), url: p.toUri(file));
var cssTree = new PerformVisitor().visitStylesheet(sassTree);
var css = toCss(cssTree);
if (css.isNotEmpty) print(css);
} on SassException catch (error, stackTrace) {

21
lib/src/parse.dart Normal file
View File

@ -0,0 +1,21 @@
// 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.
import 'ast/sass.dart';
import 'ast/selector.dart';
import 'parse/at_root_query.dart';
import 'parse/scss.dart';
import 'parse/selector.dart';
Stylesheet parseScss(String contents, {url}) =>
new ScssParser(contents, url: url).parse();
SelectorList parseSelector(String contents, {url}) =>
new SelectorParser(contents, url: url).parse();
SimpleSelector parseSimpleSelector(String contents, {url}) =>
new SelectorParser(contents, url: url).parseSimpleSelector();
AtRootQuery parseAtRootQuery(String contents, {url}) =>
new AtRootQueryParser(contents, url: url).parse();

View File

@ -0,0 +1,32 @@
// 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.
import 'package:charcode/charcode.dart';
import '../ast/sass.dart';
import 'parser.dart';
class AtRootQueryParser extends Parser {
AtRootQueryParser(String contents, {url}) : super(contents, url: url);
AtRootQuery parse() {
return wrapFormatException(() {
scanner.expectChar($lparen);
ignoreComments();
expectIdentifier("with", ignoreCase: true);
var include = !scanIdentifier("out", ignoreCase: true);
ignoreComments();
scanner.expectChar($colon);
ignoreComments();
var atRules = new Set<String>();
do {
atRules.add(identifier().toLowerCase());
ignoreComments();
} while (lookingAtIdentifier());
return new AtRootQuery(include, atRules);
});
}
}

346
lib/src/parse/parser.dart Normal file
View File

@ -0,0 +1,346 @@
// 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.
import 'package:charcode/charcode.dart';
import 'package:source_span/source_span.dart';
import 'package:string_scanner/string_scanner.dart';
import '../exception.dart';
import '../util/character.dart';
abstract class Parser {
final SpanScanner scanner;
Parser(String contents, {url})
: scanner = new SpanScanner(contents, sourceUrl: url);
// ## Tokens
String commentText() => rawText(ignoreComments);
bool scanWhitespace() {
var start = scanner.position;
ignoreComments();
return scanner.position != start;
}
void ignoreComments() {
do {
whitespace();
} while (comment());
}
void whitespace() {
while (!scanner.isDone && isWhitespace(scanner.peekChar())) {
scanner.readChar();
}
}
bool comment() {
if (scanner.peekChar() != $slash) return false;
var next = scanner.peekChar(1);
if (next == $slash) {
silentComment();
return true;
} else if (next == $asterisk) {
loudComment();
return true;
} else {
return false;
}
}
void silentComment() {
scanner.expect("//");
while (!scanner.isDone && !isNewline(scanner.readChar())) {}
}
void loudComment() {
scanner.expect("/*");
do {
while (scanner.readChar() != $asterisk) {}
} while (scanner.readChar() != $slash);
}
String identifier() {
var text = new StringBuffer();
while (scanner.scanChar($dash)) {
text.writeCharCode($dash);
}
var first = scanner.peekChar();
if (first == null) {
scanner.error("Expected identifier.");
} else if (isNameStart(first)) {
text.writeCharCode(scanner.readChar());
} else if (first == $backslash) {
text.writeCharCode(escape());
} else {
scanner.error("Expected identifier.");
}
while (true) {
var next = scanner.peekChar();
if (next == null) {
break;
} else if (isName(next)) {
text.writeCharCode(scanner.readChar());
} else if (next == $backslash) {
text.writeCharCode(escape());
} else {
break;
}
}
return text.toString();
}
String string() {
// NOTE: this logic is largely duplicated in ScssParser._interpolatedString.
// Most changes here should be mirrored there.
var quote = scanner.readChar();
if (quote != $single_quote && quote != $double_quote) {
scanner.error("Expected string.",
position: quote == null ? scanner.position : scanner.position - 1);
}
var buffer = new StringBuffer();
while (true) {
var next = scanner.peekChar();
if (next == quote) {
scanner.readChar();
break;
} else if (next == null || isNewline(next)) {
scanner.error("Expected ${new String.fromCharCode(quote)}.");
} else if (next == $backslash) {
if (isNewline(scanner.peekChar(1))) {
scanner.readChar();
scanner.readChar();
} else {
buffer.writeCharCode(escape());
}
} else {
buffer.writeCharCode(scanner.readChar());
}
}
return buffer.toString();
}
String declarationValue() {
// NOTE: this logic is largely duplicated in
// ScssParser._interpolatedDeclarationValue. Most changes here should be
// mirrored there.
var buffer = new StringBuffer();
var brackets = <int>[];
var wroteNewline = false;
loop:
while (true) {
var next = scanner.peekChar();
switch (next) {
case $backslash:
buffer.writeCharCode(escape());
wroteNewline = false;
break;
case $double_quote:
case $single_quote:
buffer.write(rawText(string));
wroteNewline = false;
break;
case $slash:
if (scanner.peekChar(1) == $asterisk) {
buffer.write(rawText(loudComment));
} else {
buffer.writeCharCode(scanner.readChar());
}
wroteNewline = false;
break;
case $space:
case $tab:
if (wroteNewline || !isWhitespace(scanner.peekChar(1))) {
buffer.writeCharCode($space);
}
scanner.readChar();
break;
case $lf:
case $cr:
case $ff:
if (!isNewline(scanner.peekChar(-1))) buffer.writeln();
scanner.readChar();
wroteNewline = true;
break;
case $lparen:
case $lbrace:
case $lbracket:
buffer.writeCharCode(next);
brackets.add(opposite(scanner.readChar()));
wroteNewline = false;
break;
case $rparen:
case $rbrace:
case $rbracket:
if (brackets.isEmpty) break loop;
buffer.writeCharCode(next);
scanner.expectChar(brackets.removeLast());
wroteNewline = false;
break;
case $exclamation:
case $semicolon:
break loop;
default:
if (next == null) break loop;
// TODO: support url()
buffer.writeCharCode(scanner.readChar());
wroteNewline = false;
break;
}
}
if (brackets.isNotEmpty) scanner.expectChar(brackets.last);
return buffer.toString();
}
String variableName() {
scanner.expectChar($dollar);
return identifier();
}
// ## Characters
int escape() {
// See https://drafts.csswg.org/css-syntax-3/#consume-escaped-code-point.
scanner.expectChar($backslash);
var first = scanner.peekChar();
if (first == null) {
return 0xFFFD;
} else if (isNewline(first)) {
scanner.error("Expected escape sequence.");
return 0;
} else if (isHex(first)) {
var value = 0;
for (var i = 0; i < 6; i++) {
var next = scanner.peekChar();
if (next == null || !isHex(next)) break;
value = (value << 4) + asHex(scanner.readChar());
}
if (isWhitespace(scanner.peekChar())) scanner.readChar();
if (value == 0 ||
(value >= 0xD800 && value <= 0xDFFF) ||
value >= 0x10FFFF) {
return 0xFFFD;
} else {
return value;
}
} else {
return scanner.readChar();
}
}
int readCharOrEscape() {
var next = scanner.readChar();
return next == $backslash ? escape() : next;
}
bool scanCharOrEscape(int expected, {bool ignoreCase: false}) {
// TODO(nweiz): Test if it's faster to split this into separate methods for
// case-sensitivity rather than checking the boolean each time.
var actual = readCharOrEscape();
return ignoreCase
? characterEqualsIgnoreCase(actual, expected)
: actual == expected;
}
bool scanCharIgnoreCase(int letter) {
if (!equalsLetterIgnoreCase(letter, scanner.peekChar())) return false;
scanner.readChar();
return true;
}
void expectCharIgnoreCase(int letter) {
var actual = scanner.readChar();
if (equalsLetterIgnoreCase(letter, actual)) return;
scanner.error('Expected "${new String.fromCharCode(letter)}".',
position: actual == null ? scanner.position : scanner.position - 1);
}
// ## Utilities
/// This is based on [the CSS algorithm][], but it assumes all backslashes
/// start escapes.
///
/// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier
bool lookingAtIdentifier() {
// See also [ScssParser._lookingAtInterpolatedIdentifier].
var first = scanner.peekChar();
if (isNameStart(first) || first == $backslash) return true;
if (first != $dash) return false;
var second = scanner.peekChar(1);
return isNameStart(second) || second == $dash || second == $backslash;
}
bool scanIdentifier(String text, {bool ignoreCase: false}) {
if (!lookingAtIdentifier()) return false;
var start = scanner.state;
for (var i = 0; i < text.length; i++) {
var next = text.codeUnitAt(i);
if (scanCharOrEscape(next, ignoreCase: ignoreCase)) continue;
scanner.state = start;
return false;
}
var next = scanner.peekChar();
if (next == null) return true;
if (!isName(next) && next != $backslash) return true;
scanner.state = start;
return false;
}
void expectIdentifier(String text, {String name, bool ignoreCase: false}) {
name ??= '"$text"';
var start = scanner.position;
for (var i = 0; i < text.length; i++) {
var next = text.codeUnitAt(i);
if (scanCharOrEscape(next, ignoreCase: ignoreCase)) continue;
scanner.error("Expected $name.", position: start);
}
var next = scanner.peekChar();
if (next == null) return;
if (!isName(next) && next != $backslash) return;
scanner.error("Expected $name", position: start);
}
String rawText(void consumer()) {
var start = scanner.position;
consumer();
return scanner.substring(start);
}
/*=T*/ wrapFormatException/*<T>*/(/*=T*/ callback()) {
try {
return callback();
} on StringScannerException catch (error) {
throw new SassException(error.message, error.span as FileSpan);
}
}
}

1590
lib/src/parse/scss.dart Normal file

File diff suppressed because it is too large Load Diff

347
lib/src/parse/selector.dart Normal file
View File

@ -0,0 +1,347 @@
// 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.
import 'package:charcode/charcode.dart';
import '../ast/selector.dart';
import '../util/character.dart';
import '../utils.dart';
import 'parser.dart';
final _selectorPseudoClasses = new Set.from(
["not", "matches", "current", "any", "has", "host", "host-context"]);
final _prefixedSelectorPseudoClasses =
new Set.from(["nth-child", "nth-last-child"]);
class SelectorParser extends Parser {
SelectorParser(String contents, {url}) : super(contents, url: url);
SelectorList parse() {
return wrapFormatException(() {
var selector = _selectorList();
scanner.expectDone();
return selector;
});
}
SimpleSelector parseSimpleSelector() {
return wrapFormatException(() {
var simple = _simpleSelector();
scanner.expectDone();
return simple;
});
}
SelectorList _selectorList() {
var components = <ComplexSelector>[];
var lineBreaks = <int>[];
ignoreComments();
var previousLine = scanner.line;
do {
ignoreComments();
var next = scanner.peekChar();
if (next == $comma) continue;
if (next == $lbrace) break;
if (scanner.line != previousLine) {
lineBreaks.add(components.length);
previousLine = scanner.line;
}
components.add(_complexSelector());
} while (scanner.scanChar($comma));
return new SelectorList(components, lineBreaks: lineBreaks);
}
ComplexSelector _complexSelector() {
var components = <ComplexSelectorComponent>[];
var lineBreaks = <int>[];
var previousLine = scanner.line;
loop:
while (true) {
ignoreComments();
ComplexSelectorComponent component;
var next = scanner.peekChar();
switch (next) {
case $plus:
scanner.readChar();
component = Combinator.nextSibling;
break;
case $gt:
scanner.readChar();
component = Combinator.child;
break;
case $tilde:
scanner.readChar();
component = Combinator.followingSibling;
break;
case $lbracket:
case $dot:
case $hash:
case $percent:
case $colon:
case $ampersand:
case $asterisk:
case $pipe:
component = _compoundSelector();
break;
default:
if (next == null || !lookingAtIdentifier()) break loop;
component = _compoundSelector();
break;
}
if (scanner.line != previousLine) {
lineBreaks.add(components.length);
previousLine = scanner.line;
}
components.add(component);
}
return new ComplexSelector(components, lineBreaks: lineBreaks);
}
CompoundSelector _compoundSelector() {
var components = <SimpleSelector>[_simpleSelector()];
while (isSimpleSelectorStart(scanner.peekChar())) {
components.add(_simpleSelector(allowParent: false));
}
// TODO: support "*E".
return new CompoundSelector(components);
}
SimpleSelector _simpleSelector({bool allowParent: true}) {
switch (scanner.peekChar()) {
case $lbracket:
return _attributeSelector();
case $dot:
return _classSelector();
case $hash:
return _idSelector();
case $percent:
return _placeholderSelector();
case $colon:
return _pseudoSelector();
case $ampersand:
if (!allowParent) return _typeOrUniversalSelector();
return _parentSelector();
default:
return _typeOrUniversalSelector();
}
}
AttributeSelector _attributeSelector() {
scanner.expectChar($lbracket);
ignoreComments();
var name = _attributeName();
ignoreComments();
if (scanner.scanChar($rbracket)) {
scanner.readChar();
return new AttributeSelector(name);
}
var operator = _attributeOperator();
ignoreComments();
var next = scanner.peekChar();
var value = next == $single_quote || next == $double_quote
? string()
: identifier();
ignoreComments();
scanner.expectChar($rbracket);
return new AttributeSelector.withOperator(name, operator, value);
}
NamespacedIdentifier _attributeName() {
if (scanner.scanChar($asterisk)) {
scanner.expectChar($pipe);
return new NamespacedIdentifier(identifier(), namespace: "*");
}
var nameOrNamespace = identifier();
if (scanner.peekChar() != $pipe || scanner.peekChar(1) == $equal) {
return new NamespacedIdentifier(nameOrNamespace);
}
scanner.readChar();
return new NamespacedIdentifier(identifier(), namespace: nameOrNamespace);
}
AttributeOperator _attributeOperator() {
var start = scanner.state;
switch (scanner.readChar()) {
case $equal:
return AttributeOperator.equal;
case $tilde:
scanner.expectChar($equal);
return AttributeOperator.include;
case $pipe:
scanner.expectChar($equal);
return AttributeOperator.dash;
case $caret:
scanner.expectChar($equal);
return AttributeOperator.prefix;
case $dollar:
scanner.expectChar($equal);
return AttributeOperator.suffix;
case $asterisk:
scanner.expectChar($equal);
return AttributeOperator.substring;
default:
scanner.error('Expected "]".', position: start.position);
throw "Unreachable";
}
}
ClassSelector _classSelector() {
scanner.expectChar($dot);
var name = identifier();
return new ClassSelector(name);
}
IDSelector _idSelector() {
scanner.expectChar($hash);
var name = identifier();
return new IDSelector(name);
}
PlaceholderSelector _placeholderSelector() {
scanner.expectChar($percent);
var name = identifier();
return new PlaceholderSelector(name);
}
ParentSelector _parentSelector() {
scanner.expectChar($ampersand);
var next = scanner.peekChar();
var suffix = isName(next) || next == $backslash ? identifier() : null;
return new ParentSelector(suffix: suffix);
}
PseudoSelector _pseudoSelector() {
scanner.expectChar($colon);
var type = scanner.scanChar($colon) ? PseudoType.element : PseudoType.klass;
var name = identifier();
if (!scanner.scanChar($lparen)) {
return new PseudoSelector(name, type);
}
ignoreComments();
var unvendored = unvendor(name);
String argument;
SelectorList selector;
if (type == PseudoType.element) {
argument = declarationValue();
} else if (_selectorPseudoClasses.contains(unvendored)) {
selector = _selectorList();
} else if (_prefixedSelectorPseudoClasses.contains(unvendored)) {
argument = rawText(_aNPlusB);
if (scanWhitespace()) {
expectIdentifier("of", ignoreCase: true);
argument += " of";
ignoreComments();
selector = _selectorList();
}
} else {
argument = declarationValue();
}
scanner.expectChar($rparen);
return new PseudoSelector(name, type,
argument: argument, selector: selector);
}
void _aNPlusB() {
switch (scanner.peekChar()) {
case $e:
case $E:
expectIdentifier("even", ignoreCase: true);
return;
case $o:
case $O:
expectIdentifier("odd", ignoreCase: true);
return;
case $plus:
case $minus:
scanner.readChar();
break;
}
var first = scanner.peekChar();
if (first != null && isDigit(first)) {
while (isDigit(scanner.peekChar())) {
scanner.readChar();
}
ignoreComments();
if (!scanCharIgnoreCase($n)) return;
} else {
expectCharIgnoreCase($n);
}
ignoreComments();
var next = scanner.peekChar();
if (next != $plus && next != $minus) return;
scanner.readChar();
ignoreComments();
var last = scanner.peekChar();
if (last == null || !isDigit(last)) scanner.error("Expected a number.");
while (isDigit(scanner.peekChar())) {
scanner.readChar();
}
}
SimpleSelector _typeOrUniversalSelector() {
var first = scanner.peekChar();
if (first == $asterisk) {
scanner.readChar();
if (!scanner.scanChar($pipe)) return new UniversalSelector();
if (scanner.scanChar($asterisk)) {
return new UniversalSelector(namespace: "*");
} else {
return new TypeSelector(
new NamespacedIdentifier(identifier(), namespace: "*"));
}
} else if (first == $pipe) {
scanner.readChar();
if (scanner.scanChar($asterisk)) {
return new UniversalSelector(namespace: "");
} else {
return new TypeSelector(
new NamespacedIdentifier(identifier(), namespace: ""));
}
}
var nameOrNamespace = identifier();
if (!scanner.scanChar($pipe)) {
return new TypeSelector(new NamespacedIdentifier(nameOrNamespace));
}
return new TypeSelector(
new NamespacedIdentifier(identifier(), namespace: nameOrNamespace));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@ import '../callable.dart';
import '../environment.dart';
import '../exception.dart';
import '../extend/extender.dart';
import '../parser.dart';
import '../parse.dart';
import '../utils.dart';
import '../value.dart';
import 'interface/statement.dart';
@ -82,7 +82,7 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
void visitAtRootRule(AtRootRule node) {
var query = node.query == null
? AtRootQuery.defaultQuery
: new Parser(_performInterpolation(node.query)).parseAtRootQuery();
: parseAtRootQuery(_performInterpolation(node.query));
var parent = _parent;
var included = <CssParentNode>[];
@ -251,7 +251,7 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
// TODO: recontextualize parse errors.
// TODO: disallow parent selectors.
var target = new Parser(targetText.value.trim()).parseSimpleSelector();
var target = parseSimpleSelector(targetText.value.trim());
_extender.addExtension(_selector.value, target, node);
}
@ -358,10 +358,8 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
throw _exception("Can't find file to import.", node.span);
}
return _importedFiles.putIfAbsent(
path,
() => new Parser(new File(path).readAsStringSync(), url: p.toUri(path))
.parse());
return _importedFiles.putIfAbsent(path,
() => parseScss(new File(path).readAsStringSync(), url: p.toUri(path)));
}
String _tryImportPathWithExtensions(String path) =>
@ -472,7 +470,7 @@ class PerformVisitor implements StatementVisitor, ExpressionVisitor<Value> {
}
var selectorText = _interpolationToValue(node.selector, trim: true);
var parsedSelector = new Parser(selectorText.value).parseSelector();
var parsedSelector = parseSelector(selectorText.value);
parsedSelector = _addExceptionSpan(
() => parsedSelector.resolveParentSelectors(_selector?.value),
node.selector.span);