Add md2pdf: Markdown-to-PDF CLI with Finder Quick Action
- bin/md2pdf: converts .md to PDF via pandoc + headless Chrome (no LaTeX needed); writes alongside source, supports -o and --css - bin/md2pdf.css: clean GitHub-style default stylesheet - bin/md2pdf-install-quickaction: installs the Finder right-click action - macos/md2pdf.workflow: version-controlled Quick Action bundle (right-click .md files to Convert to PDF)
This commit is contained in:
Executable
+152
@@ -0,0 +1,152 @@
|
||||
#!/bin/zsh
|
||||
#
|
||||
# md2pdf — convert Markdown files to PDF.
|
||||
#
|
||||
# Pipeline: pandoc (md -> self-contained styled HTML) -> Chrome headless
|
||||
# (HTML -> PDF). Uses only pandoc + Google Chrome, both assumed installed.
|
||||
#
|
||||
# Usage:
|
||||
# md2pdf FILE.md [FILE2.md ...] convert each; PDF written next to source
|
||||
# md2pdf FILE.md -o OUT.pdf explicit output (single input only)
|
||||
# md2pdf FILE.md --css STYLE.css use a custom stylesheet
|
||||
# md2pdf -h | --help
|
||||
#
|
||||
# Output defaults to the same directory and basename as the input
|
||||
# (report.md -> report.pdf). An existing PDF is overwritten.
|
||||
|
||||
emulate -L zsh
|
||||
set -o pipefail
|
||||
|
||||
prog=${0:t}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
$prog — convert Markdown files to PDF
|
||||
|
||||
Usage:
|
||||
$prog FILE.md [FILE2.md ...] Convert each file; PDF written beside the source
|
||||
$prog FILE.md -o OUT.pdf Write to an explicit path (single input only)
|
||||
$prog FILE.md --css STYLE.css Render with a custom stylesheet
|
||||
$prog -h | --help Show this help
|
||||
|
||||
Notes:
|
||||
- Output defaults to the source directory with a .pdf extension
|
||||
(e.g. notes.md -> notes.pdf) and overwrites any existing PDF.
|
||||
- Requires pandoc and Google Chrome.
|
||||
EOF
|
||||
}
|
||||
|
||||
die() { print -u2 "$prog: $*"; exit 1; }
|
||||
|
||||
# --- locate Google Chrome ---------------------------------------------------
|
||||
find_chrome() {
|
||||
local candidates=(
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
"$HOME/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
||||
)
|
||||
local c
|
||||
for c in $candidates; do
|
||||
[[ -x $c ]] && { print -r -- "$c"; return 0; }
|
||||
done
|
||||
# Fall back to Spotlight lookup by bundle id.
|
||||
local app
|
||||
app=$(mdfind "kMDItemCFBundleIdentifier == 'com.google.Chrome'" 2>/dev/null | head -1)
|
||||
if [[ -n $app && -x "$app/Contents/MacOS/Google Chrome" ]]; then
|
||||
print -r -- "$app/Contents/MacOS/Google Chrome"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- parse arguments --------------------------------------------------------
|
||||
typeset -a inputs
|
||||
local out="" css=""
|
||||
|
||||
while (( $# )); do
|
||||
case $1 in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-o|--output)
|
||||
[[ -n $2 ]] || die "option $1 requires a path"
|
||||
out=$2; shift 2 ;;
|
||||
--output=*) out=${1#*=}; shift ;;
|
||||
--css)
|
||||
[[ -n $2 ]] || die "option $1 requires a path"
|
||||
css=$2; shift 2 ;;
|
||||
--css=*) css=${1#*=}; shift ;;
|
||||
--) shift; inputs+=("$@"); break ;;
|
||||
-*) die "unknown option: $1 (try --help)" ;;
|
||||
*) inputs+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
(( ${#inputs} )) || { usage; exit 1; }
|
||||
[[ -n $out && ${#inputs} -gt 1 ]] && die "-o/--output cannot be used with multiple input files"
|
||||
|
||||
# --- resolve dependencies once ----------------------------------------------
|
||||
command -v pandoc >/dev/null 2>&1 || die "pandoc not found (install with: brew install pandoc)"
|
||||
|
||||
local chrome
|
||||
chrome=$(find_chrome) || die "Google Chrome not found — install it or use a Chromium build"
|
||||
|
||||
# Default stylesheet lives next to the real script (resolve through symlinks).
|
||||
local default_css="${0:A:h}/md2pdf.css"
|
||||
if [[ -n $css ]]; then
|
||||
[[ -f $css ]] || die "stylesheet not found: $css"
|
||||
elif [[ -f $default_css ]]; then
|
||||
css=$default_css
|
||||
fi
|
||||
|
||||
# --- per-run temp workspace, cleaned on exit --------------------------------
|
||||
local workdir
|
||||
workdir=$(mktemp -d "${TMPDIR:-/tmp}/md2pdf.XXXXXX") || die "could not create temp dir"
|
||||
trap 'rm -rf -- "$workdir"' EXIT INT TERM
|
||||
|
||||
# --- convert one file -------------------------------------------------------
|
||||
convert_one() {
|
||||
local in=$1 dest=$2
|
||||
[[ -f $in ]] || { print -u2 "✗ $in: no such file"; return 1; }
|
||||
|
||||
local html="$workdir/${in:t:r}.html"
|
||||
# --standalone gives a full HTML doc (head + linked CSS); an empty title
|
||||
# metadata suppresses pandoc's "please specify a title" warning without
|
||||
# emitting a visible title block above the document's own H1.
|
||||
local -a pandoc_args=(
|
||||
"$in" -f markdown -t html5 --standalone --embed-resources
|
||||
--metadata "title=" -o "$html"
|
||||
)
|
||||
[[ -n $css ]] && pandoc_args+=(--css "$css")
|
||||
|
||||
if ! pandoc $pandoc_args 2>"$workdir/pandoc.err"; then
|
||||
print -u2 "✗ $in: pandoc failed"
|
||||
[[ -s "$workdir/pandoc.err" ]] && print -u2 -- "$(<"$workdir/pandoc.err")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Chrome's headless PDF mode runs an independent instance and does not
|
||||
# disturb a running Chrome profile, so no --user-data-dir is needed. (An
|
||||
# isolated --user-data-dir actually makes headless Chrome hang on exit.)
|
||||
if ! "$chrome" --headless=new --disable-gpu --no-pdf-header-footer \
|
||||
--print-to-pdf="$dest" "file://$html" >/dev/null 2>"$workdir/chrome.err"; then
|
||||
print -u2 "✗ $in: Chrome failed to render PDF"
|
||||
[[ -s "$workdir/chrome.err" ]] && print -u2 -- "$(<"$workdir/chrome.err")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
[[ -s $dest ]] || { print -u2 "✗ $in: no PDF produced"; return 1; }
|
||||
print -- "✓ wrote ${dest}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- drive all inputs -------------------------------------------------------
|
||||
local failures=0 f dest
|
||||
for f in $inputs; do
|
||||
if [[ -n $out ]]; then
|
||||
dest=$out
|
||||
else
|
||||
dest="${f:h}/${f:t:r}.pdf"
|
||||
fi
|
||||
convert_one "$f" "$dest" || (( failures++ ))
|
||||
done
|
||||
|
||||
(( failures == 0 )) || exit 1
|
||||
Reference in New Issue
Block a user