renderRanges method

List<String> renderRanges(
  1. String source,
  2. Iterable<TreeSitterHtmlHighlight> highlights, {
  3. String? carriageReturnScope = 'carriage-return',
})

Renders Flutter-friendly UTF-16 highlight ranges as Tree-sitter HTML.

This adapter creates the same streaming events consumed by render, then returns the rendered lines. It does not change the native renderer API.

Implementation

List<String> renderRanges(
  String source,
  Iterable<TreeSitterHtmlHighlight> highlights, {
  String? carriageReturnScope = 'carriage-return',
}) {
  reset();
  final exact = <(int, int), TreeSitterHtmlHighlight>{};
  for (final highlight in highlights) {
    if (highlight.startUtf16 < 0 ||
        highlight.endUtf16 < highlight.startUtf16 ||
        highlight.endUtf16 > source.length) {
      throw ArgumentError.value(
        highlight,
        'highlights',
        'Highlight ranges must be in bounds.',
      );
    }
    final key = (highlight.startUtf16, highlight.endUtf16);
    final previous = exact[key];
    if (previous == null ||
        highlight.priority > previous.priority ||
        highlight.priority == previous.priority &&
            highlight.order >= previous.order) {
      exact[key] = highlight;
    }
  }
  final sorted = exact.values.toList()
    ..sort((left, right) {
      final start = left.startUtf16.compareTo(right.startUtf16);
      if (start != 0) return start;
      return right.endUtf16.compareTo(left.endUtf16);
    });
  final nesting = <TreeSitterHtmlHighlight>[];
  for (final highlight in sorted) {
    while (nesting.isNotEmpty &&
        highlight.startUtf16 >= nesting.last.endUtf16) {
      nesting.removeLast();
    }
    if (nesting.isNotEmpty && highlight.endUtf16 > nesting.last.endUtf16) {
      throw ArgumentError.value(
        highlight,
        'highlights',
        'Highlight ranges must not cross.',
      );
    }
    nesting.add(highlight);
  }

  final scopes = <String>[];
  final scopeIndices = <String, int>{};
  for (final highlight in sorted) {
    scopeIndices.putIfAbsent(highlight.scope, () {
      scopes.add(highlight.scope);
      return scopes.length - 1;
    });
  }
  final carriageIndex = carriageReturnScope == null
      ? null
      : scopeIndices.putIfAbsent(carriageReturnScope, () {
          scopes.add(carriageReturnScope);
          return scopes.length - 1;
        });
  setCarriageReturnHighlight(
    carriageIndex == null ? null : TreeSitterHighlight(carriageIndex),
  );

  final utf16ToByte = _utf16ToUtf8ByteOffsets(source);
  final starts = <int, List<TreeSitterHtmlHighlight>>{};
  final ends = <int, List<TreeSitterHtmlHighlight>>{};
  for (final highlight in sorted) {
    starts.putIfAbsent(highlight.startUtf16, () => []).add(highlight);
    ends.putIfAbsent(highlight.endUtf16, () => []).add(highlight);
  }
  final boundaries = <int>{
    0,
    source.length,
    ...starts.keys,
    ...ends.keys,
  }.toList()..sort();
  final events = <TreeSitterHighlightEvent>[];
  var previous = 0;
  for (final boundary in boundaries) {
    if (boundary > previous) {
      events.add(
        TreeSitterHighlightEvent.source(
          start: utf16ToByte[previous],
          end: utf16ToByte[boundary],
        ),
      );
    }
    if (ends[boundary] case final ending?) {
      for (final _ in ending.reversed) {
        events.add(const TreeSitterHighlightEvent.end());
      }
    }
    if (starts[boundary] case final starting?) {
      for (final highlight in starting) {
        events.add(
          TreeSitterHighlightEvent.start(
            TreeSitterHighlight(scopeIndices[highlight.scope]!),
          ),
        );
      }
    }
    previous = boundary;
  }
  render(events, utf8.encode(source), (highlight, output) {
    output.addAll(
      utf8.encode('class=${_escapeAttribute(scopes[highlight.index])}'),
    );
  });
  return lines().toList(growable: false);
}