gutterpress 0.9.0-alpha.2 → 0.10.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +18 -4
  2. package/dist/api/index.d.ts +11 -5
  3. package/dist/api/index.js +20 -15
  4. package/dist/assets/preview/scripts/preview-bridge.d.ts +1 -0
  5. package/dist/assets/preview/scripts/preview-interface.d.ts +1 -0
  6. package/dist/{audit-nhn2pjz3.js → audit-k1vnfwvc.js} +10 -7
  7. package/dist/{build-san7fv2z.js → build-hqmwgvdw.js} +14 -8
  8. package/dist/checks/source/index.d.ts +1 -0
  9. package/dist/checks/source/layout-markers.d.ts +3 -0
  10. package/dist/checks/source/local-ref-parser.d.ts +28 -0
  11. package/dist/{cli-e5zhb0xs.js → cli-0r0tq16s.js} +17 -24
  12. package/dist/cli-46ycxe6r.js +18 -0
  13. package/dist/cli-c41yr7he.js +241 -0
  14. package/dist/cli-hp9r2pzt.js +2343 -0
  15. package/dist/{index-yzrh708h.js → cli-k1065rkg.js} +2 -5
  16. package/dist/{cli-k4bd06sd.js → cli-n25qycwz.js} +4986 -5889
  17. package/dist/{cli-najycadg.js → cli-ra0ed2xt.js} +69 -253
  18. package/dist/{cli-506tg37g.js → cli-revgt4pr.js} +2 -1
  19. package/dist/{cli-wchtvxvw.js → cli-v5mp7a6q.js} +14 -1
  20. package/dist/cli.js +20 -18
  21. package/dist/{doctor-hrk0kxxz.js → doctor-dxms7ehm.js} +4 -2
  22. package/dist/engine/compiler/build.d.ts +139 -0
  23. package/dist/engine/compiler/postprocess.d.ts +23 -0
  24. package/dist/engine/compiler/tier2.d.ts +78 -0
  25. package/dist/engine/shared/cdp.d.ts +104 -0
  26. package/dist/engine/shared/content-value.d.ts +64 -0
  27. package/dist/engine/shared/gcpm-extract.d.ts +119 -0
  28. package/dist/engine/shared/margin-box-support.d.ts +12 -0
  29. package/dist/engine/shared/pdf-inspect.d.ts +24 -0
  30. package/dist/engine/shared/synthesis.d.ts +155 -0
  31. package/dist/engine-wa7y9av9.js +41 -0
  32. package/dist/engine-z4p9sr4h.js +40 -0
  33. package/dist/gutterpress-agent-1ctgfz92.js +576 -0
  34. package/dist/gutterpress-viewer-cem7dmr5.js +2349 -0
  35. package/dist/{index-bynn850m.js → index-05y3dnxq.js} +3049 -4015
  36. package/dist/index-9tyq9kks.js +708 -0
  37. package/dist/{cli-yzrh708h.js → index-mdefp0y5.js} +1 -1
  38. package/dist/{index-wchtvxvw.js → index-v5mp7a6q.js} +14 -1
  39. package/dist/index-xxg4zfrg.js +1907 -0
  40. package/dist/index.d.ts +3 -1
  41. package/dist/index.js +24 -17
  42. package/dist/lib/asset-inline.d.ts +36 -0
  43. package/dist/lib/browser-pool.d.ts +18 -0
  44. package/dist/lib/build-error.d.ts +1 -1
  45. package/dist/lib/build-preflight.d.ts +35 -2
  46. package/dist/lib/build-runner.d.ts +45 -17
  47. package/dist/lib/build-staging.d.ts +9 -55
  48. package/dist/lib/cli-args.d.ts +8 -0
  49. package/dist/lib/desktop.d.ts +2 -2
  50. package/dist/lib/embedded-assets.d.ts +1 -1
  51. package/dist/lib/engine.d.ts +33 -0
  52. package/dist/lib/ghostscript.d.ts +46 -1
  53. package/dist/lib/markdown/assemble.d.ts +18 -10
  54. package/dist/lib/markdown/gp-pin-scope.d.ts +1 -0
  55. package/dist/lib/markdown/gutterpress-css.d.ts +125 -0
  56. package/dist/lib/markdown/images.d.ts +26 -0
  57. package/dist/lib/markdown/index.d.ts +4 -2
  58. package/dist/lib/markdown/inline-source.d.ts +10 -0
  59. package/dist/lib/markdown/markers.d.ts +32 -0
  60. package/dist/lib/markdown/renderer.d.ts +8 -3
  61. package/dist/lib/markdown/source-range.d.ts +61 -0
  62. package/dist/lib/missing-asset-placeholder.d.ts +52 -0
  63. package/dist/lib/presets.d.ts +1 -1
  64. package/dist/lib/printsafe.d.ts +2 -3
  65. package/dist/lib/remote-auth/converge-merge.d.ts +48 -0
  66. package/dist/lib/remote-auth/image-clash.d.ts +17 -0
  67. package/dist/lib/remote-auth/recovery/classify.d.ts +55 -67
  68. package/dist/lib/remote-auth/recovery/inspect.d.ts +14 -13
  69. package/dist/lib/remote-auth/recovery/locks.d.ts +20 -0
  70. package/dist/lib/remote-auth/recovery/repair.d.ts +29 -0
  71. package/dist/lib/remote-auth/recovery/types.d.ts +8 -204
  72. package/dist/lib/remote-auth/sync-messages.d.ts +2 -3
  73. package/dist/lib/remote-auth/sync-types.d.ts +35 -63
  74. package/dist/lib/remote-auth/sync.d.ts +11 -18
  75. package/dist/lib/remote-auth/transport.d.ts +14 -7
  76. package/dist/lib/theme-import.d.ts +4 -5
  77. package/dist/{lint-96j9hrj4.js → lint-xjwm5ep8.js} +10 -7
  78. package/dist/{manifest.schema-z61rzw44.json → manifest.schema-zxgxnbg7.json} +21 -0
  79. package/dist/{new-8p38wavc.js → new-kwdwpf0j.js} +12 -8
  80. package/dist/{plugin-ees6nhkc.js → plugin-rg4tnn96.js} +10 -7
  81. package/dist/{preflight-1q6c2edh.js → preflight-3127y25z.js} +10 -7
  82. package/dist/preview/file-watcher.d.ts +13 -17
  83. package/dist/preview/lifecycle.d.ts +1 -1
  84. package/dist/{pagedjs-bridge-vn4hk9fx.js → preview-bridge-fz7vpk8m.js} +8 -0
  85. package/dist/preview-interface-435cczt5.js +1059 -0
  86. package/dist/{preview-y5a2zen1.js → preview-ncgfhqmw.js} +16 -9
  87. package/dist/preview-shell-c5mfa3q0.js +346 -0
  88. package/dist/{project-source-p0gn1wd5.js → project-source-ekcyp63q.js} +1 -1
  89. package/dist/{publish-rm9yb3wh.js → publish-pr0rwh6p.js} +10 -7
  90. package/dist/render.d.ts +3 -4
  91. package/dist/render.js +728 -64
  92. package/dist/{repair-zgq7q2g6.js → repair-8270smfw.js} +45 -79
  93. package/dist/schema/manifest.types.d.ts +38 -0
  94. package/dist/{source-provider-c1rjm2c0.js → source-provider-3tcj6qg2.js} +2 -2
  95. package/dist/source-provider-vanafrt9.js +40 -0
  96. package/dist/{theme-zz2ktzqs.css → theme-h5recz6c.css} +8 -7
  97. package/dist/{theme-570zmh2t.css → theme-j2bagrfx.css} +8 -7
  98. package/dist/{theme-nya4nqh6.css → theme-nn6d53zy.css} +8 -7
  99. package/dist/types.d.ts +7 -0
  100. package/dist/{validate-nr0xa6sa.js → validate-54e17rae.js} +10 -7
  101. package/package.json +6 -6
  102. package/dist/cli-yja077f6.js +0 -92
  103. package/dist/git-http-yrb4ag6z.js +0 -17
  104. package/dist/index-yja077f6.js +0 -92
  105. package/dist/lib/markdown/markdown-it-paged.d.ts +0 -30
  106. package/dist/lib/pagedjs-marker.d.ts +0 -42
  107. package/dist/lib/pagedjs.d.ts +0 -26
  108. package/dist/lib/pagination.d.ts +0 -149
  109. package/dist/lib/remote-auth/conflict-resolution.d.ts +0 -29
  110. package/dist/lib/remote-auth/recovery/abort-interrupted-operation.d.ts +0 -103
  111. package/dist/lib/remote-auth/recovery/backup.d.ts +0 -113
  112. package/dist/lib/remote-auth/recovery/context.d.ts +0 -47
  113. package/dist/lib/remote-auth/recovery/dispatch.d.ts +0 -28
  114. package/dist/lib/remote-auth/recovery/failsafe.d.ts +0 -33
  115. package/dist/lib/remote-auth/recovery/manual-guidance.d.ts +0 -28
  116. package/dist/lib/remote-auth/recovery/outcome-mapping.d.ts +0 -59
  117. package/dist/lib/remote-auth/recovery/policy.d.ts +0 -47
  118. package/dist/lib/remote-auth/recovery/recover-auth.d.ts +0 -42
  119. package/dist/lib/remote-auth/recovery/recover-binary-conflict.d.ts +0 -37
  120. package/dist/lib/remote-auth/recovery/recover-corrupt-index.d.ts +0 -40
  121. package/dist/lib/remote-auth/recovery/recover-detached-head.d.ts +0 -70
  122. package/dist/lib/remote-auth/recovery/recover-interrupted-cherry-pick.d.ts +0 -23
  123. package/dist/lib/remote-auth/recovery/recover-interrupted-merge.d.ts +0 -28
  124. package/dist/lib/remote-auth/recovery/recover-interrupted-rebase.d.ts +0 -39
  125. package/dist/lib/remote-auth/recovery/recover-merge-conflict.d.ts +0 -34
  126. package/dist/lib/remote-auth/recovery/recover-missing-git-dir.d.ts +0 -37
  127. package/dist/lib/remote-auth/recovery/recover-missing-objects.d.ts +0 -56
  128. package/dist/lib/remote-auth/recovery/recover-network.d.ts +0 -34
  129. package/dist/lib/remote-auth/recovery/recover-non-fast-forward.d.ts +0 -27
  130. package/dist/lib/remote-auth/recovery/recover-stale-lock.d.ts +0 -68
  131. package/dist/lib/remote-auth/recovery/recover-unrelated-histories.d.ts +0 -44
  132. package/dist/lib/remote-auth/recovery/recover-wrong-remote.d.ts +0 -35
  133. package/dist/lib/remote-auth/resolution-plan.d.ts +0 -64
  134. package/dist/paged.polyfill-n95pbxfn.js +0 -33288
  135. package/dist/pagedjs-interface-qxvzgwd7.js +0 -557
  136. package/dist/preview-shell-6dqexx1m.js +0 -581
  137. /package/dist/assets/{preview/scripts/pagedjs-bridge.d.ts → engine/gutterpress-agent.d.ts} +0 -0
  138. /package/dist/assets/{preview/scripts/pagedjs-interface.d.ts → engine/gutterpress-viewer.d.ts} +0 -0
