Convert Markdown to PDF via reportlab or weasyprint engines. Triggers - pdf, md to pdf, markdown to pdf, generate pdf.
79%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.codex/plugins/brewdoc/skills/md-to-pdf/SKILL.mdConvert a local Markdown file with scripts/md_to_pdf.py. Check dependencies with scripts/check_deps.sh, choose a bundled style from styles/, and write only to the requested output path. Store project preferences in .codex/md-to-pdf.config.json only after confirmation. Render and inspect the result before reporting completion.
Follow every phase below. When a phase delegates work, use Codex collaboration with only task_name and message; treat each "Codex delegation brief" block as role and message content, not executable syntax. Required approval: main presents a concrete, reviewable proposal in chat and waits for an actual user reply before dependent action. Existing authorization for the same scope remains valid; do not ask again. Optional clarification: use request_user_input_async only if exposed, or request_user_input only if available in the current runtime/mode, for optional choices and never approval. Otherwise ask in main chat. Delegated agents return unresolved questions to main. Silence, elapsed time and tool errors are not approval. Resolve <skill-directory>, <plugin-root>, <project-root>, and <arguments> before running commands.
Converts Markdown to PDF with the reportlab or weasyprint engine.
<arguments> is one free-form RU/EN prompt; the file path, the --engine <name> flag, the styles/test
tokens and the quoted LLM-preprocessing prompt may sit anywhere in it.
styles, config, test, help) wins outright; else the most distinct
whole-word keyword hits (table below). Empty -> help (documented default, read-only, asks nothing).md_file from any path-shaped token or a
filename explicitly named in prose (e.g. "convert my notes.md to pdf" -> md_file = notes.md, resolved
against cwd). Never guess a file and never silently fall through to HELP.main-chat user gate for the file path;
outcome-changing ambiguity (missing engine deps, no saved engine preference) -> ONE main-chat user gate.Print once, before Step 1 (labels literal, values in the conversation language):
PLAN — brewdoc:md-to-pdf
INPUT: <arguments verbatim, or "(empty)">
MODE: <resolved> — <explicit | matched keyword: X | default | prose-resolved file: path>
SCOPE: <md_file | engine | style question, as applicable>
DO: <2-5 imperative bullets>
RESULT: <PDF path, or the help/styles/test output>| Component | Required | Description |
|---|---|---|
md_file | per mode | Path to .md file — literal token or extracted from prose (Prompt contract rule 2) |
--engine | No | reportlab or weasyprint, anywhere; overrides saved config; strip before parsing |
custom_prompt | No | Last argument in double quotes = LLM preprocessing instructions |
| Mode | Condition | EN keywords | RU keywords | Mutates? |
|---|---|---|---|---|
| HELP | Empty or help | (empty), help | помощь | no |
| STYLES | styles or config | styles, config, configure, page size, color scheme | стили, настрой стиль, конфиг | yes |
| TEST | test | test, sample, demo, bundled test file | тест, пример, демо | no (private temp dir, removed on exit) |
| CONVERT+PROMPT | Path to .md file + quoted string at end | convert with instructions, pdf and rewrite/strip section | сконвертируй с изменениями | yes |
| CONVERT | Path to .md file (no quoted string) | convert, pdf, generate pdf, turn into pdf, .md file named in prose | конвертируй, сделай pdf, преврати в pdf | yes |
HELP and STYLES skip to Step 3.
Config = project .codex/md-to-pdf.config.json, else global ~/.codex/md-to-pdf.config.json. Engine = --engine,
else config engine, else TEST takes reportlab and writes nothing, and CONVERT modes ask via main-chat user gate
with this table (default reportlab) and save with the block below, NEW_JSON = {"engine": "<choice>"}.
| Feature | reportlab | weasyprint |
|---|---|---|
| Install | pip only | pip + brew |
| Quality | Good | Excellent |
| Speed | Fast | Moderate |
| Images | Basic | Full |
| CSS Styling | No | Yes |
| Code highlight | No | Yes (Pygments) |
Config shape:
{
"engine": "reportlab",
"pygments_theme": "github",
"version": "{PLUGIN_VERSION}",
"content_version": "{CONTENT_VERSION}",
"generated_by": "brewdoc:md-to-pdf",
"last_updated": "{LAST_UPDATED}"
}The block merges NEW_JSON's top-level keys into the project file, so engine and style saves keep each other,
and resolves the four provenance keys on every write, never hardcoded.
EXECUTE using shell (replace NEW_JSON; keep every other byte):
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
NEW="$(mktemp)"
cat > "$NEW" <<'BREWDOC_CFG_EOF'
NEW_JSON
BREWDOC_CFG_EOF
mkdir -p "$ROOT/.claude"
CFG="$ROOT/.codex/md-to-pdf.config.json" NEW="$NEW" PJ="<skill-directory>/../../.codex-plugin/plugin.json" SKILL_MD="<skill-directory>/SKILL.md" TODAY="$(date +%F)" python3 - <<'PY'
import json, os, re
cfg_p = os.environ["CFG"]
pv = json.load(open(os.environ["PJ"])).get("version")
if not pv:
raise SystemExit("no version in " + os.environ["PJ"])
cv = None
for line in open(os.environ["SKILL_MD"]):
if "brewcode-meta:" in line:
m = re.search(r"content_version=(\d+\.\d+\.\d+)", line)
if m:
cv = m.group(1)
break
if not cv:
raise SystemExit("no content_version in " + os.environ["SKILL_MD"])
data = json.load(open(cfg_p)) if os.path.exists(cfg_p) else {}
data.update(json.load(open(os.environ["NEW"])))
data["version"] = pv
data["content_version"] = cv
data["generated_by"] = "brewdoc:md-to-pdf"
data["last_updated"] = os.environ["TODAY"]
with open(cfg_p, "w") as fh:
json.dump(data, fh, indent=2)
fh.write("\n")
print("OK version=%s content_version=%s engine=%s" % (pv, cv, data.get("engine", "(unset)")))
PY
rc=$?; rm -f "$NEW"
[ "$rc" -eq 0 ] && echo "✅ config saved" || { echo "❌ config save FAILED"; exit 1; }STOP if ❌ -- fix before continuing.
EXECUTE using shell (replace ENGINE_NAME with the Step 1 engine):
bash "<skill-directory>/scripts/check_deps.sh" check ENGINE_NAME 2>&1; echo "EXIT_CODE=$?"Output contains MISSING_PIP or MISSING_SYSTEM -> main-chat user gate with the comparison table and these options:
If user chooses install, EXECUTE using shell:
bash "<skill-directory>/scripts/check_deps.sh" install ENGINE_NAME 2>&1 && echo "---INSTALL_OK---" || echo "---INSTALL_FAILED---"STOP if INSTALL_FAILED -- report error and exit. Cancel -> STOP.
Print:
MD to PDF Converter
Usage:
$brewdoc:md-to-pdf <file.md> Convert with saved engine/style
$brewdoc:md-to-pdf <file.md> --engine weasyprint Convert with specific engine
$brewdoc:md-to-pdf <file.md> "remove section X" Preprocess MD then convert
$brewdoc:md-to-pdf styles Configure page/color/font
$brewdoc:md-to-pdf test Convert bundled test file
$brewdoc:md-to-pdf help Show this help
Engines:
reportlab -- Pure Python, fast, no system deps (check_deps.sh install reportlab)
weasyprint -- HTML/CSS pipeline, best quality (pip + brew deps)EXIT after printing.
md_file; not found -> STOP with an error. Output: same directory and name, .pdf extension.EXECUTE using shell (substitute INPUT_PATH, OUTPUT_PATH, ENGINE; add --config CONFIG_PATH when a config from Step 1 exists; for weasyprint add --pygments-theme THEME when it sets pygments_theme):
python3 "<skill-directory>/scripts/md_to_pdf.py" "INPUT_PATH" "OUTPUT_PATH" --engine ENGINE 2>&1 && echo "---CONVERT_OK---" || echo "---CONVERT_FAILED---"STOP if CONVERT_FAILED -- read error output, attempt fix, retry once. If still failing -- report error.
STATUS, OUTPUT, PAGES, SIZE, ENGINE.md_file; apply the custom_prompt transformations (delete sections, rewrite headings, restructure).EXECUTE using shell (replace ORIGINAL_DIR, TRANSFORMED_MARKDOWN, OUTPUT_PATH, ENGINE; keep every other byte):
set -euo pipefail
SRC_DIR="ORIGINAL_DIR"
TMP="$(mktemp "$SRC_DIR/.tmp_XXXXXX")"
trap 'case "${TMP##*/}" in .tmp_??????) rm -f "$TMP" ;; esac' EXIT
cat > "$TMP" <<'BREWDOC_MD_EOF'
TRANSFORMED_MARKDOWN
BREWDOC_MD_EOF
python3 "<skill-directory>/scripts/md_to_pdf.py" "$TMP" "OUTPUT_PATH" --engine ENGINE 2>&1 && echo "---CONVERT_OK---" || echo "---CONVERT_FAILED---"OUTPUT_PATH is the ORIGINAL name with a .pdf extension; --config / --pygments-theme as in CONVERT. Never
write the transformed markdown with Write/Edit or to a fixed .tmp_<name>.md path: a predictable name
overwrites user files and collides with a concurrent conversion of the same source.
STOP if CONVERT_FAILED -- as in CONVERT.
preprocessing: true.Ask the four questions in one main-chat user gate call:
| # | Question | Options |
|---|---|---|
| 1 | Page size | A4 (default), Letter, Legal |
| 2 | Color scheme | Default blue (primary #1a3a5c), Dark (primary #2d3748), Custom (ask for hex values) |
| 3 | Code theme (weasyprint only) | github (default), monokai, friendly, solarized-dark, solarized-light |
| 4 | Footer format | Page {page} of {total} (default), {page}/{total}, Disabled |
Save with the Step 1 block: NEW_JSON = styles/default.json structure with the changed values, plus answer 3 as
top-level pygments_theme (passed as --pygments-theme; md_to_pdf.py never reads code.theme and reads only
page, colors, typography, code, footer). Report the saved settings table and EXIT.
The CONVERT command on the bundled file, writing into a private 0700 mktemp -d dir removed on exit.
EXECUTE using shell (replace ENGINE; --config / --pygments-theme as in CONVERT; keep every other byte):
umask 077
TEST_DIR="$(mktemp -d "${TMPDIR:-/tmp}/md-to-pdf-test.XXXXXX")" && chmod 700 "$TEST_DIR" || { echo "---CONVERT_FAILED---"; exit 1; }
trap 'case "${TEST_DIR##*/}" in md-to-pdf-test.??????) rm -rf -- "$TEST_DIR" ;; esac' EXIT
python3 "<skill-directory>/scripts/md_to_pdf.py" "<skill-directory>/test/test-all-elements.md" "$TEST_DIR/md-to-pdf-test-ENGINE.pdf" --engine ENGINE 2>&1 && echo "---CONVERT_OK---" || echo "---CONVERT_FAILED---"STOP if CONVERT_FAILED -- as in CONVERT.
Then Step 4; Output = the OUTPUT= path, marked (removed on exit).
| Parameter | Value |
|---|---|
| Source | absolute path to input MD |
| Output | absolute path to output PDF |
| Pages | PAGES= line of the script output |
| Size | SIZE= line |
| Engine | reportlab or weasyprint |
| Preprocessing | custom_prompt summary (if used) or none |
kochetkov-ma/claude-brewcode
5da825f
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.