renderDiffHtml function

String renderDiffHtml(
  1. Iterable<FileDiff> files, {
  2. required Highlighter highlighter,
  3. required Map<String, SyntaxTheme> themes,
  4. DiffMode mode = DiffMode.unified,
  5. InlineDiffMode inlineDiffMode = InlineDiffMode.mergedWords,
  6. bool expandAll = false,
  7. bool wrap = false,
  8. HtmlDecoration? decorateLine(
    1. FileDiff file,
    2. DiffLine line
    )?,
})

Renders a static file patch with unified or aligned split rows.

Implementation

String renderDiffHtml(
  Iterable<FileDiff> files, {
  required Highlighter highlighter,
  required Map<String, SyntaxTheme> themes,
  DiffMode mode = DiffMode.unified,
  InlineDiffMode inlineDiffMode = InlineDiffMode.mergedWords,
  bool expandAll = false,
  bool wrap = false,
  HtmlDecoration? Function(FileDiff file, DiffLine line)? decorateLine,
}) {
  final list = files.toList();
  final output = _HtmlOutput(themes, wrap: wrap);
  final caches = {
    for (final entry in themes.entries)
      entry.key: DiffHighlightCache(
        highlighter: highlighter,
        theme: entry.value,
        inlineDiffMode: inlineDiffMode,
      ),
  };
  String lineHtml(FileDiff file, DiffLine? line, {bool old = false}) {
    if (line == null) return '<td class="empty"></td>';
    // Split tokens wherever any theme's style or inline-diff state changes.
    final segmentsByTheme = {
      for (final entry in caches.entries)
        entry.key: entry.value.segmentsFor(file, line),
    };
    final indices = {for (final key in themes.keys) key: 0};
    final ends = {
      for (final entry in segmentsByTheme.entries)
        entry.key: entry.value.isEmpty ? 0 : entry.value.first.text.length,
    };
    var pos = 0;
    final code = StringBuffer();
    while (pos < line.text.length) {
      var next = line.text.length;
      for (final entry in segmentsByTheme.entries) {
        final key = entry.key;
        final segs = entry.value;
        var index = indices[key]!;
        while (index < segs.length && ends[key]! <= pos) {
          index++;
          if (index < segs.length) {
            ends[key] = ends[key]! + segs[index].text.length;
          }
        }
        indices[key] = index;
        if (index < segs.length && ends[key]! < next) {
          next = ends[key]!;
        }
      }
      final changed =
          segmentsByTheme.values.first[indices[themes.keys.first]!].changed;
      code.write(
        output.token(line.text.substring(pos, next), {
          for (final entry in segmentsByTheme.entries)
            entry.key: entry.value[indices[entry.key]!].style,
        }, changed ? const HtmlDecoration(className: 'changed') : null),
      );
      pos = next;
    }
    final decoration = decorateLine?.call(file, line);
    final number = old ? line.oldNumber : line.newNumber ?? line.oldNumber;
    return '<td class="${line.kind.name} ${_escape(decoration?.className ?? '')}"${decoration?.title == null ? '' : ' title="${_escape(decoration!.title!)}"'}>'
        '<span class="gutter" aria-hidden="true">${number ?? ''} ${line.kind == DiffLineKind.added
            ? '+'
            : line.kind == DiffLineKind.removed
            ? '−'
            : ' '}</span>'
        '${_escape(decoration?.before ?? '')}<code>$code</code>${_escape(decoration?.after ?? '')}</td>';
  }

  final body = StringBuffer('<table class="diff"><tbody>');
  final columns = mode == DiffMode.split ? 2 : 1;
  for (final row in DiffLayout(list, mode: mode, expandAll: expandAll)) {
    final file = list[row.file];
    body.write('<tr data-file="${_escape(file.id)}">');
    switch (row) {
      case DiffFileHeaderRow():
        body.write('<th colspan="$columns">${_escape(file.path)}</th>');
      case DiffHunkHeaderRow(:final hunk):
        body.write(
          '<td class="separator" colspan="$columns">${_escape(hunk.header)}</td>',
        );
      case DiffGapRow(:final count):
        body.write(
          '<td class="separator" colspan="$columns">$count unchanged lines</td>',
        );
      case DiffNoticeRow(:final notice):
        body.write('<td colspan="$columns">${_escape(notice.name)}</td>');
      case DiffLineRow(:final line):
        body.write(lineHtml(file, line));
      case DiffPairRow(:final left, :final right):
        body
          ..write(lineHtml(file, left, old: true))
          ..write(lineHtml(file, right));
    }
    body.write('</tr>');
  }
  body.write('</tbody></table>');
  return output.finish(body.toString());
}