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
vim --version | head -5
vim --clean -es -c "echo globpath(&runtimepath, 'syntax/2html.vim')" -c qa! 2>&1VIM - Vi IMproved ...
/usr/share/vim/vim90/syntax/2html.vim2html 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.
--cleanlimits 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
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
-dopens the two inputs in diff mode.--ends option parsing so filenames beginning with a dash are not treated as options.-Nu NONEavoids user vimrc changes; packaged plugins are not automatically loaded, so the runtime script is invoked directly.-navoids 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
#!/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
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 | headreports/a-old.html: HTML document, Unicode text, UTF-8 textVerification checks format and diff styles
Making the reviewed local script executable changes its mode, not its content.
fileconfirms 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
#!/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"
doneNull-delimited traversal preserves real filenames
find -print0plusread -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-indexor 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 2htmldocuments conversion options and behavior in the installed runtime.Vim’s
syntax/2html.vimruntime 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.
Comments and corrections