#!/bin/bash
# convert-diff - Batch diff converter for markdown files
# Compares pairs of files and outputs HTML diffs using the same template as convert script
# ===== Configuration and variables =====
CUSTOM_TEMPLATE=""
NO_TOC=false
# Function to show help
show_help() {
echo "Batch Markdown Diff Converter"
echo "Compares pairs of markdown files and outputs HTML diffs using the same template as convert script"
echo "Usage: $0 [-f] [-o outputdir] [-T template] [--no-toc] file1_rev_a.md file1_rev_b.md [file2_rev_a.md file2_rev_b.md ...]"
echo " -f: Force overwrite existing output files"
echo " -o: Output directory (default: same as first input file)"
echo " -T: Template file path (default: templates/report.html)"
echo " --no-toc: Skip table of contents generation"
echo ""
echo "Arguments:"
echo " Files must be provided in pairs (revision A, revision B)"
echo " Total number of files must be even"
echo " Output files will be named: basename_diff.html"
}
# Function to source ZDDC config files if they exist
source_config_file() {
local config_file="$1"
if [ -f "$config_file" ]; then
echo " → Loading ZDDC configuration from: $config_file"
set -a # automatically export all variables
. "$config_file"
set +a # turn off automatic export
return 0
fi
return 1
}
# Load ZDDC configuration file
load_zddc_config() {
local search_dir="$1"
# Search for zddc.conf then .zddc.conf in the search directory
if source_config_file "$search_dir/zddc.conf"; then
return 0
elif source_config_file "$search_dir/.zddc.conf"; then
return 0
fi
# No config file found - continue with defaults
return 1
}
# Function to inject diff controls and styling into HTML
inject_diff_controls() {
local html_file="$1"
if [ ! -f "$html_file" ]; then
echo "Error: HTML file not found: $html_file"
return 1
fi
# Create temporary files for each component
local temp_dir=$(mktemp -d)
local css_file="$temp_dir/diff.css"
local controls_file="$temp_dir/controls.html"
local js_file="$temp_dir/diff.js"
# Write CSS to file
cat > "$css_file" << 'EOF'
/* Diff element styling */
del {
background-color: #f8d7da;
color: #721c24;
text-decoration: line-through;
padding: 0.1em 0.2em;
border-radius: 0.2em;
}
u {
background-color: #d4edda;
color: #155724;
text-decoration: none;
padding: 0.1em 0.2em;
border-radius: 0.2em;
}
/* Dark mode diff styling */
@media (prefers-color-scheme: dark) {
u {
background-color: #1e4620;
color: #75dd79;
}
del {
background-color: #4a1e1e;
color: #f97583;
}
}
/* View mode controls */
.diff-controls {
position: fixed;
top: 20px;
right: 20px;
background: var(--bg-secondary, #ffffff);
border: 1px solid var(--border-color, #e1e5e9);
border-radius: 8px;
padding: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
font-size: 0.875rem;
}
.diff-controls h4 {
margin: 0 0 8px 0;
font-size: 0.875rem;
font-weight: 600;
color: var(--text-primary, #333);
}
.view-mode-buttons {
display: flex;
gap: 4px;
}
.view-mode-btn {
padding: 6px 12px;
border: 1px solid var(--border-color, #e1e5e9);
background: var(--bg-primary, #ffffff);
color: var(--text-color, #333);
border-radius: 4px;
cursor: pointer;
font-size: 0.75rem;
transition: all 0.2s;
}
.view-mode-btn:hover {
background: var(--hover-bg, #f8f9fa);
}
.view-mode-btn.active {
background: var(--primary-color, #007bff);
color: white;
border-color: var(--primary-color, #007bff);
}
/* View mode states */
body.view-original u {
display: none;
}
body.view-original del {
text-decoration: none;
}
body.view-final del {
display: none;
}
EOF
# Write controls HTML to file
cat > "$controls_file" << 'EOF'
View Mode
EOF
# Write JavaScript to file
cat > "$js_file" << 'EOF'
EOF
# Create working copy
local temp_html="$temp_dir/temp.html"
cp "$html_file" "$temp_html"
# Insert CSS before
sed -i '/<\/head>/r '"$css_file" "$temp_html"
# Insert controls after opening tag
sed -i '/]*>/r '"$controls_file" "$temp_html"
# Insert JavaScript before
sed -i '/<\/body>/r '"$js_file" "$temp_html"
# Replace original file
mv "$temp_html" "$html_file"
# Cleanup
rm -rf "$temp_dir"
echo " ✓ Diff controls injected successfully"
}
# Source global ZDDC config from current working directory
load_zddc_config "$(pwd)"
# Resolve script directory for template discovery
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
# Resolve symlink if needed
if [ -L "$0" ]; then
SCRIPT_TARGET=$(readlink -f "$0")
SCRIPT_TARGET_DIR=$(dirname "$SCRIPT_TARGET")
else
SCRIPT_TARGET_DIR="$SCRIPT_DIR"
fi
# Parse arguments
FORCE_OVERWRITE=false
OUTPUT_DIR=""
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
show_help
exit 0
;;
-f)
FORCE_OVERWRITE=true
echo "Force overwrite mode: ON"
shift
;;
-o)
OUTPUT_DIR="$2"
echo "Output directory: $OUTPUT_DIR"
shift 2
;;
-T)
CUSTOM_TEMPLATE="$2"
echo "Custom template: $CUSTOM_TEMPLATE"
shift 2
;;
--no-toc)
NO_TOC=true
echo "Table of contents: DISABLED"
shift
;;
-*)
echo "Unknown option: $1"
show_help
exit 1
;;
*)
break
;;
esac
done
if [ "$FORCE_OVERWRITE" = "false" ]; then
echo "Force overwrite mode: OFF (will skip existing output files)"
fi
if [ -z "$OUTPUT_DIR" ]; then
echo "Output directory: same as input files"
fi
# Validate file count
if [ $# -eq 0 ]; then
echo "Error: No input files specified"
show_help
exit 1
fi
if [ $(($# % 2)) -ne 0 ]; then
echo "Error: Number of files must be even (pairs of files for comparison)"
echo "Provided $# files, but need pairs"
show_help
exit 1
fi
TOTAL_PAIRS=$(($# / 2))
echo "Processing $TOTAL_PAIRS file pairs..."
SUCCESSFUL=0
FAILED=0
SKIPPED=0
# Process files in pairs
while [ $# -gt 0 ]; do
FILE1="$1"
FILE2="$2"
shift 2
echo ""
echo "Processing pair: $(basename "$FILE1") vs $(basename "$FILE2")"
# Validate input files exist
if [ ! -f "$FILE1" ]; then
echo " ✗ First file not found: $FILE1"
FAILED=$((FAILED + 1))
continue
fi
if [ ! -f "$FILE2" ]; then
echo " ✗ Second file not found: $FILE2"
FAILED=$((FAILED + 1))
continue
fi
# Extract base filename for output
BASENAME1=$(basename "$FILE1" .md)
BASENAME2=$(basename "$FILE2" .md)
# Generate output filename with _diff suffix (ZDDC compliant)
OUTPUT_BASENAME="${BASENAME1}_diff.html"
if [ -n "$OUTPUT_DIR" ]; then
OUTPUT_FILE="$OUTPUT_DIR/${OUTPUT_BASENAME}"
else
FILE1_DIR=$(dirname "$FILE1")
OUTPUT_FILE="$FILE1_DIR/${OUTPUT_BASENAME}"
fi
echo " → Output file: $OUTPUT_FILE"
# Check if output file already exists and skip if not forcing overwrite
if [ -f "$OUTPUT_FILE" ] && [ "$FORCE_OVERWRITE" = "false" ]; then
echo " → Output file already exists, skipping (use -f to overwrite)"
SKIPPED=$((SKIPPED + 1))
continue
fi
# Load ZDDC configuration from first file's directory
# (load_zddc_config logs the path itself, but only when a config is found)
FILE1_DIR=$(dirname "$FILE1")
load_zddc_config "$FILE1_DIR"
# Determine template to use. Diffs render with the report template (its
# _head/_doc/_scripts partials live alongside it in templates/, so pandoc
# resolves them from the template's own directory).
TEMPLATE_ABS=""
if [ -n "$CUSTOM_TEMPLATE" ]; then
if [ -f "$CUSTOM_TEMPLATE" ]; then
TEMPLATE_ABS="$CUSTOM_TEMPLATE"
echo " → Using custom template: $TEMPLATE_ABS"
else
echo " → Custom template not found: $CUSTOM_TEMPLATE; falling back to default"
fi
fi
if [ -z "$TEMPLATE_ABS" ]; then
for _tdir in "$SCRIPT_DIR/templates" "$SCRIPT_TARGET_DIR/templates"; do
if [ -f "$_tdir/report.html" ]; then
TEMPLATE_ABS="$_tdir/report.html"
break
fi
done
fi
# Create temp file for pandiff output
TEMP_DIFF=$(mktemp)
# Create temp files for metadata extraction (unique per invocation)
TEMP_METADATA_REV1=$(mktemp)
TEMP_METADATA_REV2=$(mktemp)
echo "Stage 1: Generating diff with pandiff..."
# Run pandiff to generate HTML diff
if ! pandiff --to html "$FILE1" "$FILE2" > "$TEMP_DIFF"; then
echo " ✗ Failed to generate diff"
rm -f "$TEMP_DIFF" "$TEMP_METADATA_REV1" "$TEMP_METADATA_REV2"
FAILED=$((FAILED + 1))
continue
fi
echo " ✓ Diff generated successfully"
echo "Stage 2: Adding TOC and styling with pandoc..."
# Extract metadata from both files (safe - no eval, uses heredoc)
{
# Extract YAML frontmatter and parse fields safely
awk '/^---$/{if(NR==1){p=1}else{p=0}} p && !/^---$/{print}' "$FILE1" > "$TEMP_METADATA_REV1"
rev1_tracking_number=$(grep '^tracking_number:' "$TEMP_METADATA_REV1" | sed 's/^tracking_number: *"\(.*\)"$/\1/' | head -1)
rev1_title=$(grep '^title:' "$TEMP_METADATA_REV1" | sed 's/^title: *"\(.*\)"$/\1/' | head -1)
rev1_revision=$(grep '^revision:' "$TEMP_METADATA_REV1" | sed 's/^revision: *"\(.*\)"$/\1/' | head -1)
rev1_status=$(grep '^status:' "$TEMP_METADATA_REV1" | sed 's/^status: *"\(.*\)"$/\1/' | head -1)
rev1_project=$(grep '^project:' "$TEMP_METADATA_REV1" | sed 's/^project: *"\(.*\)"$/\1/' | head -1)
}
{
awk '/^---$/{if(NR==1){p=1}else{p=0}} p && !/^---$/{print}' "$FILE2" > "$TEMP_METADATA_REV2"
rev2_tracking_number=$(grep '^tracking_number:' "$TEMP_METADATA_REV2" | sed 's/^tracking_number: *"\(.*\)"$/\1/' | head -1)
rev2_title=$(grep '^title:' "$TEMP_METADATA_REV2" | sed 's/^title: *"\(.*\)"$/\1/' | head -1)
rev2_revision=$(grep '^revision:' "$TEMP_METADATA_REV2" | sed 's/^revision: *"\(.*\)"$/\1/' | head -1)
rev2_status=$(grep '^status:' "$TEMP_METADATA_REV2" | sed 's/^status: *"\(.*\)"$/\1/' | head -1)
rev2_project=$(grep '^project:' "$TEMP_METADATA_REV2" | sed 's/^project: *"\(.*\)"$/\1/' | head -1)
}
# Clean up metadata temp files
rm -f "$TEMP_METADATA_REV1" "$TEMP_METADATA_REV2"
# Generate diff-aware header HTML with view mode classes
generate_diff_header() {
local header_html=""
# Project title (should be same for both). Append the project number from
# zddc.conf when set, e.g. "Project Name (AR 28088)"; omit the parens otherwise.
header_html="
"
# Document title with diff
if [ "$rev1_title" != "$rev2_title" ]; then
header_html="$header_html
$rev1_title$rev2_title
"
else
header_html="$header_html
$rev2_title
"
fi
# Tracking number and revision with diff
local tracking_rev_line=""
if [ "$rev1_tracking_number" != "$rev2_tracking_number" ]; then
tracking_rev_line="$rev1_tracking_number$rev2_tracking_number"
else
tracking_rev_line="$rev1_tracking_number"
fi
if [ "$rev1_revision" != "$rev2_revision" ]; then
tracking_rev_line="$tracking_rev_line Revision: $rev1_revision$rev2_revision"
else
tracking_rev_line="$tracking_rev_line Revision: $rev1_revision"
fi
if [ "$rev1_status" != "$rev2_status" ]; then
tracking_rev_line="$tracking_rev_line Status: $rev1_status$rev2_status"
else
tracking_rev_line="$tracking_rev_line Status: $rev1_status"
fi
header_html="$header_html
$tracking_rev_line
"
# Add draft marker if revision contains ~
if echo "$rev2_revision" | grep -q "~"; then
header_html="$header_html
[DRAFT Generated at $(LC_TIME=C date '+%B %d, %Y at %I:%M:%S %p %Z')]
"
fi
echo "$header_html"
}
DIFF_HEADER_HTML=$(generate_diff_header)
# Generate timestamp for conversion (force English locale, matching convert)
GENERATION_TIME=$(LC_TIME=C date '+%B %d, %Y at %I:%M:%S %p %Z')
# Set resource path to second file directory for resource resolution
FILE2_DIR=$(dirname "$FILE2")
# Build pandoc command as array (not string with eval). Header HTML is passed
# as a single array element below, so no shell escaping is needed — escaping the
# quotes here would leak backslashes into the rendered output.
PANDOC_ARGS=(
"pandoc" "$TEMP_DIFF" "-o" "$OUTPUT_FILE"
"--from" "html"
"--standalone"
)
# Only pass --template when one was actually found; pandoc errors on an empty
# --template= value, so fall back to its default template otherwise.
if [ -n "$TEMPLATE_ABS" ]; then
PANDOC_ARGS+=("--template=$TEMPLATE_ABS")
else
echo " ⚠ Warning: templates/report.html not found, using pandoc default template"
fi
# Add TOC args if not disabled
if [ "$NO_TOC" != "true" ]; then
PANDOC_ARGS+=("--toc" "--toc-depth=3")
fi
PANDOC_ARGS+=(
"--resource-path=$FILE2_DIR"
"--metadata" "title=$rev2_title"
"--metadata" "generation_time=$GENERATION_TIME"
"--metadata" "diff_mode=true"
"--metadata" "custom_header=$DIFF_HEADER_HTML"
)
# Add ZDDC configuration variables from zddc.conf (only once)
if [ -n "$client" ]; then
PANDOC_ARGS+=("--variable" "client=$client")
fi
if [ -n "$project" ]; then
PANDOC_ARGS+=("--variable" "project=$project")
fi
if [ -n "$contractor" ]; then
PANDOC_ARGS+=("--variable" "contractor=$contractor")
fi
if [ -n "$project_number" ]; then
PANDOC_ARGS+=("--variable" "project_number=$project_number")
fi
# Pass TOC status to template
if [ "$NO_TOC" = "true" ]; then
PANDOC_ARGS+=("--variable" "no-toc=true")
fi
PANDOC_ARGS+=("--section-divs" "--html-q-tags")
# Execute pandoc via array (no eval)
if "${PANDOC_ARGS[@]}"; then
echo " ✓ HTML generated successfully"
echo "Stage 3: Adding diff view controls..."
inject_diff_controls "$OUTPUT_FILE"
echo " ✓ Successfully created diff HTML: $(basename "$OUTPUT_FILE")"
SUCCESSFUL=$((SUCCESSFUL + 1))
else
echo " ✗ Failed to convert diff to HTML"
FAILED=$((FAILED + 1))
fi
# Clean up temp file
rm -f "$TEMP_DIFF"
done
echo ""
echo "=========================================="
echo "DIFF CONVERSION SUMMARY"
echo "=========================================="
echo "Total pairs processed: $TOTAL_PAIRS"
echo "Successful conversions: $SUCCESSFUL"
echo "Failed conversions: $FAILED"
echo "Skipped (existing files): $SKIPPED"
echo "=========================================="