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
@@ -0,0 +1,139 @@
1
+ import { type Browser } from "../shared/cdp.ts";
2
+ import { type GcpmModel } from "../shared/gcpm-extract.ts";
3
+ import { type Tier2Output } from "./tier2.ts";
4
+ import { type PostprocessResult } from "./postprocess.ts";
5
+ /**
6
+ * Codes are stable identifiers a UI can map to a plain-language label; the
7
+ * set is closed here so a surface's label table can be asserted complete
8
+ * against it rather than drifting silently as checks are added.
9
+ */
10
+ export type BuildDiagnosticCode = "engine.width.overflow" | "engine.width.intrinsic" | "engine.xref.broken" | "engine.abspos.leak" | "engine.layer.trapped" | "engine.multicol.dead-column" | "engine.content.overheight" | "engine.image.low-dpi";
11
+ export declare const BUILD_DIAGNOSTIC_CODES: readonly BuildDiagnosticCode[];
12
+ export interface BuildDiagnostic {
13
+ code: BuildDiagnosticCode;
14
+ severity: "warning" | "info";
15
+ /** Plain-language, already naming the offending element. */
16
+ message: string;
17
+ }
18
+ export interface BuildOptions {
19
+ input: string;
20
+ output?: string;
21
+ signature?: number;
22
+ marks?: boolean;
23
+ slugPt?: number;
24
+ bleedPt?: number;
25
+ title?: string;
26
+ author?: string;
27
+ maxPasses?: number;
28
+ /** raster resolution below which the audit warns; 0 disables. Default 300. */
29
+ dpiFloor?: number;
30
+ /** downgrade the pre-print width check from a build error to a warning */
31
+ allowShrink?: boolean;
32
+ /** reuse a warm browser (dev server) */
33
+ browser?: Browser;
34
+ onProgress?: (msg: string) => void;
35
+ }
36
+ export interface BuildResult {
37
+ bytes: Uint8Array;
38
+ tier: 1 | 2 | 3;
39
+ passes: number;
40
+ pageCount: number;
41
+ genCss: string;
42
+ geometry: Tier2Output["geometry"];
43
+ /**
44
+ * Engine-internal reasons (which tier ran, why tier 3 did not converge) —
45
+ * developer-facing, printed by the engine dev CLI. Author-facing findings
46
+ * live in {@link BuildResult.diagnostics}, never here: one finding, one
47
+ * channel.
48
+ */
49
+ notes: string[];
50
+ /**
51
+ * Author-facing print-quality findings, carried to whatever surface the
52
+ * caller has — the desktop's Problems panel, the CLI's build output. Each
53
+ * carries a stable `code` so a surface can label it in plain language
54
+ * without parsing the message.
55
+ */
56
+ diagnostics: BuildDiagnostic[];
57
+ post: PostprocessResult;
58
+ /** id -> 1-based page, the measurement channel's output (print/Chromium) */
59
+ pageMap: Record<string, number>;
60
+ converged: boolean;
61
+ /** how many times Page.printToPDF actually ran (§10: 1 in the common case) */
62
+ prints: number;
63
+ /**
64
+ * §10's predict-then-verify guess, read straight from the in-browser
65
+ * viewer (`fragmentDocument()`) against the SAME target ids as
66
+ * {@link BuildResult.pageMap} — i.e. the desktop preview's own
67
+ * fragmenter's opinion of where each id landed. `null` when Tier 3 never
68
+ * ran (no `needsMeasure` reason), so there is nothing to predict.
69
+ * Exposed for the native-vs-print parity gate
70
+ * (`scripts/native-parity-gate.ts`) — not consumed elsewhere.
71
+ */
72
+ predicted: {
73
+ pageMap: Record<string, number>;
74
+ pageCount: number;
75
+ } | null;
76
+ /**
77
+ * The deterministic device-pixel viewport (`sheetViewport`, §"deterministic
78
+ * viewport = the sheet") this build pinned Chromium to. A caller that wants
79
+ * to mount its OWN extra viewer page against the same document (the
80
+ * native-vs-print parity gate's page-count check on a Tier-1/2 book, where
81
+ * {@link BuildResult.predicted} is `null`) needs this to reproduce the same
82
+ * fragmentation the build measured — an unpinned viewport free-sizes off
83
+ * whatever window the browser happens to have open.
84
+ */
85
+ viewport: {
86
+ width: number;
87
+ height: number;
88
+ };
89
+ /**
90
+ * `counter-reset: page N` sites (id + declared start), resolved against
91
+ * measured page numbers — the input `restartedPageValues()` needs to
92
+ * convert either {@link BuildResult.pageMap} or
93
+ * {@link BuildResult.predicted} into resolved `target-counter()` values
94
+ * the same way {@link BuildResult.pageMap}'s own synthesis did.
95
+ */
96
+ resetSites: Array<{
97
+ id: string;
98
+ start: number;
99
+ }>;
100
+ }
101
+ export declare function build(opts: BuildOptions): Promise<BuildResult>;
102
+ /**
103
+ * Bare-fragment xref hrefs (`#foo`) among `sites` whose target id never
104
+ * resolved to an element, i.e. is absent from `resolved` (the id -> text map
105
+ * `targetTexts` only populates for ids it actually found). Non-bare hrefs
106
+ * (`other.html#x`, `https://…`) are the author linking elsewhere on purpose —
107
+ * skipped by construction, since only `#...` hrefs are compared. Deduped by
108
+ * href, first-seen order — one typo'd target should not produce a note per
109
+ * link site.
110
+ */
111
+ export declare function findBrokenXrefRefs(sites: Array<{
112
+ href: string;
113
+ }>, resolved: Record<string, string>): string[];
114
+ export declare function mapSignature(map: Record<string, number>, pageCount: number): string;
115
+ /**
116
+ * Per-page running strings, without touching page names.
117
+ *
118
+ * `string-set`/`string()` is unimplemented in Chromium, so the value a margin
119
+ * box should show changes page by page with nothing in CSS to express it. The
120
+ * fix is a generated `@counter-style { system: fixed; symbols: … }` with one
121
+ * symbol per page, consumed as `counter(page, gp-<name>)` — verified in S3.
122
+ * Rendering stays inside Chromium with the document's own fonts, and the
123
+ * author's `@page` rules are never renamed or rewritten.
124
+ *
125
+ * Every page context is emitted with its FULLY RESOLVED content, including the
126
+ * suppressions (`content: none`). That is not defensive style: Chromium does
127
+ * not apply page-selector specificity ACROSS stylesheets, so an `@page :left {
128
+ * @top-center { content: none } }` in the author's sheet does not beat a plain
129
+ * `@page { @top-center { … } }` in the generated sheet — the head would be
130
+ * drawn twice. Resolving here removes the dependency entirely.
131
+ */
132
+ export declare function counterStyleCss(model: GcpmModel, sources: Array<{
133
+ name: string;
134
+ id: string;
135
+ text: string;
136
+ }>, pageMap: Record<string, number>, pageCount: number, resetSites?: Array<{
137
+ id: string;
138
+ start: number;
139
+ }>, hasBlank?: boolean): string;
@@ -0,0 +1,23 @@
1
+ import type { PageTrim } from "./tier2.ts";
2
+ export interface PostprocessOptions {
3
+ geometry: PageTrim;
4
+ /** append blank pages until pageCount % signature === 0 */
5
+ signature?: number;
6
+ title?: string;
7
+ author?: string;
8
+ subject?: string;
9
+ keywords?: string[];
10
+ /** draw crop marks even if CSS didn't ask */
11
+ marks?: boolean;
12
+ }
13
+ export interface PostprocessResult {
14
+ bytes: Uint8Array;
15
+ pageCount: number;
16
+ padded: number;
17
+ boxes: {
18
+ media: number[];
19
+ bleed: number[];
20
+ trim: number[];
21
+ };
22
+ }
23
+ export declare function postprocess(input: Uint8Array, opts: PostprocessOptions): Promise<PostprocessResult>;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Tier 2 (§8) — compile-time synthesis, no measurement.
3
+ *
4
+ * Emits `gp.gen.css` (an inspectable build artifact; the author's files are
5
+ * never modified) plus the DOM hooks it needs:
6
+ *
7
+ * - running headers at CHAPTER granularity: when a `string-set` source also
8
+ * starts a named-page run, each run gets a generated named page whose
9
+ * margin-box `content` is the literal heading text — standard CSS Chromium
10
+ * executes natively, one pass, no measurement.
11
+ * - `bleed` / `marks`: rewrite `size` to trim + 2×bleed (+ 2×slug), shift
12
+ * margins so content stays put relative to trim, and compensate margin-box
13
+ * insets so headers/footers don't drift into the bleed.
14
+ */
15
+ import { type Declarations, type GcpmModel } from "../shared/gcpm-extract.ts";
16
+ export declare const DEFAULT_SLUG_PT = 18;
17
+ export interface PageTrim {
18
+ /** trim size (author's `size`) in pt */
19
+ trim: {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ /** printed media size in pt (trim + 2*bleed + 2*slug) */
24
+ media: {
25
+ width: number;
26
+ height: number;
27
+ };
28
+ bleed: number;
29
+ slug: number;
30
+ marks: string[];
31
+ }
32
+ export interface Tier2Input {
33
+ model: GcpmModel;
34
+ /** finishing options; CSS values win when both exist (§10) */
35
+ marks?: boolean;
36
+ slugPt?: number;
37
+ bleedPt?: number;
38
+ }
39
+ export interface Tier2Output {
40
+ css: string;
41
+ geometry: PageTrim;
42
+ /** true when the document uses constructs that need Tier 3 */
43
+ needsTier3: boolean;
44
+ notes: string[];
45
+ }
46
+ /** Which pages does the document need running strings on, and can Tier 2 do it? */
47
+ export declare function classify(model: GcpmModel): {
48
+ tier2Strings: string[];
49
+ tier3Strings: string[];
50
+ tier3Reasons: string[];
51
+ };
52
+ /** Pseudo-pages the author actually used, e.g. ["left", "right", "blank"]. */
53
+ export declare function pseudoVariants(model: GcpmModel): string[];
54
+ /** String names referenced by a `string()` in some margin box. */
55
+ export declare function consumedStrings(model: GcpmModel): Set<string>;
56
+ export declare function pageGeometry(model: GcpmModel, opts: Tier2Input): PageTrim;
57
+ /**
58
+ * The emitted page margin for one side.
59
+ *
60
+ * Normally the authored margin plus the bleed+slug the compiler added to the
61
+ * page size, so content stays where the author put it RELATIVE TO TRIM.
62
+ *
63
+ * A zero authored margin is different in kind: it means "content fills the
64
+ * page", and inflating it would put a white border exactly where the author
65
+ * asked for full-bleed art — the one thing bleed exists for. Chromium clips
66
+ * content to the content box (measured: nothing paints outside it, not even
67
+ * `html { background }`), so a bleeding page must have a content box that
68
+ * REACHES the bleed area. Emitting the slug alone makes the content box
69
+ * exactly the bleed box: art fills it, crop marks stay clear.
70
+ */
71
+ export declare function bleedMargin(authored: number, g: PageTrim): number;
72
+ /**
73
+ * Shift a margin box's content back toward the trim box by `inset` pt, so the
74
+ * bleed/slug the compiler added to the page size does not push running heads
75
+ * and folios outward.
76
+ */
77
+ export declare function marginBoxInset(name: string, inset: number): Declarations;
78
+ export declare function synthesize(input: Tier2Input): Tier2Output;
@@ -0,0 +1,104 @@
1
+ export declare function findChromium(): string;
2
+ /**
3
+ * Gutterpress targets exactly one engine.
4
+ *
5
+ * THE 151 INCIDENT: Chromium's Paged Media behaviour is not stable across
6
+ * milestones, and it changed once in a way that produced NO error. 151 began
7
+ * PARSING `target-counter()` while still computing it to `none`. Before 151,
8
+ * an unsupported `target-counter()` declaration was dropped by the cascade
9
+ * (invalid at parse time), so Gutterpress's `generatedContentCss()` override, which
10
+ * targets the SAME property on the SAME selector, was the only rule left
11
+ * standing. At 151, the author's declaration started parsing as valid — so
12
+ * it stayed in the cascade, and on equal specificity, source order (the
13
+ * author's stylesheet loads after Gutterpress's) let it win. Every cross-reference
14
+ * in the document disappeared, quietly.
15
+ *
16
+ * WHY THE FLOOR MOVED BACK TO 148 (measured, not assumed): the fix was never
17
+ * "run on 151" — it was `generatedContentCss()` OUT-SPECIFYING the author's
18
+ * selector, which wins the cascade whether the author's declaration is
19
+ * dropped (pre-151 regime) or retained (151+ regime). That was already
20
+ * verified by a RENDER PROBE (below) that reads back
21
+ * `getComputedStyle(el, '::after').content` instead of trusting
22
+ * `CSS.supports` — i.e. the thing that actually defends against this class of
23
+ * silent-content-loss regression is the probe, not the milestone pin. Measured
24
+ * 148 vs 151 head-to-head: the spike suite differs in exactly 2 checks (both
25
+ * assertions ABOUT Chromium's parse-vs-drop behaviour, now written to accept
26
+ * either regime — see spike/native-engine s0/s2), the parity gate output is
27
+ * byte-identical, and real 34pp/53pp book builds produce identical page counts
28
+ * and sizes on both milestones. So 151 was a *floor for the incident's
29
+ * discovery*, not a requirement of the fix. It is pinned rather than probed —
30
+ * running on anything below 148 is still an error rather than a guess — but
31
+ * 148 is the honestly-supported floor because it's also what Electron's
32
+ * bundled Chromium ships (42.1.0 → 148.0.7778.97 as of 2026-08-08), and the
33
+ * desktop app needs to drive its own Chromium for native-engine PDF export
34
+ * (see packages/desktop/electron's engine-browser module). Raising or
35
+ * lowering this floor again means re-running `bun run spikes` and treating
36
+ * every changed measurement as a finding, same as before.
37
+ */
38
+ export declare const REQUIRED_MILESTONE = 148;
39
+ /**
40
+ * Refuse to paginate on a browser below the floor. The invariant belongs to
41
+ * the `Browser` CONTRACT, not to any one way of obtaining a browser — the
42
+ * launch/connect paths and the host-injected path (an Electron
43
+ * `EngineBrowser`, `lib/engine.ts`) must enforce the identical rule with the
44
+ * identical message, or the two drift (they already had once).
45
+ */
46
+ export declare function assertMilestone(product: string, origin: string, hint?: string): void;
47
+ /**
48
+ * The document-settled probe both hosts run before measuring or printing:
49
+ * fonts ready, any pending `gp:ready` handshake, then two rAFs so layout
50
+ * settles after the font swap. ONE definition — the Electron host
51
+ * (packages/desktop/electron/engine-browser.ts) evaluates the same
52
+ * expression, and a drift between hosts would be an invisible divergence in
53
+ * when "ready" means ready.
54
+ */
55
+ export declare function readyProbeExpr(timeoutMs: number): string;
56
+ /**
57
+ * The print-quality contract both hosts pass to their print call: tagged PDF
58
+ * + document outline (named destinations — Tier 3 and the parity gate read
59
+ * them back) + CSS page size + backgrounds. ONE object, spread by both
60
+ * `SessionImpl.printToPDF` and the Electron host's `printToPDF`.
61
+ */
62
+ export declare const DEFAULT_PRINT_OPTS: {
63
+ readonly printBackground: true;
64
+ readonly preferCSSPageSize: true;
65
+ readonly generateTaggedPDF: true;
66
+ readonly generateDocumentOutline: true;
67
+ };
68
+ export interface Browser {
69
+ wsUrl: string;
70
+ version: string;
71
+ milestone: number;
72
+ newPage(): Promise<Session>;
73
+ close(): Promise<void>;
74
+ }
75
+ /** One CDP connection multiplexed over sessionIds (flat mode). */
76
+ export interface Session {
77
+ send<T = any>(method: string, params?: object): Promise<T>;
78
+ on(event: string, fn: (params: any) => void): () => void;
79
+ /** Convenience: Runtime.evaluate with awaitPromise + value return. */
80
+ evaluate<T = any>(expression: string): Promise<T>;
81
+ setContent(html: string, baseUrl?: string): Promise<void>;
82
+ navigate(url: string): Promise<void>;
83
+ /** Wait for fonts + optional `gp:ready`, per §6 of the proposal. */
84
+ waitForReady(timeoutMs?: number): Promise<void>;
85
+ printToPDF(opts?: Record<string, unknown>): Promise<Uint8Array>;
86
+ close(): Promise<void>;
87
+ }
88
+ export declare function launchChromium(opts?: {
89
+ headless?: boolean;
90
+ args?: string[];
91
+ }): Promise<Browser>;
92
+ /**
93
+ * Attach to an ALREADY-RUNNING Chromium (a pooled/pre-warmed browser owned by
94
+ * the caller, e.g. `browser-pool.ts`'s puppeteer instance via
95
+ * `browser.wsEndpoint()`) instead of spawning a new process.
96
+ *
97
+ * Deliberately the mirror image of `launchChromium`: same version pin, same
98
+ * `Session`/`newPage` machinery (`checkMilestoneAndWrap`, shared below), but
99
+ * `close()` only drops OUR websocket connection — it never sends
100
+ * `Browser.close`, kills a process, or removes a profile dir, because this
101
+ * function didn't create any of those. Ownership of the underlying browser's
102
+ * lifecycle stays entirely with whoever handed us `wsUrl`.
103
+ */
104
+ export declare function connectChromium(wsUrl: string): Promise<Browser>;
@@ -0,0 +1,64 @@
1
+ export type ContentPart = {
2
+ type: "literal";
3
+ value: string;
4
+ } | {
5
+ type: "counter";
6
+ name: string;
7
+ style: string;
8
+ } | {
9
+ type: "string";
10
+ name: string;
11
+ which: string;
12
+ } | {
13
+ type: "target-counter";
14
+ url: string;
15
+ counter: string;
16
+ style: string;
17
+ } | {
18
+ type: "target-text";
19
+ url: string;
20
+ which: string;
21
+ } | {
22
+ type: "leader";
23
+ glue: string;
24
+ } | {
25
+ type: "attr";
26
+ name: string;
27
+ as?: string;
28
+ } | {
29
+ type: "content";
30
+ which: string;
31
+ } | {
32
+ type: "keyword";
33
+ value: string;
34
+ };
35
+ export interface EvalContext {
36
+ /** 1-based page number of the page being rendered */
37
+ page?: number;
38
+ /** total page count */
39
+ pages?: number;
40
+ /** named string lookup, GCPM `string(name, which)` */
41
+ strings?: (name: string, which: string) => string | undefined;
42
+ /** `target-counter(url, page)` -> 1-based page of the target */
43
+ targetPage?: (url: string) => number | undefined;
44
+ /** `target-text(url)` -> text of the target */
45
+ targetText?: (url: string, which: string) => string | undefined;
46
+ /** the element the declaration hangs off (for attr()/content()) */
47
+ attr?: (name: string) => string | undefined;
48
+ /**
49
+ * leader() support: return placeholder text for a leader with this glue
50
+ * string (the renderers insert a marker here and later replace it with a
51
+ * measured run of glue). Absent = leaders render as nothing.
52
+ */
53
+ leader?: (glue: string) => string;
54
+ text?: string;
55
+ }
56
+ export declare function parseContent(value: string): ContentPart[];
57
+ export declare function unquote(s: string): string;
58
+ /** `attr(href url)` inside target-counter() -> the element's href. */
59
+ export declare function resolveUrlArg(arg: string, ctx: EvalContext): string;
60
+ export declare function formatCounter(n: number, style?: string): string;
61
+ export declare function evaluateContent(parts: ContentPart[], ctx: EvalContext): string;
62
+ export declare function evaluate(value: string, ctx: EvalContext): string;
63
+ /** Does this content value need Tier 3 (measurement)? */
64
+ export declare function needsMeasurement(value: string): boolean;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `gcpm-extract` (§9) — the ONLY piece of Gutterpress that reads CSS, and it never
3
+ * rewrites the author's files.
4
+ *
5
+ * Scope is deliberately narrow: `@page` blocks (incl. margin at-rules and the
6
+ * `bleed`/`marks` descriptors CSSOM drops) plus the four GCPM constructs
7
+ * Chromium doesn't implement — `string-set`, `string()`, `target-counter()` /
8
+ * `target-text()`, and `leader()`. It is NOT a general CSS parser; if general
9
+ * parsing ever feels necessary, the design has drifted (§1).
10
+ */
11
+ export interface Declarations {
12
+ [prop: string]: string;
13
+ }
14
+ export interface PageRule {
15
+ /** named page, e.g. `@page chapter:first` -> "chapter" */
16
+ name?: string;
17
+ /** pseudo-pages in source order, e.g. ["first"] or ["left"] */
18
+ pseudos: string[];
19
+ decls: Declarations;
20
+ /** "@top-center" -> { content: '...' } */
21
+ marginBoxes: Record<string, Declarations>;
22
+ raw: string;
23
+ }
24
+ export interface StringSetDecl {
25
+ selector: string;
26
+ /** string name, e.g. "chapter-title" */
27
+ name: string;
28
+ /** raw value expression, e.g. "content()" or '"§ " content(text)' */
29
+ value: string;
30
+ }
31
+ export interface PageAssignment {
32
+ selector: string;
33
+ page: string;
34
+ }
35
+ export interface BreakDecl {
36
+ selector: string;
37
+ prop: "break-before" | "break-after" | "break-inside";
38
+ value: string;
39
+ }
40
+ export interface XrefDecl {
41
+ /** selector the generated content hangs off, e.g. "a.xref::after" */
42
+ selector: string;
43
+ /** raw content value */
44
+ content: string;
45
+ fn: "target-counter" | "target-text" | "leader" | "string";
46
+ }
47
+ export interface CounterResetDecl {
48
+ /** selector the reset hangs off, e.g. ".page-chapter-start" */
49
+ selector: string;
50
+ /** the value `counter-reset: page N` sets */
51
+ start: number;
52
+ }
53
+ export interface Length {
54
+ value: number;
55
+ unit: string;
56
+ }
57
+ export interface PageGeometry {
58
+ /** trim size in pt */
59
+ width: number;
60
+ height: number;
61
+ margin: {
62
+ top: number;
63
+ right: number;
64
+ bottom: number;
65
+ left: number;
66
+ };
67
+ bleed: number;
68
+ marks: string[];
69
+ }
70
+ export interface GcpmModel {
71
+ pageRules: PageRule[];
72
+ stringSets: StringSetDecl[];
73
+ pageAssignments: PageAssignment[];
74
+ breaks: BreakDecl[];
75
+ xrefs: XrefDecl[];
76
+ /** `counter-reset: page N` declarations — native print ignores this restart (ENGINE.md §8) */
77
+ counterResets: CounterResetDecl[];
78
+ /** page names referenced by either `@page name` or a `page:` assignment */
79
+ pageNames: string[];
80
+ warnings: string[];
81
+ }
82
+ export declare const MARGIN_BOX_NAMES: readonly ["top-left-corner", "top-left", "top-center", "top-right", "top-right-corner", "bottom-left-corner", "bottom-left", "bottom-center", "bottom-right", "bottom-right-corner", "left-top", "left-middle", "left-bottom", "right-top", "right-middle", "right-bottom"];
83
+ /** Parse a declaration list (no nested blocks) into a map; last wins. */
84
+ export declare function parseDeclarations(body: string): Declarations;
85
+ /**
86
+ * The raw bodies of `@media print` blocks. The viewer re-injects these as
87
+ * screen styles: the preview must render the print stylesheet, and the browser
88
+ * won't apply print-media rules on screen (verified: `break-before` computes to
89
+ * `auto` until print emulation is on, which a plain document can't switch on).
90
+ */
91
+ export declare function mediaPrintBodies(css: string): string[];
92
+ export declare function extract(css: string): GcpmModel;
93
+ export declare function splitTopLevel(s: string, sep: string): string[];
94
+ export declare function toPt(value: string): number | undefined;
95
+ /** Named page sizes (CSS Paged Media §5.1), portrait, in pt. */
96
+ export declare const PAGE_SIZES: Record<string, [number, number]>;
97
+ export declare function parseSize(value: string): {
98
+ width: number;
99
+ height: number;
100
+ } | undefined;
101
+ export declare function parseMargin(value: string): {
102
+ top: number;
103
+ right: number;
104
+ bottom: number;
105
+ left: number;
106
+ };
107
+ /**
108
+ * Resolve the cascade of `@page` rules that apply to a given page context.
109
+ * Later rules win; specificity follows the spec's simplified order
110
+ * (unnamed < named, no-pseudo < pseudo).
111
+ */
112
+ export declare function resolvePage(model: GcpmModel, ctx?: {
113
+ name?: string;
114
+ pseudos?: string[];
115
+ }): {
116
+ decls: Declarations;
117
+ marginBoxes: Record<string, Declarations>;
118
+ geometry: PageGeometry;
119
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Declarations Chromium accepts syntactically in `@page` margin boxes but
3
+ * silently drops in native print. These were measured with deliberately
4
+ * visible values (`rotate(-12deg)`, `6px 6px 0 #c00`) and are documented in
5
+ * docs/engine-history/ENGINE.md §8.
6
+ *
7
+ * Keep the viewer and print-safety linter on this one contract: preview must
8
+ * omit the same effects native PDF omits, while every other declaration is
9
+ * handed to Chromium normally instead of guessing at a narrower allowlist.
10
+ */
11
+ export declare const MARGIN_BOX_IGNORED_PROPERTIES: ReadonlySet<string>;
12
+ export declare function isIgnoredMarginBoxProperty(property: string): boolean;
@@ -0,0 +1,24 @@
1
+ export declare const PT_PER_IN = 72;
2
+ export interface PageBoxes {
3
+ media: number[];
4
+ crop?: number[];
5
+ bleed?: number[];
6
+ trim?: number[];
7
+ }
8
+ export interface PdfFacts {
9
+ pageCount: number;
10
+ boxes: PageBoxes[];
11
+ /** destination anchor name -> zero-based page index (from link annotations) */
12
+ linkTargets: Record<string, number>;
13
+ /** named destinations (/Dests name tree) -> zero-based page index */
14
+ namedDests: Record<string, number>;
15
+ /** outline title -> zero-based page index */
16
+ outline: Array<{
17
+ title: string;
18
+ page: number;
19
+ }>;
20
+ }
21
+ export declare function inspectPdf(bytes: Uint8Array): Promise<PdfFacts>;
22
+ /** Extract per-page text as a rough sanity channel (not used for measurement). */
23
+ export declare function ptToIn(pt: number): number;
24
+ export declare function boxesEqual(a: number[] | undefined, b: number[], tol?: number): boolean;