@dkothule/md2pdf 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/md2pdf ADDED
@@ -0,0 +1,510 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 Deepak Kothule
4
+ # Standalone Markdown -> PDF converter with high-resolution Mermaid diagrams.
5
+ set -euo pipefail
6
+
7
+ resolve_script_dir() {
8
+ local source="${BASH_SOURCE[0]}"
9
+ while [[ -L "$source" ]]; do
10
+ local dir
11
+ dir="$(cd -P "$(dirname "$source")" && pwd)"
12
+ source="$(readlink "$source")"
13
+ if [[ "$source" != /* ]]; then
14
+ source="$dir/$source"
15
+ fi
16
+ done
17
+ cd -P "$(dirname "$source")" && pwd
18
+ }
19
+
20
+ trim_ws() {
21
+ printf '%s' "$1" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'
22
+ }
23
+
24
+ strip_outer_quotes() {
25
+ local value="$1"
26
+ if [[ ${#value} -ge 2 ]]; then
27
+ local first_char="${value:0:1}"
28
+ local last_char="${value: -1}"
29
+ if [[ "$first_char" == "\"" && "$last_char" == "\"" ]]; then
30
+ value="${value:1:${#value}-2}"
31
+ elif [[ "$first_char" == "'" && "$last_char" == "'" ]]; then
32
+ value="${value:1:${#value}-2}"
33
+ fi
34
+ fi
35
+ printf '%s' "$value"
36
+ }
37
+
38
+ normalize_bool() {
39
+ local value
40
+ value="$(trim_ws "$1")"
41
+ value="$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')"
42
+ case "$value" in
43
+ 1|true|yes|on) printf 'true' ;;
44
+ 0|false|no|off) printf 'false' ;;
45
+ *)
46
+ echo "Error: invalid boolean value '$1'. Use true/false." >&2
47
+ exit 1
48
+ ;;
49
+ esac
50
+ }
51
+
52
+ resolve_config_path() {
53
+ local config_file="$1"
54
+ local raw_value="$2"
55
+ if [[ "$raw_value" == /* ]]; then
56
+ printf '%s' "$raw_value"
57
+ return
58
+ fi
59
+ if [[ "$raw_value" == */* ]]; then
60
+ local cfg_dir
61
+ cfg_dir="$(cd "$(dirname "$config_file")" && pwd)"
62
+ printf '%s' "$cfg_dir/$raw_value"
63
+ return
64
+ fi
65
+ printf '%s' "$raw_value"
66
+ }
67
+
68
+ is_executable_cmd() {
69
+ local cmd="$1"
70
+ if [[ "$cmd" == */* ]]; then
71
+ [[ -x "$cmd" ]]
72
+ else
73
+ command -v "$cmd" >/dev/null 2>&1
74
+ fi
75
+ }
76
+
77
+ require_cmd() {
78
+ local cmd="$1"
79
+ local install_hint="$2"
80
+ if ! is_executable_cmd "$cmd"; then
81
+ echo "Error: required command not found: $cmd" >&2
82
+ echo "$install_hint" >&2
83
+ exit 1
84
+ fi
85
+ }
86
+
87
+ load_config_file() {
88
+ local config_file="$1"
89
+ local line_no=0
90
+ local line=""
91
+ while IFS= read -r line || [[ -n "$line" ]]; do
92
+ line_no=$((line_no + 1))
93
+ line="$(trim_ws "$line")"
94
+ if [[ -z "$line" || "${line:0:1}" == "#" ]]; then
95
+ continue
96
+ fi
97
+ if [[ "$line" != *=* ]]; then
98
+ echo "Warning: ignoring invalid line in $config_file:$line_no" >&2
99
+ continue
100
+ fi
101
+
102
+ local key
103
+ local value
104
+ key="$(trim_ws "${line%%=*}")"
105
+ value="$(trim_ws "${line#*=}")"
106
+ value="$(strip_outer_quotes "$value")"
107
+
108
+ case "$key" in
109
+ PDF_ENGINE) PDF_ENGINE="$value" ;;
110
+ PANDOC_COLUMNS) PANDOC_COLUMNS="$value" ;;
111
+ LR_MARGIN) LR_MARGIN="$value" ;;
112
+ TB_MARGIN) TB_MARGIN="$value" ;;
113
+ CLEANUP_MERMAID_ASSETS)
114
+ CLEANUP_MERMAID_ASSETS="$(normalize_bool "$value")"
115
+ ;;
116
+ MERMAID_ASSET_PREFIX) MERMAID_ASSET_PREFIX="$value" ;;
117
+ FILTER) FILTER="$(resolve_config_path "$config_file" "$value")" ;;
118
+ TABLE_STYLE) TABLE_STYLE="$(resolve_config_path "$config_file" "$value")" ;;
119
+ MERMAID_BIN) MERMAID_BIN="$(resolve_config_path "$config_file" "$value")" ;;
120
+ MERMAID_CONFIG) MERMAID_CONFIG="$(resolve_config_path "$config_file" "$value")" ;;
121
+ MERMAID_LATEX_FORMAT) MERMAID_LATEX_FORMAT="$value" ;;
122
+ MERMAID_PDF_FIT) MERMAID_PDF_FIT="$(normalize_bool "$value")" ;;
123
+ MERMAID_AUTO_PDF_FALLBACK) MERMAID_AUTO_PDF_FALLBACK="$(normalize_bool "$value")" ;;
124
+ PYTHON) PYTHON="$(resolve_config_path "$config_file" "$value")" ;;
125
+ PUPPETEER_CFG) PUPPETEER_CFG="$(resolve_config_path "$config_file" "$value")" ;;
126
+ *)
127
+ echo "Warning: unknown config key '$key' in $config_file:$line_no" >&2
128
+ ;;
129
+ esac
130
+ done < "$config_file"
131
+ }
132
+
133
+ cleanup_mermaid_assets() {
134
+ if [[ "${HAS_MERMAID:-false}" != "true" ]]; then
135
+ return
136
+ fi
137
+ if [[ "${CLEANUP_MERMAID_ASSETS}" != "true" ]]; then
138
+ return
139
+ fi
140
+ if [[ -n "${MERMAID_ASSET_DIR_ABS:-}" && -d "$MERMAID_ASSET_DIR_ABS" ]]; then
141
+ rm -rf "$MERMAID_ASSET_DIR_ABS"
142
+ fi
143
+ }
144
+
145
+ get_cli_version() {
146
+ local pkg="$PROJECT_ROOT/package.json"
147
+ if [[ -f "$pkg" ]]; then
148
+ sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$pkg" | head -n 1
149
+ return
150
+ fi
151
+ echo "unknown"
152
+ }
153
+
154
+ init_config_file() {
155
+ local template="$PROJECT_ROOT/md2pdf.config.example"
156
+ local target="$INIT_CONFIG_PATH"
157
+ if [[ -z "$target" ]]; then
158
+ target="$HOME/.config/md2pdf/config.env"
159
+ elif [[ "$target" != /* ]]; then
160
+ target="$ORIG_CWD/$target"
161
+ fi
162
+
163
+ if [[ ! -f "$template" ]]; then
164
+ echo "Error: config template not found: $template" >&2
165
+ exit 1
166
+ fi
167
+
168
+ mkdir -p "$(dirname "$target")"
169
+ if [[ -e "$target" && "$INIT_FORCE" -ne 1 ]]; then
170
+ echo "Error: config file already exists: $target" >&2
171
+ echo "Use --force to overwrite." >&2
172
+ exit 1
173
+ fi
174
+
175
+ cp "$template" "$target"
176
+ echo "Created config file: $target"
177
+ }
178
+
179
+ usage() {
180
+ cat <<EOF
181
+ Usage:
182
+ $(basename "$0") <input.md> [-o output.pdf] [--config path]
183
+ $(basename "$0") --init [path] [--force]
184
+ $(basename "$0") --version
185
+
186
+ Converts Markdown to PDF and renders Mermaid code blocks as vector assets for high-resolution output.
187
+
188
+ Options:
189
+ -o, --output <file> Output PDF path
190
+ --config <file> Additional config file to load
191
+ --init [path] Create default config file (default: \$HOME/.config/md2pdf/config.env)
192
+ --force Overwrite existing config file when used with --init
193
+ --keep-mermaid-assets Keep generated Mermaid SVG/MMD assets
194
+ --cleanup-mermaid-assets Remove generated Mermaid assets after conversion (default)
195
+ -v, --version Show version
196
+ -h, --help Show this help
197
+
198
+ Mermaid defaults:
199
+ MERMAID_CONFIG uses bundled assets/mermaid.config.json.
200
+ This disables flowchart htmlLabels for PDF-safe text rendering.
201
+ MERMAID_LATEX_FORMAT defaults to "svg" for high-resolution vector diagrams.
202
+ MERMAID_PDF_FIT defaults to "true" so Mermaid PDF assets use tight page bounds.
203
+ MERMAID_AUTO_PDF_FALLBACK defaults to "true" for foreignObject label fallback.
204
+
205
+ Config loading order:
206
+ 1) $HOME/.config/md2pdf/config.env
207
+ 2) <input-dir>/.md2pdfrc
208
+ 3) --config <file> (if provided)
209
+
210
+ Environment variables override config values when set.
211
+ See md2pdf.config.example for supported keys.
212
+ EOF
213
+ }
214
+
215
+ script_dir="$(resolve_script_dir)"
216
+ PROJECT_ROOT="$(cd "$script_dir/.." && pwd)"
217
+ ORIG_CWD="$(pwd)"
218
+
219
+ # Capture environment overrides before defaults are initialized.
220
+ HAS_ENV_PDF_ENGINE=0
221
+ HAS_ENV_PANDOC_COLUMNS=0
222
+ HAS_ENV_LR_MARGIN=0
223
+ HAS_ENV_TB_MARGIN=0
224
+ HAS_ENV_CLEANUP=0
225
+ HAS_ENV_ASSET_PREFIX=0
226
+ HAS_ENV_FILTER=0
227
+ HAS_ENV_TABLE_STYLE=0
228
+ HAS_ENV_MERMAID_BIN=0
229
+ HAS_ENV_MERMAID_CONFIG=0
230
+ HAS_ENV_MERMAID_LATEX_FORMAT=0
231
+ HAS_ENV_MERMAID_PDF_FIT=0
232
+ HAS_ENV_MERMAID_AUTO_PDF_FALLBACK=0
233
+ HAS_ENV_PYTHON=0
234
+ HAS_ENV_PUPPETEER_CFG=0
235
+
236
+ ENV_PDF_ENGINE=""
237
+ ENV_PANDOC_COLUMNS=""
238
+ ENV_LR_MARGIN=""
239
+ ENV_TB_MARGIN=""
240
+ ENV_CLEANUP=""
241
+ ENV_ASSET_PREFIX=""
242
+ ENV_FILTER=""
243
+ ENV_TABLE_STYLE=""
244
+ ENV_MERMAID_BIN=""
245
+ ENV_MERMAID_CONFIG=""
246
+ ENV_MERMAID_LATEX_FORMAT=""
247
+ ENV_MERMAID_PDF_FIT=""
248
+ ENV_MERMAID_AUTO_PDF_FALLBACK=""
249
+ ENV_PYTHON=""
250
+ ENV_PUPPETEER_CFG=""
251
+
252
+ if [[ -n "${PDF_ENGINE+x}" ]]; then HAS_ENV_PDF_ENGINE=1; ENV_PDF_ENGINE="$PDF_ENGINE"; fi
253
+ if [[ -n "${PANDOC_COLUMNS+x}" ]]; then HAS_ENV_PANDOC_COLUMNS=1; ENV_PANDOC_COLUMNS="$PANDOC_COLUMNS"; fi
254
+ if [[ -n "${LR_MARGIN+x}" ]]; then HAS_ENV_LR_MARGIN=1; ENV_LR_MARGIN="$LR_MARGIN"; fi
255
+ if [[ -n "${TB_MARGIN+x}" ]]; then HAS_ENV_TB_MARGIN=1; ENV_TB_MARGIN="$TB_MARGIN"; fi
256
+ if [[ -n "${CLEANUP_MERMAID_ASSETS+x}" ]]; then HAS_ENV_CLEANUP=1; ENV_CLEANUP="$CLEANUP_MERMAID_ASSETS"; fi
257
+ if [[ -n "${MERMAID_ASSET_PREFIX+x}" ]]; then HAS_ENV_ASSET_PREFIX=1; ENV_ASSET_PREFIX="$MERMAID_ASSET_PREFIX"; fi
258
+ if [[ -n "${FILTER+x}" ]]; then HAS_ENV_FILTER=1; ENV_FILTER="$FILTER"; fi
259
+ if [[ -n "${TABLE_STYLE+x}" ]]; then HAS_ENV_TABLE_STYLE=1; ENV_TABLE_STYLE="$TABLE_STYLE"; fi
260
+ if [[ -n "${MERMAID_BIN+x}" ]]; then HAS_ENV_MERMAID_BIN=1; ENV_MERMAID_BIN="$MERMAID_BIN"; fi
261
+ if [[ -n "${MERMAID_CONFIG+x}" ]]; then HAS_ENV_MERMAID_CONFIG=1; ENV_MERMAID_CONFIG="$MERMAID_CONFIG"; fi
262
+ if [[ -n "${MERMAID_LATEX_FORMAT+x}" ]]; then HAS_ENV_MERMAID_LATEX_FORMAT=1; ENV_MERMAID_LATEX_FORMAT="$MERMAID_LATEX_FORMAT"; fi
263
+ if [[ -n "${MERMAID_PDF_FIT+x}" ]]; then HAS_ENV_MERMAID_PDF_FIT=1; ENV_MERMAID_PDF_FIT="$MERMAID_PDF_FIT"; fi
264
+ if [[ -n "${MERMAID_AUTO_PDF_FALLBACK+x}" ]]; then HAS_ENV_MERMAID_AUTO_PDF_FALLBACK=1; ENV_MERMAID_AUTO_PDF_FALLBACK="$MERMAID_AUTO_PDF_FALLBACK"; fi
265
+ if [[ -n "${PYTHON+x}" ]]; then HAS_ENV_PYTHON=1; ENV_PYTHON="$PYTHON"; fi
266
+ if [[ -n "${PUPPETEER_CFG+x}" ]]; then HAS_ENV_PUPPETEER_CFG=1; ENV_PUPPETEER_CFG="$PUPPETEER_CFG"; fi
267
+
268
+ # Defaults
269
+ FILTER="$PROJECT_ROOT/lib/run_pandoc_mermaid_filter.sh"
270
+ TABLE_STYLE="$PROJECT_ROOT/assets/table-style.tex"
271
+ MERMAID_CONFIG="$PROJECT_ROOT/assets/mermaid.config.json"
272
+ MERMAID_LATEX_FORMAT="svg"
273
+ MERMAID_PDF_FIT="true"
274
+ MERMAID_AUTO_PDF_FALLBACK="true"
275
+ PDF_ENGINE="xelatex"
276
+ PANDOC_COLUMNS="200"
277
+ LR_MARGIN="0.7in"
278
+ TB_MARGIN="0.5in"
279
+ CLEANUP_MERMAID_ASSETS="true"
280
+ MERMAID_ASSET_PREFIX="md2pdf-mermaid"
281
+ PUPPETEER_CFG=""
282
+
283
+ PYTHON="python3"
284
+ if [[ -x "$PROJECT_ROOT/.venv/bin/python" ]]; then
285
+ PYTHON="$PROJECT_ROOT/.venv/bin/python"
286
+ fi
287
+ MERMAID_BIN="mmdc"
288
+ if [[ -x "$PROJECT_ROOT/node_modules/.bin/mmdc" ]]; then
289
+ MERMAID_BIN="$PROJECT_ROOT/node_modules/.bin/mmdc"
290
+ fi
291
+
292
+ INPUT=""
293
+ OUTPUT=""
294
+ CONFIG_PATH=""
295
+ CLI_CLEANUP_OVERRIDE=""
296
+ INIT_CONFIG=0
297
+ INIT_CONFIG_PATH=""
298
+ INIT_FORCE=0
299
+
300
+ while [[ $# -gt 0 ]]; do
301
+ case "$1" in
302
+ -h|--help)
303
+ usage
304
+ exit 0
305
+ ;;
306
+ -v|--version)
307
+ echo "md2pdf $(get_cli_version)"
308
+ exit 0
309
+ ;;
310
+ --init)
311
+ INIT_CONFIG=1
312
+ if [[ $# -ge 2 && "${2:0:1}" != "-" ]]; then
313
+ INIT_CONFIG_PATH="$2"
314
+ shift 2
315
+ else
316
+ shift
317
+ fi
318
+ ;;
319
+ --force)
320
+ INIT_FORCE=1
321
+ shift
322
+ ;;
323
+ -o|--output)
324
+ if [[ $# -lt 2 ]]; then
325
+ echo "Error: missing value for $1." >&2
326
+ exit 1
327
+ fi
328
+ OUTPUT="$2"
329
+ shift 2
330
+ ;;
331
+ --config)
332
+ if [[ $# -lt 2 ]]; then
333
+ echo "Error: missing value for --config." >&2
334
+ exit 1
335
+ fi
336
+ CONFIG_PATH="$2"
337
+ shift 2
338
+ ;;
339
+ --keep-mermaid-assets)
340
+ CLI_CLEANUP_OVERRIDE="false"
341
+ shift
342
+ ;;
343
+ --cleanup-mermaid-assets)
344
+ CLI_CLEANUP_OVERRIDE="true"
345
+ shift
346
+ ;;
347
+ -*)
348
+ echo "Error: unknown option: $1" >&2
349
+ usage >&2
350
+ exit 1
351
+ ;;
352
+ *)
353
+ if [[ -n "$INPUT" ]]; then
354
+ echo "Error: multiple input files provided. Pass exactly one Markdown file." >&2
355
+ exit 1
356
+ fi
357
+ INPUT="$1"
358
+ shift
359
+ ;;
360
+ esac
361
+ done
362
+
363
+ if [[ "$INIT_CONFIG" -eq 1 ]]; then
364
+ if [[ -n "$INPUT" ]]; then
365
+ echo "Error: --init cannot be used with an input file." >&2
366
+ exit 1
367
+ fi
368
+ init_config_file
369
+ exit 0
370
+ fi
371
+
372
+ if [[ -z "$INPUT" ]]; then
373
+ echo "Error: no input file provided." >&2
374
+ usage >&2
375
+ exit 1
376
+ fi
377
+
378
+ if [[ "$INPUT" != /* ]]; then
379
+ INPUT="$ORIG_CWD/$INPUT"
380
+ fi
381
+ if [[ ! -f "$INPUT" ]]; then
382
+ echo "Error: file not found: $INPUT" >&2
383
+ exit 1
384
+ fi
385
+
386
+ INPUT_ABS="$(cd "$(dirname "$INPUT")" && pwd)/$(basename "$INPUT")"
387
+ INPUT_DIR="$(dirname "$INPUT_ABS")"
388
+ INPUT_FILE="$(basename "$INPUT_ABS")"
389
+
390
+ GLOBAL_CONFIG="$HOME/.config/md2pdf/config.env"
391
+ INPUT_DIR_CONFIG="$INPUT_DIR/.md2pdfrc"
392
+ if [[ -f "$GLOBAL_CONFIG" ]]; then
393
+ load_config_file "$GLOBAL_CONFIG"
394
+ fi
395
+ if [[ -f "$INPUT_DIR_CONFIG" ]]; then
396
+ load_config_file "$INPUT_DIR_CONFIG"
397
+ fi
398
+ if [[ -n "$CONFIG_PATH" ]]; then
399
+ if [[ "$CONFIG_PATH" != /* ]]; then
400
+ CONFIG_PATH="$ORIG_CWD/$CONFIG_PATH"
401
+ fi
402
+ if [[ ! -f "$CONFIG_PATH" ]]; then
403
+ echo "Error: config file not found: $CONFIG_PATH" >&2
404
+ exit 1
405
+ fi
406
+ load_config_file "$CONFIG_PATH"
407
+ fi
408
+
409
+ # Re-apply environment overrides with highest precedence.
410
+ if [[ $HAS_ENV_PDF_ENGINE -eq 1 ]]; then PDF_ENGINE="$ENV_PDF_ENGINE"; fi
411
+ if [[ $HAS_ENV_PANDOC_COLUMNS -eq 1 ]]; then PANDOC_COLUMNS="$ENV_PANDOC_COLUMNS"; fi
412
+ if [[ $HAS_ENV_LR_MARGIN -eq 1 ]]; then LR_MARGIN="$ENV_LR_MARGIN"; fi
413
+ if [[ $HAS_ENV_TB_MARGIN -eq 1 ]]; then TB_MARGIN="$ENV_TB_MARGIN"; fi
414
+ if [[ $HAS_ENV_CLEANUP -eq 1 ]]; then CLEANUP_MERMAID_ASSETS="$(normalize_bool "$ENV_CLEANUP")"; fi
415
+ if [[ $HAS_ENV_ASSET_PREFIX -eq 1 ]]; then MERMAID_ASSET_PREFIX="$ENV_ASSET_PREFIX"; fi
416
+ if [[ $HAS_ENV_FILTER -eq 1 ]]; then FILTER="$ENV_FILTER"; fi
417
+ if [[ $HAS_ENV_TABLE_STYLE -eq 1 ]]; then TABLE_STYLE="$ENV_TABLE_STYLE"; fi
418
+ if [[ $HAS_ENV_MERMAID_BIN -eq 1 ]]; then MERMAID_BIN="$ENV_MERMAID_BIN"; fi
419
+ if [[ $HAS_ENV_MERMAID_CONFIG -eq 1 ]]; then MERMAID_CONFIG="$ENV_MERMAID_CONFIG"; fi
420
+ if [[ $HAS_ENV_MERMAID_LATEX_FORMAT -eq 1 ]]; then MERMAID_LATEX_FORMAT="$ENV_MERMAID_LATEX_FORMAT"; fi
421
+ if [[ $HAS_ENV_MERMAID_PDF_FIT -eq 1 ]]; then MERMAID_PDF_FIT="$(normalize_bool "$ENV_MERMAID_PDF_FIT")"; fi
422
+ if [[ $HAS_ENV_MERMAID_AUTO_PDF_FALLBACK -eq 1 ]]; then MERMAID_AUTO_PDF_FALLBACK="$(normalize_bool "$ENV_MERMAID_AUTO_PDF_FALLBACK")"; fi
423
+ if [[ $HAS_ENV_PYTHON -eq 1 ]]; then PYTHON="$ENV_PYTHON"; fi
424
+ if [[ $HAS_ENV_PUPPETEER_CFG -eq 1 ]]; then PUPPETEER_CFG="$ENV_PUPPETEER_CFG"; fi
425
+
426
+ if [[ -n "$CLI_CLEANUP_OVERRIDE" ]]; then
427
+ CLEANUP_MERMAID_ASSETS="$CLI_CLEANUP_OVERRIDE"
428
+ fi
429
+
430
+ MERMAID_LATEX_FORMAT="$(printf '%s' "$MERMAID_LATEX_FORMAT" | tr '[:upper:]' '[:lower:]')"
431
+ case "$MERMAID_LATEX_FORMAT" in
432
+ pdf|svg|png) ;;
433
+ *)
434
+ echo "Error: MERMAID_LATEX_FORMAT must be one of: pdf, svg, png." >&2
435
+ exit 1
436
+ ;;
437
+ esac
438
+
439
+ if [[ -z "$FILTER" || ! -x "$FILTER" ]]; then
440
+ echo "Error: filter script is not executable: $FILTER" >&2
441
+ exit 1
442
+ fi
443
+ if [[ -n "$TABLE_STYLE" && ! -f "$TABLE_STYLE" ]]; then
444
+ echo "Error: table style file not found: $TABLE_STYLE" >&2
445
+ exit 1
446
+ fi
447
+
448
+ require_cmd "pandoc" "Install pandoc first (or run ./install-system-deps.sh)."
449
+ require_cmd "$PDF_ENGINE" "Install '$PDF_ENGINE' or set PDF_ENGINE in config."
450
+ require_cmd "$PYTHON" "Install Python 3 (or run ./setup-local-deps.sh)."
451
+
452
+ if [[ -z "$OUTPUT" ]]; then
453
+ case "$INPUT_ABS" in
454
+ *.md) OUTPUT="${INPUT_ABS%.md}.pdf" ;;
455
+ *) OUTPUT="$INPUT_ABS.pdf" ;;
456
+ esac
457
+ elif [[ "$OUTPUT" != /* ]]; then
458
+ OUTPUT="$ORIG_CWD/$OUTPUT"
459
+ fi
460
+
461
+ HAS_MERMAID="false"
462
+ if grep -Eq '^[[:space:]]*```[[:space:]]*mermaid([[:space:]]|$)' "$INPUT_ABS"; then
463
+ HAS_MERMAID="true"
464
+ require_cmd "$MERMAID_BIN" "Install Mermaid CLI via npm (or run ./setup-local-deps.sh)."
465
+ if [[ -n "$MERMAID_CONFIG" && ! -f "$MERMAID_CONFIG" ]]; then
466
+ echo "Error: Mermaid config file not found: $MERMAID_CONFIG" >&2
467
+ exit 1
468
+ fi
469
+ RUN_ID="$(date +%s)-$$"
470
+ MERMAID_IMAGE_PREFIX="${MERMAID_ASSET_PREFIX}-${RUN_ID}"
471
+ export MERMAID_IMAGE_PREFIX
472
+ MERMAID_ASSET_DIR_ABS="$INPUT_DIR/${MERMAID_IMAGE_PREFIX}-images"
473
+ trap cleanup_mermaid_assets EXIT
474
+ fi
475
+
476
+ export MERMAID_BIN
477
+ if [[ -n "$MERMAID_CONFIG" ]]; then
478
+ export MERMAID_CONFIG
479
+ fi
480
+ export MERMAID_LATEX_FORMAT
481
+ export MERMAID_PDF_FIT
482
+ export MERMAID_AUTO_PDF_FALLBACK
483
+ export PYTHON
484
+ if [[ -n "$PUPPETEER_CFG" ]]; then
485
+ export PUPPETEER_CFG
486
+ fi
487
+
488
+ pandoc_args=(
489
+ "$INPUT_FILE"
490
+ -o "$OUTPUT"
491
+ --resource-path=.
492
+ --filter "$FILTER"
493
+ --pdf-engine="$PDF_ENGINE"
494
+ --columns="$PANDOC_COLUMNS"
495
+ -V "geometry:left=$LR_MARGIN"
496
+ -V "geometry:right=$LR_MARGIN"
497
+ -V "geometry:top=$TB_MARGIN"
498
+ -V "geometry:bottom=$TB_MARGIN"
499
+ )
500
+ if [[ -n "$TABLE_STYLE" ]]; then
501
+ pandoc_args+=( -H "$TABLE_STYLE" )
502
+ fi
503
+
504
+ cd "$INPUT_DIR"
505
+ pandoc "${pandoc_args[@]}"
506
+
507
+ echo "PDF created: $OUTPUT"
508
+ if [[ "$HAS_MERMAID" == "true" && "$CLEANUP_MERMAID_ASSETS" != "true" ]]; then
509
+ echo "Mermaid assets kept at: $MERMAID_ASSET_DIR_ABS"
510
+ fi
@@ -0,0 +1,93 @@
1
+ # md2pdf Architecture
2
+
3
+ ## Purpose
4
+
5
+ `md2pdf` converts Markdown to PDF with a specific focus on Mermaid diagrams rendered as vector assets so diagrams stay sharp at any zoom level in the final PDF.
6
+
7
+ ## Conversion pipeline
8
+
9
+ ```mermaid
10
+ flowchart LR
11
+ A[Markdown file] --> B[md2pdf CLI]
12
+ B --> C[Pandoc]
13
+ C --> D[Mermaid filter]
14
+ D --> E[Mermaid CLI mmdc]
15
+ E --> F[SVG or PDF vector assets]
16
+ C --> G[PDF engine]
17
+ F --> G
18
+ G --> H[Output PDF]
19
+ ```
20
+
21
+ ## Mermaid rendering strategy
22
+
23
+ ```mermaid
24
+ flowchart TD
25
+ A[Fenced mermaid block] --> B[Render SVG]
26
+ B --> C{foreignObject in SVG?}
27
+ C -->|No| D[Use SVG in PDF pipeline]
28
+ C -->|Yes| E{MERMAID_AUTO_PDF_FALLBACK=true?}
29
+ E -->|No| D
30
+ E -->|Yes| F[Render Mermaid PDF with --pdfFit]
31
+ F --> G[Use fit-to-content PDF asset]
32
+ ```
33
+
34
+ ## Components
35
+
36
+ - `bin/md2pdf`
37
+ - Main CLI entrypoint.
38
+ - Loads config from global/project/optional explicit config file.
39
+ - Invokes pandoc with the Mermaid filter and PDF engine.
40
+ - Optionally cleans temporary Mermaid assets after conversion.
41
+ - `lib/pandoc_mermaid_filter.py`
42
+ - Detects fenced Mermaid blocks during pandoc filtering.
43
+ - Calls `mmdc` to render each diagram.
44
+ - Uses `MERMAID_LATEX_FORMAT=svg` by default for high-resolution vector output.
45
+ - Rewrites Mermaid blocks to image references so pandoc includes Mermaid-rendered assets.
46
+ - `lib/run_pandoc_mermaid_filter.sh`
47
+ - Wrapper that runs the Python filter with configured Python executable.
48
+ - `assets/table-style.tex`
49
+ - Optional LaTeX header for table formatting.
50
+ - `assets/mermaid.config.json`
51
+ - Default Mermaid config for PDF-safe text rendering (`flowchart.htmlLabels=false`).
52
+ - Ensures edge labels use opaque backgrounds so arrow lines do not strike through label text.
53
+
54
+ ## Configuration model
55
+
56
+ Load order (later overrides earlier):
57
+
58
+ 1. `$HOME/.config/md2pdf/config.env`
59
+ 2. `<input-markdown-directory>/.md2pdfrc`
60
+ 3. `--config <file>`
61
+ 4. Environment variables
62
+ 5. CLI cleanup flags (`--keep-mermaid-assets`, `--cleanup-mermaid-assets`)
63
+
64
+ Defaults:
65
+
66
+ - `PDF_ENGINE=xelatex`
67
+ - `LR_MARGIN=0.7in`
68
+ - `TB_MARGIN=0.5in`
69
+ - `MERMAID_CONFIG=assets/mermaid.config.json`
70
+ - `MERMAID_LATEX_FORMAT=svg`
71
+ - `MERMAID_PDF_FIT=true`
72
+ - `CLEANUP_MERMAID_ASSETS=true`
73
+ - `MERMAID_ASSET_PREFIX=md2pdf-mermaid`
74
+
75
+ ## Temporary Mermaid assets
76
+
77
+ For each conversion run with Mermaid content, md2pdf creates:
78
+
79
+ - `<MERMAID_ASSET_PREFIX>-<run-id>-images/`
80
+
81
+ This directory contains `.mmd` and Mermaid output files (`.svg` by default).
82
+ By default it is removed after conversion (`CLEANUP_MERMAID_ASSETS=true`).
83
+
84
+ ## Finder Quick Action (macOS)
85
+
86
+ - `scripts/install_md2pdf_quick_action.sh` creates a Finder Quick Action.
87
+ - `scripts/uninstall_md2pdf_quick_action.sh` removes it.
88
+ - These scripts are macOS-only and independent of Linux CLI support.
89
+
90
+ ## Validation and samples
91
+
92
+ - Smoke test: `tests/architecture-smoke-test.md`
93
+ - Full Mermaid sample suite: `tests/samples/mermaid-all-diagram-types.md`
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 Deepak Kothule
4
+ set -euo pipefail
5
+
6
+ OS="$(uname -s)"
7
+
8
+ install_macos() {
9
+ if ! command -v brew >/dev/null 2>&1; then
10
+ echo "Homebrew is required on macOS. Install from https://brew.sh and retry."
11
+ exit 1
12
+ fi
13
+
14
+ brew update
15
+ brew install pandoc librsvg node python
16
+
17
+ if ! command -v xelatex >/dev/null 2>&1; then
18
+ echo "Installing BasicTeX for xelatex..."
19
+ brew install --cask basictex
20
+ echo "BasicTeX installed. You may need to restart your shell before retrying."
21
+ fi
22
+ }
23
+
24
+ install_debian_ubuntu() {
25
+ sudo apt-get update
26
+ sudo apt-get install -y \
27
+ pandoc \
28
+ librsvg2-bin \
29
+ nodejs \
30
+ npm \
31
+ python3 \
32
+ python3-venv \
33
+ python3-pip \
34
+ texlive-xetex
35
+ }
36
+
37
+ if [[ "$OS" == "Darwin" ]]; then
38
+ install_macos
39
+ elif [[ "$OS" == "Linux" ]]; then
40
+ if [[ -f /etc/debian_version ]]; then
41
+ install_debian_ubuntu
42
+ else
43
+ echo "Unsupported Linux distro by this script."
44
+ echo "Please install: pandoc, xelatex, librsvg (rsvg-convert), node/npm, python3+venv."
45
+ exit 1
46
+ fi
47
+ else
48
+ echo "Unsupported OS: $OS"
49
+ exit 1
50
+ fi
51
+
52
+ echo "System dependencies installed."