#!/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="
${rev2_project}${project_number:+ ($project_number)}
" # 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 "=========================================="