@@ -2,12 +2,18 @@ import { type LoadedPlugin } from "./renderer";
2
2
  /** Reader injected by the host: resolve a project-root-relative file → its text. */
3
3
  export type ReadText = (relPath: string) => Promise<string>;
4
4
  /**
5
- * One author-mistake warning emitted by `markdown-it-paged` (ARCH finding #4).
6
- * Mirrors the shape `markdown-it-paged.js`'s `warn()` pushes onto
7
- * `env.layoutWarnings` — see that file's header comment for the 8 warning
8
- * `type`s (`ambiguous_marker_token`, `section_without_page`, `nested_spread`,
5
+ * One author-mistake warning emitted by Gutterpress's marker parser (ARCH finding #4).
6
+ * Mirrors the shape `markers.js`'s `warn()` pushes onto
7
+ * `env.layoutWarnings` — see that file's header comment for the warning
8
+ * `type`s (`ambiguous_marker_token`, `unrecognized_marker_token`,
9
+ * `extra_bare_marker_token`, `unknown_marker`, `nested_spread`,
9
10
  * `continue_without_section`, `spread_without_pages`, `spread_eof_close`,
10
- * `page_outside_spread`, `implicit_page`).
11
+ * `page_outside_spread`, `pin_outside_page`).
12
+ *
13
+ * `section_without_page` and `implicit_page` were REMOVED 2026-08-12: a
14
+ * @section with no open @page is valid authoring (audited, 17/17 false
15
+ * positives across two real books), and the `implicitPage` option that
16
+ * produced the latter was unreachable and latently broken.
11
17
  */
