Created
September 15, 2023 08:54
-
-
Save mezoni/dd6e7cc288de4d54285be62ad5c82fcd to your computer and use it in GitHub Desktop.
csv_parser.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| void main(List<String> args) { | |
| final parser = CsvParser(); | |
| final rows = parseString(parser.parseStart, _source); | |
| for (var i = 0; i < rows.length; i++) { | |
| final row = rows[i]; | |
| print('Row #$i'); | |
| print(row); | |
| } | |
| } | |
| const _source = ''' | |
| 123,"multi | |
| line",1 | |
| 456,def😄, | |
| '''; | |
| class CsvParser { | |
| const CsvParser(); | |
| List<List<String>>? parseStart(State<StringReader> state) { | |
| List<List<String>>? $0; | |
| // v:Rows Eof | |
| final $1 = state.pos; | |
| List<List<String>>? $2; | |
| $2 = parseRows(state); | |
| if (state.ok) { | |
| fastParseEof(state); | |
| if (state.ok) { | |
| $0 = $2; | |
| } | |
| } | |
| if (!state.ok) { | |
| state.pos = $1; | |
| } | |
| return $0; | |
| } | |
| List<List<String>>? parseRows(State<StringReader> state) { | |
| List<List<String>>? $0; | |
| // h:Row t:(RowEnding v:Row)* Eol? | |
| final $1 = state.pos; | |
| List<String>? $2; | |
| $2 = parseRow(state); | |
| if (state.ok) { | |
| List<List<String>>? $3; | |
| final $4 = <List<String>>[]; | |
| while (true) { | |
| List<String>? $5; | |
| // RowEnding v:Row | |
| final $6 = state.pos; | |
| fastParseRowEnding(state); | |
| if (state.ok) { | |
| List<String>? $7; | |
| $7 = parseRow(state); | |
| if (state.ok) { | |
| $5 = $7; | |
| } | |
| } | |
| if (!state.ok) { | |
| state.pos = $6; | |
| } | |
| if (!state.ok) { | |
| state.ok = true; | |
| break; | |
| } | |
| $4.add($5!); | |
| } | |
| if (state.ok) { | |
| $3 = $4; | |
| } | |
| if (state.ok) { | |
| fastParseEol(state); | |
| state.ok = true; | |
| if (state.ok) { | |
| List<List<String>>? $$; | |
| final h = $2!; | |
| final t = $3!; | |
| $$ = [h, ...t]; | |
| $0 = $$; | |
| } | |
| } | |
| } | |
| if (!state.ok) { | |
| state.pos = $1; | |
| } | |
| return $0; | |
| } | |
| List<String>? parseRow(State<StringReader> state) { | |
| List<String>? $0; | |
| // h:Field t:(',' v:Field)* | |
| final $1 = state.pos; | |
| String? $2; | |
| $2 = parseField(state); | |
| if (state.ok) { | |
| List<String>? $3; | |
| final $4 = <String>[]; | |
| while (true) { | |
| String? $5; | |
| // ',' v:Field | |
| final $6 = state.pos; | |
| const $8 = ','; | |
| matchLiteral1(state, 44, $8, const ErrorExpectedTags([$8])); | |
| if (state.ok) { | |
| String? $7; | |
| $7 = parseField(state); | |
| if (state.ok) { | |
| $5 = $7; | |
| } | |
| } | |
| if (!state.ok) { | |
| state.pos = $6; | |
| } | |
| if (!state.ok) { | |
| state.ok = true; | |
| break; | |
| } | |
| $4.add($5!); | |
| } | |
| if (state.ok) { | |
| $3 = $4; | |
| } | |
| if (state.ok) { | |
| List<String>? $$; | |
| final h = $2!; | |
| final t = $3!; | |
| $$ = [h, ...t]; | |
| $0 = $$; | |
| } | |
| } | |
| if (!state.ok) { | |
| state.pos = $1; | |
| } | |
| return $0; | |
| } | |
| String? parseField(State<StringReader> state) { | |
| String? $0; | |
| // String | |
| $0 = parseString(state); | |
| if (state.ok) { | |
| $0 = $0; | |
| } | |
| if (!state.ok) { | |
| // Text | |
| $0 = parseText(state); | |
| if (state.ok) { | |
| $0 = $0; | |
| } | |
| } | |
| return $0; | |
| } | |
| String? parseText(State<StringReader> state) { | |
| String? $0; | |
| // $[^,"\n\r]* | |
| final $2 = state.pos; | |
| while (true) { | |
| state.ok = state.pos < state.input.length; | |
| if (state.ok) { | |
| final $3 = state.input.readChar(state.pos); | |
| state.ok = !($3 == 13 || $3 == 10 || $3 == 34 || $3 == 44); | |
| if (state.ok) { | |
| state.pos += state.input.count; | |
| } | |
| } | |
| if (!state.ok) { | |
| state.fail(const ErrorUnexpectedCharacter()); | |
| } | |
| if (!state.ok) { | |
| state.ok = true; | |
| break; | |
| } | |
| } | |
| if (state.ok) { | |
| $0 = state.input.substring($2, state.pos); | |
| } | |
| if (state.ok) { | |
| $0 = $0; | |
| } | |
| return $0; | |
| } | |
| String? parseString(State<StringReader> state) { | |
| String? $0; | |
| // OpenQuote v:Chars CloseQuote | |
| final $1 = state.pos; | |
| fastParseOpenQuote(state); | |
| if (state.ok) { | |
| List<int>? $2; | |
| $2 = parseChars(state); | |
| if (state.ok) { | |
| fastParseCloseQuote(state); | |
| if (state.ok) { | |
| String? $$; | |
| final v = $2!; | |
| $$ = String.fromCharCodes(v); | |
| $0 = $$; | |
| } | |
| } | |
| } | |
| if (!state.ok) { | |
| state.pos = $1; | |
| } | |
| return $0; | |
| } | |
| void fastParseOpenQuote(State<StringReader> state) { | |
| // Spaces '"' | |
| final $0 = state.pos; | |
| fastParseSpaces(state); | |
| if (state.ok) { | |
| const $1 = '"'; | |
| matchLiteral1(state, 34, $1, const ErrorExpectedTags([$1])); | |
| } | |
| if (!state.ok) { | |
| state.pos = $0; | |
| } | |
| } | |
| void fastParseSpaces(State<StringReader> state) { | |
| // [ \t]* | |
| while (true) { | |
| state.ok = state.pos < state.input.length; | |
| if (state.ok) { | |
| final $1 = state.input.readChar(state.pos); | |
| state.ok = $1 == 9 || $1 == 32; | |
| if (state.ok) { | |
| state.pos += state.input.count; | |
| } | |
| } | |
| if (!state.ok) { | |
| state.fail(const ErrorUnexpectedCharacter()); | |
| } | |
| if (!state.ok) { | |
| state.ok = true; | |
| break; | |
| } | |
| } | |
| } | |
| List<int>? parseChars(State<StringReader> state) { | |
| List<int>? $0; | |
| // ([^"] / '""')* | |
| final $2 = <int>[]; | |
| while (true) { | |
| int? $3; | |
| // [^"] | |
| state.ok = state.pos < state.input.length; | |
| if (state.ok) { | |
| final $7 = state.input.readChar(state.pos); | |
| state.ok = $7 != 34; | |
| if (state.ok) { | |
| state.pos += state.input.count; | |
| $3 = $7; | |
| } | |
| } | |
| if (!state.ok) { | |
| state.fail(const ErrorUnexpectedCharacter()); | |
| } | |
| if (state.ok) { | |
| $3 = $3; | |
| } | |
| if (!state.ok) { | |
| // '""' | |
| const $5 = '""'; | |
| matchLiteral(state, $5, const ErrorExpectedTags([$5])); | |
| if (state.ok) { | |
| int? $$; | |
| $$ = 0x22; | |
| $3 = $$; | |
| } | |
| } | |
| if (!state.ok) { | |
| state.ok = true; | |
| break; | |
| } | |
| $2.add($3!); | |
| } | |
| if (state.ok) { | |
| $0 = $2; | |
| } | |
| if (state.ok) { | |
| $0 = $0; | |
| } | |
| return $0; | |
| } | |
| void fastParseCloseQuote(State<StringReader> state) { | |
| // '"' Spaces | |
| final $0 = state.pos; | |
| const $1 = '"'; | |
| matchLiteral1(state, 34, $1, const ErrorExpectedTags([$1])); | |
| if (state.ok) { | |
| fastParseSpaces(state); | |
| } | |
| if (!state.ok) { | |
| state.pos = $0; | |
| } | |
| } | |
| void fastParseRowEnding(State<StringReader> state) { | |
| // Eol !Eof | |
| final $0 = state.pos; | |
| fastParseEol(state); | |
| if (state.ok) { | |
| final $1 = state.pos; | |
| fastParseEof(state); | |
| state.ok = !state.ok; | |
| if (!state.ok) { | |
| state.pos = $1; | |
| } | |
| } | |
| if (!state.ok) { | |
| state.pos = $0; | |
| } | |
| } | |
| void fastParseEol(State<StringReader> state) { | |
| // [\n\r] | |
| state.ok = state.pos < state.input.length; | |
| if (state.ok) { | |
| final $3 = state.input.readChar(state.pos); | |
| state.ok = $3 == 10 || $3 == 13; | |
| if (state.ok) { | |
| state.pos += state.input.count; | |
| } | |
| } | |
| if (!state.ok) { | |
| state.fail(const ErrorUnexpectedCharacter()); | |
| } | |
| if (!state.ok) { | |
| // '\r\n' | |
| const $1 = '\r\n'; | |
| matchLiteral(state, $1, const ErrorExpectedTags([$1])); | |
| } | |
| } | |
| void fastParseEof(State<StringReader> state) { | |
| // !. | |
| state.ok = state.pos >= state.input.length; | |
| if (!state.ok) { | |
| state.fail(const ErrorExpectedEndOfInput()); | |
| } | |
| } | |
| @pragma('vm:prefer-inline') | |
| String? matchLiteral( | |
| State<StringReader> state, String string, ParseError error) { | |
| state.ok = state.input.startsWith(string, state.pos); | |
| if (state.ok) { | |
| state.pos += state.input.count; | |
| return string; | |
| } else { | |
| state.fail(error); | |
| } | |
| return null; | |
| } | |
| @pragma('vm:prefer-inline') | |
| String? matchLiteral1( | |
| State<StringReader> state, int char, String string, ParseError error) { | |
| final input = state.input; | |
| if (state.pos < input.length) { | |
| final c = input.readChar(state.pos); | |
| if (c == char) { | |
| state.pos += state.input.count; | |
| state.ok = true; | |
| return string; | |
| } | |
| } | |
| state.fail(error); | |
| return null; | |
| } | |
| } | |
| void fastParseString( | |
| void Function(State<StringReader> state) fastParse, | |
| String source, { | |
| String Function(StringReader input, int offset, List<ErrorMessage> errors)? | |
| errorMessage, | |
| }) { | |
| final input = StringReader(source); | |
| final result = tryFastParse( | |
| fastParse, | |
| input, | |
| errorMessage: errorMessage, | |
| ); | |
| if (result.ok) { | |
| return; | |
| } | |
| errorMessage ??= errorMessage; | |
| final message = result.errorMessage; | |
| throw FormatException(message); | |
| } | |
| O parseInput<I, O>( | |
| O? Function(State<I> state) parse, | |
| I input, { | |
| String Function(I input, int offset, List<ErrorMessage> errors)? errorMessage, | |
| }) { | |
| final result = tryParse( | |
| parse, | |
| input, | |
| errorMessage: errorMessage, | |
| ); | |
| return result.getResult(); | |
| } | |
| O parseString<O>( | |
| O? Function(State<StringReader> state) parse, | |
| String source, { | |
| String Function(StringReader input, int offset, List<ErrorMessage> errors)? | |
| errorMessage, | |
| }) { | |
| final input = StringReader(source); | |
| final result = tryParse( | |
| parse, | |
| input, | |
| errorMessage: errorMessage, | |
| ); | |
| return result.getResult(); | |
| } | |
| ParseResult<I, void> tryFastParse<I>( | |
| void Function(State<I> state) fastParse, | |
| I input, { | |
| String Function(I input, int offset, List<ErrorMessage> errors)? errorMessage, | |
| }) { | |
| final result = _parse<I, void>( | |
| fastParse, | |
| input, | |
| errorMessage: errorMessage, | |
| ); | |
| return result; | |
| } | |
| ParseResult<I, O> tryParse<I, O>( | |
| O? Function(State<I> state) parse, | |
| I input, { | |
| String Function(I input, int offset, List<ErrorMessage> errors)? errorMessage, | |
| }) { | |
| final result = _parse<I, O>( | |
| parse, | |
| input, | |
| errorMessage: errorMessage, | |
| ); | |
| return result; | |
| } | |
| ParseResult<I, O> _createParseResult<I, O>( | |
| State<I> state, | |
| O? result, { | |
| String Function(I input, int offset, List<ErrorMessage> errors)? errorMessage, | |
| }) { | |
| final input = state.input; | |
| if (state.ok) { | |
| return ParseResult( | |
| failPos: state.failPos, | |
| input: input, | |
| ok: true, | |
| pos: state.pos, | |
| result: result, | |
| ); | |
| } | |
| final offset = state.failPos; | |
| final normalized = _normalize(input, offset, state.getErrors()) | |
| .map((e) => e.getErrorMessage(input, offset)) | |
| .toList(); | |
| String? message; | |
| if (errorMessage != null) { | |
| message = errorMessage(input, offset, normalized); | |
| } else if (input is StringReader) { | |
| if (input.hasSource) { | |
| message = _errorMessage(input.source, offset, normalized); | |
| } else { | |
| message = _errorMessageWithoutSource(input, offset, normalized); | |
| } | |
| } else if (input is String) { | |
| message = _errorMessage(input, offset, normalized); | |
| } else { | |
| message = normalized.join('\n'); | |
| } | |
| return ParseResult( | |
| errors: normalized, | |
| failPos: state.failPos, | |
| input: input, | |
| errorMessage: message, | |
| ok: false, | |
| pos: state.pos, | |
| result: result, | |
| ); | |
| } | |
| String _errorMessage(String source, int offset, List<ErrorMessage> errors) { | |
| final sb = StringBuffer(); | |
| final errorInfoList = errors | |
| .map((e) => (length: e.length, message: e.toString())) | |
| .toSet() | |
| .toList(); | |
| for (var i = 0; i < errorInfoList.length; i++) { | |
| int max(int x, int y) => x > y ? x : y; | |
| int min(int x, int y) => x < y ? x : y; | |
| if (sb.isNotEmpty) { | |
| sb.writeln(); | |
| sb.writeln(); | |
| } | |
| final errorInfo = errorInfoList[i]; | |
| final length = errorInfo.length; | |
| final message = errorInfo.message; | |
| final start = min(offset + length, offset); | |
| final end = max(offset + length, offset); | |
| var row = 1; | |
| var lineStart = 0, next = 0, pos = 0; | |
| while (pos < source.length) { | |
| final c = source.codeUnitAt(pos++); | |
| if (c == 0xa || c == 0xd) { | |
| next = c == 0xa ? 0xd : 0xa; | |
| if (pos < source.length && source.codeUnitAt(pos) == next) { | |
| pos++; | |
| } | |
| if (pos - 1 >= start) { | |
| break; | |
| } | |
| row++; | |
| lineStart = pos; | |
| } | |
| } | |
| final inputLen = source.length; | |
| final lineLimit = min(80, inputLen); | |
| final start2 = start; | |
| final end2 = min(start2 + lineLimit, end); | |
| final errorLen = end2 - start; | |
| final extraLen = lineLimit - errorLen; | |
| final rightLen = min(inputLen - end2, extraLen - (extraLen >> 1)); | |
| final leftLen = min(start, max(0, lineLimit - errorLen - rightLen)); | |
| var index = start2 - 1; | |
| final list = <int>[]; | |
| for (var i = 0; i < leftLen && index >= 0; i++) { | |
| var cc = source.codeUnitAt(index--); | |
| if ((cc & 0xFC00) == 0xDC00 && (index > 0)) { | |
| final pc = source.codeUnitAt(index); | |
| if ((pc & 0xFC00) == 0xD800) { | |
| cc = 0x10000 + ((pc & 0x3FF) << 10) + (cc & 0x3FF); | |
| index--; | |
| } | |
| } | |
| list.add(cc); | |
| } | |
| final column = start - lineStart + 1; | |
| final left = String.fromCharCodes(list.reversed); | |
| final end3 = min(inputLen, start2 + (lineLimit - leftLen)); | |
| final indicatorLen = max(1, errorLen); | |
| final right = source.substring(start2, end3); | |
| var text = left + right; | |
| text = text.replaceAll('\n', ' '); | |
| text = text.replaceAll('\r', ' '); | |
| text = text.replaceAll('\t', ' '); | |
| sb.writeln('line $row, column $column: $message'); | |
| sb.writeln(text); | |
| sb.write(' ' * leftLen + '^' * indicatorLen); | |
| } | |
| return sb.toString(); | |
| } | |
| String _errorMessageWithoutSource( | |
| StringReader input, int offset, List<ErrorMessage> errors) { | |
| final sb = StringBuffer(); | |
| final errorInfoList = errors | |
| .map((e) => (length: e.length, message: e.toString())) | |
| .toSet() | |
| .toList(); | |
| for (var i = 0; i < errorInfoList.length; i++) { | |
| int max(int x, int y) => x > y ? x : y; | |
| int min(int x, int y) => x < y ? x : y; | |
| if (sb.isNotEmpty) { | |
| sb.writeln(); | |
| sb.writeln(); | |
| } | |
| final errorInfo = errorInfoList[i]; | |
| final length = errorInfo.length; | |
| final message = errorInfo.message; | |
| final start = min(offset + length, offset); | |
| final end = max(offset + length, offset); | |
| final inputLen = input.length; | |
| final lineLimit = min(80, inputLen); | |
| final start2 = start; | |
| final end2 = min(start2 + lineLimit, end); | |
| final errorLen = end2 - start; | |
| final indicatorLen = max(1, errorLen); | |
| var text = input.substring(start, lineLimit); | |
| text = text.replaceAll('\n', ' '); | |
| text = text.replaceAll('\r', ' '); | |
| text = text.replaceAll('\t', ' '); | |
| sb.writeln('offset $offset: $message'); | |
| sb.writeln(text); | |
| sb.write('^' * indicatorLen); | |
| } | |
| return sb.toString(); | |
| } | |
| List<ParseError> _normalize<I>(I input, int offset, List<ParseError> errors) { | |
| final result = errors.toList(); | |
| if (input case final StringReader input) { | |
| if (offset >= input.length) { | |
| result.add(const ErrorUnexpectedEndOfInput()); | |
| result.removeWhere((e) => e is ErrorUnexpectedCharacter); | |
| } | |
| } else if (input case final ChunkedData<StringReader> input) { | |
| if (input.isClosed && offset == input.start + input.data.length) { | |
| result.add(const ErrorUnexpectedEndOfInput()); | |
| result.removeWhere((e) => e is ErrorUnexpectedCharacter); | |
| } | |
| } | |
| final foundTags = | |
| result.whereType<ErrorExpectedTag>().map((e) => e.tag).toList(); | |
| if (foundTags.isNotEmpty) { | |
| result.removeWhere((e) => e is ErrorExpectedTag); | |
| result.add(ErrorExpectedTags(foundTags)); | |
| } | |
| final expectedTags = result.whereType<ErrorExpectedTags>().toList(); | |
| if (expectedTags.isNotEmpty) { | |
| result.removeWhere((e) => e is ErrorExpectedTags); | |
| final tags = <String>{}; | |
| for (final error in expectedTags) { | |
| tags.addAll(error.tags); | |
| } | |
| final tagList = tags.toList(); | |
| tagList.sort(); | |
| final error = ErrorExpectedTags(tagList); | |
| result.add(error); | |
| } | |
| return result; | |
| } | |
| ParseResult<I, O> _parse<I, O>( | |
| O? Function(State<I> input) parse, | |
| I input, { | |
| String Function(I input, int offset, List<ErrorMessage> errors)? errorMessage, | |
| }) { | |
| final state = State(input); | |
| final result = parse(state); | |
| return _createParseResult<I, O>( | |
| state, | |
| result, | |
| errorMessage: errorMessage, | |
| ); | |
| } | |
| abstract class ChunkedData<T> implements Sink<T> { | |
| void Function()? handler; | |
| bool _isClosed = false; | |
| int buffering = 0; | |
| T data; | |
| int end = 0; | |
| bool sleep = false; | |
| int start = 0; | |
| final T _empty; | |
| ChunkedData(T empty) | |
| : data = empty, | |
| _empty = empty; | |
| bool get isClosed => _isClosed; | |
| @override | |
| void add(T data) { | |
| if (_isClosed) { | |
| throw StateError('Chunked data sink already closed'); | |
| } | |
| if (buffering != 0) { | |
| this.data = join(this.data, data); | |
| } else { | |
| start = end; | |
| this.data = data; | |
| } | |
| end = start + getLength(this.data); | |
| sleep = false; | |
| while (!sleep) { | |
| final h = handler; | |
| handler = null; | |
| if (h == null) { | |
| break; | |
| } | |
| h(); | |
| } | |
| if (buffering == 0) { | |
| // | |
| } | |
| } | |
| @override | |
| void close() { | |
| if (_isClosed) { | |
| return; | |
| } | |
| _isClosed = true; | |
| sleep = false; | |
| while (!sleep) { | |
| final h = handler; | |
| handler = null; | |
| if (h == null) { | |
| break; | |
| } | |
| h(); | |
| } | |
| if (buffering != 0) { | |
| throw StateError('On closing, an incomplete buffering was detected'); | |
| } | |
| final length = getLength(data); | |
| if (length != 0) { | |
| data = _empty; | |
| } | |
| } | |
| int getLength(T data); | |
| T join(T data1, T data2); | |
| } | |
| class ErrorExpectedCharacter extends ParseError { | |
| static const message = 'Expected a character {0}'; | |
| final int char; | |
| const ErrorExpectedCharacter(this.char); | |
| @override | |
| ErrorMessage getErrorMessage(Object? input, int? offset) { | |
| final value = ParseError.escape(char); | |
| final hexValue = char.toRadixString(16); | |
| final argument = '$value (0x$hexValue)'; | |
| return ErrorMessage(0, ErrorExpectedCharacter.message, [argument]); | |
| } | |
| } | |
| class ErrorExpectedEndOfInput extends ParseError { | |
| static const message = 'Expected an end of input'; | |
| const ErrorExpectedEndOfInput(); | |
| @override | |
| ErrorMessage getErrorMessage(Object? input, offset) { | |
| return ErrorMessage(0, ErrorExpectedEndOfInput.message); | |
| } | |
| } | |
| class ErrorExpectedIntegerValue extends ParseError { | |
| static const message = 'Expected an integer value {0}'; | |
| final int size; | |
| final int value; | |
| const ErrorExpectedIntegerValue(this.size, this.value); | |
| @override | |
| ErrorMessage getErrorMessage(Object? input, int? offset) { | |
| var argument = value.toRadixString(16); | |
| if (const [8, 16, 24, 32, 40, 48, 56, 64].contains(size)) { | |
| argument = argument.padLeft(size >> 2, '0'); | |
| } | |
| argument = '0x$argument'; | |
| if (value >= 0 && value <= 0x10ffff) { | |
| argument = '$argument (${ParseError.escape(value)})'; | |
| } | |
| return ErrorMessage(0, ErrorExpectedIntegerValue.message, [argument]); | |
| } | |
| } | |
| class ErrorExpectedTag extends ParseError { | |
| static const message = 'Expected: {0}'; | |
| final String tag; | |
| const ErrorExpectedTag(this.tag); | |
| @override | |
| ErrorMessage getErrorMessage(Object? input, int? offset) { | |
| return ErrorMessage(0, ErrorExpectedTag.message); | |
| } | |
| } | |
| class ErrorExpectedTags extends ParseError { | |
| static const message = 'Expected: {0}'; | |
| final List<String> tags; | |
| const ErrorExpectedTags(this.tags); | |
| @override | |
| ErrorMessage getErrorMessage(Object? input, int? offset) { | |
| final list = tags.map(ParseError.escape).toList(); | |
| list.sort(); | |
| final argument = list.join(', '); | |
| return ErrorMessage(0, ErrorExpectedTags.message, [argument]); | |
| } | |
| } | |
| class ErrorMessage extends ParseError { | |
| final List<Object?> arguments; | |
| @override | |
| final int length; | |
| final String text; | |
| const ErrorMessage(this.length, this.text, [this.arguments = const []]); | |
| @override | |
| ErrorMessage getErrorMessage(Object? input, int? offset) { | |
| return this; | |
| } | |
| @override | |
| String toString() { | |
| var result = text; | |
| for (var i = 0; i < arguments.length; i++) { | |
| final argument = arguments[i]; | |
| result = result.replaceAll('{$i}', argument.toString()); | |
| } | |
| return result; | |
| } | |
| } | |
| class ErrorUnexpectedCharacter extends ParseError { | |
| static const message = 'Unexpected character {0}'; | |
| final int? char; | |
| const ErrorUnexpectedCharacter([this.char]); | |
| @override | |
| ErrorMessage getErrorMessage(Object? input, int? offset) { | |
| var argument = '<?>'; | |
| var char = this.char; | |
| if (input is StringReader && input.hasSource) { | |
| if (offset case final int offset) { | |
| if (offset < input.length) { | |
| char = input.readChar(offset); | |
| } else { | |
| argument = '<EOF>'; | |
| } | |
| } | |
| } | |
| if (char != null) { | |
| final hexValue = char.toRadixString(16); | |
| final value = ParseError.escape(char); | |
| argument = '$value (0x$hexValue)'; | |
| } | |
| return ErrorMessage(0, ErrorUnexpectedCharacter.message, [argument]); | |
| } | |
| } | |
| class ErrorUnexpectedEndOfInput extends ParseError { | |
| static const message = 'Unexpected end of input'; | |
| const ErrorUnexpectedEndOfInput(); | |
| @override | |
| ErrorMessage getErrorMessage(Object? input, int? offset) { | |
| return ErrorMessage(0, ErrorUnexpectedEndOfInput.message); | |
| } | |
| } | |
| class ErrorUnexpectedInput extends ParseError { | |
| static const message = 'Unexpected input'; | |
| @override | |
| final int length; | |
| const ErrorUnexpectedInput(this.length); | |
| @override | |
| ErrorMessage getErrorMessage(Object? input, int? offset) { | |
| return ErrorMessage(length, ErrorUnexpectedInput.message); | |
| } | |
| } | |
| class ErrorUnknownError extends ParseError { | |
| static const message = 'Unknown error'; | |
| const ErrorUnknownError(); | |
| @override | |
| ErrorMessage getErrorMessage(Object? input, int? offset) { | |
| return ErrorMessage(0, ErrorUnknownError.message); | |
| } | |
| } | |
| abstract class ParseError { | |
| const ParseError(); | |
| int get length => 0; | |
| ErrorMessage getErrorMessage(Object? input, int? offset); | |
| @override | |
| String toString() { | |
| final message = getErrorMessage(null, null); | |
| return message.toString(); | |
| } | |
| static String escape(Object? value, [bool quote = true]) { | |
| if (value is int) { | |
| if (value >= 0 && value <= 0xd7ff || | |
| value >= 0xe000 && value <= 0x10ffff) { | |
| value = String.fromCharCode(value); | |
| } else { | |
| return value.toString(); | |
| } | |
| } else if (value is! String) { | |
| return value.toString(); | |
| } | |
| final map = { | |
| '\b': '\\b', | |
| '\f': '\\f', | |
| '\n': '\\n', | |
| '\r': '\\r', | |
| '\t': '\\t', | |
| '\v': '\\v', | |
| }; | |
| var result = value.toString(); | |
| for (final key in map.keys) { | |
| result = result.replaceAll(key, map[key]!); | |
| } | |
| if (quote) { | |
| result = "'$result'"; | |
| } | |
| return result; | |
| } | |
| } | |
| class ParseResult<I, O> { | |
| final String errorMessage; | |
| final List<ErrorMessage> errors; | |
| final int failPos; | |
| final I input; | |
| final bool ok; | |
| final int pos; | |
| final O? result; | |
| ParseResult({ | |
| this.errorMessage = '', | |
| this.errors = const [], | |
| required this.failPos, | |
| required this.input, | |
| required this.ok, | |
| required this.pos, | |
| required this.result, | |
| }); | |
| O getResult() { | |
| if (!ok) { | |
| throw FormatException(errorMessage); | |
| } | |
| return result as O; | |
| } | |
| } | |
| class State<T> { | |
| Object? context; | |
| final List<ParseError?> errors = List.filled(64, null, growable: false); | |
| int errorCount = 0; | |
| int failPos = 0; | |
| final T input; | |
| bool ok = false; | |
| int pos = 0; | |
| State(this.input); | |
| @pragma('vm:prefer-inline') | |
| void fail(ParseError error) { | |
| ok = false; | |
| if (pos >= failPos) { | |
| if (failPos < pos) { | |
| failPos = pos; | |
| errorCount = 0; | |
| } | |
| if (errorCount < errors.length) { | |
| errors[errorCount++] = error; | |
| } | |
| } | |
| } | |
| @pragma('vm:prefer-inline') | |
| void failAll(List<ParseError> errors) { | |
| ok = false; | |
| if (pos >= failPos) { | |
| if (failPos < pos) { | |
| failPos = pos; | |
| errorCount = 0; | |
| } | |
| for (var i = 0; i < errors.length; i++) { | |
| if (errorCount < errors.length) { | |
| this.errors[errorCount++] = errors[i]; | |
| } | |
| } | |
| } | |
| } | |
| @pragma('vm:prefer-inline') | |
| void failAllAt(int offset, List<ParseError> errors) { | |
| ok = false; | |
| if (offset >= failPos) { | |
| if (failPos < offset) { | |
| failPos = offset; | |
| errorCount = 0; | |
| } | |
| for (var i = 0; i < errors.length; i++) { | |
| if (errorCount < errors.length) { | |
| this.errors[errorCount++] = errors[i]; | |
| } | |
| } | |
| } | |
| } | |
| @pragma('vm:prefer-inline') | |
| void failAt(int offset, ParseError error) { | |
| ok = false; | |
| if (offset >= failPos) { | |
| if (failPos < offset) { | |
| failPos = offset; | |
| errorCount = 0; | |
| } | |
| if (errorCount < errors.length) { | |
| errors[errorCount++] = error; | |
| } | |
| } | |
| } | |
| List<ParseError> getErrors() { | |
| return List.generate(errorCount, (i) => errors[i]!); | |
| } | |
| @override | |
| String toString() { | |
| if (input case final StringReader input) { | |
| if (input.hasSource) { | |
| final source = input.source; | |
| if (pos >= source.length) { | |
| return '$pos:'; | |
| } | |
| var length = source.length - pos; | |
| length = length > 40 ? 40 : length; | |
| final string = source.substring(pos, pos + length); | |
| return '$pos:$string'; | |
| } | |
| } | |
| return super.toString(); | |
| } | |
| @pragma('vm:prefer-inline') | |
| // ignore: unused_element | |
| bool _canHandleError(int failPos, int errorCount) { | |
| return failPos == this.failPos | |
| ? errorCount < this.errorCount | |
| : failPos < this.failPos; | |
| } | |
| @pragma('vm:prefer-inline') | |
| // ignore: unused_element | |
| void _removeLastErrors(int failPos, int errorCount) { | |
| if (this.failPos == failPos) { | |
| this.errorCount = errorCount; | |
| } else if (this.failPos > failPos) { | |
| this.errorCount = 0; | |
| } | |
| } | |
| } | |
| abstract interface class StringReader { | |
| factory StringReader(String source) { | |
| return _StringReader(source); | |
| } | |
| int get count; | |
| bool get hasSource; | |
| int get length; | |
| String get source; | |
| int indexOf(String string, int start); | |
| bool matchChar(int char, int offset); | |
| int readChar(int offset); | |
| bool startsWith(String string, [int index = 0]); | |
| String substring(int start, [int? end]); | |
| } | |
| class StringReaderChunkedData extends ChunkedData<StringReader> { | |
| StringReaderChunkedData() : super(StringReader('')); | |
| @override | |
| int getLength(StringReader data) => data.length; | |
| @override | |
| StringReader join(StringReader data1, StringReader data2) => data1.length != 0 | |
| ? StringReader('${data1.source}${data2.source}') | |
| : data2; | |
| } | |
| class _StringReader implements StringReader { | |
| @override | |
| final bool hasSource = true; | |
| @override | |
| final int length; | |
| @override | |
| int count = 0; | |
| @override | |
| final String source; | |
| _StringReader(this.source) : length = source.length; | |
| @override | |
| int indexOf(String string, int start) { | |
| return source.indexOf(string, start); | |
| } | |
| @override | |
| @pragma('vm:prefer-inline') | |
| bool matchChar(int char, int offset) { | |
| if (offset < length) { | |
| final c = source.runeAt(offset); | |
| count = char > 0xffff ? 2 : 1; | |
| if (c == char) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| @override | |
| @pragma('vm:prefer-inline') | |
| int readChar(int offset) { | |
| final result = source.runeAt(offset); | |
| count = result > 0xffff ? 2 : 1; | |
| return result; | |
| } | |
| @override | |
| @pragma('vm:prefer-inline') | |
| bool startsWith(String string, [int index = 0]) { | |
| if (source.startsWith(string, index)) { | |
| count = string.length; | |
| return true; | |
| } | |
| return false; | |
| } | |
| @override | |
| @pragma('vm:prefer-inline') | |
| String substring(int start, [int? end]) { | |
| final result = source.substring(start, end); | |
| count = result.length; | |
| return result; | |
| } | |
| @override | |
| String toString() { | |
| return source; | |
| } | |
| } | |
| extension on String { | |
| @pragma('vm:prefer-inline') | |
| // ignore: unused_element | |
| int runeAt(int index) { | |
| final w1 = codeUnitAt(index++); | |
| if (w1 > 0xd7ff && w1 < 0xe000) { | |
| if (index < length) { | |
| final w2 = codeUnitAt(index); | |
| if ((w2 & 0xfc00) == 0xdc00) { | |
| return 0x10000 + ((w1 & 0x3ff) << 10) + (w2 & 0x3ff); | |
| } | |
| } | |
| throw FormatException('Invalid UTF-16 character', this, index - 1); | |
| } | |
| return w1; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment