borghei/md-document
> Convert authored markdown into a polished self-contained HTML document with a table of contents, numbered figures and tables, cross-references, footnotes, and print-ready CSS. Use when publishing a report, whitepaper, or memo.
npx skills add https://github.com/borghei/Claude-Skills --skill md-document
Turn an authored markdown file into a single HTML document you can email, host,
or print — semantic structure, an automatic table of contents, figures and
tables numbered and referenceable by number, footnotes, and a print stylesheet
that survives contact with a PDF exporter. One file out, no runtime dependencies,
no external assets.
Before converting, confirm these inputs. If any is unknown or vague, ASK — do not assume:
Stop rule: ask only the 2-3 that most change the output. If the user says "just draft it," proceed and list your assumptions at the top of the artifact.
references, but the auditor explains what to do about each one.
--css to override.[?fig:name] markers where numbers should be.
python3 markdown-html/md-document/scripts/crossref_auditor.py \
--input markdown-html/md-document/assets/sample_document.md --format text
python3 markdown-html/md-document/scripts/md_to_html.py \
--input markdown-html/md-document/assets/sample_document.md \
--out build/report.html --toc-depth 2 --format text
the exported file carries its own print rules.
margin setting overrides @page and will clip content.
python3 markdown-html/md-document/scripts/print_profile.py \
--input markdown-html/md-document/assets/sample_print_profile.json \
--out build/print.css --format text
cat markdown-html/md-document/assets/document_theme.css build/print.css > build/full.css
python3 markdown-html/md-document/scripts/md_to_html.py \
--input markdown-html/md-document/assets/sample_document.md \
--css build/full.css --out build/report.html
warning severity so unlabelled figures block, not justbroken references.
footnotes — both render as visible defects.
python3 markdown-html/md-document/scripts/crossref_auditor.py \
--input markdown-html/md-document/assets/sample_document.md --max-severity warning --format json
python3 markdown-html/md-document/scripts/md_to_html.py \
--input markdown-html/md-document/assets/sample_document.md --out build/report.html --format json
| Document length | TOC | --toc-depth |
|-----------------|-----|---------------|
| Under 3 pages | none — omit [TOC] | n/a |
| 3-10 pages | yes | 1 (## only) |
| 10-30 pages | yes | 2 (default) [PROVEN] |
| Over 30 pages | yes | 2, plus per-section navigation |
If the TOC exceeds one screen, reduce the depth. A contents list longer than the
first section is a navigation failure, not thoroughness.
| Situation | Write | Not |
|-----------|-------|-----|
| Pointing at a figure | [@fig:access] | "the chart below" |
| Pointing at a table | [@tbl:policies] | "see the table above" |
| Pointing at a section | [@sec:context] | "as discussed earlier" |
| A caveat that breaks the sentence | a footnote | a parenthetical |
| Evidence the argument depends on | body text | a footnote |
[PROVEN] Never use positional language in a document that may be paginated.
"Below" breaks when the table lands on the next page, breaks silently when a
section is reordered, and means nothing to a reader navigating by heading.
| | Alt text | Caption |
|---|---------|---------|
| Audience | Non-sighted readers | Everyone |
| Length | 15-125 characters | One or two sentences |
| Says | What the image depicts | What to conclude, plus the number |
| Fails as | "chart", "figure 3", "" | "See above" |
The auditor flags placeholder alt text (chart, image, screenshot, empty) at
error severity and alt text under 15 or over 125 characters at warning.
| Decision | Default | Change when |
|----------|---------|-------------|
| Page size | A4 [RECOMMENDED] | Audience is exclusively North American → Letter |
| Side margins | 25-30mm | Never below 20mm — the measure exceeds 90 characters |
| Body size | 11pt | 12pt for older audiences or dense reference material |
| Mirrored margins | off | The document will be bound double-sided |
| break-inside: avoid | figures, tables, code | Never on an element taller than one page |
At A4 with 20mm margins the text column is ~92 characters — well outside the
55-85 comfort band. Widening the margins is the fix; max-width: none on main
is what causes the problem.
Mistake: Writing positional prose — "the chart below", "as shown above" — instead of a numbered cross-reference.
Why it happens: It reads naturally while drafting, when the author can see the whole document at once and the table genuinely is below.
Instead: Write [@tbl:policies]. Pagination moves content, reordering breaks positional claims silently, and a reader navigating by heading has no "below". The auditor cannot detect a broken "below"; it fails the build on a broken [@tbl:policies].
Mistake: Adding an escape hatch so authors can drop <div class="..."> or an embed into the markdown.
Why it happens: A real formatting need appears that the subset does not cover, and passing HTML through is a one-line change.
Instead: Extend the subset or the stylesheet. The escape-then-render ordering is the entire security model — the moment raw HTML passes through, every document becomes an injection vector, and the converter can no longer be pointed at contributed content. There is deliberately no --allow-html flag.
Mistake: Writing one string and letting it serve as both the figure caption and the alt attribute.
Why it happens: The converter falls back to exactly this when no caption is given, which makes it look sanctioned.
Instead: Write both. The caption tells a sighted reader what to conclude; the alt text describes what the figure shows to someone who cannot see it. "Figure 3. Costs fall 40% under Policy B" is a fine caption and useless alt text — it states the conclusion without describing the chart.
Mistake: Building the document for screen, then bolting on a print stylesheet when someone asks for a PDF.
Why it happens: Print feels like a rendering detail rather than a design constraint, and the screen version already looks finished.
Instead: Decide print-or-not before converting. Retrofitted print CSS produces the classic failures — stranded headings, tables split mid-row, dark theme reaching paper as invisible gray text, a 92-character measure. print_profile.py exists so the geometry is a reviewed input, not an afterthought.
Mistake: Treating a passing conversion as evidence the document is ready to publish.
Why it happens: The gate is automated and green, which reads as authoritative.
Instead: The gate checks that references resolve and footnotes are defined. It cannot see a stranded heading, a figure separated from its caption, a table split across pages, or a PDF whose margins clipped the content. Proof every page of the actual output at 100% zoom before publishing.
| File | Purpose |
|------|---------|
| scripts/md_to_html.py | CLI: convert markdown to a self-contained HTML document; gates on broken references |
| scripts/md_render.py | Markdown subset parser, escaping-first inline renderer, label numbering — imported by md_to_html.py, not a CLI |
| scripts/crossref_auditor.py | Audit labels, references, alt text, and heading hierarchy; CI gate |
| scripts/print_profile.py | Generate a print/PDF stylesheet from a JSON page profile |
| references/markdown-conventions.md | Supported syntax, labelling contract, escaping and URL-allowlist model |
| references/print-and-pdf-production.md | Paged media, break control, export mechanics, proofing checklist |
| assets/sample_document.md | Working document exercising every construct; converts clean |
| assets/sample_print_profile.json | A4 double-sided print profile with running heads |
| assets/document_theme.css | Bundled theme inlined by default — this skill's own copy |
| assets/document_outline_template.md | Starting structure for a new report or memo |
All scripts share one exit-code contract: 0 clean, 2 gate failed (findings at or above the threshold), 1 the tool itself errored. A CI job can therefore tell a real defect from a broken invocation.
Three CLI tools, one module. md_render.py is a library, not a fourth
command — it holds the parser that md_to_html.py imports. A single-file
converter came to 429 lines, well over the 300-line ceiling. Splitting CLI from
parser is the remedy the tool-design standard prescribes for an oversized script,
and same-directory imports keep the package self-contained: md-slides carries
its own separate slide_render.py rather than importing this one.
Take borghei/md-document from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.