parseInline function

List<MdNode> parseInline(
  1. String text,
  2. bool useDollar
)

Implementation

List<MdNode> parseInline(String text, bool useDollar) {
  final n = text.length;

  // Most runs of an assistant's prose contain no markup at all. Finding that
  // out costs one scan, and skips the buffer, the delimiter tables and the
  // dispatch loop entirely.
  var plainUntil = 0;
  while (plainUntil < n && !_canStartConstruct(text.codeUnitAt(plainUntil))) {
    plainUntil += 1;
  }
  if (plainUntil == n) {
    return n == 0 ? <MdNode>[] : <MdNode>[MdText(text: text)];
  }

  final delims = _Delims(text);
  final nodes = <MdNode>[];
  final buf = StringBuffer();
  var i = 0;

  void flush() {
    if (buf.isNotEmpty) {
      nodes.add(MdText(text: buf.toString()));
      buf.clear();
    }
  }

  while (i < n) {
    final c = text.codeUnitAt(i);

    // Fast path: a run of characters that cannot begin a construct is copied
    // through in one piece. This is the bulk of ordinary prose, and skipping
    // the dispatch chain for it is what keeps the parser's cost close to a
    // scan.
    if (!_canStartConstruct(c)) {
      var j = i + 1;
      while (j < n && !_canStartConstruct(text.codeUnitAt(j))) {
        j += 1;
      }
      buf.write(text.substring(i, j));
      i = j;
      continue;
    }

    var matched = false;

    // ![alt](url)
    if (c == _bang &&
        i + 1 < n &&
        text.codeUnitAt(i + 1) == _openBracket &&
        delims.bracket.containsKey(i + 1)) {
      final r = _tryImage(text, i, delims);
      if (r != null) {
        flush();
        nodes.add(r.node);
        i = r.next;
        matched = true;
      }
    }

    // [text](url)  or  [123] source tag
    if (!matched && c == _openBracket) {
      final link = _tryLink(text, i, useDollar, delims);
      if (link != null) {
        flush();
        nodes.add(link.node);
        i = link.next;
        matched = true;
      } else {
        final tag = _trySourceTag(text, i);
        if (tag != null) {
          flush();
          nodes.add(tag.node);
          i = tag.next;
          matched = true;
        }
      }
    }

    // **bold**  /  *italic*
    if (!matched && c == _star) {
      // Emphasis is decided by the length of the *run* of asterisks, not by
      // the first one. Reading `***both***` as `**` starting at the second
      // asterisk left a stray `*` inside the bold, and closing a single `*`
      // with `indexOf('*')` landed on the opening half of a nested `**`, which
      // dropped the bold and cut the italic into pieces.
      var run = 1;
      while (i + run < n && text.codeUnitAt(i + run) == _star) {
        run += 1;
      }

      // `***x***` is both.
      if (run >= 3) {
        final close = _nextStarRun(text, i + run, 3);
        if (close != -1) {
          final inner = text.substring(i + 3, close);
          if (inner.trim().isNotEmpty) {
            flush();
            nodes.add(
              MdBold(
                children: [MdItalic(children: parseInline(inner, useDollar))],
              ),
            );
            i = close + 3;
            matched = true;
          }
        }
      }
      if (!matched && run == 2) {
        final close = _nextStarRun(text, i + 2, 2);
        if (close != -1) {
          final inner = text.substring(i + 2, close);
          if (inner.trim().isNotEmpty) {
            flush();
            nodes.add(MdBold(children: parseInline(inner, useDollar)));
            i = close + 2;
            matched = true;
          }
        }
      }
      if (!matched && run == 1) {
        // Only a lone asterisk closes an italic; a `**` inside it opens a
        // bold, which the recursive parse below then claims.
        final close = _nextStarRun(text, i + 1, 1, exact: true);
        if (close != -1) {
          final inner = text.substring(i + 1, close);
          if (inner.trim().isNotEmpty) {
            flush();
            nodes.add(MdItalic(children: parseInline(inner, useDollar)));
            i = close + 1;
            matched = true;
          }
        }
      }
    }

    // ~~strike~~
    if (!matched &&
        c == _tilde &&
        i + 1 < n &&
        text.codeUnitAt(i + 1) == _tilde) {
      final end = text.indexOf('~~', i + 2);
      if (end != -1) {
        flush();
        nodes.add(
          MdStrike(
            children: parseInline(text.substring(i + 2, end), useDollar),
          ),
        );
        i = end + 2;
        matched = true;
      }
    }

    // `code`
    if (!matched && c == _backtick) {
      final end = text.indexOf('`', i + 1);
      if (end != -1) {
        flush();
        nodes.add(MdInlineCode(text: text.substring(i + 1, end)));
        i = end + 1;
        matched = true;
      }
    }

    // <u>underline</u>
    if (!matched && c == _lt && text.startsWith('<u>', i)) {
      final end = text.indexOf('</u>', i + 3);
      if (end != -1) {
        flush();
        nodes.add(
          MdUnderline(
            children: parseInline(text.substring(i + 3, end), useDollar),
          ),
        );
        i = end + 4;
        matched = true;
      }
    }

    // \[ block latex \] in an inline position.
    //
    // The block parser claims `\[` only when it opens a line, so block maths
    // written mid-sentence — or after a list marker, `1. Result: \[ x^2 \]` —
    // used to survive as literal text. The syntax is recognised wherever it
    // appears; it still renders as a block, because that is what it is.
    if (!matched &&
        c == _backslash &&
        i + 1 < n &&
        text.codeUnitAt(i + 1) == _openBracket) {
      final end = text.indexOf('\\]', i + 2);
      if (end != -1) {
        flush();
        nodes.add(MdBlockLatex(tex: text.substring(i + 2, end).trim()));
        i = end + 2;
        matched = true;
      }
    }

    // \( inline latex \)
    if (!matched &&
        c == _backslash &&
        i + 1 < n &&
        text.codeUnitAt(i + 1) == _openParen) {
      final end = text.indexOf('\\)', i + 2);
      if (end != -1) {
        flush();
        nodes.add(MdInlineLatex(tex: text.substring(i + 2, end).trim()));
        i = end + 2;
        matched = true;
      }
    }

    // $$ … $$  /  $ … $  (only when enabled)
    if (!matched && useDollar && c == _dollar) {
      if (i + 1 < n && text.codeUnitAt(i + 1) == _dollar) {
        final end = text.indexOf(r'$$', i + 2);
        if (end != -1) {
          flush();
          nodes.add(MdInlineLatex(tex: text.substring(i + 2, end).trim()));
          i = end + 2;
          matched = true;
        }
      }
      if (!matched) {
        final end = text.indexOf(r'$', i + 1);
        if (end != -1) {
          final inner = text.substring(i + 1, end);
          if (inner.trim().isNotEmpty) {
            flush();
            nodes.add(MdInlineLatex(tex: inner.trim()));
            i = end + 1;
            matched = true;
          }
        }
      }
    }

    // \| — the GFM escape for a literal pipe. Table cells are split before
    // this runs (see _splitPipes in block_parser.dart), which is what lets a
    // pipe reach a cell at all; here the backslash is dropped so the reader
    // sees `|`. Only `|` is unescaped: a general \X rule would change how
    // \*, \_ and friends render across every document.
    if (!matched &&
        c == _backslash &&
        i + 1 < n &&
        text.codeUnitAt(i + 1) == _pipe) {
      buf.writeCharCode(_pipe);
      i += 2;
      matched = true;
    }

    if (!matched) {
      buf.writeCharCode(c);
      i += 1;
    }
  }

  flush();
  return nodes;
}