@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/LICENSE +21 -0
- package/README.md +248 -0
- package/assets/mermaid.config.json +12 -0
- package/assets/table-style.tex +7 -0
- package/bin/md2pdf +510 -0
- package/docs/ARCHITECTURE.md +93 -0
- package/install-system-deps.sh +52 -0
- package/lib/pandoc_mermaid_filter.py +128 -0
- package/lib/run_pandoc_mermaid_filter.sh +7 -0
- package/md2pdf.config.example +54 -0
- package/package.json +36 -0
- package/requirements.txt +3 -0
- package/scripts/convert-md-to-pdf.sh +10 -0
- package/scripts/install_md2pdf_quick_action.sh +259 -0
- package/scripts/uninstall_md2pdf_quick_action.sh +24 -0
- package/setup-local-deps.sh +31 -0
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."
|