A highlighted HTML diff is useful when a reviewer cannot open Vim or you need a static artifact for an offline report. The important detail is easy to miss: Vim’s 2html converter exports the active buffer with its current diff highlighting. It does not automatically create a GitHub-style two-column report containing both files.

What the Vim export contains

  • The active buffer’s text, syntax colors, and diff highlight groups.

  • A standalone HTML document with embedded CSS by default.

  • Only one side’s text unless you separately export both windows.

  • No interactive expand/collapse, comments, or patch application.

  • Potentially sensitive source content and absolute filenames in titles/metadata, so review before sharing.

1. Confirm Vim includes the converter

Any terminalbash
vim --version | head -5
vim --clean -es -c "echo globpath(&runtimepath, 'syntax/2html.vim')" -c qa! 2>&1
VIM - Vi IMproved ...
/usr/share/vim/vim90/syntax/2html.vim

2html is a runtime script

  • Vim distributions normally ship syntax/2html.vim; minimal packages may omit runtime files.

  • The runtime location/version varies by OS/package.

  • --clean limits user configuration interference, while the packaged runtime remains available.

  • Headless Ex mode can suppress ordinary screen output; redirect diagnostics deliberately.

  • Install the full supported Vim/runtime package rather than downloading an unreviewed script.

2. Export one side of a diff

Directory containing old.c and new.cbash
vim -Nu NONE -n -d -- old.c new.c \
  -c "runtime syntax/2html.vim" \
  -c "w! old-side-diff.html" \
  -c "qa!"
Creates old-side-diff.html containing the active old.c buffer with diff highlights.

The command is deterministic about inputs and output

  • -d opens the two inputs in diff mode.

  • -- ends option parsing so filenames beginning with a dash are not treated as options.

  • -Nu NONE avoids user vimrc changes; packaged plugins are not automatically loaded, so the runtime script is invoked directly.

  • -n avoids swap-file creation for this read-only report task.

  • The first window is active initially, so its text is converted.

  • w! overwrites the named report; choose a new/reviewed destination and never an input filename.

  • qa! exits all windows after the HTML buffer is written.

3. Use a wrapper that validates paths

vimdiff-to-html.shbash
#!/usr/bin/env bash
set -euo pipefail
 
