CtrlK
BlogDocsLog inGet started
Tessl Logo
SkillPublic

md-to-pdf

Convert Markdown to PDF via reportlab or weasyprint engines. Triggers - pdf, md to pdf, markdown to pdf, generate pdf.

Quality

79%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

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.md
SKILL.md
Quality
Evals
Security

Markdown to PDF

Convert 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.

Complete native workflow

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.

MD to PDF

Converts Markdown to PDF with the reportlab or weasyprint engine.

Prompt contract

<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.

  1. Mode: a literal mode token (styles, config, test, help) wins outright; else the most distinct whole-word keyword hits (table below). Empty -> help (documented default, read-only, asks nothing).
  2. Scope: the prompt may be a full sentence, not just tokens. Take 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.
  3. Ask: a sentence that implies CONVERT with no resolvable file -> ONE 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>

Step 0: Parse Arguments

ComponentRequiredDescription
md_fileper modePath to .md file — literal token or extracted from prose (Prompt contract rule 2)
--engineNoreportlab or weasyprint, anywhere; overrides saved config; strip before parsing
custom_promptNoLast argument in double quotes = LLM preprocessing instructions
ModeConditionEN keywordsRU keywordsMutates?
HELPEmpty or help(empty), helpпомощьno
STYLESstyles or configstyles, config, configure, page size, color schemeстили, настрой стиль, конфигyes
TESTtesttest, sample, demo, bundled test fileтест, пример, демоno (private temp dir, removed on exit)
CONVERT+PROMPTPath to .md file + quoted string at endconvert with instructions, pdf and rewrite/strip sectionсконвертируй с изменениямиyes
CONVERTPath to .md file (no quoted string)convert, pdf, generate pdf, turn into pdf, .md file named in proseконвертируй, сделай pdf, преврати в pdfyes

HELP and STYLES skip to Step 3.

Step 1: Engine Selection

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>"}.

Featurereportlabweasyprint
Installpip onlypip + brew
QualityGoodExcellent
SpeedFastModerate
ImagesBasicFull
CSS StylingNoYes
Code highlightNoYes (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.

Step 2: Dependency Check

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:

  • "Install ENGINE_NAME dependencies"
  • "Switch to OTHER_ENGINE" (if the other engine is available)
  • "Cancel"

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.

Step 3: Mode Execution

HELP Mode

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.

CONVERT Mode

  1. Read 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.

  1. Parse the result lines STATUS, OUTPUT, PAGES, SIZE, ENGINE.

CONVERT+PROMPT Mode

  1. Read md_file; apply the custom_prompt transformations (delete sections, rewrite headings, restructure).
  2. Write, convert and clean up in ONE Bash call.

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.

  1. Parse the result lines, then Step 4 with preprocessing: true.

STYLES Mode

Ask the four questions in one main-chat user gate call:

#QuestionOptions
1Page sizeA4 (default), Letter, Legal
2Color schemeDefault blue (primary #1a3a5c), Dark (primary #2d3748), Custom (ask for hex values)
3Code theme (weasyprint only)github (default), monokai, friendly, solarized-dark, solarized-light
4Footer formatPage {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.

TEST Mode

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).

Step 4: Report Results

ParameterValue
Sourceabsolute path to input MD
Outputabsolute path to output PDF
PagesPAGES= line of the script output
SizeSIZE= line
Enginereportlab or weasyprint
Preprocessingcustom_prompt summary (if used) or none
Repository

kochetkov-ma/claude-brewcode

Path

.codex/plugins/brewdoc/skills/md-to-pdf/SKILL.md

Last updated
First committed

Is this your skill?

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.