|
| 1 | +# JDH Code Appendix — Design Spec |
| 2 | + |
| 3 | +**Date:** 2026-06-08 |
| 4 | +**Status:** Approved |
| 5 | +**Repos:** `jdh-cli`, `jdh-typst-template` |
| 6 | + |
| 7 | +## Goal |
| 8 | + |
| 9 | +Add an optional **Code Appendix** to JDH PDF exports: a clean page break and |
| 10 | +section after the main article body that lists every eligible code block in |
| 11 | +manuscript order, styled like the main flow (narrative gray / hermeneutics |
| 12 | +cyan) but **never truncated**. Each appendix entry shows the **same paragraph |
| 13 | +number** as in the main document so readers can cross-reference truncated |
| 14 | +excerpts in the body with full listings in the appendix. |
| 15 | + |
| 16 | +Fixture scale (BHmHNQKJaSWT): **1** narrative code block, **10** hermeneutics |
| 17 | +code blocks (11 appendix entries total). |
| 18 | + |
| 19 | +## Scope |
| 20 | + |
| 21 | +### In scope |
| 22 | + |
| 23 | +- Eligible **narrative** and **hermeneutics** block-level code (see eligibility) |
| 24 | +- Manuscript-order replay from a Typst registry populated during main-body render |
| 25 | +- Full code (no `max-lines` clip, no fade, no “N lines more” footer) |
| 26 | +- Paragraph numbers in appendix matching main-flow semantics (Option A for |
| 27 | + hermeneutics) |
| 28 | +- Page break + section heading **before References** (`template.typ`) |
| 29 | +- Opt-in via project config (`meta-jdh.yml` or frontmatter) |
| 30 | +- Thin MyST anchor plugin + Typst registry/renderer (**Hybrid D**) |
| 31 | +- Unit tests for MyST eligibility / anchor injection; manual PDF verification |
| 32 | +- Documentation in `jdh-cli/docs/plugins/` |
| 33 | + |
| 34 | +### Out of scope |
| 35 | + |
| 36 | +- Figure boilerplate code (`code:fig:*` / figure-tagged cells) |
| 37 | +- Inline `` `code` `` |
| 38 | +- HTML export |
| 39 | +- Tables, figures, or prose in the appendix |
| 40 | +- Improve-pipeline duplication of code into `article.md` |
| 41 | +- Changing main-flow truncation defaults (`jdh-theme.code.max-lines`) |
| 42 | +- Adding visible paragraph numbers to hermeneutics code in the main body |
| 43 | + |
| 44 | +## Decisions (approved) |
| 45 | + |
| 46 | +### Technical approach: D — Hybrid (thin MyST anchors + Typst registry) |
| 47 | + |
| 48 | +MyST assigns stable sequence IDs and injects minimal Typst hooks; Typst captures |
| 49 | +code text, paragraph numbers, and kind at **render time** during main-body layout, |
| 50 | +then replays the registry after `[-CONTENT-]`. Matches the hermeneutics / |
| 51 | +narrative-code / jdh-table pattern without duplicating source in markdown. |
| 52 | + |
| 53 | +Not approach A (MyST-only appendix directive with duplicated bodies), B (improve |
| 54 | +step appends appendix markdown), or C (Typst-only without MyST sequencing). |
| 55 | + |
| 56 | +### Hermeneutics paragraph numbers: Option A |
| 57 | + |
| 58 | +Use the paragraph number of the **preceding numbered block** in main flow — the |
| 59 | +last prose paragraph or heading that received a paragraph number **before the |
| 60 | +opening of the hermeneutics block** (`:::{hermeneutics}`). All code excerpts |
| 61 | +inside that block share that number. |
| 62 | + |
| 63 | +Not Option B (dedicated hidden/visible ¶ per hermeneutics code excerpt in main |
| 64 | +flow). |
| 65 | + |
| 66 | +Implementation: at hermeneutics code render time, freeze |
| 67 | +`jdh-last-para-num` (see § Paragraph numbers) — the counter value after the |
| 68 | +last `p-step` on a numbered block before the hermeneutics wrapper opened. |
| 69 | + |
| 70 | +### Placement |
| 71 | + |
| 72 | +`template.typ`: after `[-CONTENT-]`, **before** `#bibliography(...)` (References). |
| 73 | + |
| 74 | +### Styling |
| 75 | + |
| 76 | +Appendix entries reuse main-flow visual language: |
| 77 | + |
| 78 | +| Kind | Main-flow wrapper | Appendix styling | |
| 79 | +|------|-------------------|------------------| |
| 80 | +| Narrative | `#narrative-code-block` | Same gray fill / bleed; no truncation footer | |
| 81 | +| Hermeneutics | `#hermeneutics-block` + code markers | Same cyan fill; sidebar markers optional (see Open items) | |
| 82 | + |
| 83 | +Typography from `jdh-theme.code` (font, size, line-height) applies in both |
| 84 | +flows. |
| 85 | + |
| 86 | +### Truncation |
| 87 | + |
| 88 | +Truncation is **Typst-only** today (`jdh-theme.code.max-lines`, `fade-lines`). |
| 89 | +Appendix sets `in-appendix-block` state so `show raw.where(block: true)` skips |
| 90 | +clipping and the “N lines more” overlay. Main-body behaviour unchanged. |
| 91 | + |
| 92 | +### Config |
| 93 | + |
| 94 | +Feature is **opt-in** (default off). See § Configuration. |
| 95 | + |
| 96 | +## Architecture |
| 97 | + |
| 98 | +Three layers, mirroring jdh-table / narrative-code: |
| 99 | + |
| 100 | +``` |
| 101 | +MyST build Main-body Typst Appendix Typst |
| 102 | +────────── ─────────────── ────────────── |
| 103 | +code-appendix.mjs → jdh-code-appendix-enter → jdh-render-code-appendix() |
| 104 | + inject seq anchors register {seq, kind, pagebreak + heading |
| 105 | + (after narrative/ content, paraNum} replay registry |
| 106 | + hermeneutics wraps) during normal render in-appendix-block: true |
| 107 | + (truncated in body) full code, frozen ¶ |
| 108 | +``` |
| 109 | + |
| 110 | +```mermaid |
| 111 | +flowchart TB |
| 112 | + subgraph myst [MyST build] |
| 113 | + NC[narrative-code.mjs] |
| 114 | + HM[hermeneutics.mjs] |
| 115 | + CA[code-appendix.mjs] |
| 116 | + NC --> CA |
| 117 | + HM --> CA |
| 118 | + end |
| 119 | + subgraph main [Main body render] |
| 120 | + REG[(jdh-code-appendix-registry state)] |
| 121 | + NCW["#narrative-code-block"] |
| 122 | + HMW["#hermeneutics-block + code"] |
| 123 | + NCW -->|register entry| REG |
| 124 | + HMW -->|register entry| REG |
| 125 | + end |
| 126 | + subgraph appendix [After CONTENT in template.typ] |
| 127 | + REN["#jdh-render-code-appendix()"] |
| 128 | + REG --> REN |
| 129 | + REN --> REFS["#bibliography References"] |
| 130 | + end |
| 131 | + myst --> main |
| 132 | + main --> appendix |
| 133 | +``` |
| 134 | + |
| 135 | +### Registry entry shape |
| 136 | + |
| 137 | +Each eligible code block appends one record during main-body render: |
| 138 | + |
| 139 | +| Field | Type | Description | |
| 140 | +|-------|------|-------------| |
| 141 | +| `seq` | int | Manuscript order (from MyST anchor) | |
| 142 | +| `kind` | `"narrative"` \| `"hermeneutics"` | Styling branch | |
| 143 | +| `content` | string | Full raw source (`it.text`) | |
| 144 | +| `para-num` | int | Frozen paragraph number for appendix margin | |
| 145 | + |
| 146 | +Registry is a Typst `state("jdh-code-appendix-registry", ())` array, appended |
| 147 | +in `seq` order (MyST guarantees monotonic IDs). |
| 148 | + |
| 149 | +## Eligibility |
| 150 | + |
| 151 | +Mirror existing plugin rules. Include: |
| 152 | + |
| 153 | +| Source | Eligible? | Notes | |
| 154 | +|--------|-----------|-------| |
| 155 | +| Block `code` wrapped by `#narrative-code-block` | Yes | Untagged or `tags=["narrative"]` | |
| 156 | +| Block `code` inside `#hermeneutics-block` | Yes | One registry entry per code fence | |
| 157 | +| Code inside tables / admonitions (narrative rules) | Yes | Same as narrative-code | |
| 158 | +| `code:fig:*` / figure pipeline output | **No** | Removed by hide-figure-code | |
| 159 | +| Inline code | **No** | Not block-level | |
| 160 | +| Plain `raw` blocks outside wrappers | **No** | Not styled code | |
| 161 | + |
| 162 | +When `project.jdh.code_appendix` is false, MyST plugin is inert (no anchors; |
| 163 | +Typst renderer emits nothing). |
| 164 | + |
| 165 | +## Paragraph numbers |
| 166 | + |
| 167 | +Global counter: `counter("jdh-paragraph")` in `jdh.typ`. Title, abstract, and |
| 168 | +front matter are not numbered. |
| 169 | + |
| 170 | +### Shared Typst state |
| 171 | + |
| 172 | +Add `jdh-last-para-num` state, updated whenever a numbered block completes |
| 173 | +`p-step` (body paragraphs, headings, narrative code). Hermeneutics code reads |
| 174 | +this snapshot; it does **not** call `p-step` in main flow today. |
| 175 | + |
| 176 | +Add `jdh-hermeneutics-block-para-num` state, set once when `#hermeneutics-block` |
| 177 | +opens: copy `jdh-last-para-num` at block entry. All code registrations inside |
| 178 | +that block use `jdh-hermeneutics-block-para-num` (Option A — preceding block |
| 179 | +before the hermeneutics wrapper). |
| 180 | + |
| 181 | +### Narrative code |
| 182 | + |
| 183 | +At existing `#narrative-code-block` render site (where `p-display` + `p-step` |
| 184 | +already run), register with `para-num` = counter value **after** `p-step` — the |
| 185 | +same number shown in the main-flow margin. |
| 186 | + |
| 187 | +### Hermeneutics code |
| 188 | + |
| 189 | +At hermeneutics code render (inside `#hermeneutics-block`, no main-flow |
| 190 | +`p-display`), register with `para-num` = `jdh-hermeneutics-block-para-num`. |
| 191 | + |
| 192 | +Example (BHmHNQKJaSWT): narrative block shows ¶ 42 in main body → appendix |
| 193 | +entry 1 shows **42**. Hermeneutics excerpt following ¶ 58 prose → appendix |
| 194 | +entries show **58** (not a new number for the cyan code box). |
| 195 | + |
| 196 | +### Appendix display |
| 197 | + |
| 198 | +Appendix replays entries with **frozen** `para-num` in the margin column. |
| 199 | +Replay must **not** call `p-step` (no double-counting). Use a dedicated |
| 200 | +`p-display-frozen(n)` helper that prints a literal number instead of |
| 201 | +`counter.display()`. |
| 202 | + |
| 203 | +Appendix section heading uses `p-skip` (does not consume or display a new ¶). |
| 204 | + |
| 205 | +## Typst (`jdh-typst-template`) |
| 206 | + |
| 207 | +### New state |
| 208 | + |
| 209 | +```typst |
| 210 | +#let in-appendix-block = state("jdh-in-appendix-block", false) |
| 211 | +#let jdh-code-appendix-registry = state("jdh-code-appendix-registry", ()) |
| 212 | +#let jdh-last-para-num = state("jdh-last-para-num", 0) |
| 213 | +#let jdh-hermeneutics-block-para-num = state("jdh-hermeneutics-block-para-num", 0) |
| 214 | +``` |
| 215 | + |
| 216 | +### Registry helpers |
| 217 | + |
| 218 | +```typst |
| 219 | +#let jdh-code-appendix-enabled = state("jdh-code-appendix-enabled", false) |
| 220 | +
|
| 221 | +#let jdh-code-appendix-register(kind, seq, content, para-num) = context { |
| 222 | + if not jdh-code-appendix-enabled.get() { return } |
| 223 | + jdh-code-appendix-registry.update(entries => { |
| 224 | + entries + ((kind: kind, seq: seq, content: content, para-num: para-num)) |
| 225 | + }) |
| 226 | +} |
| 227 | +``` |
| 228 | + |
| 229 | +Called from `#narrative-code-block` and the hermeneutics code path in |
| 230 | +`show raw.where(block: true)` after content is known. |
| 231 | + |
| 232 | +### `in-appendix-block` truncation bypass |
| 233 | + |
| 234 | +In `show raw.where(block: true)`, when `in-appendix-block.get()`: |
| 235 | + |
| 236 | +- Skip `max-lines` / `hidden-lines` / fade / “N lines more” |
| 237 | +- Render full `it` |
| 238 | +- Keep font, fill, and wrapper styling |
| 239 | + |
| 240 | +### `#jdh-render-code-appendix()` |
| 241 | + |
| 242 | +Called from `template.typ` when enabled (via frontmatter flag passed into |
| 243 | +`#show: template.with(...)`): |
| 244 | + |
| 245 | +1. If registry empty → return nothing |
| 246 | +2. `#pagebreak()` (recto/odd break: open item) |
| 247 | +3. `#p-skip.update(true)` around appendix heading |
| 248 | +4. `#heading(level: 1)[Appendix: Code Listings]` (wording open item) |
| 249 | +5. `#in-appendix-block.update(true)` |
| 250 | +6. For each registry entry in `seq` order: |
| 251 | + - `#p-display-frozen(entry.para-num)` |
| 252 | + - Wrap in `#narrative-code-block` or `#hermeneutics-block` (appendix variant) |
| 253 | + - Render `raw(block: true, entry.content, lang: ...)` |
| 254 | +7. `#in-appendix-block.update(false)` |
| 255 | + |
| 256 | +### `template.typ` hook |
| 257 | + |
| 258 | +```typst |
| 259 | +[-CONTENT-] |
| 260 | +
|
| 261 | +#if doc.jdh.code-appendix ?? false { |
| 262 | + #jdh-render-code-appendix() |
| 263 | +} |
| 264 | +
|
| 265 | +#if doc.bibtex { |
| 266 | + #bibliography("[-doc.bibtex-]", ...) |
| 267 | +} |
| 268 | +``` |
| 269 | + |
| 270 | +Exact frontmatter path wired during implementation (`doc.jdh.code-appendix` or |
| 271 | +export option — see Open items). |
| 272 | + |
| 273 | +### Theme tokens (optional) |
| 274 | + |
| 275 | +```typst |
| 276 | +code-appendix: ( |
| 277 | + heading: "Appendix: Code Listings", |
| 278 | + enabled-default: false, |
| 279 | +) |
| 280 | +``` |
| 281 | + |
| 282 | +Under `jdh-theme` for heading text override. |
| 283 | + |
| 284 | +## MyST (`jdh-cli`) |
| 285 | + |
| 286 | +### Plugin: `code-appendix.mjs` |
| 287 | + |
| 288 | +New bundled plugin (or extend `narrative-code.mjs` + `hermeneutics.mjs` — prefer |
| 289 | +**single dedicated plugin** that runs after both, stage `document`): |
| 290 | + |
| 291 | +1. Read `project.jdh.code_appendix` from MyST config (if unavailable, no-op) |
| 292 | +2. Walk AST in document order; find eligible `code` nodes (same predicates as |
| 293 | + narrative-code + code under `block[kind=hermeneutics]`) |
| 294 | +3. Assign monotonic `seq` (1, 2, 3, …) |
| 295 | +4. Insert raw Typst anchor immediately before each code node (inside existing |
| 296 | + wrapper if already wrapped): |
| 297 | + |
| 298 | + ```typst |
| 299 | + #jdh-code-appendix-seq-enter(3, kind: "narrative") |
| 300 | + ``` |
| 301 | + |
| 302 | + Matching `#jdh-code-appendix-seq-leave(3)` after the code node (or combine |
| 303 | + into enter-only if registration happens entirely in Typst wrappers). |
| 304 | + |
| 305 | +5. Set `jdh-code-appendix-enabled` via raw Typst preamble when feature on: |
| 306 | + |
| 307 | + ```typst |
| 308 | + #jdh-code-appendix-enabled.update(true) |
| 309 | + ``` |
| 310 | + |
| 311 | +Plugin bundled and deployed like the other four JDH plugins (always listed in |
| 312 | +workdir `myst.yml`). Transform and Typst hooks no-op when `code_appendix` is |
| 313 | +false. |
| 314 | + |
| 315 | +### Interaction with existing plugins |
| 316 | + |
| 317 | +| Plugin | Change | |
| 318 | +|--------|--------| |
| 319 | +| `narrative-code.mjs` | No eligibility change; code-appendix runs after wrap | |
| 320 | +| `hermeneutics.mjs` | No change; code-appendix finds inner `code` nodes | |
| 321 | +| `hide-figure-code.mjs` | Figure code never reaches appendix | |
| 322 | + |
| 323 | +Transform order in `myst.yml`: hermeneutics → hide-figure-code → jdh-table → |
| 324 | +narrative-code → **code-appendix** (last). |
| 325 | + |
| 326 | +## Configuration |
| 327 | + |
| 328 | +Opt-in on the article project. Proposed shape in `meta-jdh.yml`: |
| 329 | + |
| 330 | +```yaml |
| 331 | +project: |
| 332 | + jdh: |
| 333 | + code_appendix: true |
| 334 | +``` |
| 335 | +
|
| 336 | +When true: |
| 337 | +
|
| 338 | +- MyST plugin injects anchors and enables Typst registry |
| 339 | +- Typst template receives equivalent flag for `jdh-render-code-appendix()` |
| 340 | + |
| 341 | +When false (default): plugin is loaded but inert; no appendix pages. |
| 342 | + |
| 343 | +Default bundled `meta-jdh.yml`: `code_appendix: false` (or key omitted). |
| 344 | + |
| 345 | +Articles override in repo `meta-jdh.yml` or `myst.yml` frontmatter. |
| 346 | + |
| 347 | +## Testing |
| 348 | + |
| 349 | +| Test | Covers | |
| 350 | +|------|--------| |
| 351 | +| `test/code-appendix.test.ts` | Eligibility predicates, seq ordering, hermeneutics vs narrative, figure/inline exclusion, plugin no-op when disabled | |
| 352 | +| `test/bundled-plugins.test.ts` | Plugin deployed when config enabled | |
| 353 | +| Manual — BHmHNQKJaSWT | Rebuild PDF with `code_appendix: true`: 11 entries, order matches manuscript, narrative shows same ¶ as main, hermeneutics entries show preceding-block ¶, no truncation, appendix before References | |
| 354 | +| Manual — disabled | Default build unchanged (no appendix section) | |
| 355 | + |
| 356 | +## Repos touched |
| 357 | + |
| 358 | +| Repo | Files (indicative) | |
| 359 | +|------|-------------------| |
| 360 | +| **jdh-cli** | `templates/plugins/code-appendix.mjs`, `src/steps/common/init-myst-config.ts`, `templates/meta-jdh.yml` (docs default), `test/code-appendix.test.ts`, `docs/plugins/code-appendix.md`, `docs/plugins.md`, `docs/typst.md` | |
| 361 | +| **jdh-typst-template** | `jdh.typ` (state, registry, truncation bypass, `jdh-render-code-appendix`, `p-display-frozen`, `jdh-last-para-num` updates), `template.typ` (hook before bibliography) | |
| 362 | + |
| 363 | +Article repos (e.g. BHmHNQKJaSWT): set `code_appendix: true` in `meta-jdh.yml` |
| 364 | +to enable; no pipeline step changes. |
| 365 | + |
| 366 | +## Phased delivery |
| 367 | + |
| 368 | +1. **Phase 1:** Typst registry + `in-appendix-block` truncation bypass + |
| 369 | + `jdh-render-code-appendix()` + `template.typ` hook; manual test with hard-coded |
| 370 | + registry entry |
| 371 | +2. **Phase 2:** MyST `code-appendix.mjs` anchors + config opt-in + paragraph |
| 372 | + number capture (narrative + Option A hermeneutics) |
| 373 | +3. **Phase 3:** Docs, automated tests, BHmHNQKJaSWT verification, theme polish |
| 374 | + (heading wording, hermeneutics markers in appendix) |
| 375 | + |
| 376 | +## Open items |
| 377 | + |
| 378 | +| Item | Notes | |
| 379 | +|------|-------| |
| 380 | +| Appendix heading text | Default “Appendix: Code Listings”; confirm with JDH editorial / PDF guideline | |
| 381 | +| Hermeneutics sidebar markers in appendix | Recommend **omit** markers in appendix (full code, not “excerpt”); confirm visually | |
| 382 | +| Page break style | `#pagebreak()` vs `#pagebreak(to: "odd")` for recto start | |
| 383 | +| Frontmatter key path | `project.jdh.code_appendix` (MyST) vs `doc.jdh.code-appendix` (Typst) — align in implementation | |
| 384 | +| Multiple code fences in one hermeneutics block | All share block-exterior ¶ (Option A); duplicate numbers in appendix are intentional | |
| 385 | +| Language tag on replay | Preserve from original `code` node (`lang` field) for raw block | |
| 386 | +| Empty appendix | No page break if zero eligible blocks | |
| 387 | + |
| 388 | +## References |
| 389 | + |
| 390 | +- Prior exploration: task 67021dee (Hybrid D recommendation, BHmHNQKJaSWT inventory) |
| 391 | +- Existing patterns: `narrative-code.mjs`, `hermeneutics.mjs`, `jdh.typ` code truncation |
| 392 | +- Table spec (same doc series): `2026-06-07-jdh-table-design.md` |
| 393 | +- Fixture: `BHmHNQKJaSWT/_improved/article.md` — 1 narrative + 10 hermeneutics code blocks |
0 commit comments