if (( $# != 3 )); then
  printf "Usage: %s OLD_FILE NEW_FILE OUTPUT_HTML\n" "$0" >&2
  exit 64
fi
 
old_file=$1
new_file=$2
output=$3
 
[[ -f $old_file ]] || { printf "Missing: %s\n" "$old_file" >&2; exit 66; }
[[ -f $new_file ]] || { printf "Missing: %s\n" "$new_file" >&2; exit 66; }
[[ $output != "$old_file" && $output != "$new_file" ]] || {
  printf "Output must differ from both inputs\n" >&2
  exit 64
}
 
mkdir -p -- "$(dirname -- "$output")"
 
vim -Nu NONE -n -d -- "$old_file" "$new_file" \
  -c "runtime syntax/2html.vim" \
  -c "execute 'write! ' . fnameescape('$output')" \
  -c "qa!"

The shell handles filenames; Vim handles escaping

  • Strict mode stops on failures, unset variables, and failed pipelines.

  • Exactly three positional arguments make the interface unambiguous.

  • Regular-file checks prevent confusing new empty buffers.

  • The output/input comparison blocks obvious overwrites; production code can additionally canonicalize paths/symlinks.

  • Shell quoting preserves spaces and glob characters.

  • fnameescape() protects Vim command-special characters in the output path.

  • Do not pass untrusted paths into hand-built Ex command strings without stronger validation.

4. Run and verify the report

Project directorybash
chmod u+x vimdiff-to-html.sh
./vimdiff-to-html.sh first/a.c second/a.c reports/a-old.html
file reports/a-old.html
rg -n "<!DOCTYPE html>|DiffAdd|DiffChange|DiffDelete" reports/a-old.html | head
reports/a-old.html: HTML document, Unicode text, UTF-8 text

Verification checks format and diff styles

  • Making the reviewed local script executable changes its mode, not its content.

  • file confirms the result resembles HTML rather than an accidentally copied source file.

  • The search checks document markup and common Vim diff highlight CSS classes.

  • Open the file in a browser and compare changed/added/deleted regions against vimdiff.

  • A lack of diff classes may mean identical files or the conversion did not retain diff highlighting.

  • Treat the HTML as code disclosure; store/share it under the source repository’s access policy.

5. Batch matching files safely

batch-vimdiff-html.shbash
#!/usr/bin/env bash
set -euo pipefail
 
left=${1:?left tree required}
right=${2:?right tree required}
reports=${3:?report directory required}
 
mkdir -p -- "$reports"
 
find "$left" -type f -name "*.c" -print0 |
while IFS= read -r -d "" left_file; do
  relative=${left_file#"$left"/}
  right_file=$right/$relative
  output=$reports/${relative%.c}.html
 
  if [[ ! -f $right_file ]]; then
    printf "Missing counterpart: %s\n" "$right_file" >&2
    continue
  fi
 
  ./vimdiff-to-html.sh "$left_file" "$right_file" "$output"
done

Null-delimited traversal preserves real filenames

  • find -print0 plus read -d "" handles spaces, tabs, quotes, and newlines.

  • No for f in $(find ...) word splitting occurs.

  • The relative path preserves subdirectories and prevents same-basename report collisions.

  • Missing counterparts are reported explicitly instead of opening empty Vim buffers.

  • The script never changes working directory inside the loop.

  • It creates needed parent directories through the single-file wrapper.

  • It does not delete a previous report tree; use a fresh build directory or explicit reviewed cleanup policy.

Handle files that exist on only one side

  • A missing right-side file represents deletion/addition at tree level, not a normal two-file content comparison.

  • Record missing paths in an index or separate added/deleted list.

  • To visualize against empty content, create an explicit temporary empty file with mktemp, label the report clearly, and remove the temporary file.

  • Do not create placeholder files inside either source tree.

  • Use git diff --no-index or a directory-diff tool when tree-level additions/deletions are central.

Export the other side separately

Because 2html converts the active buffer, a complete static record can include an “old-side” and “new-side” HTML file. Switch to the other diff window before invoking the converter, or run a second deterministic command with input order reversed. Label outputs clearly; neither artifact is a two-column HTML diff.

When you need a true side-by-side HTML report

  • Use a dedicated, reviewed HTML diff generator that models both inputs and line mapping.

  • Pin its version/configuration and treat generated HTML as potentially active content.

  • Escape all source text and avoid unsafe inline script injection.

  • Verify moved/added/deleted lines, binary files, large files, encoding, tabs, and long lines.

  • Keep Vim 2html for faithful syntax/diff highlighting of one active buffer, not for capabilities it does not claim.

Appearance controls worth testing

  • Colorscheme and background mode affect exported colors.

  • Line numbers, wrapping, tabs, folds, and conceal can alter the snapshot.

  • Syntax detection depends on filetype/runtime settings.

  • User vimrc/plugins can make reports non-reproducible; use explicit minimal configuration.

  • Dark reports must retain contrast when printed or viewed under browser forced-color/high-contrast modes.

  • Avoid exporting invisible secrets hidden only by folds/conceal; source remains in the HTML.

Encoding and binary boundaries

2html is intended for text buffers. Establish the input encoding before conversion and verify non-ASCII identifiers/comments in the browser. Detect binary or undecodable files before Vim, then report them separately or use a binary-aware comparison tool rather than embedding corrupted bytes in HTML.

Text-report checks

  • Confirm both inputs decode under the intended encoding.

  • Verify the generated document declares UTF-8 and renders representative non-ASCII text.

  • Reject NUL-containing/binary inputs from this pipeline.

  • Test tabs, combining characters, bidirectional text, and very long lines where the codebase uses them.

  • Preserve source bytes untouched; conversion happens only in Vim buffers/output.

Reproducible report metadata

Record the two source revisions or checksums, relative paths, Vim version, colorscheme, converter settings, generation timestamp, and wrapper revision outside or inside a reviewed index. Without provenance, a beautiful report cannot prove which files it compared.

Generate an index without injecting filenames

A batch is easier to review with an index listing matched, missing-left, missing-right, failed, and generated reports. Escape filenames as HTML text and URL-encode link targets; never concatenate arbitrary repository paths into markup because filenames can contain HTML-significant characters.

Index invariants

  • Every compared relative path maps to at most one predictable artifact.

  • Counts reconcile across matched, missing, skipped, and failed categories.

  • Link text is HTML-escaped and URLs are encoded safely.

  • The index contains no absolute workstation paths or secret environment values.

  • A nonzero failed count makes CI fail rather than publishing a partial “success.”

Browser security and accessibility

Generated source HTML should be treated as untrusted content even when the converter escapes buffer text. Prefer download/offline artifacts or a sandboxed separate origin with a restrictive Content Security Policy. Check text contrast, zoom, keyboard scrolling, printed output, and whether color is the only signal distinguishing changes.

Publication controls

  • Serve reports only to authorized reviewers.

  • Use a restrictive CSP and disable script execution when no interactivity is required.

  • Set a safe content type and avoid same-origin privilege with sensitive applications.

  • Retain textual diff markers or labels so changes are understandable without color.

  • Expire artifacts according to source-code retention policy and revoke shared links when review ends.

Common failures

  • Output contains source text, not HTML: the converter command was unavailable/not executed; invoke the runtime script and inspect Vim errors.

  • Only one file appears: expected—2html exports the active buffer.

  • No highlights: files may be identical, diff state was lost, or the colorscheme rendered indistinct styles.

  • Filename with spaces breaks: quote paths and use null-delimited batch traversal.

  • Nested files overwrite reports: preserve relative directories, not only basenames.

  • Vim hangs: ensure all Ex commands are supplied and finish with qa!; use CI timeout as a guard.

  • Report exposes local paths: inspect/redact title metadata before external distribution.

  • Batch erased old data: never aim destructive cleanup at a reused/unvalidated report path.

CI and publication checklist

  • Vim/runtime version is recorded or containerized.

  • Inputs are immutable and output lives in an isolated artifact directory.

  • Wrapper validates both inputs and blocks direct overwrite.

  • Batch traversal is null-delimited and preserves relative paths.

  • Missing counterpart files are reported.

  • Each report begins as HTML and contains expected diff styling when differences exist.

  • Reports are compared to a textual diff and browser-inspected.

  • Secrets, proprietary code, absolute paths, and license restrictions are reviewed before sharing.

  • Artifacts have retention/access controls and are not blindly served as trusted same-origin HTML.

Primary references

  • Vim’s built-in :help 2html documents conversion options and behavior in the installed runtime.

  • Vim’s syntax/2html.vim runtime script is the authoritative implementation shipped with the selected Vim version.

  • Vim documentation provides searchable help for diff mode, Ex commands, filename escaping, and HTML conversion.

  • GNU findutils manual documents null-delimited filename handling.

  • Bash manual documents strict-mode-related shell behavior, quoting, and parameter expansion.