# Shojiku, for AI agents You are reading the machine-readable companion to this site's pages. Shojiku renders business documents (invoices, receipts, forms, worksheets) from three files: `templates.yml` (all layout and styling), `definitions.yml` (the declared data catalog) and `params.json` (the data). Rendering is deterministic — the same inputs produce the same bytes on the CLI, in Docker, in every SDK and in browser WASM — and network-free. To work with Shojiku, register its MCP server and drive the validate → preview → inspect loop against the engine's own output: ```bash claude mcp add shojiku -- \ docker run --rm -i --entrypoint shojiku-mcp \ -v "$PWD:/work" -w /work ghcr.io/kengos/shojiku:edge ``` Then install the product skills (`npx skills add kengos/shojiku`) and follow `shojiku-template-author` — it carries the authoring loop, the wire gotchas, and the command table. The engine is the only source of rendering truth: validate until clean, preview every page, look at the pixels. Answer the reader in their own language; the material below is English. ## Bundled examples (23) - examples/business/invoice-ja/ — Invoice (ja): 22 line items paginate with a repeating table header, per-tax-rate totals, QR + link; `params-short.json` renders a 3-item single page from the same template. - examples/typography/novel-ja/ — Vertical short story, paperback style (ja): Run, Melos! (excerpt): vertical columns paginate with their ruby, strict kinsoku + hanging punctuation, tate-chu-yoko in the colophon, vertical page numbers. - examples/business/invoice-en/ — Invoice (en-US): Letter-size US invoice: USD with cents, plural-aware quantities (`1 item` / `24 items`), Net-30 terms, payment box + pay-online QR. - examples/forms/certificate-en/ — Certificate (en-US): Letter-landscape certificate: double-rule frame, wide letter spacing, real italics, verification QR. - examples/business/receipt-zh-tw/ — Receipt (zh-TW): The locale-pack story: the same receipt geometry as ja / zh-CN / 80mm en-US, with currency, dates, tax wording, and font fallback swapped by the pack. - examples/forms/application-form-ja/ — Application form, filled ↔ blank (ja): ONE template, two params files: form marks, 〒 entry cells, wareki with a blank-form `placeholder`; not a single pt shifts. - examples/business/restaurant-menu-us/ — Restaurant menu (en + vertical writing): An American Japanese restaurant's specials: English menu, USD prices, and the vertical 正直亭 brand column + per-dish vertical names carrying the Japanese feel. - examples/business/event-tickets-ja/ — Event tickets (ja): 2×4 n-up imposition with per-ticket QR, trim marks for the cutter, `placeholder` seat fallback; 14 attendees flow onto sheet 2 automatically. - examples/business/estimate-ja/ — Estimate: The invoice's sibling: single-rate one-pager, estimate-terms box, discount row. - examples/business/delivery-note-ja/ — Delivery note: Between estimate and invoice: quantity-bundling `headerGroups`, data-driven row styling that tints only rows with items remaining, a receipt-stamp field; partial ↔ complete delivery as two data files. - examples/business/pickup-slip-ja/ — Pickup slip: The Thinreports migration artifact — the migration walkthrough's result. - examples/business/catalog-ja/ — Product catalog: Variable-height `repeat_flow` cards with dynamic images. - examples/business/shipping-labels-ja/ — Shipping labels: 2×3 n-up labels with 〒 cells and an overflowing contents list. - examples/forms/rirekisho-ja/ — Rirekisho (JIS-style résumé): A3 spread, blank ↔ filled from one template. - examples/forms/certificate-ja/ — Certificate (ja): Landscape certificate with a double-rule frame and full bleed. - examples/typography/kokugo-print-ja/ — Kokugo worksheet: Horizontal + vertical char grids with ruby and practice cells. - examples/typography/genkoyoshi-ja/ — Genkoyoshi (vertical): 200-character manuscript paper with Aozora ruby. - examples/typography/genkoyoshi-yoko-ja/ — Genkoyoshi (horizontal): The 400-character horizontal sheet. - examples/business/receipt-ja/ — Receipt (ja): The quickstart document: containers with `%` widths, a boxed total, tax breakdown, issuer block and QR. - examples/business/receipt-us/ — Receipt (80mm thermal, en-US): A custom-size 80mm thermal-printer receipt. - examples/business/receipt-zh-cn/ — Receipt (zh-CN): The simplified-Chinese member of the locale set. - examples/business/receipt-hi-in/ — Receipt (hi-IN): Devanagari conjuncts + lakh/crore digit grouping. - examples/business/receipt-fil-ph/ — Receipt (fil-PH): Latin face + Philippine peso from the fil-PH pack. --- ## docs/engine/README.md (template reference index) # Shojiku template reference The complete authorable surface of the Shojiku engine, one page per feature (MDN-style): what you can write in `templates.yml`, what each key means, its defaults, and the diagnostics it can produce. This is the human- and machine-readable source for "what syntax exists" — the same material the MCP tool surface returns; keep it accurate against the code (see the curation rules in [../README.md](../README.md)). Feature availability per engine build is machine-checkable: `shojiku capabilities` prints the key list, and each page notes its capability keys. Template authors targeting one engine build can ignore them — they exist so GUIs/SDKs can gate features across engine versions. The development-facing companion [features.md](features.md) carries the implemented-capability inventory and the decision log — *that* a feature exists and why it is shaped that way; the pages here carry only *how to author it*. ## A minimal template ```yaml page: { size: A4, margin: 25 } sections: body: type: flow items: - type: text text: "Hello {customer.name}" ``` Full file structure (bands, bodies, styles registry): [template.md](template.md). ## Rendering a template ```bash # PDF (the core command) shojiku render --templates templates.yml --params params.json \ --definitions definitions.yml --output out.pdf # --output - writes to stdout # Static + data checks, diagnostics as JSON (definitions/params optional) shojiku validate --templates templates.yml --params params.json # Per-page preview PNGs ({page} is replaced by the 1-based page number) shojiku preview --templates templates.yml --params params.json \ --output "page-{page}.png" --scale 2.0 # 2.0 px/pt ≈ 144 dpi # The resolved layout tree + box index as JSON (GUI/AI surface) shojiku inspect --templates templates.yml --params params.json # This build's feature keys (no inputs needed) shojiku capabilities ``` Useful defaults on `render` / `preview` / `inspect`: - `--lang ` selects the locale (default: the template `defaults.locale`, then `ja-JP`); `--locale-dir` / `--font-dir` (repeatable) locate the packs, adding to `$SHOJIKU_LOCALE_DIR` / `$SHOJIKU_FONT_DIR` then `./packs/{locale,fonts}`. See [fonts.md](fonts.md). - A font pack whose faces are **pinned** (`sha256` + `url:`) but absent is downloaded into `$SHOJIKU_CACHE_DIR` before rendering; `--offline` refuses instead, and `--font-fetch-allow ` trusts an extra source. Rendering itself never uses the network. See [fonts.md](fonts.md). - `--assets-dir` is what image `src:` paths resolve against (default: **the template file's directory**). Asset policy: `--asset-mode open|bundled-only`, `--allow/deny-dynamic-image `. - Diagnostics print to stderr; `validate` exits non-zero on errors. Try the bundled example: ```bash shojiku render --templates examples/business/receipt-ja/templates.yml \ --params examples/business/receipt-ja/params.json \ --definitions examples/business/receipt-ja/definitions.yml --output receipt.pdf # or via the Docker image (`make docker-build` builds the local # `shojiku-ci:local` tag; its default command renders exactly this example): docker run --rm shojiku-ci:local > receipt.pdf # your own files: mount them and pass normal CLI arguments docker run --rm -v "$PWD:/work" shojiku-ci:local render \ --templates /work/templates.yml --params /work/params.json \ --output /work/out.pdf ``` Getting the binaries in the first place (source build or Docker, plus MCP-server registration for AI agents) is covered in the [quickstart](../quickstart.md). ## Item types Every `items:` entry is a map with a `type:`. Where an item may appear depends on which part of the page holds it. A page ([template.md](template.md)) is made of optional **bands** — `header` and `footer`, repeated at the top/bottom of every page — and one **body** between them, which is either `type: flow` (items stack top-to-bottom and paginate onto new pages as they run out of room — the usual choice, [flow.md](flow.md)) or `type: absolute` (every item pinned at its own `box.x`/`box.y`, single page). The placement column below uses: - **F** — in a flow body - **A** — in an absolute body - **B** — in a band (header/footer) - **C** — inside a `container` item, wherever that container sits - **cell** — inside a `repeat` cell / `repeat_flow` card | `type:` | What it draws | Allowed placement | Page | | --- | --- | --- | --- | | `text` | static / interpolated / bound text; rich `spans:` | F A B C cell | [text.md](text.md) | | `rect` | rectangle (border/fill) | F A B C cell | [rect.md](rect.md) | | `line` | stroked segment (pt-only) | F A B C cell | [line.md](line.md) | | `image` | PNG/JPEG/GIF/WebP/SVG asset | F A B C cell | [image.md](image.md) | | `qr_code` | layout-time vector QR | F A B C cell | [qr_code.md](qr_code.md) | | `list` | one line per array entry + overflow clamp | F A B C cell | [list.md](list.md) | | `container` | nestable box: origin, size, style cascade | F A B C cell | [container.md](container.md) | | `table` | paginating data-driven rows; a column binds a value or hosts a `cell:` sub-template; `box:` narrows it in flow or places it as one bounded block elsewhere | F A B C (not cell) | [table.md](table.md) | | `repeat` | imposition / n-up grid of data-scoped cells | F only | [repeat.md](repeat.md) | | `repeat_flow` | flowing card list, one card per element | F only | [repeat_flow.md](repeat_flow.md) | | `page_break` | start a fresh page | F only | [page_break.md](page_break.md) | | `char_grid` | manuscript-paper / workbook character cells (+ruby) | F A B C cell | [char_grid.md](char_grid.md) | | `ellipse` | box-inscribed oval; circled-option mark or decoration | F A B C cell | [form_marks.md](form_marks.md) | | `checkbox` | always-drawn frame + params-driven check | F A B C cell | [form_marks.md](form_marks.md) | | `page_number` | `{page} / {pages}` | B only | [page_number.md](page_number.md) | Disallowed placements warn and skip (never a hard failure) — codes in [diagnostics.md](diagnostics.md). ## Concepts | Page | Covers | | --- | --- | | [template.md](template.md) | file structure: `page` / `styles` / `defaults` / `formats` / `sections`, bands, bodies, common item keys | | [defaults.md](defaults.md) | document presentation defaults: root style (rem root), per-type format defaults, the `formats:` registry | | [page.md](page.md) | page size, orientation, margin; the margin box as coordinate origin | | [length.md](length.md) | units: pt, `%`, `mm`/`cm`/`in`, `em`/`rem`; round-trip; guards | | [box.md](box.md) | `box:` geometry — x/y/w/h, margin/padding, minWidth/maxWidth/minHeight/maxHeight | | [flex.md](flex.md) | default child placement: direction, gap, alignItems, justifyContent, auto margins | | [grid.md](grid.md) | `box.type: grid` — column/row tracks, fill order | | [flow.md](flow.md) | stacking, gap, pagination, what splits and what moves whole | | [style.md](style.md) | every style property, the cascade, named styles, box decoration | | [text.md](text.md) | wrapping, kinsoku, `textOverflow`, long-text pagination | | [vertical_text.md](vertical_text.md) | `writingMode: vertical_rl` / `textOrientation` — vertical text (plain, spans, list, table cells, page_number) | | [link.md](link.md) | `link: { url }` hyperlinks on text/image/spans → PDF annotations | | [data-binding.md](data-binding.md) | `data:` bindings, `{key:format}` interpolation, `bindings:` named declarations, params, format types | | [definitions.md](definitions.md) | `definitions.yml`: the OpenAPI-shaped schema (properties/items, `format` hints, constraints, display variants, params validation) | | [fonts.md](fonts.md) | locales, lang packs, valid `fontFamily` face ids | | [layout-model.md](layout-model.md) | the resolve invariant, box tree, caps, box index | | [diagnostics.md](diagnostics.md) | every diagnostic code by stage | ## The `box:` keys at a glance | Key | Meaning | Page | | --- | --- | --- | | `x` `y` | offset from the parent origin (authoring either opts a container child out of flex) | [box.md](box.md) | | `w` `h` | border-box size; omitted = fill width / auto height | [box.md](box.md) | | `minWidth` `maxWidth` `minHeight` `maxHeight` | CSS-order size clamps | [box.md](box.md) | | `margin` | outer spacing; per-side map; `auto` sides | [box.md](box.md) | | `padding` | inner spacing (non-negative) | [box.md](box.md) | | `type` | child layout mode: `flex` (default) \| `grid` | [flex.md](flex.md) / [grid.md](grid.md) | | `direction` `gap` `alignItems` `justifyContent` | flex keys (grid reuses some) | [flex.md](flex.md) | | `flexGrow` | child's weighted share of leftover row width | [flex.md](flex.md) | | `columns` `rows` `columnGap` `rowGap` | grid tracks & gaps | [grid.md](grid.md) | | `columnSpan` `rowSpan` | grid child's track span (≥ 1) | [grid.md](grid.md) | ## Style properties at a glance Inherited: `fontSize` `fontFamily` `fontWeight` `fontStyle` `letterSpacing` `lineHeight` `color` `textAlign` `lineBreak` `textSpacingTrim` `hangingPunctuation` `writingMode` `textOrientation` `textCombineUpright`. Not inherited: `verticalAlign` `backgroundColor` `borderWidth` `borderColor` `borderStyle` `borderRadius` `textOverflow` `overflow` `textDecoration` `opacity`. Full table with defaults and value sets: [style.md](style.md). ## Reading order for new authors 1. [template.md](template.md) — the file skeleton 2. [box.md](box.md) + [length.md](length.md) — placing things 3. [style.md](style.md) + [fonts.md](fonts.md) — making them look right 4. [flow.md](flow.md) + [table.md](table.md) — variable-length content 5. [data-binding.md](data-binding.md) — wiring in params AI agents authoring a template end-to-end (three files → validate → preview loop) should also load the step-by-step playbook in [skills/shojiku-template-author/](../../skills/shojiku-template-author/SKILL.md) (AI-only — written as instructions to the agent). Runnable examples live in the **repository source** (`examples/` at the repo root) — a docs-only distribution (e.g. what an MCP consumer sees) does not include them; the snippets on each feature page are the self-contained fallback. Each example directory commits its **rendered output** (`output.pdf` + `preview-.png`) next to the sources, so you can see what a template produces without rendering anything; `make examples` regenerates them all. The set (gallery order and one-line pitches: README.md § Gallery): [`examples/business/invoice-ja`](../../examples/business/invoice-ja) (a multi-page A4 invoice: paginating table with repeating header, pre-computed totals, QR + link; `params-short.json` renders the single-page variant), [`examples/business/estimate-ja`](../../examples/business/estimate-ja) (the invoice's sibling: single-rate one-pager, estimate-terms box, a negative discount row), [`examples/business/delivery-note-ja`](../../examples/business/delivery-note-ja) (a delivery note: `headerGroups` spanning band, data-driven `row.conditionalStyles`, a receipt-stamp field — partial ↔ complete delivery as two params files), [`examples/business/pickup-slip-ja`](../../examples/business/pickup-slip-ja) (the Thinreports-migration worked example — the [migration walkthrough](../migration-thinreports.md)'s result, with the legacy `.tlf` + Ruby host beside it), [`examples/forms/application-form-ja`](../../examples/forms/application-form-ja) (an A4 application form: form marks, 〒 entry cells, wareki `placeholder` — a blank ↔ filled-sample params pair), [`examples/business/event-tickets-ja`](../../examples/business/event-tickets-ja) (2×4 n-up event tickets with per-element QR; `params-few.json` = one sheet), [`examples/business/catalog-ja`](../../examples/business/catalog-ja) (a product catalog: `repeat_flow` variable-height cards, dynamic images, `fit: cover`), [`examples/business/shipping-labels-ja`](../../examples/business/shipping-labels-ja) (2×3 n-up shipping labels: 〒 cells, `list` + an overflow-count line, per-order QR), [`examples/forms/certificate-ja`](../../examples/forms/certificate-ja) (an A4 landscape certificate: double border, mincho + letter spacing, seal/medal SVGs, wareki), [`examples/typography/kokugo-print-ja`](../../examples/typography/kokugo-print-ja) (a kokugo worksheet: kanji practice cells + ruby, a vertical copying grid, answer cells), [`examples/typography/novel-ja`](../../examples/typography/novel-ja) (a B5 vertical paperback booklet: ruby-paginating vertical body, strict kinsoku + hanging punctuation, a tate-chu-yoko colophon, vertical page numbers), [`examples/business/restaurant-menu-us`](../../examples/business/restaurant-menu-us) (a US Japanese-restaurant menu: English menu + USD prices with vertical Japanese accents — `writingMode: vertical_rl` brand column + per-dish dish names, double border, mincho), [`examples/business/invoice-en`](../../examples/business/invoice-en) (Letter US-style invoice: USD cents, plural-aware quantities, Net-30 terms block), [`examples/forms/certificate-en`](../../examples/forms/certificate-en) (Letter-landscape certificate: real italics, en-US dates), [`examples/business/receipt-ja`](../../examples/business/receipt-ja) (an A4 receipt: containers, `%`, named styles), [`examples/business/receipt-us`](../../examples/business/receipt-us) (80mm thermal, custom page size), [`examples/business/receipt-zh-tw`](../../examples/business/receipt-zh-tw) / [`examples/business/receipt-zh-cn`](../../examples/business/receipt-zh-cn) (the same receipt geometry under zh locale packs), [`examples/business/receipt-hi-in`](../../examples/business/receipt-hi-in) (Devanagari conjuncts + lakh/crore digit grouping) / [`examples/business/receipt-fil-ph`](../../examples/business/receipt-fil-ph) (Latin face + the Philippine peso), [`examples/typography/genkoyoshi-ja`](../../examples/typography/genkoyoshi-ja) (a B5 vertical 200-cell genkoyoshi: `char_grid` + aozora ruby) and its horizontal twin [`examples/typography/genkoyoshi-yoko-ja`](../../examples/typography/genkoyoshi-yoko-ja), [`examples/forms/rirekisho-ja`](../../examples/forms/rirekisho-ja) (an A3 landscape JIS-style rirekisho: custom page size, 2-column header, full-width tables), and [`examples/dev/layout-showcase`](../../examples/dev/layout-showcase) — the component-showcase document (rich spans, hyperlinks, flex/grid, overflow policies, zebra table, list, QR, SVG gradient, `repeat_flow`, `repeat` imposition starting in place with trim guides and a document-scoped cell value (`breakBefore: auto`, `cutMarks`, `scope: document`), `page_break`), one labeled section per feature family, each demo followed by a code panel showing the YAML that produces it. The showcase is the **visual index** of the engine, Bootstrap-docs style: its rendered pages show the look and the syntax side by side; open the feature's reference page for the full key table. It grows with the engine — every new authorable feature adds a showcase section (demo + code panel) in the cycle that ships it. --- ## docs/engine/diagnostics.md (the complete diagnostic-code registry) # Diagnostics reference Everything the engine wants to tell a human, a GUI, or an AI flows through structured diagnostics: ```json { "severity": "warning", "code": "invalid_font_size", "category": "layout", "message": "fontSize -5 is not a positive finite number; using 10", "path": "sections.body.items[2]", "args": { "value": -5, "default": 10 }, "origin": "layout/src/engine/resolve.rs:126" } ``` with severity `error` | `warning` | `info`. The engine **degrades, it does not panic**: hostile or wrong input produces a diagnostic and a defined fallback. `shojiku validate` reports the static set; `render`/`inspect`/`preview` add the layout-time set. The fields separate concerns so a localizing consumer (the React GUI's ICU catalog) can render its own message and the engine never translates: - **`code`** — a stable machine-readable identifier and the catalog key. Codes and their per-code **arg keys are an append-only frozen contract**: new codes are added, existing ones are never renamed or repurposed. - **`args`** — typed interpolation data (`String | Number | Bool`, serialized as bare JSON scalars); a consumer formats its own localized message from these. String args are control-character-stripped and length-clipped (untrusted echo). Omitted when empty. - **`message`** — the English default rendering of `code`'s template filled with `args`; a fallback for AI/CLI/dev, not the translation. - **`category`** — a coarse, **re-categorizable** domain (`parse` | `data` | `style` | `layout` | `font` | `asset` | `format` | `limits`). Not part of the frozen contract; the emitting module is never folded into `code`. - **`path`** — **always a structural address in the template** (`sections.body.items[2]`, `….cell.items[0]`, `….columns[1]`, `….headerGroups[0]`), in the same grammar as the `inspect` box index, so a consumer can select the offending node. Engine-synthesized structure only: a data key rides in `args.key`, never here. Layout-stage diagnostics carry the innermost item being laid out — including `page_overflow`, which names the item that ran the page count away. Only what is raised before the walk descends into anything carries no path (`page_margin_too_large`, `orientation_ignored`). A once-per-key warning (an unknown `fontFamily`, a repeated formatter degradation) names the FIRST item that triggered it. Capability key: `diagnostics.layout.path`. - **`origin`** — the engine source location (`file:line`) that emitted it. Non-contract, free to churn, and safe to strip from untrusted output; a GUI hides it, AI reads it to investigate. Omitted when absent. Duplicate diagnostics sharing a `(code, path, message)` are collapsed at the output boundary (a warning re-emitted for one item across a measure and a render pass), keeping the first occurrence. ## Parse errors Malformed YAML/JSON, non-finite numbers (`.nan`/`.inf`), unknown keys anywhere in the template (**every wire struct rejects them** — a typo'd key is never a silent no-op), negative padding, `padding: auto`, shorthand edge strings, invalid page sizes, and unknown enum values are **structural parse failures**: the template is rejected outright, so none of the codes below apply. `shojiku validate` surfaces the failure as a single diagnostic instead of an opaque error so a GUI can render it inline: | Code | Severity | Meaning | | --- | --- | --- | | `parse_error` | error | a structural parse failure, with `args` `what` (which artifact) + `path` (the field path, e.g. `sections.body`) + `detail` (the underlying message) + `line`/`column` when known. An error inside an internally-tagged item (`Body`/`Item`) truncates the path to the enum boundary and omits `line`/`column`, so the location is never over-promised | | `non_finite_number` | error | a `.nan`/`.inf`/overflowing number anywhere in the artifact (`args` `what`) | ## Validation (static, `shojiku validate`) | Code | Severity | Meaning | | --- | --- | --- | | `unknown_data_key` | error | a binding key is not in definitions (scalar, column, cell, or card scope) | | `empty_definitions` | warning | definitions was supplied but declares no properties; every binding will read as `unknown_data_key` (check the file's top-level structure) | | `unknown_format` | error | a format variant not declared for the field | | `not_an_array` | error | a table/repeat/list source key is not an array property | | `interpolation_key_charset` | warning | a `{…}` that looks like an intended key uses characters outside `[A-Za-z0-9_.]`, so it prints its own braces; declare a name under `bindings:` to reach the key | | `unused_binding` | warning | a `bindings:` declaration that no string in the item references | | `binding_shadows_key` | warning | a declared name that also resolved at the ambient scope, redirected by the declaration (which wins); attaching options to the same key is silent | | `invalid_binding_name` | warning | a `bindings:` name outside the reference charset, so no `{name}` could reach it | | `definitions_format_ignored` | warning | a known semantic `format` (`currency`, `date-time`, …) sits on a base type it does not apply to; the base type is used | | `definitions_enum_labels_ignored` | warning | a labeled `enum` member (`{ value, label }`) on a field that is not plain text; its values render unlabeled | | `params_missing_required` | warning | a schema-`required` key is absent or `null` in params | | `params_type_mismatch` | warning | a params value's JSON type differs from the declared schema type (`integer` rejects fractional numbers) | | `params_out_of_range` | warning | a number violates the schema's `minimum`/`maximum` | | `params_length_out_of_range` | warning | a string violates `minLength`/`maxLength` (characters), or an array `minItems`/`maxItems` | | `params_enum_mismatch` | warning | a params value is not one of the schema's declared `enum` values | | `params_unknown_key` | warning | a params key is not declared in definitions (the unknown subtree is not entered); location rides the `key` arg, never `path` | | `image_source_conflict` / `image_source_missing` | error | both / neither of `src`+`data` on an image | | `column_content_conflict` / `column_content_missing` | error | both / neither of `data`+`cell` on a table column (`type`/`fit` beside `cell` conflict too); `cell` wins the render — see [table.md](table.md) | | `undefined_style_name` | warning | `styleNames` references a name not in `styles:` | | `too_many_styles` / `too_many_style_names` | warning | registry > 256 / name list > 16; extras ignored | | `span_content_conflict` | warning | `spans` beside `text`/`data` (spans win), or a span with both `text` and `data` (data wins) | | `empty_span` | warning | a span with neither `text` nor `data`; renders nothing | | `empty_ruby_entry` | warning | a `ruby` entry with an empty `base` or `text`; entry skipped | | `ruby_entry_too_long` | warning | a `ruby` entry whose `base` or `text` exceeds 64 characters; entry skipped | | `too_many_spans` | warning | over the 256-span cap; extras dropped | | `too_many_ruby_entries` | warning | over the 256 `ruby`-entries cap; only the first 256 apply | | `ignored_span_style` | warning | span-inert style keys on a span's inline `style` | | `shape_style_ignored` | warning | inert (text/box) keys on a shape item's (`rect`/`ellipse`/`checkbox`/`mark`) inline `style` | | `shape_border_sides_ignored` | warning | per-side `borderWidth` on `ellipse`/`checkbox`/`mark` reduced to the top side | | `layout_key_on_leaf` | warning | flex/grid keys on a non-container box | | `grid_key_ignored` | warning | grid keys without `box.type: grid` | | `table_pagination_key_ignored` | warning | pagination keys (`keepTogether`, `emptyBehavior` …) on a non-flow (bounded) table | | `ignored_column_key` | warning | `fit` on a non-image table column | | `mark_content_conflict` | warning | checkbox sets both `checked` and `data` (`data` wins) | | `mark_binding_not_boolean` | warning | an `equals`-less mark binding targets a non-boolean field | | `row_condition_not_boolean` | warning | an `equals`-less `row.conditionalStyles` entry targets a non-boolean field | | `reserved_format_name` | warning | a `formats:` registry entry shadows a builtin variant name; ignored | | `too_many_formats` | warning | `formats:` registry over the 256-entry cap; extras ignored | | `too_many_row_conditions` | warning | a table's `row.conditionalStyles` over the 16-entry cap; extras ignored | | `too_many_bindings` | warning | an item's `bindings:` over the 256-entry cap; advisory only — every declaration still resolves | | `container_depth_exceeded` | error | nesting > 32 (also enforced independently at layout) | ## Layout — geometry & resolution | Code | Severity | Meaning | | --- | --- | --- | | `length_out_of_range` | warning | resolved length exceeds ±1e6 pt; default used | | `percent_of_auto` | warning | `%` against an auto-height parent; value dropped | | `page_margin_too_large` | warning | margins consume a page axis; that axis falls back to 0 | | `orientation_ignored` | warning | `orientation: landscape` on a custom `{ w, h }` page size is ignored; express the orientation in the dimensions | | `container_depth_exceeded` | error | nesting > 32; subtree skipped | | `container_overflow` | warning | content taller than a definite-`h` content box; suppressed by `overflow: hidden` | | `section_overflow` | warning | an unsplittable item is taller than the flow region | | `horizontal_overflow` | warning | a fixed-width flex row exceeds its parent content box, or a definite-width flow item reaches past the region's right edge; suppressed under `overflow: hidden`. Absolute-body/band items past the page edge draw silently (no warning) — check the preview | | `page_overflow` | error | layout exceeded 500 pages; output truncated | | `grid_tracks_clamped` | warning | grid `columns`/`rows` outside 1..=64 tracks; clamped | | `grid_cell_overflow` | warning | grid child taller than its explicit row track | | `imposition_grid_clamped` | warning | `repeat` grid over 64 cells/page (or a zero axis); clamped | | `char_grid_clamped` | warning | `char_grid` outside 1..=4096 cells/sheet; clamped | | `char_grid_markup_clamped` | warning | an aozora note asks for more cells than the grid holds (a large-writing scale > `min(columns, lines)`, an indent/raise past the line); clamped | | `invalid_cell_size` | warning | `char_grid` cell size not positive and finite; item skipped | | `vertical_text_unsupported` | warning | `writingMode: vertical_rl` reached a text `mark:` (the circled-text overlay), whose glyph-band overlay is horizontal-only; the mark is skipped. Rich `spans` / `list` / table cells / `page_number` now render vertically. See [vertical_text.md](vertical_text.md) | | `vertical_style_ignored` | warning | registered but no longer emitted: the block-level knobs (`textOverflow`, `textDecoration`, `verticalAlign`, `textSpacingTrim`, `hangingPunctuation`) now apply on vertical blocks ([vertical_text.md](vertical_text.md)); the code stays for the append-only GUI catalog contract | ## Layout — placement rules | Code | Meaning | | --- | --- | | `table_in_cell` | `table` inside a `repeat` cell / `repeat_flow` card / a table column's `cell:`; skipped (everywhere else a table places as a bounded block — see [table.md](table.md)) | | `repeat_in_absolute_body` / `repeat_in_band` / `repeat_in_container` | `repeat` outside a flow body; skipped | | `repeat_flow_in_absolute_body` / `repeat_flow_in_band` / `repeat_flow_in_container` | `repeat_flow` outside a flow body; skipped | | `page_break_in_absolute_body` / `page_break_in_band` / `page_break_in_container` | `page_break` outside a flow body; skipped | | `page_number_in_body` / `page_number_in_container` | `page_number` outside a band; skipped | | `grid_span_clamped` | warning | `columnSpan`/`rowSpan` beyond the axis; clamped | | `span_outside_grid` | warning | span keys on a child of a non-grid box; inert | | `grid_fr_no_basis` | warning | `fr` row tracks in an auto-height container; sized as auto rows | | `cut_marks_clipped` | warning | `cutMarks` have no room outside the grid on a sheet side; those ticks are omitted | | `header_group_span_clamped` | warning | `headerGroups` spans exceed the table's columns | ## Layout — content & data | Code | Meaning | | --- | --- | | `missing_data` (warning) / `not_an_array` (error) | params problems at a bound key; `missing_data` is suppressed when a binding/field `placeholder` covers a blank value (data-binding.md) | | `format_error` | value cannot render as the requested type; suppressed for a blank value covered by a `placeholder`, but NOT for a present-but-invalid one | | `empty_text_item` / `empty_qr_code_item` / `empty_image_item` / `empty_char_grid_item` | neither `text`/`src` nor `data` set | | `char_grid_overflow` | content past a band/absolute `char_grid`'s single sheet; dropped | | `ruby_markup_invalid` | malformed aozora markup (unclosed `《`, empty reading, no base, over-cap, dangling `\|`; a large-writing note whose `「…」` target is not just before it or asks for a scale < 2; a placement note off a line head or a second one on a line); rendered literally | | `aozora_note_ignored` | a `[#…]` aozora note the engine does not act on (not a sheet break, a large-writing, or a placement note); rendered literally | | `ruby_overflow` | a reading overflows its base run even at the 4pt floor | | `ruby_base_not_found` | a `ruby` entry's `base` never occurs in the drawn text; reading skipped | | `rect_missing_size` / `image_missing_size` / `qr_missing_size` | required `box.w`/`box.h` absent | | `text_overflow` | wrapped text exceeds a definite `h` (`textOverflow: visible`, or `shrink` at its 4pt floor) | | `span_overflow_unsupported` | `textOverflow: shrink`/`ellipsis` on a rich (`spans:`) block; falls back to `visible` | | `unknown_font_family` | `fontFamily` matches no loaded family; the default face is used (warns once per family) | | `missing_glyph` | characters the font cannot map (tofu); deduped per block | | `unknown_format_variant` | a picked format variant exists nowhere; the default form rendered (deduped) | | `unknown_currency` | a currency code without display data; the code itself used as symbol/name | | `unknown_unit` | a semantic unit key missing from the locale pack; rendered verbatim | | `format_pattern_ignored` | an inline `format: { pattern }` on a non-dated type; the default form rendered | | `mark_missing_size` | a form mark without a positive `box.w`/`box.h` (a checkbox may omit them — it auto-sizes; an `ellipse` may not); skipped | | `mark_equals_type_mismatch` | a mark's params value type differs from its `equals` literal; not drawn | | `mark_value_not_bool` | an `equals`-less mark binding's value is not a boolean; not drawn | | `row_condition_type_mismatch` | a row's value type differs from a `conditionalStyles` entry's `equals`; the layer is not applied | | `row_condition_value_not_bool` | a row's value is not a boolean under an `equals`-less `conditionalStyles` entry; the layer is not applied | | `qr_content_too_long` / `qr_module_too_small` | QR content over 1 KiB (skipped) / modules under 1 pt (drawn) | | `unsupported_link_scheme` / `link_url_too_long` / `empty_link_url` | resolved `link.url` outside http/https/mailto/tel (or control chars) / over 2048 bytes / empty — the link is dropped, the item still renders | | `table_too_wide` | sized columns exceed the flow width | | `row_overflow` | a row overflows with `autoPageBreak: false` | | `invalid_column_width` / `invalid_row_height` / `invalid_cell_padding` | negative table geometry; clamped to 0 / treated as auto | ## Layout — style guards (hostile values) | Code | Fallback | | --- | --- | | `invalid_font_size` | 10 pt | | `invalid_line_height` | 1.4 | | `invalid_letter_spacing` | 0 (magnitude cap ±1000 pt) | | `invalid_flex_grow` | 0 (negative / non-finite `flexGrow`) | | `invalid_border_width` | no border (cap 0..=1000 pt) | | `invalid_border_radius` | square corners (a negative or non-finite radius) | | `border_radius_ignored` | square corners (a per-side/`double` border, a `table`, or a form mark cannot round) | | `invalid_opacity` | opaque (out-of-range / non-finite `opacity`; never invisible) | | `invalid_color` | default color (echo snippet-capped) | ## Assets (`prepare_assets`) | Code | Meaning | | --- | --- | | `missing_asset` / `assets_root_missing` / `asset_traversal` | bundled path absent / no assets root / path escapes the root | | `invalid_image_asset` (error) / `invalid_image_data` (warning) | undecodable static asset / dynamic data | | `remote_asset_unsupported` | remote URL source; the render path has no network I/O | | `dynamic_image_denied` | params-bound image blocked by the asset policy | | `svg_unsupported` | SVG constructs outside the subset parser | | `cell_image_assets_capped` | per-element cell images (table columns + repeat/repeat_flow cells) over the shared 1000-load cap; the rest are skipped | ## See also - [layout-model.md](layout-model.md) — the resolve caps that emit the geometry codes - Each feature page lists the codes it can emit. --- ## docs/quickstart.md # Quickstart From zero to a rendered PDF, then an AI agent driving the engine. Nothing to install but Docker. ## 1. Get the engine The image carries the CLI, the MCP server, the font and locale packs, and every bundled example. It runs on x86-64 and arm64. ```bash docker pull ghcr.io/kengos/shojiku:edge ``` `edge` tracks `main`. It is the only tag that exists — Shojiku is pre-1.0, so there is no `latest` and no version tag yet. The same image is mirrored to Docker Hub as `kengos/shojiku:edge` if you prefer that registry; ghcr is what the rest of this page uses because it does not rate-limit anonymous pulls. Prefer to build it yourself, or want the binaries outside a container? [from-source.md](from-source.md). ## 2. Render a bundled example The image's default command renders the bundled Japanese receipt to stdout, so this needs no arguments and no files of your own: ```bash docker run --rm ghcr.io/kengos/shojiku:edge > receipt.pdf ``` Every bundled example is inside the image under `/opt/shojiku/examples`, and any of them renders the same way: ```bash docker run --rm ghcr.io/kengos/shojiku:edge render \ --templates /opt/shojiku/examples/business/invoice-en/templates.yml \ --params /opt/shojiku/examples/business/invoice-en/params.json \ --definitions /opt/shojiku/examples/business/invoice-en/definitions.yml \ --output - > invoice.pdf ``` ## 3. Start from an example of your own The fastest way to a document you can edit is to lift a bundled one out of the image. The image is distroless and has no shell, so this uses `docker cp` rather than `sh`: ```bash cid=$(docker create ghcr.io/kengos/shojiku:edge) docker cp "$cid:/opt/shojiku/examples/business/invoice-en" ./my-doc docker rm "$cid" ``` **Copy the whole DIRECTORY, not the three files.** A template can reference siblings — `invoice-en` pulls in `assets/logo.svg` — and taking only `templates.yml`, `params.json` and `definitions.yml` gets you `error[invalid_image_asset]` on the first render. To just LOOK at one file without copying anything, send `docker cp` to stdout — it writes a tar stream, so pipe it through `tar -xO`: ```bash cid=$(docker create ghcr.io/kengos/shojiku:edge) docker cp "$cid:/opt/shojiku/examples/business/receipt-ja/templates.yml" - | tar -xO docker rm "$cid" ``` `docker run --entrypoint cat …` will NOT work: the image carries the engine and nothing else, so there is no `cat` and no `sh` in it to run. That is deliberate — a smaller attack surface than a shell-bearing base — and `docker cp` is the way around it, because it reads the container filesystem from the outside rather than executing anything inside. Now edit `my-doc/templates.yml` (the layout) or `my-doc/params.json` (the data), and render by mounting the directory. Fonts and locale packs are already inside the image, so this is the only mount you need: ```bash docker run --rm -v "$PWD/my-doc:/work" ghcr.io/kengos/shojiku:edge render \ --templates /work/templates.yml --params /work/params.json \ --definitions /work/definitions.yml \ --output /work/out.pdf ``` **To SEE whether your edit landed, render a preview instead of a PDF.** `preview` writes one PNG per page, which you can open without a PDF viewer — and unlike grepping the PDF, it actually shows you the result. (Text inside a Shojiku PDF is stored as subsetted font glyph indices, so `strings out.pdf | grep "your name"` finds nothing even when the change is there. That is normal, not a failed render.) ```bash docker run --rm -v "$PWD/my-doc:/work" ghcr.io/kengos/shojiku:edge preview \ --templates /work/templates.yml --params /work/params.json \ --definitions /work/definitions.yml \ --output '/work/page-{page}.png' ``` The `{page}` placeholder is required whenever the document has more than one page; without it the command refuses rather than overwriting page 1 with page 2. Every bundled example under `/opt/shojiku/examples` works as a starting point — the [gallery](../README.md#gallery) shows what each one is. **Do not drop `--definitions`.** It is optional to the CLI, and leaving it out still exits 0 with no diagnostic — but the field TYPES live there, so a currency field renders as a bare `8,360` instead of `¥8,360`, and a quantity loses its counter. The degradation is silent and visible only in the output. ([engine/definitions.md](engine/definitions.md) is the reference for the file.) ## 4. Write your own template The template reference is [engine/](engine/README.md) — one page per feature (items, box model, styles, tables, repeats, …), plus the `validate` / `preview` / `inspect` commands that make the write→check→look loop fast. Or skip hand-writing entirely and use an agent (next section). ## With an AI coding agent (MCP) `shojiku-mcp` is a stdio MCP server exposing `validate` / `render_preview` / `inspect_layout` / `capabilities`. It ships in the same image, and stdio is exactly what `docker run -i` gives you — for Claude Code: ```bash claude mcp add shojiku -- \ docker run --rm -i --entrypoint shojiku-mcp \ -v "$PWD:/work" -w /work ghcr.io/kengos/shojiku:edge ``` Other clients take the same server as JSON. The command is identical everywhere; what differs is which file it goes in, the top-level key, and — the part that actually bites — how you spell the directory to mount. ```json { "mcpServers": { "shojiku": { "command": "docker", "args": [ "run", "--rm", "-i", "--entrypoint", "shojiku-mcp", "-v", "/absolute/path/to/your/documents:/work", "-w", "/work", "ghcr.io/kengos/shojiku:edge" ] } } } ``` **The path is written out in full on purpose.** `$PWD` is a SHELL expansion — it works in the `claude mcp add` command line above, and it is not something a JSON config loader expands. Each editor has its own variable syntax instead, and using the wrong one passes the literal string to `docker -v`, which silently mounts a directory named `${PWD}`. An absolute path is the one spelling that works everywhere. | Client | File | Top-level key | Workspace variable | | --- | --- | --- | --- | | Claude Code | `.mcp.json` in the project | `mcpServers` | — (use the CLI above) | | Claude Desktop | macOS `~/Library/Application Support/Claude/claude_desktop_config.json`, Windows `%APPDATA%\Claude\claude_desktop_config.json` | `mcpServers` | none — it has no project, so absolute is the only option | | VS Code (Copilot) | `.vscode/mcp.json` in the project | **`servers`** | `${workspaceFolder}` | | Cursor | `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` | `mcpServers` | check its docs; absolute always works | | Windsurf, Cline, Zed, … | that client's own MCP config | `mcpServers` | check its docs; absolute always works | Whatever you mount is the whole of what the agent can reach: it reads your templates from there and writes PDFs back there, and the container sees nothing else on your machine. ### Checking the server yourself Before wiring it into an editor, you can confirm the server runs and see what it offers. This asks it to introduce itself and list its tools: ```bash printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \ | docker run --rm -i --entrypoint shojiku-mcp ghcr.io/kengos/shojiku:edge ``` You should get two JSON lines back: the server naming itself `shojiku-mcp`, then the four tools with their schemas. If the first line never arrives, the usual cause is a missing `-i` — without it the server sees EOF and exits before answering. Then ask for the document you need: > Make an A4 receipt with our logo, a tax-breakdown table, and a QR code > linking to the receipt page. The agent writes `templates.yml` / `definitions.yml`, renders a preview, inspects the layout tree, and iterates on the diagnostics until the output matches. The agent playbooks (AI-only instructions) are: - [skills/shojiku-template-author/](../skills/shojiku-template-author/SKILL.md) — authoring a template from natural-language requirements (its **Engine access** section is the canonical MCP-first / CLI-fallback command table the other two reference). - [skills/shojiku-render-debugger/](../skills/shojiku-render-debugger/SKILL.md) — diagnosing why an existing template renders wrong. - [skills/shojiku-thinreports-migrator/](../skills/shojiku-thinreports-migrator/SKILL.md) — migrating a legacy Thinreports report by visual regeneration. [migration-thinreports.md](migration-thinreports.md) walks one such migration end to end (legacy `.tlf` + Ruby host → the re-authored bundled example), if you want to see the method before pointing an agent at your own report.