renderDiffHtml function
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? decorateLine()?,
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());
}