550 lines
17 KiB
Bash
550 lines
17 KiB
Bash
#!/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'
|
|
<!-- Diff View Controls -->
|
|
<div class="diff-controls">
|
|
<h4>View Mode</h4>
|
|
<div class="view-mode-buttons">
|
|
<button class="view-mode-btn active" data-mode="diff">Diff</button>
|
|
<button class="view-mode-btn" data-mode="original">Original</button>
|
|
<button class="view-mode-btn" data-mode="final">Final</button>
|
|
</div>
|
|
</div>
|
|
EOF
|
|
|
|
# Write JavaScript to file
|
|
cat > "$js_file" << 'EOF'
|
|
<script>
|
|
// View mode toggle functionality
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
var buttons = document.querySelectorAll(".view-mode-btn");
|
|
var body = document.body;
|
|
|
|
buttons.forEach(function(button) {
|
|
button.addEventListener("click", function() {
|
|
// Remove active class from all buttons
|
|
buttons.forEach(function(btn) { btn.classList.remove("active"); });
|
|
|
|
// Add active class to clicked button
|
|
this.classList.add("active");
|
|
|
|
// Update body class for view mode
|
|
body.className = body.className.replace(/view-\w+/g, "");
|
|
if (this.dataset.mode !== "diff") {
|
|
body.classList.add("view-" + this.dataset.mode);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
EOF
|
|
|
|
# Create working copy
|
|
local temp_html="$temp_dir/temp.html"
|
|
cp "$html_file" "$temp_html"
|
|
|
|
# Insert CSS before </head>
|
|
sed -i '/<\/head>/r '"$css_file" "$temp_html"
|
|
|
|
# Insert controls after opening <body> tag
|
|
sed -i '/<body[^>]*>/r '"$controls_file" "$temp_html"
|
|
|
|
# Insert JavaScript before </body>
|
|
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="<div class=\"header-line client-project\">${rev2_project}${project_number:+ ($project_number)}</div>"
|
|
|
|
# Document title with diff
|
|
if [ "$rev1_title" != "$rev2_title" ]; then
|
|
header_html="$header_html<div class=\"header-line document-title\"><del>$rev1_title</del><ins>$rev2_title</ins></div>"
|
|
else
|
|
header_html="$header_html<div class=\"header-line document-title\">$rev2_title</div>"
|
|
fi
|
|
|
|
# Tracking number and revision with diff
|
|
local tracking_rev_line=""
|
|
if [ "$rev1_tracking_number" != "$rev2_tracking_number" ]; then
|
|
tracking_rev_line="<del>$rev1_tracking_number</del><ins>$rev2_tracking_number</ins>"
|
|
else
|
|
tracking_rev_line="$rev1_tracking_number"
|
|
fi
|
|
|
|
if [ "$rev1_revision" != "$rev2_revision" ]; then
|
|
tracking_rev_line="$tracking_rev_line Revision: <del>$rev1_revision</del><ins>$rev2_revision</ins>"
|
|
else
|
|
tracking_rev_line="$tracking_rev_line Revision: $rev1_revision"
|
|
fi
|
|
|
|
if [ "$rev1_status" != "$rev2_status" ]; then
|
|
tracking_rev_line="$tracking_rev_line Status: <del>$rev1_status</del><ins>$rev2_status</ins>"
|
|
else
|
|
tracking_rev_line="$tracking_rev_line Status: $rev1_status"
|
|
fi
|
|
|
|
header_html="$header_html<div class=\"header-line\">$tracking_rev_line</div>"
|
|
|
|
# Add draft marker if revision contains ~
|
|
if echo "$rev2_revision" | grep -q "~"; then
|
|
header_html="$header_html<div class=\"header-line metadata-line draft-line\"><span class=\"draft-status\">[DRAFT Generated at $(LC_TIME=C date '+%B %d, %Y at %I:%M:%S %p %Z')]</span></div>"
|
|
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 "=========================================="
|