@tobisk/markdown-manuals
v1.3.4
Published
Render a folder of Obsidian-flavored Markdown as an offline HTML manual and PDF.
Maintainers
Readme
Markdown Manual Renderer
@tobisk/markdown-manuals turns a numbered folder of CommonMark, GFM, and
Obsidian-flavored Markdown into two outputs from one parsed document model:
- a searchable, responsive, self-contained offline HTML manual and deterministic ZIP; and
- a paginated PDF produced with React-PDF, including metadata, bookmarks, internal links, running headers, footers, and page numbers.
It supports headings, paragraphs, emphasis, code, links, local images, tables, nested lists,
task lists, blockquotes, Obsidian callouts, [[wikilinks]], environment placeholders, and
build-time Mermaid diagrams. Raw Markdown HTML is not executed.
[!IMPORTANT] This project was vibe coded to solve a particular documentation problem in ToskLight. It was built through rapid, AI-assisted iteration against that manual rather than designed from a complete general-purpose publishing specification. The repository includes automated and visual checks, but adopters should review generated documents carefully and expect to adapt hooks and configuration for their own publishing requirements.
Try the example
The repository contains a complete small manual under examples/basic. After
installing dependencies, render it with:
npm run exampleThis writes offline HTML, a ZIP, and an A4 PDF under examples/basic/output/. The example covers
folder-derived heading levels, Obsidian callouts, environment placeholders, Mermaid, tables, and a
custom render hook.
CLI
npx --package @tobisk/markdown-manuals markdown-manual build \
--source docs/help \
--config docs/help/manual.json \
--allowed-env-vars LIGHT_MANUAL_VERSION \
--html-dir .artifacts/manual/html \
--html-archive .artifacts/manual/manual-html.zip \
--pdf .artifacts/manual/manual.pdfThe three output flags are optional overrides. Paths in the JSON config are resolved relative to the config file; CLI paths are resolved relative to the current working directory.
Configuration
{
"title": "ToskLight Operator Manual",
"subtitle": "Control, Architect, and Pixel",
"author": "ToskLight contributors",
"language": "en",
"version": "${LIGHT_MANUAL_VERSION}",
"description": "ToskLight operator documentation",
"brand": { "logo": "../../assets/brand/icon.png" },
"chapterIndexNames": ["index.md", "index.markdown"],
"output": {
"htmlDir": "../../.artifacts/generated/manual/html/tosklight-manual",
"htmlArchive": "../../.artifacts/generated/manual/html/tosklight-manual-html.zip",
"pdf": "../../.artifacts/generated/manual/pdf/tosklight-manual.pdf"
},
"theme": {
"accent": "#0f8f82",
"navigationBackground": "#071621",
"calloutDanger": "#b42318",
"callouts": { "warning": "#b7791f", "tip": "#16803c" }
},
"pdf": {
"pageSize": "A4",
"contentsDepth": 3,
"header": "{title} - {chapter}",
"footer": "ToskLight Operator Manual",
"margins": { "top": 54, "right": 50, "bottom": 54, "left": 50 }
},
"mermaid": {
"theme": "base",
"backgroundColor": "transparent",
"width": 1400,
"height": 900,
"scale": 2,
"themeVariables": {
"primaryColor": "#d7f3ef",
"primaryBorderColor": "#0f8f82",
"lineColor": "#425466",
"edgeLabelBackground": "#ffffff"
}
},
"inlineTokens": [
{ "pattern": "\\[([A-Z0-9.]+)\\]", "kind": "desk-key", "labelGroup": 1 }
],
"hookModules": ["manual-hooks.mjs"],
"layout": {
"columns": 1,
"columnGap": 18,
"hierarchyHeadings": true,
"maxHeadingDepth": 6,
"justifyText": true
}
}${NAME} placeholders in both the JSON configuration and Markdown sources are read from the
environment only when NAME is listed by --allowed-env-vars. The option accepts a comma-separated
allowlist such as --allowed-env-vars APP_VERSION,BUILD_NUMBER. Unlisted placeholders remain
literal even when the process has a matching environment variable, preventing Markdown from
accidentally exposing CI secrets or other ambient values. An allowed but unset variable fails the
build. Write \${NAME} when the literal placeholder syntax should be printed. Inline-token patterns
are JavaScript regular-expression sources and must not match an empty string.
layout.columns accepts 1 or 2. In two-column mode, major headings, tables, figures, code,
and callouts span both columns. With hierarchyHeadings enabled, a folder index keeps the folder's
heading level, an ordinary file is one level deeper, and nested folders deepen the hierarchy even
when they have no index. maxHeadingDepth caps the effective level.
Every top-level folder index becomes a dedicated section-divider page in PDF and a full-height divider in HTML. If a standalone image is immediately before or after that index's H1, the image stays with the centered title. With hierarchical headings enabled, effective H2 headings are rendered as labelled chapters, H3 headings as ruled sections, H4 headings in the accent color, and H5 headings as compact subheads.
Render hooks
Hook modules are resolved relative to the configuration file. They export one hook, an array of hooks, or a default export. A hook can replace any normalized Markdown node before both HTML and PDF rendering:
export default {
name: "control-keys",
transform(node, context) {
if (node.type !== "inlineToken" || node.data?.kind !== "desk-key") return;
return {
...node,
data: {
...node.data,
presentation: {
component: "key-sequence",
keys: [{ label: node.value, variant: node.value === "REC" ? "record" : "regular" }],
},
},
};
},
};The built-in key-sequence presentation supports regular, record, clear, preload,
keyboard, and shift variants. Keys accept the vector-backed shift and backspace icons.
Use the command-line presentation on an inlineCode node to render a desk command-line readout.
Use control-sequence with ordered { text } and { keys } segments to place padded keys inside
one dark inline command strip.
Contents directives
Keep an individual heading out of generated contents by putting <!-- toc: exclude --> directly
before it. Put <!-- toc: exclude-headings --> anywhere on a page to retain the page title while
excluding every lower-level heading on that page. Both directives are removed from HTML and PDF.
Table directives
Configure a particular table in its Markdown source rather than coupling document layout to a render hook. Put a directive immediately before the table:
<!-- table: columns=12,15,18,55; rows-per-page=17; row-weight=1.5; continue-after-table -->
| Desk key | Button | Computer keyboard | What it does |
| --- | --- | --- | --- |columns contains positive relative widths and must have exactly one value per table column.
rows-per-page and row-weight tune unusually long PDF tables. continue-after-table permits
following content to share the same generated PDF page. Unknown options, invalid numbers, column
count mismatches, and directives that are not directly followed by a table fail the build.
Image directives
Size a standalone image from its Markdown source with a directive immediately before it:
<!-- image: width=72% -->
Widths accept positive percentages up to 100%, CSS pixels such as 320px, or PDF points such as
240pt. Pixel widths are converted to points for PDF output. The directive is removed from both
outputs and fails the build unless the next block is a standalone image. PDF pagination keeps every
heading with the first meaningful block that follows it, including an image, paragraph, table,
list, callout, or code block.
Mermaid diagrams
Use an ordinary fenced Mermaid block:
```mermaid
flowchart LR
Control[ToskLight Control] -->|Art-Net or sACN| Architect[ToskLight Architect]
Control -->|DMX and CITP/MSEX| Pixel[ToskLight Pixel]
```The build uses Mermaid's official renderer once and embeds its scalable SVG in HTML and a
high-resolution PNG in the React-PDF document. Invalid diagrams fail with the diagram number and
Mermaid's parsing error. The mermaid configuration controls the Mermaid theme, theme variables,
background, viewport, and scale. Puppeteer supplies the isolated build-time browser; the generated
HTML does not need Mermaid JavaScript or a network connection.
Obsidian syntax
> [!danger] Graphic missing
> Add the missing workflow diagram here.
See [[Patching|the patching chapter]].Callout types are case-insensitive. [!note]+ and [!note]- fold markers are retained in the
shared model; static HTML and PDF intentionally render their content expanded. A wikilink resolves
by unique page title, basename, or root-relative Markdown path. ![[image.png|Caption]] embeds a
local image.
The renderer supports Obsidian's standard callout types and aliases: note; abstract, summary,
and tldr; info; todo; tip, hint, and important; success, check, and done;
question, help, and faq; warning, caution, and attention; failure, fail, and
missing; danger and error; bug; example; and quote and cite. Each family has its own
semantic accent and tinted background in both HTML and PDF. Unknown callout types remain visible
and use the note appearance. theme.callouts can override an individual type or family color.
CommonMark line-break rules are preserved across both outputs. A single source newline inside a paragraph becomes a space, two trailing spaces before a newline create a visible hard break, and a blank line starts a new paragraph.
Development
npm install
npm run typecheck
npm test
npm run build
npm run exampleThe package targets Node.js 22 or newer. It does not fetch remote images, execute raw Markdown HTML, or allow local Markdown assets to escape the supplied source root.
Releases
Forgejo is authoritative for main, release commits, and vX.Y.Z tags. Its configured mirror
delivers those commits and tags to GitHub. Forgejo CI runs semantic-release after typecheck, tests,
build, and package inspection; it updates package.json, package-lock.json, and CHANGELOG.md,
then creates the release commit and tag. GitHub Actions rebuilds a mirrored tag, publishes
@tobisk/markdown-manuals through npm trusted publishing, and creates the GitHub Release.
No npm token is stored in the repository.
Conventional Commits control releases: fix: and perf: create a patch, feat: creates a minor,
and ! or a BREAKING CHANGE: footer creates a major. Documentation, tests, refactors, build, CI,
and chores do not release by default. Preview the next release locally with
npm run release:dry-run; it requires access to full repository history and the Forgejo remote but
does not publish in dry-run mode. The remote is private, so the local Git credentials must be able
to read it.
Publishing requires two repository-side settings:
- Forgejo secret
SEMANTIC_RELEASE_TOKEN, scoped to push the release commit and tags. - An npm trusted publisher for package
@tobisk/markdown-manuals, GitHub repositorykellertobias/markdown-docgen, workflowrelease.yml.
