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.
- package/README.md +18 -4
- package/dist/api/index.d.ts +11 -5
- package/dist/api/index.js +20 -15
- package/dist/assets/preview/scripts/preview-bridge.d.ts +1 -0
- package/dist/assets/preview/scripts/preview-interface.d.ts +1 -0
- package/dist/{audit-nhn2pjz3.js → audit-k1vnfwvc.js} +10 -7
- package/dist/{build-san7fv2z.js → build-hqmwgvdw.js} +14 -8
- package/dist/checks/source/index.d.ts +1 -0
- package/dist/checks/source/layout-markers.d.ts +3 -0
- package/dist/checks/source/local-ref-parser.d.ts +28 -0
- package/dist/{cli-e5zhb0xs.js → cli-0r0tq16s.js} +17 -24
- package/dist/cli-46ycxe6r.js +18 -0
- package/dist/cli-c41yr7he.js +241 -0
- package/dist/cli-hp9r2pzt.js +2343 -0
- package/dist/{index-yzrh708h.js → cli-k1065rkg.js} +2 -5
- package/dist/{cli-k4bd06sd.js → cli-n25qycwz.js} +4986 -5889
- package/dist/{cli-najycadg.js → cli-ra0ed2xt.js} +69 -253
- package/dist/{cli-506tg37g.js → cli-revgt4pr.js} +2 -1
- package/dist/{cli-wchtvxvw.js → cli-v5mp7a6q.js} +14 -1
- package/dist/cli.js +20 -18
- package/dist/{doctor-hrk0kxxz.js → doctor-dxms7ehm.js} +4 -2
- package/dist/engine/compiler/build.d.ts +139 -0
- package/dist/engine/compiler/postprocess.d.ts +23 -0
- package/dist/engine/compiler/tier2.d.ts +78 -0
- package/dist/engine/shared/cdp.d.ts +104 -0
- package/dist/engine/shared/content-value.d.ts +64 -0
- package/dist/engine/shared/gcpm-extract.d.ts +119 -0
- package/dist/engine/shared/margin-box-support.d.ts +12 -0
- package/dist/engine/shared/pdf-inspect.d.ts +24 -0
- package/dist/engine/shared/synthesis.d.ts +155 -0
- package/dist/engine-wa7y9av9.js +41 -0
- package/dist/engine-z4p9sr4h.js +40 -0
- package/dist/gutterpress-agent-1ctgfz92.js +576 -0
- package/dist/gutterpress-viewer-cem7dmr5.js +2349 -0
- package/dist/{index-bynn850m.js → index-05y3dnxq.js} +3049 -4015
- package/dist/index-9tyq9kks.js +708 -0
- package/dist/{cli-yzrh708h.js → index-mdefp0y5.js} +1 -1
- package/dist/{index-wchtvxvw.js → index-v5mp7a6q.js} +14 -1
- package/dist/index-xxg4zfrg.js +1907 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +24 -17
- package/dist/lib/asset-inline.d.ts +36 -0
- package/dist/lib/browser-pool.d.ts +18 -0
- package/dist/lib/build-error.d.ts +1 -1
- package/dist/lib/build-preflight.d.ts +35 -2
- package/dist/lib/build-runner.d.ts +45 -17
- package/dist/lib/build-staging.d.ts +9 -55
- package/dist/lib/cli-args.d.ts +8 -0
- package/dist/lib/desktop.d.ts +2 -2
- package/dist/lib/embedded-assets.d.ts +1 -1
- package/dist/lib/engine.d.ts +33 -0
- package/dist/lib/ghostscript.d.ts +46 -1
- package/dist/lib/markdown/assemble.d.ts +18 -10
- package/dist/lib/markdown/gp-pin-scope.d.ts +1 -0
- package/dist/lib/markdown/gutterpress-css.d.ts +125 -0
- package/dist/lib/markdown/images.d.ts +26 -0
- package/dist/lib/markdown/index.d.ts +4 -2
- package/dist/lib/markdown/inline-source.d.ts +10 -0
- package/dist/lib/markdown/markers.d.ts +32 -0
- package/dist/lib/markdown/renderer.d.ts +8 -3
- package/dist/lib/markdown/source-range.d.ts +61 -0
- package/dist/lib/missing-asset-placeholder.d.ts +52 -0
- package/dist/lib/presets.d.ts +1 -1
- package/dist/lib/printsafe.d.ts +2 -3
- package/dist/lib/remote-auth/converge-merge.d.ts +48 -0
- package/dist/lib/remote-auth/image-clash.d.ts +17 -0
- package/dist/lib/remote-auth/recovery/classify.d.ts +55 -67
- package/dist/lib/remote-auth/recovery/inspect.d.ts +14 -13
- package/dist/lib/remote-auth/recovery/locks.d.ts +20 -0
- package/dist/lib/remote-auth/recovery/repair.d.ts +29 -0
- package/dist/lib/remote-auth/recovery/types.d.ts +8 -204
- package/dist/lib/remote-auth/sync-messages.d.ts +2 -3
- package/dist/lib/remote-auth/sync-types.d.ts +35 -63
- package/dist/lib/remote-auth/sync.d.ts +11 -18
- package/dist/lib/remote-auth/transport.d.ts +14 -7
- package/dist/lib/theme-import.d.ts +4 -5
- package/dist/{lint-96j9hrj4.js → lint-xjwm5ep8.js} +10 -7
- package/dist/{manifest.schema-z61rzw44.json → manifest.schema-zxgxnbg7.json} +21 -0
- package/dist/{new-8p38wavc.js → new-kwdwpf0j.js} +12 -8
- package/dist/{plugin-ees6nhkc.js → plugin-rg4tnn96.js} +10 -7
- package/dist/{preflight-1q6c2edh.js → preflight-3127y25z.js} +10 -7
- package/dist/preview/file-watcher.d.ts +13 -17
- package/dist/preview/lifecycle.d.ts +1 -1
- package/dist/{pagedjs-bridge-vn4hk9fx.js → preview-bridge-fz7vpk8m.js} +8 -0
- package/dist/preview-interface-435cczt5.js +1059 -0
- package/dist/{preview-y5a2zen1.js → preview-ncgfhqmw.js} +16 -9
- package/dist/preview-shell-c5mfa3q0.js +346 -0
- package/dist/{project-source-p0gn1wd5.js → project-source-ekcyp63q.js} +1 -1
- package/dist/{publish-rm9yb3wh.js → publish-pr0rwh6p.js} +10 -7
- package/dist/render.d.ts +3 -4
- package/dist/render.js +728 -64
- package/dist/{repair-zgq7q2g6.js → repair-8270smfw.js} +45 -79
- package/dist/schema/manifest.types.d.ts +38 -0
- package/dist/{source-provider-c1rjm2c0.js → source-provider-3tcj6qg2.js} +2 -2
- package/dist/source-provider-vanafrt9.js +40 -0
- package/dist/{theme-zz2ktzqs.css → theme-h5recz6c.css} +8 -7
- package/dist/{theme-570zmh2t.css → theme-j2bagrfx.css} +8 -7
- package/dist/{theme-nya4nqh6.css → theme-nn6d53zy.css} +8 -7
- package/dist/types.d.ts +7 -0
- package/dist/{validate-nr0xa6sa.js → validate-54e17rae.js} +10 -7
- package/package.json +6 -6
- package/dist/cli-yja077f6.js +0 -92
- package/dist/git-http-yrb4ag6z.js +0 -17
- package/dist/index-yja077f6.js +0 -92
- package/dist/lib/markdown/markdown-it-paged.d.ts +0 -30
- package/dist/lib/pagedjs-marker.d.ts +0 -42
- package/dist/lib/pagedjs.d.ts +0 -26
- package/dist/lib/pagination.d.ts +0 -149
- package/dist/lib/remote-auth/conflict-resolution.d.ts +0 -29
- package/dist/lib/remote-auth/recovery/abort-interrupted-operation.d.ts +0 -103
- package/dist/lib/remote-auth/recovery/backup.d.ts +0 -113
- package/dist/lib/remote-auth/recovery/context.d.ts +0 -47
- package/dist/lib/remote-auth/recovery/dispatch.d.ts +0 -28
- package/dist/lib/remote-auth/recovery/failsafe.d.ts +0 -33
- package/dist/lib/remote-auth/recovery/manual-guidance.d.ts +0 -28
- package/dist/lib/remote-auth/recovery/outcome-mapping.d.ts +0 -59
- package/dist/lib/remote-auth/recovery/policy.d.ts +0 -47
- package/dist/lib/remote-auth/recovery/recover-auth.d.ts +0 -42
- package/dist/lib/remote-auth/recovery/recover-binary-conflict.d.ts +0 -37
- package/dist/lib/remote-auth/recovery/recover-corrupt-index.d.ts +0 -40
- package/dist/lib/remote-auth/recovery/recover-detached-head.d.ts +0 -70
- package/dist/lib/remote-auth/recovery/recover-interrupted-cherry-pick.d.ts +0 -23
- package/dist/lib/remote-auth/recovery/recover-interrupted-merge.d.ts +0 -28
- package/dist/lib/remote-auth/recovery/recover-interrupted-rebase.d.ts +0 -39
- package/dist/lib/remote-auth/recovery/recover-merge-conflict.d.ts +0 -34
- package/dist/lib/remote-auth/recovery/recover-missing-git-dir.d.ts +0 -37
- package/dist/lib/remote-auth/recovery/recover-missing-objects.d.ts +0 -56
- package/dist/lib/remote-auth/recovery/recover-network.d.ts +0 -34
- package/dist/lib/remote-auth/recovery/recover-non-fast-forward.d.ts +0 -27
- package/dist/lib/remote-auth/recovery/recover-stale-lock.d.ts +0 -68
- package/dist/lib/remote-auth/recovery/recover-unrelated-histories.d.ts +0 -44
- package/dist/lib/remote-auth/recovery/recover-wrong-remote.d.ts +0 -35
- package/dist/lib/remote-auth/resolution-plan.d.ts +0 -64
- package/dist/paged.polyfill-n95pbxfn.js +0 -33288
- package/dist/pagedjs-interface-qxvzgwd7.js +0 -557
- package/dist/preview-shell-6dqexx1m.js +0 -581
- /package/dist/assets/{preview/scripts/pagedjs-bridge.d.ts → engine/gutterpress-agent.d.ts} +0 -0
- /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;
|