362 lines
9.3 KiB
Dart
362 lines
9.3 KiB
Dart
import '../ast/ast.dart';
|
|
import 'parselet/parselet.dart';
|
|
import 'scanner.dart';
|
|
|
|
class Parser {
|
|
final List<JaelError> errors = [];
|
|
final Scanner scanner;
|
|
|
|
Token _current;
|
|
int _index = -1;
|
|
|
|
Parser(this.scanner);
|
|
|
|
Token get current => _current;
|
|
|
|
int _nextPrecedence() {
|
|
var tok = peek();
|
|
if (tok == null) return 0;
|
|
|
|
var parser = infixParselets[tok.type];
|
|
return parser?.precedence ?? 0;
|
|
}
|
|
|
|
bool next(TokenType type) {
|
|
if (_index >= scanner.tokens.length - 1) return false;
|
|
var peek = scanner.tokens[_index + 1];
|
|
|
|
if (peek.type != type) return false;
|
|
|
|
_current = peek;
|
|
_index++;
|
|
return true;
|
|
}
|
|
|
|
Token peek() {
|
|
if (_index >= scanner.tokens.length - 1) return null;
|
|
return scanner.tokens[_index + 1];
|
|
}
|
|
|
|
Token maybe(TokenType type) => next(type) ? _current : null;
|
|
|
|
void skipExtraneous(TokenType type) {
|
|
while (next(type)) {
|
|
// Skip...
|
|
}
|
|
}
|
|
|
|
Document parseDocument() {
|
|
var doctype = parseDoctype();
|
|
|
|
if (doctype == null) {
|
|
var root = parseElement();
|
|
if (root == null) return null;
|
|
return new Document(null, root);
|
|
}
|
|
|
|
var root = parseElement();
|
|
|
|
if (root == null) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Missing root element after !DOCTYPE declaration.', doctype.span));
|
|
return null;
|
|
}
|
|
|
|
return new Document(doctype, root);
|
|
}
|
|
|
|
StringLiteral implicitString() {
|
|
if (next(TokenType.string)) {
|
|
return prefixParselets[TokenType.string].parse(this, _current);
|
|
} else if (next(TokenType.text)) {
|
|
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
Doctype parseDoctype() {
|
|
if (!next(TokenType.lt)) return null;
|
|
var lt = _current;
|
|
|
|
if (!next(TokenType.doctype)) {
|
|
_index--;
|
|
return null;
|
|
}
|
|
var doctype = _current, html = parseIdentifier();
|
|
if (html?.span?.text?.toLowerCase() != 'html') {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Expected "html" in doctype declaration.', html?.span ?? doctype.span));
|
|
return null;
|
|
}
|
|
|
|
var public = parseIdentifier();
|
|
if (public == null) {
|
|
if (!next(TokenType.gt)) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Expected ">" in doctype declaration.', html.span));
|
|
return null;
|
|
}
|
|
|
|
return new Doctype(lt, doctype, html, null, null, null, _current);
|
|
}
|
|
|
|
if (public?.span?.text?.toLowerCase() != 'public') {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Expected "public" in doctype declaration.', public?.span ?? html.span));
|
|
return null;
|
|
}
|
|
|
|
var stringParser = prefixParselets[TokenType.string];
|
|
|
|
if (!next(TokenType.string)) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Expected string in doctype declaration.', public.span));
|
|
return null;
|
|
}
|
|
|
|
var name = stringParser.parse(this, _current);
|
|
|
|
if (!next(TokenType.string)) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Expected string in doctype declaration.', name.span));
|
|
return null;
|
|
}
|
|
|
|
var url = stringParser.parse(this, _current);
|
|
|
|
if (!next(TokenType.gt)) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Expected ">" in doctype declaration.', url.span));
|
|
return null;
|
|
}
|
|
|
|
return new Doctype(lt, doctype, html, public, name, url, _current);
|
|
}
|
|
|
|
ElementChild parseElementChild() =>
|
|
parseHtmlComment() ??
|
|
parseInterpolation() ??
|
|
parseText() ??
|
|
parseElement();
|
|
|
|
HtmlComment parseHtmlComment() =>
|
|
next(TokenType.htmlComment) ? new HtmlComment(_current) : null;
|
|
|
|
Text parseText() => next(TokenType.text) ? new Text(_current) : null;
|
|
|
|
Interpolation parseInterpolation() {
|
|
if (!next(TokenType.doubleCurlyL)) return null;
|
|
var doubleCurlyL = _current;
|
|
|
|
var expression = parseExpression(0);
|
|
|
|
if (expression == null) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Missing expression in interpolation.', doubleCurlyL.span));
|
|
return null;
|
|
}
|
|
|
|
if (!next(TokenType.doubleCurlyR)) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Missing closing "}}" in interpolation.', expression.span));
|
|
return null;
|
|
}
|
|
|
|
return new Interpolation(doubleCurlyL, expression, _current);
|
|
}
|
|
|
|
Element parseElement() {
|
|
if (!next(TokenType.lt)) return null;
|
|
var lt = _current;
|
|
|
|
if (next(TokenType.slash)) {
|
|
// We entered a closing tag, don't keep reading...
|
|
_index -= 2;
|
|
return null;
|
|
}
|
|
|
|
var tagName = parseIdentifier();
|
|
|
|
if (tagName == null) {
|
|
errors.add(
|
|
new JaelError(JaelErrorSeverity.error, 'Missing tag name.', lt.span));
|
|
return null;
|
|
}
|
|
|
|
List<Attribute> attributes = [];
|
|
var attribute = parseAttribute();
|
|
|
|
while (attribute != null) {
|
|
attributes.add(attribute);
|
|
attribute = parseAttribute();
|
|
}
|
|
|
|
if (next(TokenType.slash)) {
|
|
// Try for self-closing...
|
|
var slash = _current;
|
|
|
|
if (!next(TokenType.gt)) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Missing ">" in self-closing "${tagName.name}" tag.', slash.span));
|
|
return null;
|
|
}
|
|
|
|
return new SelfClosingElement(lt, tagName, attributes, slash, _current);
|
|
}
|
|
|
|
if (!next(TokenType.gt)) {
|
|
errors.add(new JaelError(
|
|
JaelErrorSeverity.error,
|
|
'Missing ">" in "${tagName.name}" tag.',
|
|
attributes.isEmpty ? tagName.span : attributes.last.span));
|
|
return null;
|
|
}
|
|
|
|
var gt = _current;
|
|
|
|
// Implicit self-closing
|
|
if (Element.selfClosing.contains(tagName.name)) {
|
|
return new SelfClosingElement(lt, tagName, attributes, null, gt);
|
|
}
|
|
|
|
List<ElementChild> children = [];
|
|
var child = parseElementChild();
|
|
|
|
while (child != null) {
|
|
if (child is! HtmlComment) children.add(child);
|
|
child = parseElementChild();
|
|
}
|
|
|
|
// Parse closing tag
|
|
if (!next(TokenType.lt)) {
|
|
errors.add(new JaelError(
|
|
JaelErrorSeverity.error,
|
|
'Missing closing tag for "${tagName.name}" tag.',
|
|
children.isEmpty ? tagName.span : children.last.span));
|
|
return null;
|
|
}
|
|
|
|
var lt2 = _current;
|
|
|
|
if (!next(TokenType.slash)) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Missing "/" in "${tagName.name}" closing tag.', lt2.span));
|
|
return null;
|
|
}
|
|
|
|
var slash = _current, tagName2 = parseIdentifier();
|
|
|
|
if (tagName2 == null) {
|
|
errors.add(new JaelError(
|
|
JaelErrorSeverity.error,
|
|
'Missing "${tagName.name}" in "${tagName.name}" closing tag.',
|
|
slash.span));
|
|
return null;
|
|
}
|
|
|
|
if (tagName2.name != tagName.name) {
|
|
errors.add(new JaelError(
|
|
JaelErrorSeverity.error,
|
|
'Mismatched closing tags. Expected "${tagName.span}"; got "${tagName2.name}" instead.',
|
|
lt2.span));
|
|
return null;
|
|
}
|
|
|
|
if (!next(TokenType.gt)) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Missing ">" in "${tagName.name}" closing tag.', tagName2.span));
|
|
return null;
|
|
}
|
|
|
|
return new RegularElement(
|
|
lt, tagName, attributes, gt, children, lt2, slash, tagName2, _current);
|
|
}
|
|
|
|
Attribute parseAttribute() {
|
|
var name = parseIdentifier();
|
|
if (name == null) return null;
|
|
|
|
if (!next(TokenType.equals)) return new Attribute(name, null, null);
|
|
|
|
var equals = _current;
|
|
var value = parseExpression(0);
|
|
|
|
if (value == null) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Missing expression in attribute.', equals.span));
|
|
return null;
|
|
}
|
|
|
|
return new Attribute(name, equals, value);
|
|
}
|
|
|
|
Expression parseExpression(int precedence) {
|
|
// Only consume a token if it could potentially be a prefix parselet
|
|
|
|
for (var type in prefixParselets.keys) {
|
|
if (next(type)) {
|
|
var left = prefixParselets[type].parse(this, _current);
|
|
|
|
while (precedence < _nextPrecedence()) {
|
|
_current = scanner.tokens[++_index];
|
|
var infix = infixParselets[_current.type];
|
|
var newLeft = infix.parse(this, left, _current);
|
|
|
|
if (newLeft == null) {
|
|
if (_current.type == TokenType.gt)
|
|
_index--;
|
|
return left;
|
|
}
|
|
left = newLeft;
|
|
}
|
|
|
|
return left;
|
|
}
|
|
}
|
|
|
|
// Nothing was parsed; return null.
|
|
return null;
|
|
}
|
|
|
|
Identifier parseIdentifier() =>
|
|
next(TokenType.id) ? new Identifier(_current) : null;
|
|
|
|
KeyValuePair parseKeyValuePair() {
|
|
var key = parseExpression(0);
|
|
if (key == null) return null;
|
|
|
|
if (!next(TokenType.colon)) return new KeyValuePair(key, null, null);
|
|
|
|
var colon = _current, value = parseExpression(0);
|
|
|
|
if (value == null) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Missing expression in key-value pair.', colon.span));
|
|
return null;
|
|
}
|
|
|
|
return new KeyValuePair(key, colon, value);
|
|
}
|
|
|
|
NamedArgument parseNamedArgument() {
|
|
var name = parseIdentifier();
|
|
if (name == null) return null;
|
|
|
|
if (!next(TokenType.colon)) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Missing ":" in named argument.', name.span));
|
|
return null;
|
|
}
|
|
|
|
var colon = _current, value = parseExpression(0);
|
|
|
|
if (value == null) {
|
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
|
'Missing expression in named argument.', colon.span));
|
|
return null;
|
|
}
|
|
|
|
return new NamedArgument(name, colon, value);
|
|
}
|
|
}
|