12
18
  export interface LayoutWarning {
13
19
  line: number;
@@ -28,9 +34,9 @@ export interface AssembleBookHtmlOptions {
28
34
  * Inlining is what makes a stylesheet's location irrelevant to the output, so
29
35
  * themes (`themes/<id>/theme.css`) and shared design systems
30
36
  * (`../design-guide/styles/guide.css`) need no copying, no flattening and no
31
- * destination indirection. It is also what Paged.js does to the document
32
- * anyway — it deletes every `<link>`/`<style>` and re-emits the CSS inline —
33
- * so a `<link>` never survived the render path to begin with.
37
+ * destination indirection. The assembled document therefore has one
38
+ * deterministic CSS payload, with no output-relative stylesheet links to
39
+ * relocate or lose during staging.
34
40
  */
35
41
  projectCss?: string;
36
42
  title?: string;
@@ -43,10 +49,12 @@ export interface AssembleBookHtmlOptions {
43
49
  * output is unaffected.
44
50
  */
45
51
  wrapChapters?: boolean;
52
+ /** Add a layout-neutral source-file id to source-mapped preview blocks. */
53
+ annotateSourceChapters?: boolean;
46
54
  /**
47
55
  * ARCH finding #4: per-chapter callback receiving any `env.layoutWarnings`
48
- * `markdown-it-paged` computed while rendering `file` (only called when
49
- * that chapter produced at least one). `file` is the same canonical
56
+ * Gutterpress's marker parser computed while rendering `file` (only called
57
+ * when that chapter produced at least one). `file` is the same canonical
50
58
  * chapter id used for `data-chapter-src`, so a host can attribute a warning
51
59
  * to the exact source file. Additive/optional — omitting it reproduces the
52
60
  * prior throwaway-env behavior exactly, so this cannot change output for
@@ -0,0 +1 @@
1
+ export default function gpPinScope(md: any): void;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * GUTTERPRESS_CSS — the `gp-*` author vocabulary.
3
+ *
4
+ * This is Gutterpress's broad author-vocabulary stylesheet. The separate
5
+ * `MARKER_CSS` block owns only the `.page`/`.spread`/`.section`/`.chapter`
6
+ * structure emitted by Gutterpress's marker parser; this file owns the `gp-*`
7
+ * classes authors apply to content. Keeping the two core blocks separate makes
8
+ * that ownership boundary explicit without implying an external plugin.
9
+ *
10
+ * Injected by assemble.ts immediately AFTER `MARKER_CSS` and BEFORE user plugin
11
+ * and project CSS, so the cascade order is: marker layout primitives ->
12
+ * Gutterpress vocabulary -> plugin CSS -> the author's stylesheets last. An
13
+ * author overriding a `gp-*` class at equal specificity still wins.
14
+ *
15
+ * Also ships the author-facing `gp-*` image/block vocabulary (CLAUDE.md §0 —
16
+ * a behavior broadly useful to non-technical authors belongs in core, not a
17
+ * project layer; see UX finding M17). markdown-it-attrs is bundled by
18
+ * default (renderer.ts), so `![Art](x.jpg){.gp-right .gp-small}` already
19
+ * attaches the classes to the rendered `<img>` — these rules are what make
20
+ * them actually do something print-safe. The gp-* vocabulary REPLACED the
21
+ * five pre-vocabulary utility names (.center/.float-left/.float-right/
22
+ * .full-width/.full-bleed), which were removed rather than kept as
23
+ * aliases — one vocabulary, no duplicate way to spell each layout. Books
24
+ * migrate by renaming the classes in their markdown; the desktop editor
25
+ * recognizes the old names when editing an image and rewrites them to the
26
+ * gp-* names in place.
27
+ *
28
+ * .gp-left — floats left, text wraps.
29
+ * .gp-right — floats right, text wraps.
30
+ * .gp-center — centers a block element.
31
+ * .gp-full — fills the page's content width.
32
+ * .gp-small — width 25% of the column. Sizes compose with any
33
+ * .gp-medium — width 50%. position (including .gp-pin, where
34
+ * .gp-large — width 75%. the % resolves against the page box).
35
+ * .gp-tight — float/shape clearance 0.5em (sets --gp-gap; default 1em).
36
+ * .gp-loose — float/shape clearance 2em (sets --gp-gap).
37
+ * .gp-shape — wraps text to a floated image's alpha silhouette
38
+ * (shape-outside; the pipeline mirrors the src into
39
+ * --gp-shape because CSS url() contexts can't read
40
+ * attr()). Floats only; inert elsewhere.
41
+ * .gp-pin — pins within the nearest @page/@spread container;
42
+ * centered on both axes unless combined with the edge
43
+ * modifiers .gp-top/.gp-bottom/.gp-left/.gp-right.
44
+ * .gp-bleed — forces its own page (break-before) and spans it
45
+ * edge-to-edge horizontally. This does NOT cancel the
46
+ * top/bottom margins, extend past the trim into printer
47
+ * bleed overage, or remove headers/footers — none of
48
+ * that is implemented.
49
+ *
50
+ * The mechanism is a named page: the rule below assigns the
51
+ * element's page to `@page gp-full-bleed`, which has zero
52
+ * side margins, so the page's own CONTENT box IS the sheet
53
+ * and a plain `width: 100%` already reaches both edges —
54
+ * with no shrink-to-fit trigger, because nothing out-dents
55
+ * past the content box. MEASURED (Chromium 148, 6x4in
56
+ * sheet, 0.75in margins): the earlier mechanism (a negative
57
+ * out-dent of the real page margins, inherited from the
58
+ * Paged.js era, where the polyfill published them as
59
+ * `--pagedjs-margin-left/right`) shrank the WHOLE document
60
+ * ~10% under native print (text run 204.4pt -> 182.9pt),
61
+ * because the shrink-to-fit trigger is the page CONTENT
62
+ * box, not the sheet — that failure mode is why the named
63
+ * page exists. Paged.js has since been removed
64
+ * (native-only-migration-plan.md Phase 6), so the out-dent
65
+ * (whose custom properties nothing sets any more, making it
66
+ * a permanent no-op) went with it.
67
+ *
68
+ * KNOWN GAP: on the bleed page, native's running head/folio
69
+ * move onto the trim line (margin boxes are positioned by
70
+ * the page's own margins, which are now zero on this named
71
+ * page). This is not fixed in core — see
72
+ * docs/native-engine-styling-guide.md §9 for the one-line
73
+ * author remedy (`@top-center { content: none }` etc. on
74
+ * `@page gp-full-bleed`).
75
+ *
76
+ * A standalone `![Art](x.jpg){.gp-bleed}` markdown image
77
+ * is rendered as `<p><img class="gp-bleed"></p>` — a
78
+ * naked markdown-it standalone-image wrap, not something
79
+ * this plugin controls. The `<p>`'s UA default vertical
80
+ * margin sits above/below an image sized to the page's
81
+ * full content box, overflows the box by that margin, and
82
+ * on native print pushes the whole page onto a spurious
83
+ * extra sheet, which then renders BLANK (the art landed on
84
+ * the sheet after). MEASURED (300dpi, 6x9in sheet, a
85
+ * 4-source-file fixture book): with the paragraph margin
86
+ * left at UA default, native emits 8pp with page 6 fully
87
+ * blank (0 dark pixels of 540,000 sampled); zeroing the
88
+ * wrapping paragraph's margin below gives the intended
89
+ * 7pp with the art bleeding edge-to-edge on page 6. Scoped
90
+ * to `:only-child` so a `.gp-bleed` image sharing a
91
+ * paragraph with other inline content keeps its margin.
92
+ *
93
+ * Rule ORDER within the gp-* block is the contract: flow positions → sizes
94
+ * → spacing → .gp-pin → pin-edge modifiers. Everything is flat 0-1-0
95
+ * specificity, so combining classes resolves by source order: a later flow
96
+ * position wins (left < right < center < full < bleed); an explicit size's
97
+ * max-width:100% lifts the floats' 50% cap; .gp-pin's margin:0 /
98
+ * max-width:100% beat the float declarations; the edge modifiers beat
99
+ * .gp-pin's center defaults.
100
+ *
101
+ * .gp-pin semantics, stated honestly: it pins within its @page/@spread
102
+ * CONTAINER, not "the sheet edge". A .page div that fragments across
103
+ * several printed sheets is ONE containing block, so align-self:end is the
104
+ * end of the run. For the single-page layouts pin is meant for (title
105
+ * pages, chapter openers, watermark pages) the two are the same thing. A
106
+ * .gp-pin outside any @page/@spread resolves against the document canvas
107
+ * and can print on a completely different sheet — the gp_pin_scope_check
108
+ * core rule below warns at parse time (`pin_outside_page`), and the
109
+ * compiler's engine.abspos.leak diagnostic catches the raw-HTML cases the
110
+ * token walk can't see.
111
+ *
112
+ * Pin CSS mechanics (all three are load-bearing, verified by
113
+ * paged-css-image-pin.test.ts): `inset: 0` is REQUIRED — abspos
114
+ * self-alignment aligns within the inset-modified containing block, and
115
+ * with auto insets that collapses to the static-position rectangle, where
116
+ * alignment does nothing. The center defaults must be EXPLICIT — `normal`
117
+ * alignment behaves as `start` for abspos replaced elements, not center.
118
+ * And `justify-self` on .gp-left/.gp-right does double duty (flow float +
119
+ * pin edge) because self-alignment does not apply to in-flow floats — it is
120
+ * inert until .gp-pin makes the element abspos. If Chrome ever ships
121
+ * block-level self-alignment for in-flow boxes, a non-floated element
122
+ * carrying gp-left could start shifting; that is standards-tracking per
123
+ * CLAUDE.md ("Chrome wins once it ships"), not a bug in the author's book.
124
+ */
125
+ export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary. One vocabulary, gp-* only \u2014 the\n pre-vocabulary utility names (.center/.float-left/.float-right/\n .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename\n the classes in their markdown (see the migration note). Source ORDER is\n the contract \u2014 see the doctrine comment above. */\n\n/* flow positions */\n.gp-left {\n float: left;\n margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;\n max-width: 50%;\n}\n.gp-right {\n float: right;\n margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);\n max-width: 50%;\n}\n.gp-center {\n display: block;\n float: none;\n margin-left: auto;\n margin-right: auto;\n max-width: 100%;\n}\n.gp-full {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n@page gp-full-bleed { margin-left: 0; margin-right: 0; }\n.gp-bleed {\n display: block;\n float: none;\n break-before: page;\n page: gp-full-bleed;\n max-width: none;\n width: 100%;\n margin-left: 0;\n margin-right: 0;\n}\n\n/* sizes \u2014 AFTER the flow positions so max-width:100% lifts the floats' 50%\n cap at equal specificity */\n.gp-small { width: 25%; max-width: 100%; }\n.gp-medium { width: 50%; max-width: 100%; }\n.gp-large { width: 75%; max-width: 100%; }\n\n/* float clearance presets \u2014 consumed by var(--gp-gap) in the float rules\n above and by .gp-shape's shape-margin below; --gp-gap itself is\n author-settable CSS */\n.gp-tight { --gp-gap: 0.5em; }\n.gp-loose { --gp-gap: 2em; }\n\n/* column runs \u2014 plain CSS Multi-column, exposed as author vocabulary so\n \"put this in two columns\" does not require borrowing a styled container\n from the book's own component layer. That borrowing is what this exists\n to prevent: a book whose theme paints .section chrome by default gives\n every author who opens a section just to start a column run a panel they\n did not ask for, and the book then needs a reset rule to take it back.\n With a neutral primitive the author opts into columns and nothing else.\n\n Permanent vocabulary, not a shim: Chromium implements multicol natively\n and these rules are the standard properties verbatim, so there is no\n spec gap here to remove later. Deliberately minimal \u2014 column-fill is\n NOT set, because the correct value depends on whether the run fragments\n across pages (auto packs each page's columns; the CSS initial balance is\n right for a run that fits on one page) and only the author knows which.\n --gp-column-gap is author-settable. */\n.gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }\n.gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }\n\n/* shape wrap \u2014 text follows the image's alpha silhouette instead of its\n rectangular box. shape-outside only applies to floats, so this is inert\n without .gp-left/.gp-right (and under .gp-pin, which un-floats). The\n shape URL cannot be written in CSS (url() contexts can't read attr()),\n so the image renderer rule (images.ts) mirrors the src into an inline\n --gp-shape:url(...) custom property whenever it sees this class --\n authors only ever type the class. threshold 0.2 ignores near-transparent\n anti-aliasing halos; shape-margin shares the float-gap vocabulary. */\nimg.gp-shape {\n shape-outside: var(--gp-shape);\n shape-image-threshold: 0.2;\n shape-margin: var(--gp-gap, 1em);\n}\n\n/* pin \u2014 within the nearest positioned ancestor (.page/.spread, rule above).\n inset:0 and the explicit centers are load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n inset: 0;\n align-self: center;\n justify-self: center;\n margin: 0;\n max-width: 100%;\n}\n\n/* pin edge modifiers \u2014 AFTER .gp-pin to beat its center defaults;\n justify-self is inert on in-flow floats, so gp-left/gp-right safely do\n double duty as flow float + pin edge */\n.gp-top { align-self: start; }\n.gp-bottom { align-self: end; }\n.gp-left { justify-self: start; }\n.gp-right { justify-self: end; }\n\n/* wrapper-margin neutralization (same pattern and rationale as the\n .gp-bleed paragraph-margin note in the doctrine comment; for pin, the\n emptied paragraph would otherwise leave a phantom margin gap in flow) */\n:where(p:has(> img.gp-bleed:only-child)) { margin: 0; }\n:where(p:has(> img.gp-pin:only-child)) { margin: 0; }\n\n/* depth \u2014 a named ladder for z-index, so books stop hand-tuning bare\n integers. A real book measured 21 z-index declarations using only four\n distinct values (-1, 0, 1, 2), each written literally at its use site.\n The custom properties are the author-settable surface (a book needing a\n deeper stack raises them once); the classes are the shorthand.\n\n NOT named \"layer\": CSS Paged Media 3 \u00A73.1 already defines \"page layers\"\n (page background, canvas, borders, contents, margin boxes) and those are\n parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a\n different concept would collide with the spec vocabulary this project\n tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the\n ladder avoids those words too.\n\n .gp-behind is the one that earns its place: it puts a pinned image UNDER\n the page's text, which is otherwise impossible to express without a bare\n negative z-index. \"Above\" needs no class \u2014 an out-of-flow pin already\n paints above in-flow content.\n\n Two things silently defeat .gp-behind, neither visible at the use site:\n - a stacking context on the .page/.spread ancestor (z-index, isolation,\n opacity, filter, transform on it traps the negative layer inside).\n Core keeps .page/.spread at 'position: relative; z-index: auto'\n precisely so they are not stacking contexts.\n - a clipping ancestor (overflow other than visible), the same mechanism\n that clips a .gp-bleed plate back to the wrapper's width.\n The build-time engine.layer.trapped audit reports both against the live\n ancestor chain. printsafe/page-containment is only an early source hint for\n declarations written directly on .page/.spread. */\n:root {\n --gp-z-behind: -1;\n --gp-z-base: 0;\n --gp-z-raised: 1;\n --gp-z-front: 2;\n}\n.gp-behind { z-index: var(--gp-z-behind); }\n.gp-base { z-index: var(--gp-z-base); }\n.gp-raised { z-index: var(--gp-z-raised); }\n.gp-front { z-index: var(--gp-z-front); }\n";
@@ -15,11 +15,37 @@ import type MarkdownIt from "markdown-it";
15
15
  * author's own folder layout is the layout that ships. Paths are emitted
16
16
  * verbatim; the build resolves them against the project root, which is the
17
17
  * frame `book.html` is served from.
18
+ *
19
+ * One render-time ADDITION (not a src rewrite): an image carrying the
20
+ * `.gp-shape` class gets an inline `--gp-shape: url("<src>")` custom
21
+ * property, mirroring the src byte-for-byte. MARKER_CSS's `img.gp-shape`
22
+ * rule reads it for `shape-outside` — CSS cannot reference an element's own
23
+ * src in a url() context (attr() is blocked there), so the pipeline is the
24
+ * only place the mirror can happen. Authors only ever type the class.
25
+ * Raw-HTML `<img>` tags are not touched — `.gp-shape` is a markdown-image
26
+ * feature; raw HTML authors write the style attribute themselves.
18
27
  */
19
28
  /** Env slot the rule appends to. Absent when nothing referenced an image. */
20
29
  export interface ImageRefEnv {
21
30
  imageRefs?: string[];
22
31
  }
32
+ /**
33
+ * Extract URL candidates with the same important boundaries as the HTML
34
+ * Standard's srcset parser.
35
+ *
36
+ * A comma is legal inside a URL (most visibly in `data:image/...,...`), so a
37
+ * `split(",")` invents bogus local assets from payload text. The browser first
38
+ * consumes a URL through ASCII whitespace, then parses descriptors until the
39
+ * candidate-separating comma. A trailing comma on a descriptor-less URL is
40
+ * the separator and is removed; earlier commas remain part of the URL.
41
+ */
42
+ export interface SrcsetUrlCandidate {
43
+ url: string;
44
+ /** Half-open offsets of the URL itself (separators/descriptors excluded). */
45
+ start: number;
46
+ end: number;
47
+ }
48
+ export declare function parseSrcsetUrlCandidates(input: string): SrcsetUrlCandidate[];
23
49
  export declare function registerImageRule(md: MarkdownIt): void;
24
50
  /**
25
51
  * Collect local image references from raw HTML in rendered output.
@@ -39,10 +39,12 @@ export declare function renderChapters(inputDir: string, opts?: {
39
39
  pluginCss?: string;
40
40
  /** Wrap each source file for incremental preview pagination. */
41
41
  wrapChapters?: boolean;
42
+ /** Add source-file ids to source-mapped preview blocks without wrappers. */
43
+ annotateSourceChapters?: boolean;
42
44
  /**
43
45
  * ARCH finding #4: per-chapter author-mistake warnings computed by
44
- * markdown-it-paged (`env.layoutWarnings`), forwarded straight through
45
- * from {@link assembleBookHtml}. See that option's docstring — omitting
46
+ * Gutterpress's marker parser (`env.layoutWarnings`), forwarded straight
47
+ * through from {@link assembleBookHtml}. See that option's docstring — omitting
46
48
  * it is fully backward compatible.
47
49
  */
48
50
  onChapterWarnings?: (file: string, warnings: LayoutWarning[]) => void;
@@ -0,0 +1,10 @@
1
+ import type MarkdownIt from "markdown-it";
2
+ export declare const SOURCE_TOKEN_ATTR = "data-gp-source-token";
3
+ export declare const SOURCE_OCCURRENCE_ATTR = "data-gp-source-occurrence";
4
+ /**
5
+ * Attach the exact source token and its literal occurrence to rendered
6
+ * Markdown images and links. The Markdown parser remains the sole syntax
7
+ * authority; desktop menus consume these coordinates instead of reparsing or
8
+ * guessing from DOM src/text values.
9
+ */
10
+ export declare function registerInlineSourceMetadata(md: MarkdownIt): void;
@@ -0,0 +1,32 @@
1
+ export default function plugin(md: any, pluginOptions?: {}): void;
2
+ /**
3
+ * The minimal CSS the DOM this module emits requires. Author utility
4
+ * vocabulary lives in gutterpress-css.ts — see the ownership note above.
5
+ * Consumers should inject this into <head> after their user stylesheets so
6
+ * the layout contract (page/section/column breaks) wins at equal specificity.
7
+ *
8
+ * `.page`/`.spread` are given `position: relative` so they are the containing
9
+ * block for any abspos descendant: a mispinned `bottom: 0` now fails LOCALLY
10
+ * on its own page instead of resolving against the document canvas and
11
+ * painting on the last page of the book.
12
+ *
13
+ * The break/orphan rules below (`break-after` on headings, image sizing,
14
+ * first-child glue) are all `:where()` so they carry zero specificity —
15
+ * author CSS at any specificity wins outright, reusing this same
16
+ * after-author injection point.
17
+ *
18
+ * `vertical-align: bottom` on a lone image is a print-correctness rule, not
19
+ * cosmetics. An `<img>` in a `<p>` is inline-level, so its line box adds
20
+ * half-leading/descender space UNDER the image: an image sized to exactly the
21
+ * page content box (a book capping art at `page-height - margins`, or art that
22
+ * naturally fills the page) produces a paragraph a few px TALLER than the box
23
+ * it was sized to fit. MEASURED (Chromium 148, field guide chapter 1, 10in
24
+ * content box): a 956px image made a 963.59px paragraph — a 3.6px overflow
25
+ * that pushed the enclosing named-page wrapper's bottom edge onto the NEXT
26
+ * sheet, so Chromium named that sheet after the PREVIOUS page name and the new
27
+ * template's running head and folio silently vanished. `vertical-align:
28
+ * bottom` collapses the line box onto the image, keeping the image inline (so
29
+ * `text-align: center` still centers it — `display: block` would not).
30
+ *
31
+ */
32
+ export const MARKER_CSS: "\n/* The UA default of 8px body margin is a screen affordance with no meaning\n in paged media, and engines disagree about it: a polyfill that treats the\n page div as the page box drops it, native print keeps it. Left in place it\n insets EVERY page's content by 8px per side, and -- measured, 300dpi,\n 6x4in sheet -- it is what stops a full-width block from reaching the\n paper: it lands at 0.080..5.917in of a 6in sheet instead of\n 0.000..6.000in, because width:100% resolves against the BODY content box,\n not the page's. Zeroing it here (first in the cascade) makes the two\n agree. Authors who want a body margin still just declare one. */\nbody { margin: 0; }\n\n.gp-page-break { break-before: page; }\n.page { break-before: page; }\n.spread { break-before: page; }\n:where(.page, .spread) { position: relative; }\n.gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }\n\n:where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }\n:where(img, svg, video) { max-width: 100%; }\n:where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }\n:where(.section, figure) > :where(:first-child) { break-before: avoid; }\n\n";
@@ -2,8 +2,8 @@
2
2
  * Pure (node-free) markdown rendering core.
3
3
  *
4
4
  * §1/§8 / ADR 0004: this module imports ONLY pure JS — markdown-it and its
5
- * plugins, the inlined `markdown-it-paged.js`, and the node-free leveled
6
- * logger (console-only). It contains NO `node:*`,
5
+ * plugins, Gutterpress's inlined marker parser (`markers.js`), and the node-free
6
+ * leveled logger (console-only). It contains NO `node:*`,
7
7
  * NO `fs`/`path`/`url`, and NO filesystem access, so it can be imported by the
8
8
  * browser renderer (the PWA WebAdapter, #33) AND bundled into the
9
9
  * `bun build --compile` CLI binary alike.
@@ -79,7 +79,12 @@ export declare const BUILTIN_OPTIONAL_PLUGINS: Record<string, GutterpressPlugin>
79
79
  *
80
80
  * Built-in pipeline (runs before any user plugins):
81
81
  * markdown-it-attrs → markdown-it-footnote → markdown-it-deflist →
82
- * markdown-it-source-map → markdown-it-paged
82
+ * markdown-it-source-map → Gutterpress markers
83
+ *
84
+ * The `source_range` core rule (source-range.ts, `data-source-range`) is
85
+ * registered LAST — after any custom (manifest) plugins — so it always sees
86
+ * the final token stream. It is additive alongside `markdown-it-source-map`'s
87
+ * `data-source-line`, whose coverage (level-0 blocks only) is unchanged.
83
88
  *
84
89
  * markdown-it-deflist adds the standard (PHP Markdown Extra / Pandoc)
85
90
  * definition-list syntax — `Term` / `: definition` — emitting plain
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `source_range` — pure (node-free) markdown-it core rule.
3
+ *
4
+ * Annotates every open block-level token (`*_open`, nesting === 1) and every
5
+ * self-closing block token with a usable range (`fence`, `hr`, `html_block`,
6
+ * plus this project's own `layout_page_break` / `layout_column_break`) with
7
+ * `data-source-range="<start>:<end>"` — markdown-it's own `token.map`
8
+ * semantics verbatim: 0-based line index, half-open `[start, end)`.
9
+ *
10
+ * Range source, in priority order:
11
+ * 1. `token.map` — ordinary markdown blocks (paragraphs, headings, list
12
+ * items, blockquotes, table rows, fences, footnote definitions, …).
13
+ * 2. `token.meta.line` — the 1-based marker line threaded onto
14
+ * `layout_*_open` / `layout_page_break` / `layout_column_break` tokens
15
+ * by `markdown-it-paged.js` (see that file's header and the
16
+ * `t.meta = …` assignment sites). Converted to the same half-open
17
+ * convention as `[line - 1, line)`.
18
+ *
19
+ * Both sources are rejected unless `Number.isFinite` — a malformed/missing
20
+ * line (the `@continue`-drops-`__line` bug class) must skip the token, not
21
+ * silently resolve to a `NaN`-derived or whole-document range. This is the
22
+ * exact "fail wrong" this primitive exists to prevent downstream (see
23
+ * `docs/inline-editing-plan.md` §1 principle 3, §2.6, ADR 0009).
24
+ *
25
+ * This rule is registered UNCONDITIONALLY, after any custom (manifest)
26
+ * plugins have been applied — see `renderer.ts` — so it sees the final
27
+ * token stream regardless of what plugins are or are not configured, and so
28
+ * a user plugin's own core rules (which may add/rewrite tokens) are
29
+ * annotated too.
30
+ *
31
+ * `state.tokens` is already ONE FLAT array covering every block nesting
32
+ * depth (list items inside blockquotes, table rows, nested `@section`s, …) —
33
+ * markdown-it's block-level parser never nests these via `token.children`,
34
+ * so a single pass over `state.tokens` reaches every nesting level with no
35
+ * recursion. `token.children` exists only on `inline` tokens, holding
36
+ * text-level marks (`strong_open`, `em_open`, `link_open`, `code_inline`,
37
+ * …); those carry no usable per-token `map` and are deliberately NOT
38
+ * walked here — annotating them would do nothing (their own attrs are
39
+ * never rendered; `Renderer.renderInline` renders only their children) and
40
+ * would blur the intentional "one range per block" contract this primitive
41
+ * promises consumers.
42
+ *
43
+ * Idempotency: uses `token.attrSet` (not `attrPush`) so a token re-rendered
44
+ * on a shared `MarkdownIt` instance gets its attribute overwritten, never
45
+ * duplicated.
46
+ *
47
+ * Deliberate gap (locked in by a negative test, see `renderer.test.ts`):
48
+ * raw HTML blocks (`html_block`) DO retain `token.map` and so DO get
49
+ * `data-source-range` set here — but markdown-it's own `html_block`
50
+ * renderer rule (`return token.content`) discards `token.attrs` entirely,
51
+ * so the attribute never reaches rendered output. Overriding that renderer
52
+ * rule to wrap raw HTML in a synthetic element was rejected (own blast
53
+ * radius); see `docs/inline-editing-plan.md` §2.6.
54
+ */
55
+ import type { RuleCore } from "markdown-it/lib/parser_core.mjs";
56
+ /** The attribute this rule writes. Exported so tests/consumers don't hardcode the string. */
57
+ export declare const SOURCE_RANGE_ATTR = "data-source-range";
58
+ export declare const SOURCE_CHAPTER_ATTR = "data-chapter-src";
59
+ /** The core rule itself — registered via `md.core.ruler.push("source_range", sourceRangeRule)` in renderer.ts. */
60
+ export declare const sourceRangeRule: RuleCore;
61
+ export default sourceRangeRule;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * A visible stand-in for an image the book references but does not have.
3
+ *
4
+ * WHY THIS EXISTS: a missing image used to abort the whole build
5
+ * (`Could not copy asset … ENOENT`). For a non-technical author that is the
6
+ * worst possible failure mode — one stale image path in a 273-page book and
7
+ * nothing renders at all, with a filesystem error as the only explanation.
8
+ * Worse, it makes the book unbuildable by anyone who does not already have
9
+ * the missing file, which is exactly the state the dc-op-manual field guide
10
+ * was in: two chapters referenced art that exists nowhere in the repo, so
11
+ * every tool and every reviewer had to hand-patch placeholders in to build
12
+ * it at all.
13
+ *
14
+ * The fix is NOT to substitute something invisible. A silently-blank image
15
+ * is how a missing illustration ships to print. This paints an unmistakable
16
+ * magenta/black checkerboard: the build completes, the author is warned by
17
+ * path, and the hole is impossible to miss when flipping through the PDF.
18
+ *
19
+ * FORMAT: a hand-encoded PNG, because the alternative — embedding a fixture
20
+ * file — cannot adapt its dimensions, and a wrongly-shaped placeholder
21
+ * distorts the surrounding layout while the author is trying to judge it.
22
+ * PNG is the only format written. The staging pipeline writes it to the
23
+ * dedicated `.png` path returned by {@link placeholderOutputPath} and rewrites
24
+ * every rendered reference to that path, so a missing `.jpg`/`.webp`/etc. can
25
+ * never turn this loud fallback into a browser broken-image icon.
26
+ */
27
+ /**
28
+ * Collision-resistant, output-relative path for a missing image's PNG.
29
+ *
30
+ * Keeping placeholders under one engine-owned directory avoids overwriting an
31
+ * author's real file (which an appended `.missing.png` sibling name could do),
32
+ * while hashing the original output path keeps repeat references deterministic.
33
+ */
34
+ export declare function placeholderOutputPath(missingOutputPath: string): string;
35
+ /**
36
+ * Rewrite missing local image URLs in rendered HTML to their real PNG paths.
37
+ *
38
+ * Covers ordinary `src`, every `srcset` candidate, and CSS `url()` tokens in
39
+ * actual `<style>` blocks / `style` attributes. The structural boundary is
40
+ * load-bearing: prose examples and scripts may legitimately contain the same
41
+ * `url("missing.jpg")` text and must remain byte-for-byte authored. The CSS
42
+ * pass is required for `.gp-shape`: its mirrored `--gp-shape` URL must point
43
+ * at the same staged PNG before `inlineShapeUrls()` reads and embeds it.
44
+ */
45
+ export declare function rewriteMissingImageReferences(html: string, replacements: ReadonlyMap<string, string>): string;
46
+ /**
47
+ * Encode a checkerboard PNG of `width`×`height` at `cell` pixels per square.
48
+ * Truecolor (8-bit RGB, no alpha) with filter byte 0 per scanline — the
49
+ * simplest encoding that every decoder handles, and small enough that the
50
+ * deflate cost is irrelevant next to a book build.
51
+ */
52
+ export declare function placeholderPng(width?: number, height?: number, cell?: number): Uint8Array;
@@ -8,7 +8,7 @@ import type { ResolvedConfig } from "../schema/manifest.types";
8
8
  * Every preset value is overridable from the manifest, leaf by leaf
9
9
  * (resolveConfig's mergeShape; precedence cli > manifest > target > preset).
10
10
  */
11
- export interface VendorPreset extends Omit<ResolvedConfig, "title" | "authors" | "targets" | "page"> {
11
+ export interface VendorPreset extends Omit<ResolvedConfig, "title" | "authors" | "targets" | "page" | "engine"> {
12
12
  /**
13
13
  * Base page geometry in points, or `null` for `custom` — the one preset
14
14
  * with no built-in trim, which therefore REQUIRES the manifest to supply
@@ -1,7 +1,7 @@
1
1
  export declare const ruleRemoteUrls = "printsafe/no-remote-urls";
2
2
  export declare const ruleRiskyProps = "printsafe/no-risky-print-effects";
3
- export declare const rulePagedjsCrashSelectors = "printsafe/no-pagedjs-crash-selectors";
4
3
  export declare const ruleSyntax = "printsafe/syntax-error";
4
+ export declare const rulePageContainment = "printsafe/page-containment";
5
5
  export interface PrintSafeWarning {
6
6
  rule: string;
7
7
  severity: "error" | "warning";
@@ -11,7 +11,6 @@ export interface PrintSafeWarning {
11
11
  }
12
12
  /**
13
13
  * Run print-safety checks against a CSS string. Returns one warning per finding
14
- * (errors for remote URLs / crash-prone selectors / syntax errors; warnings for
15
- * risky print effects).
14
+ * (errors for remote URLs / syntax errors; warnings for risky print effects).
16
15
  */
17
16
  export declare function checkCss(css: string, from?: string): PrintSafeWarning[];
@@ -0,0 +1,48 @@
1
+ import type { GitCache, ImageClash } from "./sync-types.ts";
2
+ export type { ImageClash };
3
+ export interface ConvergeResult {
4
+ /** The branch tip after the merge (the merge commit, or the ff/no-op tip). */
5
+ oid: string;
6
+ /**
7
+ * Files whose text now contains BOTH versions inside git conflict markers —
8
+ * the writer should blend these. Empty when everything merged cleanly.
9
+ */
10
+ combinedFiles: string[];
11
+ /** Clashing images (newer version kept) for the non-blocking picker. */
12
+ imageClashes: ImageClash[];
13
+ }
14
+ /** Snapshot message for the equalization commit (driver-unreachable cases). */
15
+ export declare const CONVERGE_PREPARE_MESSAGE = "Getting your changes ready to combine with the online version";
16
+ /** Snapshot message for the post-merge restore commit (newer-wins/edits). */
17
+ export declare const CONVERGE_RESTORE_MESSAGE = "Kept the newest version of files that can't be combined";
18
+ /** The merge commit message (same wording the old sync used). */
19
+ export declare const CONVERGE_MERGE_MESSAGE = "Combined your changes with the online version";
20
+ /**
21
+ * diff3 merge that NEVER reports a conflict: clean hunks merge exactly like
22
+ * git's default driver; clashing hunks keep BOTH versions inside standard
23
+ * git conflict markers. Exported for tests.
24
+ */
25
+ export declare function mergeWithMarkers(baseContent: string, ourContent: string, theirContent: string): string;
26
+ /**
27
+ * Merge `theirs` into `branch` so that the merge ALWAYS lands, applying the
28
+ * fixed converge policy documented in the module header. The working tree is
29
+ * synced to the result. Caller holds the repo lock and passes its
30
+ * function-scoped object cache.
31
+ *
32
+ * Throws only for genuinely unmergeable situations that the caller maps to a
33
+ * plain error outcome (e.g. unrelated histories when `allowUnrelated` is
34
+ * false — a wrong online address must not silently splice two books).
35
+ */
36
+ export declare function convergeMerge(params: {
37
+ dir: string;
38
+ cache: GitCache;
39
+ branch: string;
40
+ theirs: string;
41
+ author: {
42
+ name: string;
43
+ email: string;
44
+ };
45
+ authorName?: string;
46
+ authorEmail?: string;
47
+ allowUnrelatedHistories?: boolean;
48
+ }): Promise<ConvergeResult>;
@@ -0,0 +1,17 @@
1
+ /** Read one version's exact bytes by BLOB oid. */
2
+ export declare function readImageVersion(options: {
3
+ projectDir: string;
4
+ oid: string;
5
+ }): Promise<Uint8Array>;
6
+ /**
7
+ * Keep a specific version of a clashing image: write the blob's exact bytes
8
+ * to the file and snapshot. Serialized on the per-repo lock like every other
9
+ * mutating operation.
10
+ */
11
+ export declare function keepImageVersion(options: {
12
+ projectDir: string;
13
+ path: string;
14
+ oid: string;
15
+ authorName?: string;
16
+ authorEmail?: string;
17
+ }): Promise<void>;