dart-sass/lib/src/parse/sass.dart

309 lines
8.8 KiB
Dart
Raw Normal View History

2016-09-11 02:15:49 +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:string_scanner/string_scanner.dart';
import '../ast/sass.dart';
import '../util/character.dart';
import 'stylesheet.dart';
2016-10-10 08:51:20 +02:00
/// A parser for the indented syntax.
2016-09-11 02:15:49 +02:00
class SassParser extends StylesheetParser {
2016-10-02 09:07:36 +02:00
int get currentIndentation => _currentIndentation;
2016-09-11 02:15:49 +02:00
var _currentIndentation = 0;
2016-10-02 09:07:36 +02:00
2016-10-10 08:51:20 +02:00
/// The indentation level of the next source line after the scanner's
/// position, or `null` if that hasn't been computed yet.
///
/// A source line is any line that's not entirely whitespace.
2016-09-11 02:15:49 +02:00
int _nextIndentation;
2016-10-10 08:51:20 +02:00
/// The beginning of the next source line after the scanner's position, or
/// `null` if that hasn't been computed yet.
///
/// A source line is any line that's not entirely whitespace.
2016-09-11 02:15:49 +02:00
LineScannerState _nextIndentationEnd;
2016-10-10 08:51:20 +02:00
/// Whether the document is indented using spaces or tabs.
///
/// If this is `true`, the document is indented using spaces. If it's `false`,
/// the document is indented using tabs. If it's `null`, we haven't yet seen
/// the indentation character used by the document.
2016-09-11 02:15:49 +02:00
bool _spaces;
bool get indented => true;
SassParser(String contents, {url, bool color: false})
: super(contents, url: url, color: color);
2016-09-11 02:15:49 +02:00
2016-10-30 21:06:23 +01:00
void expectStatementSeparator() {
if (!atEndOfStatement()) scanner.expectChar($lf);
}
2016-09-11 02:15:49 +02:00
bool atEndOfStatement() {
var next = scanner.peekChar();
return next == null || isNewline(next);
}
bool lookingAtChildren() =>
2016-10-02 09:07:36 +02:00
atEndOfStatement() && _peekIndentation() > currentIndentation;
bool scanElse(int ifIndentation) {
if (_peekIndentation() != ifIndentation) return false;
var start = scanner.state;
var startIndentation = currentIndentation;
var startNextIndentation = _nextIndentation;
var startNextIndentationEnd = _nextIndentationEnd;
_readIndentation();
if (scanner.scanChar($at) && scanIdentifier('else')) return true;
scanner.state = start;
_currentIndentation = startIndentation;
_nextIndentation = startNextIndentation;
_nextIndentationEnd = startNextIndentationEnd;
return false;
}
2016-09-11 02:15:49 +02:00
List<Statement> children(Statement child()) {
var children = <Statement>[];
_whileIndentedLower(() {
children.add(_child(child));
});
return children;
}
List<Statement> statements(Statement statement()) {
var first = scanner.peekChar();
if (first == $tab || first == $space) {
scanner.error("Indenting at the beginning of the document is illegal.",
position: 0, length: scanner.position);
}
var statements = <Statement>[];
while (!scanner.isDone) {
2016-10-19 01:22:42 +02:00
var child = _child(statement);
if (child != null) statements.add(child);
2016-09-11 02:15:49 +02:00
var indentation = _readIndentation();
assert(indentation == 0);
}
return statements;
}
2016-10-11 09:27:19 +02:00
/// Consumes a child of the current statement.
///
/// This consumes children that are allowed at all levels of the document; the
/// [child] parameter is called to consume any children that are specifically
/// allowed in the caller's context.
2016-09-11 02:15:49 +02:00
Statement _child(Statement child()) {
switch (scanner.peekChar()) {
// Ignore empty lines.
case $cr:
case $lf:
return null;
2016-09-11 02:15:49 +02:00
case $dollar:
return variableDeclaration();
break;
case $slash:
switch (scanner.peekChar(1)) {
case $slash:
return _silentComment();
break;
case $asterisk:
return _loudComment();
break;
default:
return child();
break;
}
break;
default:
return child();
break;
}
}
2016-10-11 09:27:19 +02:00
/// Consumes an indented-style silent comment.
2016-09-11 02:15:49 +02:00
Comment _silentComment() {
var start = scanner.state;
scanner.expect("//");
var buffer = new StringBuffer();
2016-10-02 09:07:36 +02:00
var parentIndentation = currentIndentation;
2016-09-11 02:15:49 +02:00
while (true) {
buffer.write("//");
// Skip the first two indentation characters because we're already writing
// "//".
2016-10-02 09:07:36 +02:00
for (var i = 2; i < currentIndentation - parentIndentation; i++) {
2016-09-11 02:15:49 +02:00
buffer.writeCharCode($space);
}
while (!scanner.isDone && !isNewline(scanner.peekChar())) {
buffer.writeCharCode(scanner.readChar());
}
buffer.writeln();
if (_peekIndentation() <= parentIndentation) break;
_readIndentation();
}
return new Comment(buffer.toString(), scanner.spanFrom(start),
silent: true);
}
2016-10-11 09:27:19 +02:00
/// Consumes an indented-style loud context.
2016-09-11 02:15:49 +02:00
Comment _loudComment() {
var start = scanner.state;
scanner.expect("/*");
var first = true;
var buffer = new StringBuffer("/*");
2016-10-02 09:07:36 +02:00
var parentIndentation = currentIndentation;
2016-09-11 02:15:49 +02:00
while (true) {
if (!first) {
buffer.writeln();
buffer.write(" * ");
}
first = false;
2016-10-02 09:07:36 +02:00
for (var i = 3; i < currentIndentation - parentIndentation; i++) {
2016-09-11 02:15:49 +02:00
buffer.writeCharCode($space);
}
while (!scanner.isDone && !isNewline(scanner.peekChar())) {
buffer.writeCharCode(scanner.readChar());
}
if (_peekIndentation() <= parentIndentation) break;
_readIndentation();
}
buffer.write(" */");
print(buffer);
return new Comment(buffer.toString(), scanner.spanFrom(start),
silent: false);
}
void whitespace() {
2016-10-11 09:27:19 +02:00
// This overrides whitespace consumption so that it doesn't consume newlines
// or loud comments.
2016-09-11 02:15:49 +02:00
while (!scanner.isDone) {
var next = scanner.peekChar();
if (next != $tab && next != $space) break;
scanner.readChar();
}
if (scanner.peekChar() == $slash && scanner.peekChar(1) == $slash) {
silentComment();
}
}
2016-10-11 09:27:19 +02:00
/// As long as the scanner's position is indented beneath the starting line,
/// runs [body] to consume the next statement.
2016-09-11 02:15:49 +02:00
void _whileIndentedLower(void body()) {
2016-10-02 09:07:36 +02:00
var parentIndentation = currentIndentation;
2016-09-11 02:15:49 +02:00
int childIndentation;
while (_peekIndentation() > parentIndentation) {
var indentation = _readIndentation();
childIndentation ??= indentation;
if (childIndentation != indentation) {
scanner.error(
"Inconsistent indentation, expected $childIndentation spaces.",
position: scanner.position - scanner.column,
length: scanner.column);
}
body();
}
}
2016-10-11 09:27:19 +02:00
/// Consumes indentation whitespace and returns the indentation level of the
/// next line.
2016-09-11 02:15:49 +02:00
int _readIndentation() {
if (_nextIndentation == null) _peekIndentation();
_currentIndentation = _nextIndentation;
scanner.state = _nextIndentationEnd;
_nextIndentation = null;
_nextIndentationEnd = null;
2016-10-02 09:07:36 +02:00
return currentIndentation;
2016-09-11 02:15:49 +02:00
}
2016-10-11 09:27:19 +02:00
/// Returns the indentation level of the next line.
2016-09-11 02:15:49 +02:00
int _peekIndentation() {
if (_nextIndentation != null) return _nextIndentation;
if (scanner.isDone) {
_nextIndentation = 0;
_nextIndentationEnd = scanner.state;
return 0;
}
var start = scanner.state;
if (!scanCharIf(isNewline)) {
scanner.error("Expected newline.", position: scanner.position);
}
bool containsTab;
bool containsSpace;
do {
containsTab = false;
containsSpace = false;
_nextIndentation = 0;
while (true) {
var next = scanner.peekChar();
if (next == $space) {
containsSpace = true;
} else if (next == $tab) {
containsTab = true;
} else {
break;
}
_nextIndentation++;
scanner.readChar();
}
if (scanner.isDone) {
_nextIndentation = 0;
_nextIndentationEnd = scanner.state;
scanner.state = start;
return 0;
}
} while (scanCharIf(isNewline));
_checkIndentationConsistency(containsTab, containsSpace);
if (_nextIndentation > 0) _spaces ??= containsSpace;
2016-09-11 02:15:49 +02:00
_nextIndentationEnd = scanner.state;
scanner.state = start;
return _nextIndentation;
}
2016-10-11 09:27:19 +02:00
/// Ensures that the document uses consistent characters for indentation.
///
/// The [containsTab] and [containsSpace] parameters refer to a single line of
/// indentation that has just been parsed.
2016-09-11 02:15:49 +02:00
void _checkIndentationConsistency(bool containsTab, bool containsSpace) {
if (containsTab) {
if (containsSpace) {
scanner.error("Tabs and spaces may not be mixed.",
position: scanner.position - scanner.column,
length: scanner.column);
} else if (_spaces == true) {
scanner.error("Expected spaces, was tabs.",
position: scanner.position - scanner.column,
length: scanner.column);
}
} else if (_spaces == false) {
scanner.error("Expected tabs, was spaces.",
position: scanner.position - scanner.column, length: scanner.column);
}
}
}