| // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| library parser; | |
| class Token { | |
| static final RegExp _alpha = RegExp(r'^[0-9a-zA-Z_\-@]+$'); | |
| final String? text; | |
| Token? next; | |
| Token(this.text); | |
| bool get eof => text == null; | |
| bool get isName { | |
| if (text == null || text!.isEmpty) return false; | |
| return _alpha.hasMatch(text!); | |
| } | |
| bool get isComment => text != null && text!.startsWith('//'); | |
| @override | |
| String toString() => text == null ? 'EOF' : text!; | |
| } | |
| class Tokenizer { | |
| static final alphaNum = | |
| '@abcdefghijklmnopqrstuvwxyz-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; | |
| static final whitespace = ' \n\t\r'; | |
| String text; | |
| Token? _head; | |
| Token? _last; | |
| final _newLineRegExp = RegExp('\r?\n'); | |
| Tokenizer(this.text); | |
| Token? tokenize() { | |
| _emit(null); | |
| for (int i = 0; i < text.length; i++) { | |
| String c = text[i]; | |
| if (whitespace.contains(c)) { | |
| // skip | |
| } else if (c == '/' && _peek(i) == '/') { | |
| int index = text.indexOf(_newLineRegExp, i); | |
| if (index == -1) index = text.length; | |
| _emit(text.substring(i, index)); | |
| i = index; | |
| } else if (alphaNum.contains(c)) { | |
| int start = i; | |
| while (alphaNum.contains(_peek(i))) { | |
| i++; | |
| } | |
| _emit(text.substring(start, i + 1)); | |
| } else { | |
| _emit(c); | |
| } | |
| } | |
| _emit(null); | |
| _head = _head!.next; | |
| return _head; | |
| } | |
| void _emit(String? value) { | |
| Token token = Token(value); | |
| _head ??= token; | |
| if (_last != null) _last!.next = token; | |
| _last = token; | |
| } | |
| String _peek(int i) { | |
| i += 1; | |
| return i < text.length ? text[i] : String.fromCharCodes([0]); | |
| } | |
| @override | |
| String toString() { | |
| StringBuffer buf = StringBuffer(); | |
| Token t = _head!; | |
| buf.write('[$t]\n'); | |
| while (!t.eof) { | |
| t = t.next!; | |
| buf.write('[$t]\n'); | |
| } | |
| return buf.toString().trim(); | |
| } | |
| } | |
| abstract class Parser { | |
| final Token? startToken; | |
| Token? current; | |
| Parser(this.startToken); | |
| Token expect(String text) { | |
| Token t = advance()!; | |
| if (text != t.text) fail('expected $text, got $t'); | |
| return t; | |
| } | |
| bool consume(String text) { | |
| if (peek()!.text == text) { | |
| advance(); | |
| return true; | |
| } else { | |
| return false; | |
| } | |
| } | |
| Token? peek() => current == null | |
| ? startToken | |
| : current!.eof | |
| ? current | |
| : current!.next; | |
| Token expectName() { | |
| Token t = advance()!; | |
| if (!t.isName) fail('expected name token, got $t'); | |
| return t; | |
| } | |
| Token? advance() { | |
| if (current == null) { | |
| current = startToken; | |
| } else if (!current!.eof) { | |
| current = current!.next; | |
| } | |
| return current; | |
| } | |
| String? collectComments() { | |
| StringBuffer buf = StringBuffer(); | |
| while (peek()!.isComment) { | |
| Token t = advance()!; | |
| String str = t.text!.substring(2); | |
| if (str.startsWith(' ')) str = str.substring(1); | |
| if (str.startsWith(' ')) { | |
| buf.write('\n - ${str.substring(2)}'); | |
| } else if (str.isEmpty) { | |
| buf.write('\n\n'); | |
| } else { | |
| buf.write('$str '); | |
| } | |
| } | |
| if (buf.isEmpty) return null; | |
| return buf | |
| .toString() | |
| .split('\n') | |
| .map((line) => line.trimRight()) | |
| .join('\n') | |
| .trim(); | |
| } | |
| String? consumeString() { | |
| StringBuffer buf = StringBuffer(); | |
| String startQuotation = advance()!.text!; | |
| if (startQuotation != '"' && startQuotation != "'") { | |
| return null; | |
| } | |
| while (peek()!.text != startQuotation) { | |
| Token t = advance()!; | |
| if (t.text == null) { | |
| throw FormatException('Reached EOF'); | |
| } | |
| buf.write('${t.text} '); | |
| } | |
| advance(); | |
| return buf.toString().trim(); | |
| } | |
| void validate(bool result, String message) { | |
| if (!result) throw 'expected $message'; | |
| } | |
| void fail(String message) => throw message; | |
| } |