tokenizeExpression function

List<ExpressionToken> tokenizeExpression(
  1. String source, {
  2. ExpressionSyntax syntax = ExpressionSyntax.standard,
})

source as tokens, ending with one of kind end.

Throws ExpressionSyntaxException for a character syntax does not take.

Implementation

List<ExpressionToken> tokenizeExpression(
  String source, {
  ExpressionSyntax syntax = ExpressionSyntax.standard,
}) {
  final tokens = <ExpressionToken>[];
  var line = 1;
  var column = 1;
  var at = 0;
  final single = '(),.+-*/${syntax.punctuation}';
  const pairs = <String>['<=', '>=', '==', '!=', '&&', '||'];
  const logical = '<>!?:';

  void advance(int count) {
    for (var i = 0; i < count; i++) {
      if (source[at + i] == '\n') {
        line++;
        column = 1;
      } else {
        column++;
      }
    }
    at += count;
  }

  while (at < source.length) {
    final ch = source[at];
    if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') {
      advance(1);
      continue;
    }
    // `//` to the end of the line. A file is a thing an author comments,
    // and a language with no comments is one whose files grow a README
    // beside them.
    if (ch == '/' && at + 1 < source.length && source[at + 1] == '/') {
      while (at < source.length && source[at] != '\n') {
        advance(1);
      }
      continue;
    }

    final startLine = line;
    final startColumn = column;
    if (_isDigit(ch) ||
        (ch == '.' && at + 1 < source.length && _isDigit(source[at + 1]))) {
      var end = at;
      while (end < source.length &&
          (_isDigit(source[end]) || source[end] == '.')) {
        end++;
      }
      if (syntax.exponents &&
          end < source.length &&
          (source[end] == 'e' || source[end] == 'E')) {
        var digits = end + 1;
        if (digits < source.length &&
            (source[digits] == '+' || source[digits] == '-')) {
          digits++;
        }
        if (digits < source.length && _isDigit(source[digits])) {
          end = digits;
          while (end < source.length && _isDigit(source[end])) {
            end++;
          }
        }
      }
      final text = source.substring(at, end);
      final value = double.tryParse(text);
      if (value == null) {
        throw ExpressionSyntaxException(
          '"$text" is not a number.',
          startLine,
          startColumn,
        );
      }
      advance(end - at);
      tokens.add(
        ExpressionToken('number', text, startLine, startColumn, number: value),
      );
      continue;
    }
    if (_isNameStart(ch)) {
      var end = at;
      while (end < source.length && _isNamePart(source[end])) {
        end++;
      }
      final text = source.substring(at, end);
      advance(end - at);
      tokens.add(ExpressionToken('name', text, startLine, startColumn));
      continue;
    }
    if (syntax.units && ch == '[') {
      final close = source.indexOf(']', at);
      final newline = source.indexOf('\n', at);
      if (close < 0 || (newline >= 0 && newline < close)) {
        throw ExpressionSyntaxException(
          'a unit in "[" is never closed with "]".',
          startLine,
          startColumn,
        );
      }
      final text = source.substring(at + 1, close).trim();
      advance(close + 1 - at);
      tokens.add(ExpressionToken('unit', text, startLine, startColumn));
      continue;
    }
    if (syntax.logic && at + 1 < source.length) {
      final pair = source.substring(at, at + 2);
      if (pairs.contains(pair)) {
        advance(2);
        tokens.add(ExpressionToken(pair, pair, startLine, startColumn));
        continue;
      }
    }
    if (single.contains(ch) || (syntax.logic && logical.contains(ch))) {
      advance(1);
      tokens.add(ExpressionToken(ch, ch, startLine, startColumn));
      continue;
    }
    throw ExpressionSyntaxException(
      '"$ch" is not part of this language.',
      startLine,
      startColumn,
    );
  }
  tokens.add(ExpressionToken('end', '', line, column));
  return tokens;
}