2016-09-10 23:29:01 +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.
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// The abstract base class for all parsers.
|
|
|
|
///
|
|
|
|
/// This provides utility methods and common token parsing. Unless specified
|
|
|
|
/// otherwise, a parse method throws a [SassFormatException] if it fails to
|
|
|
|
/// parse.
|
2016-09-10 23:29:01 +02:00
|
|
|
abstract class Parser {
|
2016-10-10 08:51:20 +02:00
|
|
|
/// The scanner that scans through the text being parsed.
|
2016-09-10 23:29:01 +02:00
|
|
|
final SpanScanner scanner;
|
|
|
|
|
|
|
|
Parser(String contents, {url})
|
|
|
|
: scanner = new SpanScanner(contents, sourceUrl: url);
|
|
|
|
|
|
|
|
// ## Tokens
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes whitespace, including any comments.
|
|
|
|
void whitespace() {
|
|
|
|
do {
|
|
|
|
whitespaceWithoutComments();
|
|
|
|
} while (scanComment());
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Like [whitespace], but returns whether any was consumed.
|
2016-09-10 23:29:01 +02:00
|
|
|
bool scanWhitespace() {
|
|
|
|
var start = scanner.position;
|
2016-09-10 23:48:24 +02:00
|
|
|
whitespace();
|
2016-09-10 23:29:01 +02:00
|
|
|
return scanner.position != start;
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes whitespace, but not comments.
|
2016-09-10 23:48:24 +02:00
|
|
|
void whitespaceWithoutComments() {
|
2016-09-10 23:29:01 +02:00
|
|
|
while (!scanner.isDone && isWhitespace(scanner.peekChar())) {
|
|
|
|
scanner.readChar();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes and ignores a comment if possible.
|
|
|
|
///
|
|
|
|
/// Returns whether the comment was consumed.
|
|
|
|
bool scanComment() {
|
2016-09-10 23:29:01 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes and ignores a silent (Sass-style) comment.
|
2016-09-10 23:29:01 +02:00
|
|
|
void silentComment() {
|
|
|
|
scanner.expect("//");
|
2016-09-11 02:15:49 +02:00
|
|
|
while (!scanner.isDone && !isNewline(scanner.peekChar())) {
|
|
|
|
scanner.readChar();
|
|
|
|
}
|
2016-09-10 23:29:01 +02:00
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes and ignores a loud (CSS-style) comment.
|
2016-09-10 23:29:01 +02:00
|
|
|
void loudComment() {
|
|
|
|
scanner.expect("/*");
|
|
|
|
do {
|
|
|
|
while (scanner.readChar() != $asterisk) {}
|
|
|
|
} while (scanner.readChar() != $slash);
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes a plain CSS identifier.
|
2016-10-20 07:54:47 +02:00
|
|
|
///
|
|
|
|
/// If [unit] is `true`, this doesn't parse a `-` followed by a digit. This
|
|
|
|
/// ensures that `1px-2px` parses as subtraction rather than the unit
|
|
|
|
/// `px-2px`.
|
|
|
|
String identifier({bool unit: false}) {
|
|
|
|
// NOTE: this logic is largely duplicated in ScssParser.identifier.
|
|
|
|
// Most changes here should be mirrored there.
|
|
|
|
|
2016-09-10 23:29:01 +02:00
|
|
|
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) {
|
2016-12-19 07:42:11 +01:00
|
|
|
text.write(escape());
|
2016-09-10 23:29:01 +02:00
|
|
|
} else {
|
|
|
|
scanner.error("Expected identifier.");
|
|
|
|
}
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
var next = scanner.peekChar();
|
|
|
|
if (next == null) {
|
|
|
|
break;
|
2016-10-20 07:54:47 +02:00
|
|
|
} else if (unit && next == $dash) {
|
|
|
|
// Disallow `-` followed by a digit in units.
|
|
|
|
var second = scanner.peekChar(1);
|
|
|
|
if (second != null && isDigit(second)) break;
|
|
|
|
text.writeCharCode(scanner.readChar());
|
2016-09-10 23:29:01 +02:00
|
|
|
} else if (isName(next)) {
|
|
|
|
text.writeCharCode(scanner.readChar());
|
|
|
|
} else if (next == $backslash) {
|
2016-12-19 07:42:11 +01:00
|
|
|
text.write(escape());
|
2016-09-10 23:29:01 +02:00
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return text.toString();
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes a plain CSS string.
|
|
|
|
///
|
|
|
|
/// This returns the parsed contents of the string—that is, it doesn't include
|
|
|
|
/// quotes and its escapes are resolved.
|
2016-09-10 23:29:01 +02:00
|
|
|
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 {
|
2016-12-19 07:42:11 +01:00
|
|
|
buffer.writeCharCode(escapeCharacter());
|
2016-09-10 23:29:01 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
buffer.writeCharCode(scanner.readChar());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return buffer.toString();
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes tokens until it reaches a top-level `":"`, `"!"`, `")"`, `"]"`,
|
|
|
|
/// or `"}"` and returns their contents as a string.
|
2016-09-10 23:29:01 +02:00
|
|
|
String declarationValue() {
|
|
|
|
// NOTE: this logic is largely duplicated in
|
2016-09-25 02:27:44 +02:00
|
|
|
// StylesheetParser._interpolatedDeclarationValue. Most changes here should
|
|
|
|
// be mirrored there.
|
2016-09-10 23:29:01 +02:00
|
|
|
|
|
|
|
var buffer = new StringBuffer();
|
|
|
|
var brackets = <int>[];
|
|
|
|
var wroteNewline = false;
|
|
|
|
loop:
|
|
|
|
while (true) {
|
|
|
|
var next = scanner.peekChar();
|
|
|
|
switch (next) {
|
|
|
|
case $backslash:
|
2016-12-19 07:42:11 +01:00
|
|
|
buffer.write(escape());
|
2016-09-10 23:29:01 +02:00
|
|
|
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;
|
|
|
|
|
2016-10-03 09:07:09 +02:00
|
|
|
case $u:
|
|
|
|
case $U:
|
|
|
|
var url = tryUrl();
|
|
|
|
if (url != null) {
|
|
|
|
buffer.write(url);
|
|
|
|
} else {
|
|
|
|
buffer.writeCharCode(scanner.readChar());
|
|
|
|
}
|
|
|
|
wroteNewline = false;
|
|
|
|
break;
|
|
|
|
|
2016-09-10 23:29:01 +02:00
|
|
|
default:
|
|
|
|
if (next == null) break loop;
|
|
|
|
|
2016-10-03 09:07:09 +02:00
|
|
|
if (lookingAtIdentifier()) {
|
|
|
|
buffer.write(identifier());
|
|
|
|
} else {
|
|
|
|
buffer.writeCharCode(scanner.readChar());
|
|
|
|
}
|
2016-09-10 23:29:01 +02:00
|
|
|
wroteNewline = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (brackets.isNotEmpty) scanner.expectChar(brackets.last);
|
|
|
|
return buffer.toString();
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes a `url()` token if possible, and returns `null` otherwise.
|
2016-10-03 09:07:09 +02:00
|
|
|
String tryUrl() {
|
|
|
|
// NOTE: this logic is largely duplicated in ScssParser._tryUrlContents.
|
|
|
|
// Most changes here should be mirrored there.
|
|
|
|
|
|
|
|
var start = scanner.state;
|
|
|
|
if (!scanIdentifier("url", ignoreCase: true)) return null;
|
|
|
|
|
|
|
|
if (!scanner.scanChar($lparen)) {
|
|
|
|
scanner.state = start;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
whitespace();
|
|
|
|
|
|
|
|
// Match Ruby Sass's behavior: parse a raw URL() if possible, and if not
|
|
|
|
// backtrack and re-parse as a function expression.
|
|
|
|
var buffer = new StringBuffer()..write("url(");
|
|
|
|
while (true) {
|
|
|
|
var next = scanner.peekChar();
|
|
|
|
if (next == null) {
|
|
|
|
break;
|
|
|
|
} else if (next == $percent ||
|
|
|
|
next == $ampersand ||
|
|
|
|
next == $hash ||
|
|
|
|
(next >= $asterisk && next <= $tilde) ||
|
|
|
|
next >= 0x0080) {
|
|
|
|
buffer.writeCharCode(scanner.readChar());
|
|
|
|
} else if (next == $backslash) {
|
2016-12-19 07:42:11 +01:00
|
|
|
buffer.write(escape());
|
2016-10-03 09:07:09 +02:00
|
|
|
} else if (isWhitespace(next)) {
|
|
|
|
whitespace();
|
|
|
|
if (scanner.peekChar() != $rparen) break;
|
|
|
|
} else if (next == $rparen) {
|
|
|
|
buffer.writeCharCode(scanner.readChar());
|
|
|
|
return buffer.toString();
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
scanner.state = start;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes a Sass variable name, and returns its name without the dollar
|
|
|
|
/// sign.
|
2016-09-10 23:29:01 +02:00
|
|
|
String variableName() {
|
|
|
|
scanner.expectChar($dollar);
|
|
|
|
return identifier();
|
|
|
|
}
|
|
|
|
|
|
|
|
// ## Characters
|
|
|
|
|
2016-12-19 07:42:11 +01:00
|
|
|
/// Consumes an escape sequence and returns the text that defines it.
|
|
|
|
String escape() {
|
|
|
|
// See https://drafts.csswg.org/css-syntax-3/#consume-escaped-code-point.
|
|
|
|
|
|
|
|
var buffer = new StringBuffer();
|
|
|
|
scanner.expectChar($backslash);
|
|
|
|
buffer.writeCharCode($backslash);
|
|
|
|
var first = scanner.peekChar();
|
|
|
|
if (first == null) {
|
|
|
|
// Do nothing.
|
|
|
|
} else if (isNewline(first)) {
|
|
|
|
scanner.error("Expected escape sequence.");
|
|
|
|
return null;
|
|
|
|
} else if (isHex(first)) {
|
|
|
|
for (var i = 0; i < 6; i++) {
|
|
|
|
var next = scanner.peekChar();
|
|
|
|
if (next == null || !isHex(next)) break;
|
|
|
|
buffer.writeCharCode(scanner.readChar());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isWhitespace(scanner.peekChar())) {
|
|
|
|
buffer.writeCharCode(scanner.readChar());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
buffer.writeCharCode(scanner.readChar());
|
|
|
|
}
|
|
|
|
|
|
|
|
return buffer.toString();
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes an escape sequence and returns the character it represents.
|
2016-12-19 07:42:11 +01:00
|
|
|
int escapeCharacter() {
|
2016-09-10 23:29:01 +02:00
|
|
|
// 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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
// Consumes the next character if it matches [condition].
|
|
|
|
//
|
|
|
|
// Returns whether or not the character was consumed.
|
2016-09-11 02:15:49 +02:00
|
|
|
bool scanCharIf(bool condition(int character)) {
|
|
|
|
var next = scanner.peekChar();
|
|
|
|
if (!condition(next)) return false;
|
|
|
|
scanner.readChar();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes the next character if it's equal to [letter], ignoring ASCII
|
|
|
|
/// case.
|
2016-09-10 23:29:01 +02:00
|
|
|
bool scanCharIgnoreCase(int letter) {
|
|
|
|
if (!equalsLetterIgnoreCase(letter, scanner.peekChar())) return false;
|
|
|
|
scanner.readChar();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes the next character and asserts that it's equal to [letter],
|
|
|
|
/// ignoring ASCII case.
|
2016-09-10 23:29:01 +02:00
|
|
|
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
|
|
|
|
|
2016-10-21 02:50:02 +02:00
|
|
|
/// Returns whether the scanner is immediately before a number.
|
|
|
|
///
|
|
|
|
/// This follows [the CSS algorithm][].
|
|
|
|
///
|
|
|
|
/// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#starts-with-a-number
|
|
|
|
bool lookingAtNumber() {
|
|
|
|
var first = scanner.peekChar();
|
|
|
|
if (first == null) return false;
|
|
|
|
if (isDigit(first)) return true;
|
|
|
|
|
|
|
|
if (first == $dot) {
|
|
|
|
var second = scanner.peekChar(1);
|
|
|
|
return second != null && isDigit(second);
|
|
|
|
} else if (first == $plus || first == $minus) {
|
|
|
|
var second = scanner.peekChar(1);
|
|
|
|
if (second == null) return false;
|
|
|
|
if (isDigit(second)) return true;
|
2016-11-13 22:26:52 +01:00
|
|
|
if (second != $dot) return false;
|
2016-10-21 02:50:02 +02:00
|
|
|
|
|
|
|
var third = scanner.peekChar(2);
|
|
|
|
return third != null && isDigit(third);
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Returns whether the scanner is immediately before a plain CSS identifier.
|
|
|
|
///
|
2016-09-10 23:29:01 +02:00
|
|
|
/// 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();
|
2016-10-29 01:55:56 +02:00
|
|
|
if (first == null) return false;
|
2016-09-10 23:29:01 +02:00
|
|
|
if (isNameStart(first) || first == $backslash) return true;
|
|
|
|
|
|
|
|
if (first != $dash) return false;
|
|
|
|
var second = scanner.peekChar(1);
|
2016-10-29 01:55:56 +02:00
|
|
|
return second != null &&
|
|
|
|
(isNameStart(second) || second == $dash || second == $backslash);
|
2016-09-10 23:29:01 +02:00
|
|
|
}
|
|
|
|
|
2016-11-13 22:26:52 +01:00
|
|
|
/// Returns whether the scanner is immediately before a sequence of characters
|
|
|
|
/// that could be part of a plain CSS identifier body.
|
|
|
|
bool lookingAtIdentifierBody() {
|
|
|
|
var next = scanner.peekChar();
|
|
|
|
return next != null && (isName(next) || next == $backslash);
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes an identifier if its name exactly matches [text].
|
|
|
|
///
|
|
|
|
/// If [ignoreCase] is `true`, does a case-insensitive match.
|
2016-09-10 23:29:01 +02:00
|
|
|
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);
|
2016-12-19 07:42:11 +01:00
|
|
|
if (scanCharIgnoreCase(next)) continue;
|
2016-09-10 23:29:01 +02:00
|
|
|
scanner.state = start;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-11-13 22:26:52 +01:00
|
|
|
if (!lookingAtIdentifierBody()) return true;
|
2016-09-10 23:29:01 +02:00
|
|
|
scanner.state = start;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Consumes an identifier and asserts that its name exactly matches [text].
|
|
|
|
///
|
|
|
|
/// If [ignoreCase] is `true`, does a case-insensitive match.
|
2016-09-10 23:29:01 +02:00
|
|
|
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);
|
2016-12-19 07:42:11 +01:00
|
|
|
if (scanCharIgnoreCase(next)) continue;
|
2016-09-10 23:29:01 +02:00
|
|
|
scanner.error("Expected $name.", position: start);
|
|
|
|
}
|
|
|
|
|
2016-11-13 22:26:52 +01:00
|
|
|
if (!lookingAtIdentifierBody()) return;
|
2016-09-10 23:29:01 +02:00
|
|
|
scanner.error("Expected $name", position: start);
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Runs [consumer] and returns the source text that it consumes.
|
2016-09-10 23:29:01 +02:00
|
|
|
String rawText(void consumer()) {
|
|
|
|
var start = scanner.position;
|
|
|
|
consumer();
|
|
|
|
return scanner.substring(start);
|
|
|
|
}
|
|
|
|
|
2016-11-14 20:11:21 +01:00
|
|
|
/// Prints a source span highlight of the current location being scanned.
|
|
|
|
///
|
|
|
|
/// If [message] is passed, prints that as well. This is intended for use when
|
|
|
|
/// debugging parser failures.
|
|
|
|
void debug([String message]) {
|
|
|
|
if (message == null) {
|
|
|
|
print(scanner.emptySpan.highlight(color: true));
|
|
|
|
} else {
|
|
|
|
print(scanner.emptySpan.message(message, color: true));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-10 08:51:20 +02:00
|
|
|
/// Runs [callback] and wraps any [SourceSpanFormatException] it throws in a
|
|
|
|
/// [SassFormatException].
|
2016-10-03 07:30:29 +02:00
|
|
|
/*=T*/ wrapSpanFormatException/*<T>*/(/*=T*/ callback()) {
|
2016-09-10 23:29:01 +02:00
|
|
|
try {
|
|
|
|
return callback();
|
2016-10-03 07:30:29 +02:00
|
|
|
} on SourceSpanFormatException catch (error) {
|
2016-09-25 04:47:45 +02:00
|
|
|
throw new SassFormatException(error.message, error.span as FileSpan);
|
2016-09-10 23:29:01 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|