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

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-xxegv0t0.js} +10 -7
  7. package/dist/{build-san7fv2z.js → build-w0vb6mgq.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-46ycxe6r.js +18 -0
  12. package/dist/{cli-k4bd06sd.js → cli-5czby0dd.js} +4986 -5889
  13. package/dist/cli-c41yr7he.js +241 -0
  14. package/dist/{cli-e5zhb0xs.js → cli-f4rcbt2t.js} +17 -24
  15. package/dist/{cli-najycadg.js → cli-gk3wsdpb.js} +69 -253
  16. package/dist/{index-yzrh708h.js → cli-k1065rkg.js} +2 -5
  17. package/dist/{cli-506tg37g.js → cli-revgt4pr.js} +2 -1
  18. package/dist/{cli-wchtvxvw.js → cli-v5mp7a6q.js} +14 -1
  19. package/dist/cli-yp3p0sf2.js +2343 -0
  20. package/dist/cli.js +20 -18
  21. package/dist/{doctor-hrk0kxxz.js → doctor-ctgn1dnt.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-eng4xh2q.js +41 -0
  32. package/dist/engine-q5skhgms.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-a4kr77td.js +1907 -0
  36. package/dist/{cli-yzrh708h.js → index-mdefp0y5.js} +1 -1
  37. package/dist/index-mvehxh0e.js +708 -0
  38. package/dist/{index-bynn850m.js → index-s416qpdv.js} +3049 -4015
  39. package/dist/{index-wchtvxvw.js → index-v5mp7a6q.js} +14 -1
  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-gsh6qwrq.js} +10 -7
  78. package/dist/{manifest.schema-z61rzw44.json → manifest.schema-zxgxnbg7.json} +21 -0
  79. package/dist/{new-8p38wavc.js → new-nkhycqy4.js} +12 -8
  80. package/dist/{plugin-ees6nhkc.js → plugin-129wcs93.js} +10 -7
  81. package/dist/{preflight-1q6c2edh.js → preflight-41hcejg9.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-fnqb2y4v.js +1015 -0
  86. package/dist/{preview-y5a2zen1.js → preview-pftsvr9z.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-dnjvb4jg.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-a3enjttn.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-ph1xffqy.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
package/dist/render.js CHANGED
@@ -1,7 +1,49 @@
1
- // src/lib/markdown/markdown-it-paged.js
1
+ // src/lib/markdown/markers.js
2
+ function unwrapAttrBraces(token) {
3
+ if (typeof token !== "string")
4
+ return token;
5
+ let t = token.trim();
6
+ if (t.startsWith("{"))
7
+ t = t.slice(1).trim();
8
+ if (t.endsWith("}"))
9
+ t = t.slice(0, -1).trim();
10
+ return t || token;
11
+ }
2
12
  function isBareToken(token) {
3
13
  return token && !token.includes("=") && !token.startsWith(".") && !token.startsWith("#");
4
14
  }
15
+ var KNOWN_KINDS = [
16
+ "chapter",
17
+ "spread",
18
+ "page",
19
+ "section",
20
+ "continue",
21
+ "page-break",
22
+ "column-break",
23
+ "end-section"
24
+ ];
25
+ var BARE_TOKEN_RE = /^[A-Za-z0-9_][A-Za-z0-9_.:-]*$/;
26
+ function editDistance(a, b) {
27
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
28
+ for (let i = 1;i <= a.length; i++) {
29
+ const cur = [i];
30
+ for (let j = 1;j <= b.length; j++) {
31
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
32
+ }
33
+ prev = cur;
34
+ }
35
+ return prev[b.length];
36
+ }
37
+ function nearestKind(word) {
38
+ const w = word.toLowerCase();
39
+ if (w.length < 3)
40
+ return null;
41
+ for (const kind of KNOWN_KINDS) {
42
+ if (editDistance(w, kind) <= 1)
43
+ return kind;
44
+ }
45
+ return null;
46
+ }
5
47
  function escapeHtml(s) {
6
48
  return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
7
49
  }
@@ -38,9 +80,11 @@ function parseMarkerLine(line) {
38
80
  }
39
81
  if (buf)
40
82
  tokens.push(buf);
83
+ for (let i = 1;i < tokens.length; i++)
84
+ tokens[i] = unwrapAttrBraces(tokens[i]);
41
85
  const head = tokens[0];
42
86
  const kind = head.slice(1);
43
- if (!["chapter", "spread", "page", "section", "continue", "page-break", "column-break", "end-section"].includes(kind))
87
+ if (!KNOWN_KINDS.includes(kind))
44
88
  return null;
45
89
  if (kind === "page-break" || kind === "column-break" || kind === "end-section" || kind === "continue") {
46
90
  return { kind, name: null, attrs: {} };
@@ -62,6 +106,18 @@ function parseMarkerLine(line) {
62
106
  const classes = [];
63
107
  let sawExplicitAttr = false;
64
108
  let hasAmbiguousBareToken = false;
109
+ const unknownTokens = [];
110
+ for (const t of body) {
111
+ if (t.startsWith(".") || t.startsWith("#")) {
112
+ if (!BARE_TOKEN_RE.test(t.slice(1).trim()))
113
+ unknownTokens.push(t);
114
+ continue;
115
+ }
116
+ if (t.indexOf("=") > 0)
117
+ continue;
118
+ if (!BARE_TOKEN_RE.test(t))
119
+ unknownTokens.push(t);
120
+ }
65
121
  for (let idx = 0;idx < body.length; idx++) {
66
122
  if (idx === nameIndex) {
67
123
  if (sawExplicitAttr && isBareToken(body[idx]))
@@ -104,6 +160,11 @@ function parseMarkerLine(line) {
104
160
  const marker = { kind, name, attrs };
105
161
  if (hasAmbiguousBareToken)
106
162
  marker.__ambiguousBareToken = true;
163
+ if (unknownTokens.length)
164
+ marker.__unknownTokens = unknownTokens;
165
+ if (bareTokens.length > 1 && !hasAmbiguousBareToken && !unknownTokens.length) {
166
+ marker.__extraBareTokens = { tokens: bareTokens, named: name !== null };
167
+ }
107
168
  return marker;
108
169
  }
109
170
  function warn(env, line, type, message, marker) {
@@ -148,7 +209,6 @@ function attachDataAttrs(token, kind, name, attrs) {
148
209
  }
149
210
  function plugin(md, pluginOptions = {}) {
150
211
  const options = {
151
- implicitPage: false,
152
212
  preferPagesInSpreads: false,
153
213
  ...pluginOptions
154
214
  };
@@ -169,6 +229,19 @@ function plugin(md, pluginOptions = {}) {
169
229
  delete parsed.__ambiguousBareToken;
170
230
  warn(state.env, startLine + 1, "ambiguous_marker_token", "A bare marker token after a key=value attribute is being interpreted as the marker name (or an extra class). Use comma-separated classes (class=a,b) or .class shorthand instead.", parsed);
171
231
  }
232
+ if (parsed.__unknownTokens) {
233
+ const unknown = parsed.__unknownTokens;
234
+ delete parsed.__unknownTokens;
235
+ for (const t of unknown) {
236
+ warn(state.env, startLine + 1, "unrecognized_marker_token", `@${parsed.kind}: "${t}" is not something a marker understands, so it was kept verbatim as a class name. Write a class as .my-class (or {.my-class}), an id as #my-id, and anything else as key=value.`, parsed);
237
+ }
238
+ }
239
+ if (parsed.__extraBareTokens) {
240
+ const { tokens, named } = parsed.__extraBareTokens;
241
+ delete parsed.__extraBareTokens;
242
+ const list = tokens.map((t) => `"${t}"`).join(", ");
243
+ warn(state.env, startLine + 1, "extra_bare_marker_token", named ? `@${parsed.kind}: only the first plain word (${list.split(", ")[0]}) is used as the name; the rest (${tokens.slice(1).map((t) => `"${t}"`).join(", ")}) became class names instead. Quote a name that contains spaces ("${tokens.join(" ")}"), or write classes as .${tokens.slice(1).join(" .")}.` : `@${parsed.kind}: ${list} are several plain words, but a marker has only one name slot — so NONE of them was used as the name and they all became class names instead. Quote a name that contains spaces ("${tokens.join(" ")}"), or write classes as .${tokens.join(" .")}.`, parsed);
244
+ }
172
245
  state.line = startLine + 1;
173
246
  return true;
174
247
  }
@@ -176,9 +249,33 @@ function plugin(md, pluginOptions = {}) {
176
249
  alt: ["paragraph", "reference", "blockquote", "list"]
177
250
  });
178
251
  const SCOPE_CLOSE_ORDER = ["section", "page", "spread", "chapter"];
252
+ function scanForMistypedMarkers(state) {
253
+ for (let tokenIndex = 0;tokenIndex < state.tokens.length; tokenIndex++) {
254
+ const tok = state.tokens[tokenIndex];
255
+ if (tok.type !== "inline" || !tok.map)
256
+ continue;
257
+ const opener = state.tokens[tokenIndex - 1];
258
+ if (!opener || opener.type !== "paragraph_open")
259
+ continue;
260
+ const lines = tok.content.split(`
261
+ `);
262
+ for (let i = 0;i < lines.length; i++) {
263
+ const m = /^@([A-Za-z][A-Za-z0-9-]*)(?=\s|$)/.exec(lines[i].trim());
264
+ if (!m)
265
+ continue;
266
+ if (KNOWN_KINDS.includes(m[1]))
267
+ continue;
268
+ const suggestion = nearestKind(m[1]);
269
+ if (!suggestion)
270
+ continue;
271
+ warn(state.env, tok.map[0] + i + 1, "unknown_marker", `"@${m[1]}" is not a marker Gutterpress knows, so this line was left as ordinary text. Did you mean "@${suggestion}"? (marker names are lower-case).`, null);
272
+ }
273
+ }
274
+ }
179
275
  md.core.ruler.after("block", "layout_transform", function(state) {
180
276
  if (!state.env.__layoutMarkersUsed)
181
277
  return;
278
+ scanForMistypedMarkers(state);
182
279
  const out = [];
183
280
  setDepth(state.env, 0);
184
281
  const stack = {
@@ -217,6 +314,7 @@ function plugin(md, pluginOptions = {}) {
217
314
  };
218
315
  function openChapter(meta) {
219
316
  const t = new state.Token("layout_chapter_open", "div", 1);
317
+ t.meta = { line: meta.__line };
220
318
  const classes = meta.attrs?.class || "";
221
319
  addClasses(t, "chapter", classes);
222
320
  attachDataAttrs(t, "chapter", meta.name, meta.attrs || {});
@@ -231,6 +329,7 @@ function plugin(md, pluginOptions = {}) {
231
329
  }
232
330
  function openSpread(meta) {
233
331
  const t = new state.Token("layout_spread_open", "div", 1);
332
+ t.meta = { line: meta.__line };
234
333
  addClasses(t, "spread", meta.attrs && meta.attrs.class ? meta.attrs.class : "");
235
334
  attachDataAttrs(t, "spread", meta.name, meta.attrs || {});
236
335
  stack.open({ kind: "spread", noPagesYet: true, sawAnyPage: false }, t);
@@ -238,6 +337,7 @@ function plugin(md, pluginOptions = {}) {
238
337
  function openPage(meta) {
239
338
  const chapter = stack.get("chapter");
240
339
  const t = new state.Token("layout_page_open", "div", 1);
340
+ t.meta = { line: meta.__line };
241
341
  const explicit = meta.attrs && meta.attrs.class ? meta.attrs.class : "";
242
342
  const counterClass = chapter ? chapter.counterClass : "";
243
343
  const tokens = explicit ? explicit.split(/\s+/) : [];
@@ -267,7 +367,7 @@ function plugin(md, pluginOptions = {}) {
267
367
  const t = new state.Token("layout_section_open", "div", 1);
268
368
  addClasses(t, "section", meta.attrs && meta.attrs.class ? meta.attrs.class : "");
269
369
  attachDataAttrs(t, "section", meta.name, meta.attrs || {});
270
- t.meta = { hasColumnBreak: false };
370
+ t.meta = { hasColumnBreak: false, line: meta.__line };
271
371
  stack.open({
272
372
  kind: "section",
273
373
  meta: { name: meta.name || null, attrs: { ...meta.attrs || {} } },
@@ -303,14 +403,6 @@ function plugin(md, pluginOptions = {}) {
303
403
  }
304
404
  if (kind === "section") {
305
405
  stack.close("section");
306
- if (!stack.has("page")) {
307
- if (options.implicitPage) {
308
- warn(state.env, line, "implicit_page", '@section used without an open @page; creating an implicit page wrapper (data-page="auto").', meta);
309
- openPage({ name: "auto", attrs: {}, __line: line });
310
- } else {
311
- warn(state.env, line, "section_without_page", "@section used without an open @page; region will render but will not be wrapped in a page.", meta);
312
- }
313
- }
314
406
  const spread = stack.get("spread");
315
407
  if (spread && spread.noPagesYet) {
316
408
  warn(state.env, line, "spread_without_pages", "@section inside a spread without an explicit @page. Allowed (adhoc spread), but explicit @page markers give stronger control.", meta);
@@ -328,9 +420,10 @@ function plugin(md, pluginOptions = {}) {
328
420
  name: section.meta.name,
329
421
  attrs: { ...section.meta.attrs || {} }
330
422
  };
423
+ contMeta.__line = line;
331
424
  const cls = (contMeta.attrs.class || "").split(/\s+/).filter(Boolean);
332
- if (!cls.includes("gutterpress-continued"))
333
- cls.push("gutterpress-continued");
425
+ if (!cls.includes("gp-continued"))
426
+ cls.push("gp-continued");
334
427
  contMeta.attrs.class = cls.join(" ");
335
428
  stack.close("section");
336
429
  openSection(contMeta);
@@ -338,7 +431,8 @@ function plugin(md, pluginOptions = {}) {
338
431
  }
339
432
  if (kind === "page-break") {
340
433
  const t = new state.Token("layout_page_break", "div", 0);
341
- t.attrSet("class", "md-page-break");
434
+ t.meta = { line };
435
+ t.attrSet("class", "gp-page-break");
342
436
  t.attrSet("aria-hidden", "true");
343
437
  out.push(t);
344
438
  continue;
@@ -352,7 +446,8 @@ function plugin(md, pluginOptions = {}) {
352
446
  }
353
447
  }
354
448
  const t = new state.Token("layout_column_break", "div", 0);
355
- t.attrSet("class", "md-column-break");
449
+ t.meta = { line };
450
+ t.attrSet("class", "gp-column-break");
356
451
  t.attrSet("aria-hidden", "true");
357
452
  out.push(t);
358
453
  continue;
@@ -389,7 +484,11 @@ function plugin(md, pluginOptions = {}) {
389
484
  const cls = token.attrGet("class") || "";
390
485
  if (cls.includes("col-split") && token.meta && token.meta.hasColumnBreak) {
391
486
  setDepth(env, getDepth(env) + 1);
392
- return `<div class="${escapeAttr(cls)}"><div class="col">
487
+ const rangeAttr = token.attrGet("data-source-range");
488
+ const rangeHtml = rangeAttr ? ` data-source-range="${escapeAttr(rangeAttr)}"` : "";
489
+ const chapterAttr = token.attrGet("data-chapter-src");
490
+ const chapterHtml = chapterAttr ? ` data-chapter-src="${escapeAttr(chapterAttr)}"` : "";
491
+ return `<div class="${escapeAttr(cls)}"${rangeHtml}${chapterHtml}><div class="col">
393
492
  `;
394
493
  }
395
494
  return self.renderToken(tokens, idx, opts);
@@ -403,8 +502,12 @@ function plugin(md, pluginOptions = {}) {
403
502
  return self.renderToken(tokens, idx, opts);
404
503
  };
405
504
  md.renderer.rules.layout_page_break = (tokens, idx) => {
406
- const cls = tokens[idx].attrGet("class") || "md-page-break";
407
- return `<div class="${escapeAttr(cls)}" aria-hidden="true"></div>
505
+ const cls = tokens[idx].attrGet("class") || "gp-page-break";
506
+ const rangeAttr = tokens[idx].attrGet("data-source-range");
507
+ const rangeHtml = rangeAttr ? ` data-source-range="${escapeAttr(rangeAttr)}"` : "";
508
+ const chapterAttr = tokens[idx].attrGet("data-chapter-src");
509
+ const chapterHtml = chapterAttr ? ` data-chapter-src="${escapeAttr(chapterAttr)}"` : "";
510
+ return `<div class="${escapeAttr(cls)}" aria-hidden="true"${rangeHtml}${chapterHtml}></div>
408
511
  `;
409
512
  };
410
513
  md.renderer.rules.layout_column_break = (tokens, idx, opts, env) => {
@@ -412,32 +515,192 @@ function plugin(md, pluginOptions = {}) {
412
515
  return `</div><div class="col">
413
516
  `;
414
517
  }
415
- const cls = tokens[idx].attrGet("class") || "md-column-break";
416
- return `<div class="${escapeAttr(cls)}" aria-hidden="true"></div>
518
+ const cls = tokens[idx].attrGet("class") || "gp-column-break";
519
+ const rangeAttr = tokens[idx].attrGet("data-source-range");
520
+ const rangeHtml = rangeAttr ? ` data-source-range="${escapeAttr(rangeAttr)}"` : "";
521
+ const chapterAttr = tokens[idx].attrGet("data-chapter-src");
522
+ const chapterHtml = chapterAttr ? ` data-chapter-src="${escapeAttr(chapterAttr)}"` : "";
523
+ return `<div class="${escapeAttr(cls)}" aria-hidden="true"${rangeHtml}${chapterHtml}></div>
417
524
  `;
418
525
  };
419
526
  md.renderer.rules.layout_marker = () => "";
420
527
  }
421
- var PAGED_CSS = `
422
- .md-page-break { break-before: page; }
528
+ var MARKER_CSS = `
529
+ /* The UA default of 8px body margin is a screen affordance with no meaning
530
+ in paged media, and engines disagree about it: a polyfill that treats the
531
+ page div as the page box drops it, native print keeps it. Left in place it
532
+ insets EVERY page's content by 8px per side, and -- measured, 300dpi,
533
+ 6x4in sheet -- it is what stops a full-width block from reaching the
534
+ paper: it lands at 0.080..5.917in of a 6in sheet instead of
535
+ 0.000..6.000in, because width:100% resolves against the BODY content box,
536
+ not the page's. Zeroing it here (first in the cascade) makes the two
537
+ agree. Authors who want a body margin still just declare one. */
538
+ body { margin: 0; }
539
+
540
+ .gp-page-break { break-before: page; }
423
541
  .page { break-before: page; }
424
542
  .spread { break-before: page; }
425
- .section { break-inside: avoid; }
426
- .section.col-split { break-inside: auto; }
427
- .md-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }
543
+ :where(.page, .spread) { position: relative; }
544
+ .gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }
428
545
 
429
- .center { display: block; margin-left: auto; margin-right: auto; max-width: 100%; }
430
- .float-left { float: left; margin: 0 1em 1em 0; max-width: 50%; }
431
- .float-right { float: right; margin: 0 0 1em 1em; max-width: 50%; }
432
- .full-width { display: block; width: 100%; max-width: 100%; }
433
- .full-bleed {
546
+ :where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }
547
+ :where(img, svg, video) { max-width: 100%; }
548
+ :where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }
549
+ :where(.section, figure) > :where(:first-child) { break-before: avoid; }
550
+
551
+ `;
552
+
553
+ // src/lib/markdown/gutterpress-css.ts
554
+ var GUTTERPRESS_CSS = `
555
+ /* gp-* author image/block vocabulary. One vocabulary, gp-* only — the
556
+ pre-vocabulary utility names (.center/.float-left/.float-right/
557
+ .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename
558
+ the classes in their markdown (see the migration note). Source ORDER is
559
+ the contract — see the doctrine comment above. */
560
+
561
+ /* flow positions */
562
+ .gp-left {
563
+ float: left;
564
+ margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;
565
+ max-width: 50%;
566
+ }
567
+ .gp-right {
568
+ float: right;
569
+ margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);
570
+ max-width: 50%;
571
+ }
572
+ .gp-center {
434
573
  display: block;
574
+ float: none;
575
+ margin-left: auto;
576
+ margin-right: auto;
577
+ max-width: 100%;
578
+ }
579
+ .gp-full {
580
+ display: block;
581
+ float: none;
582
+ width: 100%;
583
+ max-width: 100%;
584
+ }
585
+ @page gp-full-bleed { margin-left: 0; margin-right: 0; }
586
+ .gp-bleed {
587
+ display: block;
588
+ float: none;
435
589
  break-before: page;
590
+ page: gp-full-bleed;
436
591
  max-width: none;
437
- width: calc(100% + var(--pagedjs-margin-left, 0px) + var(--pagedjs-margin-right, 0px));
438
- margin-left: calc(-1 * var(--pagedjs-margin-left, 0px));
439
- margin-right: calc(-1 * var(--pagedjs-margin-right, 0px));
592
+ width: 100%;
593
+ margin-left: 0;
594
+ margin-right: 0;
595
+ }
596
+
597
+ /* sizes — AFTER the flow positions so max-width:100% lifts the floats' 50%
598
+ cap at equal specificity */
599
+ .gp-small { width: 25%; max-width: 100%; }
600
+ .gp-medium { width: 50%; max-width: 100%; }
601
+ .gp-large { width: 75%; max-width: 100%; }
602
+
603
+ /* float clearance presets — consumed by var(--gp-gap) in the float rules
604
+ above and by .gp-shape's shape-margin below; --gp-gap itself is
605
+ author-settable CSS */
606
+ .gp-tight { --gp-gap: 0.5em; }
607
+ .gp-loose { --gp-gap: 2em; }
608
+
609
+ /* column runs — plain CSS Multi-column, exposed as author vocabulary so
610
+ "put this in two columns" does not require borrowing a styled container
611
+ from the book's own component layer. That borrowing is what this exists
612
+ to prevent: a book whose theme paints .section chrome by default gives
613
+ every author who opens a section just to start a column run a panel they
614
+ did not ask for, and the book then needs a reset rule to take it back.
615
+ With a neutral primitive the author opts into columns and nothing else.
616
+
617
+ Permanent vocabulary, not a shim: Chromium implements multicol natively
618
+ and these rules are the standard properties verbatim, so there is no
619
+ spec gap here to remove later. Deliberately minimal — column-fill is
620
+ NOT set, because the correct value depends on whether the run fragments
621
+ across pages (auto packs each page's columns; the CSS initial balance is
622
+ right for a run that fits on one page) and only the author knows which.
623
+ --gp-column-gap is author-settable. */
624
+ .gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }
625
+ .gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }
626
+
627
+ /* shape wrap — text follows the image's alpha silhouette instead of its
628
+ rectangular box. shape-outside only applies to floats, so this is inert
629
+ without .gp-left/.gp-right (and under .gp-pin, which un-floats). The
630
+ shape URL cannot be written in CSS (url() contexts can't read attr()),
631
+ so the image renderer rule (images.ts) mirrors the src into an inline
632
+ --gp-shape:url(...) custom property whenever it sees this class --
633
+ authors only ever type the class. threshold 0.2 ignores near-transparent
634
+ anti-aliasing halos; shape-margin shares the float-gap vocabulary. */
635
+ img.gp-shape {
636
+ shape-outside: var(--gp-shape);
637
+ shape-image-threshold: 0.2;
638
+ shape-margin: var(--gp-gap, 1em);
440
639
  }
640
+
641
+ /* pin — within the nearest positioned ancestor (.page/.spread, rule above).
642
+ inset:0 and the explicit centers are load-bearing; see doctrine comment. */
643
+ .gp-pin {
644
+ position: absolute;
645
+ inset: 0;
646
+ align-self: center;
647
+ justify-self: center;
648
+ margin: 0;
649
+ max-width: 100%;
650
+ }
651
+
652
+ /* pin edge modifiers — AFTER .gp-pin to beat its center defaults;
653
+ justify-self is inert on in-flow floats, so gp-left/gp-right safely do
654
+ double duty as flow float + pin edge */
655
+ .gp-top { align-self: start; }
656
+ .gp-bottom { align-self: end; }
657
+ .gp-left { justify-self: start; }
658
+ .gp-right { justify-self: end; }
659
+
660
+ /* wrapper-margin neutralization (same pattern and rationale as the
661
+ .gp-bleed paragraph-margin note in the doctrine comment; for pin, the
662
+ emptied paragraph would otherwise leave a phantom margin gap in flow) */
663
+ :where(p:has(> img.gp-bleed:only-child)) { margin: 0; }
664
+ :where(p:has(> img.gp-pin:only-child)) { margin: 0; }
665
+
666
+ /* depth — a named ladder for z-index, so books stop hand-tuning bare
667
+ integers. A real book measured 21 z-index declarations using only four
668
+ distinct values (-1, 0, 1, 2), each written literally at its use site.
669
+ The custom properties are the author-settable surface (a book needing a
670
+ deeper stack raises them once); the classes are the shorthand.
671
+
672
+ NOT named "layer": CSS Paged Media 3 §3.1 already defines "page layers"
673
+ (page background, canvas, borders, contents, margin boxes) and those are
674
+ parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a
675
+ different concept would collide with the spec vocabulary this project
676
+ tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the
677
+ ladder avoids those words too.
678
+
679
+ .gp-behind is the one that earns its place: it puts a pinned image UNDER
680
+ the page's text, which is otherwise impossible to express without a bare
681
+ negative z-index. "Above" needs no class — an out-of-flow pin already
682
+ paints above in-flow content.
683
+
684
+ Two things silently defeat .gp-behind, neither visible at the use site:
685
+ - a stacking context on the .page/.spread ancestor (z-index, isolation,
686
+ opacity, filter, transform on it traps the negative layer inside).
687
+ Core keeps .page/.spread at 'position: relative; z-index: auto'
688
+ precisely so they are not stacking contexts.
689
+ - a clipping ancestor (overflow other than visible), the same mechanism
690
+ that clips a .gp-bleed plate back to the wrapper's width.
691
+ The build-time engine.layer.trapped audit reports both against the live
692
+ ancestor chain. printsafe/page-containment is only an early source hint for
693
+ declarations written directly on .page/.spread. */
694
+ :root {
695
+ --gp-z-behind: -1;
696
+ --gp-z-base: 0;
697
+ --gp-z-raised: 1;
698
+ --gp-z-front: 2;
699
+ }
700
+ .gp-behind { z-index: var(--gp-z-behind); }
701
+ .gp-base { z-index: var(--gp-z-base); }
702
+ .gp-raised { z-index: var(--gp-z-raised); }
703
+ .gp-front { z-index: var(--gp-z-front); }
441
704
  `;
442
705
 
443
706
  // src/lib/markdown/chapter-id.ts
@@ -485,6 +748,42 @@ function debug(message, ...args) {
485
748
  // src/lib/markdown/renderer.ts
486
749
  import markdownItAttrs from "markdown-it-attrs";
487
750
  import markdownItFootnote from "markdown-it-footnote";
751
+
752
+ // src/lib/markdown/gp-pin-scope.js
753
+ function warn2(env, line, type, message, marker) {
754
+ if (!env.layoutWarnings)
755
+ env.layoutWarnings = [];
756
+ env.layoutWarnings.push({ line, type, message, marker });
757
+ }
758
+ function gpPinScope(md) {
759
+ const PIN_OUTSIDE_PAGE_MSG = "A .gp-pin element is not inside any @page or @spread, so it is pinned to the whole document instead of the page it sits on — it can print on a completely different sheet. Move it inside an @page or @spread block, or remove .gp-pin.";
760
+ function hasPinClass(token) {
761
+ const cls = token.attrGet && token.attrGet("class");
762
+ return typeof cls === "string" && cls.split(/\s+/).includes("gp-pin");
763
+ }
764
+ md.core.ruler.push("gp_pin_scope_check", function(state) {
765
+ let depth = 0;
766
+ for (const token of state.tokens) {
767
+ if (token.type === "layout_page_open" || token.type === "layout_spread_open") {
768
+ depth += 1;
769
+ } else if (token.type === "layout_page_close" || token.type === "layout_spread_close") {
770
+ depth -= 1;
771
+ } else if (depth === 0) {
772
+ if (token.type === "inline" && token.children) {
773
+ for (const child of token.children) {
774
+ if (child.type === "image" && hasPinClass(child)) {
775
+ warn2(state.env, (token.map?.[0] ?? 0) + 1, "pin_outside_page", PIN_OUTSIDE_PAGE_MSG);
776
+ }
777
+ }
778
+ } else if (token.nesting === 1 && hasPinClass(token)) {
779
+ warn2(state.env, (token.map?.[0] ?? 0) + 1, "pin_outside_page", PIN_OUTSIDE_PAGE_MSG);
780
+ }
781
+ }
782
+ }
783
+ });
784
+ }
785
+
786
+ // src/lib/markdown/renderer.ts
488
787
  import markdownItSourceMap from "markdown-it-source-map";
489
788
  import markdownItDeflist from "markdown-it-deflist";
490
789
  import markdownItMark from "markdown-it-mark";
@@ -493,16 +792,77 @@ import markdownItSup from "markdown-it-sup";
493
792
  import markdownItAbbr from "markdown-it-abbr";
494
793
 
495
794
  // src/lib/markdown/images.ts
795
+ var isSrcsetSpace = (char) => char === "\t" || char === `
796
+ ` || char === "\f" || char === "\r" || char === " ";
797
+ function parseSrcsetUrlCandidates(input) {
798
+ const candidates = [];
799
+ let position = 0;
800
+ while (position < input.length) {
801
+ while (position < input.length && (isSrcsetSpace(input[position]) || input[position] === ",")) {
802
+ position++;
803
+ }
804
+ if (position >= input.length)
805
+ break;
806
+ const urlStart = position;
807
+ while (position < input.length && !isSrcsetSpace(input[position]))
808
+ position++;
809
+ let urlEnd = position;
810
+ if (input[urlEnd - 1] === ",") {
811
+ while (urlEnd > urlStart && input[urlEnd - 1] === ",")
812
+ urlEnd--;
813
+ if (urlEnd > urlStart) {
814
+ candidates.push({
815
+ url: input.slice(urlStart, urlEnd),
816
+ start: urlStart,
817
+ end: urlEnd
818
+ });
819
+ }
820
+ continue;
821
+ }
822
+ let parenDepth = 0;
823
+ while (position < input.length) {
824
+ const char = input[position];
825
+ if (char === "(")
826
+ parenDepth++;
827
+ else if (char === ")" && parenDepth > 0)
828
+ parenDepth--;
829
+ else if (char === "," && parenDepth === 0) {
830
+ position++;
831
+ break;
832
+ }
833
+ position++;
834
+ }
835
+ if (urlEnd > urlStart) {
836
+ candidates.push({
837
+ url: input.slice(urlStart, urlEnd),
838
+ start: urlStart,
839
+ end: urlEnd
840
+ });
841
+ }
842
+ }
843
+ return candidates;
844
+ }
496
845
  function registerImageRule(md) {
497
846
  const defaultRender = md.renderer.rules.image ?? ((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options));
498
847
  md.renderer.rules.image = (tokens, idx, options, env, self) => {
499
848
  const token = tokens[idx];
849
+ for (const child of token.children ?? []) {
850
+ if (child.type === "text_special" || child.type === "code_inline")
851
+ child.type = "text";
852
+ }
500
853
  const srcIdx = token.attrIndex("src");
501
854
  if (srcIdx >= 0 && token.attrs) {
502
855
  const src = token.attrs[srcIdx][1];
503
856
  if (src) {
504
857
  const bucket = env ?? {};
505
858
  (bucket.imageRefs ??= []).push(src);
859
+ const cls = token.attrGet("class");
860
+ if (cls && cls.split(/\s+/).includes("gp-shape")) {
861
+ const cssUrl = src.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
862
+ const decl = `--gp-shape:url("${cssUrl}")`;
863
+ const existing = token.attrGet("style");
864
+ token.attrSet("style", existing ? `${decl}; ${existing}` : decl);
865
+ }
506
866
  }
507
867
  }
508
868
  return defaultRender(tokens, idx, options, env, self);
@@ -510,25 +870,337 @@ function registerImageRule(md) {
510
870
  }
511
871
  function collectHtmlImageRefs(html) {
512
872
  const refs = [];
513
- const srcRe = /<img\b[^>]*?\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
514
- let m;
515
- while (m = srcRe.exec(html)) {
516
- const src = m[1] ?? m[2] ?? m[3];
873
+ const tags = [];
874
+ const collectTags = (active) => {
875
+ for (const match of active.matchAll(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g)) {
876
+ if (/^<(?:img|source)\b/i.test(match[0]))
877
+ tags.push(match[0]);
878
+ }
879
+ };
880
+ const protectedRegion = /<!--[\s\S]*?-->|<(script|style|pre|code|textarea)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
881
+ let last = 0;
882
+ for (const match of html.matchAll(protectedRegion)) {
883
+ const index = match.index ?? 0;
884
+ collectTags(html.slice(last, index));
885
+ last = index + match[0].length;
886
+ }
887
+ collectTags(html.slice(last));
888
+ const attr = (tag, name) => {
889
+ const match = new RegExp(`[\\t\\n\\f\\r ]${name}[\\t\\n\\f\\r ]*=[\\t\\n\\f\\r ]*(?:"([^"]*)"|'([^']*)'|([^\\t\\n\\f\\r >]+))`, "i").exec(tag);
890
+ return match?.[1] ?? match?.[2] ?? match?.[3];
891
+ };
892
+ for (const tag of tags) {
893
+ if (!/^<img\b/i.test(tag))
894
+ continue;
895
+ const src = attr(tag, "src");
517
896
  if (src)
518
897
  refs.push(src);
519
898
  }
520
- const srcsetRe = /<(?:img|source)\b[^>]*?\bsrcset\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
521
- while (m = srcsetRe.exec(html)) {
522
- const list = m[1] ?? m[2] ?? "";
523
- for (const candidate of list.split(",")) {
524
- const url = candidate.trim().split(/\s+/)[0];
525
- if (url)
526
- refs.push(url);
527
- }
899
+ for (const tag of tags) {
900
+ const list = attr(tag, "srcset") ?? "";
901
+ refs.push(...parseSrcsetUrlCandidates(list).map((candidate) => candidate.url));
528
902
  }
529
903
  return refs;
530
904
  }
531
905
 
906
+ // src/lib/markdown/source-range.ts
907
+ var SOURCE_RANGE_ATTR = "data-source-range";
908
+ var SOURCE_CHAPTER_ATTR = "data-chapter-src";
909
+ var SELF_CLOSING_BLOCK_TYPES = new Set([
910
+ "fence",
911
+ "hr",
912
+ "html_block",
913
+ "layout_page_break",
914
+ "layout_column_break"
915
+ ]);
916
+ function isFiniteRangeTuple(value) {
917
+ return Array.isArray(value) && value.length === 2 && Number.isFinite(value[0]) && Number.isFinite(value[1]);
918
+ }
919
+ function resolveRange(token) {
920
+ if (isFiniteRangeTuple(token.map)) {
921
+ return [token.map[0], token.map[1]];
922
+ }
923
+ const meta = token.meta;
924
+ const line = meta?.line;
925
+ if (typeof line === "number" && Number.isFinite(line)) {
926
+ return [line - 1, line];
927
+ }
928
+ return null;
929
+ }
930
+ function isAnnotationTarget(token) {
931
+ if (token.nesting === 1)
932
+ return true;
933
+ return token.nesting === 0 && SELF_CLOSING_BLOCK_TYPES.has(token.type);
934
+ }
935
+ function annotateToken(token, chapter) {
936
+ if (!isAnnotationTarget(token))
937
+ return;
938
+ const range = resolveRange(token);
939
+ if (!range)
940
+ return;
941
+ token.attrSet(SOURCE_RANGE_ATTR, `${range[0]}:${range[1]}`);
942
+ if (chapter)
943
+ token.attrSet(SOURCE_CHAPTER_ATTR, chapter);
944
+ }
945
+ var sourceRangeRule = (state) => {
946
+ const chapter = typeof state.env?.sourceChapter === "string" ? state.env.sourceChapter : null;
947
+ for (const token of state.tokens) {
948
+ annotateToken(token, chapter);
949
+ }
950
+ };
951
+
952
+ // src/lib/markdown/inline-source.ts
953
+ var SOURCE_TOKEN_ATTR = "data-gp-source-token";
954
+ var SOURCE_OCCURRENCE_ATTR = "data-gp-source-occurrence";
955
+ var RESERVED_ATTRS = new Set([SOURCE_TOKEN_ATTR, SOURCE_OCCURRENCE_ATTR]);
956
+ function isHtmlSpace(char) {
957
+ return char === " " || char === "\t" || char === `
958
+ ` || char === "\r" || char === "\f";
959
+ }
960
+ function stripReservedAttrsFromTag(html) {
961
+ let out = "";
962
+ let inTag = false;
963
+ let quote = "";
964
+ for (let i = 0;i < html.length; ) {
965
+ const char = html[i];
966
+ if (!inTag) {
967
+ out += char;
968
+ if (char === "<")
969
+ inTag = true;
970
+ i++;
971
+ continue;
972
+ }
973
+ if (quote) {
974
+ out += char;
975
+ if (char === quote)
976
+ quote = "";
977
+ i++;
978
+ continue;
979
+ }
980
+ if (char === '"' || char === "'") {
981
+ quote = char;
982
+ out += char;
983
+ i++;
984
+ continue;
985
+ }
986
+ if (char === ">") {
987
+ inTag = false;
988
+ out += char;
989
+ i++;
990
+ continue;
991
+ }
992
+ if (!isHtmlSpace(char)) {
993
+ out += char;
994
+ i++;
995
+ continue;
996
+ }
997
+ let nameStart = i;
998
+ while (isHtmlSpace(html[nameStart]))
999
+ nameStart++;
1000
+ let nameEnd = nameStart;
1001
+ while (nameEnd < html.length && !isHtmlSpace(html[nameEnd]) && html[nameEnd] !== "=" && html[nameEnd] !== "/" && html[nameEnd] !== ">")
1002
+ nameEnd++;
1003
+ const name = html.slice(nameStart, nameEnd).toLowerCase();
1004
+ if (!RESERVED_ATTRS.has(name)) {
1005
+ out += html.slice(i, nameStart);
1006
+ i = nameStart;
1007
+ continue;
1008
+ }
1009
+ i = nameEnd;
1010
+ while (isHtmlSpace(html[i]))
1011
+ i++;
1012
+ if (html[i] !== "=")
1013
+ continue;
1014
+ i++;
1015
+ while (isHtmlSpace(html[i]))
1016
+ i++;
1017
+ const valueQuote = html[i];
1018
+ if (valueQuote === '"' || valueQuote === "'") {
1019
+ for (i++;i < html.length && html[i] !== valueQuote; i++) {}
1020
+ if (html[i] === valueQuote)
1021
+ i++;
1022
+ } else {
1023
+ while (i < html.length && !isHtmlSpace(html[i]) && html[i] !== ">")
1024
+ i++;
1025
+ }
1026
+ }
1027
+ return out;
1028
+ }
1029
+ function tagEnd(html, start) {
1030
+ let quote = "";
1031
+ for (let i = start + 1;i < html.length; i++) {
1032
+ const char = html[i];
1033
+ if (quote) {
1034
+ if (char === quote)
1035
+ quote = "";
1036
+ } else if (char === '"' || char === "'")
1037
+ quote = char;
1038
+ else if (char === ">")
1039
+ return i + 1;
1040
+ }
1041
+ return html.length;
1042
+ }
1043
+ function tagIdentity(tag) {
1044
+ let i = 1;
1045
+ while (isHtmlSpace(tag[i]))
1046
+ i++;
1047
+ const closing = tag[i] === "/";
1048
+ if (closing) {
1049
+ i++;
1050
+ while (isHtmlSpace(tag[i]))
1051
+ i++;
1052
+ }
1053
+ const start = i;
1054
+ while (i < tag.length) {
1055
+ const char = tag[i].toLowerCase();
1056
+ if (char >= "a" && char <= "z" || char >= "0" && char <= "9" || char === "-")
1057
+ i++;
1058
+ else
1059
+ break;
1060
+ }
1061
+ return i > start ? { name: tag.slice(start, i).toLowerCase(), closing } : null;
1062
+ }
1063
+ function findProtectedClose(html, tag, from) {
1064
+ const needle = `</${tag}`;
1065
+ for (let at = html.indexOf(needle, from);at >= 0; at = html.indexOf(needle, at + needle.length)) {
1066
+ const boundary = html[at + needle.length];
1067
+ if (boundary === ">" || boundary === "/" || isHtmlSpace(boundary))
1068
+ return at;
1069
+ }
1070
+ return -1;
1071
+ }
1072
+ function stripReservedRawHtmlAttrs(html, initialProtectedTag = "") {
1073
+ const protectedTags = new Set([
1074
+ "script",
1075
+ "style",
1076
+ "textarea",
1077
+ "title",
1078
+ "xmp",
1079
+ "iframe",
1080
+ "noembed",
1081
+ "noframes",
1082
+ "noscript",
1083
+ "plaintext"
1084
+ ]);
1085
+ const lower = html.toLowerCase();
1086
+ let out = "";
1087
+ let i = 0;
1088
+ let protectedTag = initialProtectedTag;
1089
+ while (i < html.length) {
1090
+ if (protectedTag) {
1091
+ if (protectedTag === "plaintext")
1092
+ return { html: out + html.slice(i), protectedTag };
1093
+ const closeAt = findProtectedClose(lower, protectedTag, i);
1094
+ if (closeAt < 0)
1095
+ return { html: out + html.slice(i), protectedTag };
1096
+ out += html.slice(i, closeAt);
1097
+ i = closeAt;
1098
+ protectedTag = "";
1099
+ continue;
1100
+ }
1101
+ if (html.startsWith("<!--", i)) {
1102
+ const closeAt = html.indexOf("-->", i + 4);
1103
+ const end2 = closeAt < 0 ? html.length : closeAt + 3;
1104
+ out += html.slice(i, end2);
1105
+ i = end2;
1106
+ continue;
1107
+ }
1108
+ if (html[i] !== "<") {
1109
+ out += html[i++];
1110
+ continue;
1111
+ }
1112
+ const end = tagEnd(html, i);
1113
+ const tag = html.slice(i, end);
1114
+ const identity = tagIdentity(tag);
1115
+ if (!identity) {
1116
+ out += tag;
1117
+ } else if (!identity.closing && (identity.name === "img" || identity.name === "a")) {
1118
+ out += stripReservedAttrsFromTag(tag);
1119
+ } else {
1120
+ out += tag;
1121
+ if (!identity.closing && protectedTags.has(identity.name)) {
1122
+ protectedTag = identity.name;
1123
+ }
1124
+ }
1125
+ i = end;
1126
+ }
1127
+ return { html: out, protectedTag };
1128
+ }
1129
+ function occurrenceAt(source, token, target) {
1130
+ let occurrence = 0;
1131
+ let from = 0;
1132
+ while (from < target) {
1133
+ const found = source.indexOf(token, from);
1134
+ if (found < 0 || found >= target)
1135
+ break;
1136
+ occurrence++;
1137
+ from = found + token.length;
1138
+ }
1139
+ return occurrence;
1140
+ }
1141
+ function registerInlineSourceMetadata(md) {
1142
+ md.core.ruler.push("inline_source_raw_html", (state) => {
1143
+ let protectedTag = "";
1144
+ for (const block of state.tokens) {
1145
+ if (block.type === "html_block") {
1146
+ const sanitized = stripReservedRawHtmlAttrs(block.content, protectedTag);
1147
+ block.content = sanitized.html;
1148
+ protectedTag = sanitized.protectedTag;
1149
+ continue;
1150
+ }
1151
+ if (!block.children)
1152
+ continue;
1153
+ for (const token of block.children) {
1154
+ if (token.type !== "html_inline")
1155
+ continue;
1156
+ const sanitized = stripReservedRawHtmlAttrs(token.content, protectedTag);
1157
+ token.content = sanitized.html;
1158
+ protectedTag = sanitized.protectedTag;
1159
+ }
1160
+ }
1161
+ });
1162
+ const ruler = md.inline.ruler;
1163
+ for (const [ruleName, tokenType] of [["image", "image"], ["link", "link_open"]]) {
1164
+ const entry = ruler.__rules__.find((rule) => rule.name === ruleName);
1165
+ if (!entry)
1166
+ continue;
1167
+ const original = entry.fn;
1168
+ ruler.at(ruleName, (state, silent) => {
1169
+ const sourceStart = state.pos;
1170
+ const tokenStart = state.tokens.length;
1171
+ const matched = original(state, silent);
1172
+ if (!matched || silent)
1173
+ return matched;
1174
+ const token = state.tokens.slice(tokenStart).find((candidate) => candidate.type === tokenType);
1175
+ if (!token)
1176
+ return matched;
1177
+ const sourceToken = state.src.slice(sourceStart, state.pos);
1178
+ if (sourceToken.includes(`
1179
+ `) || sourceToken.includes("\r"))
1180
+ return matched;
1181
+ token.meta ??= {};
1182
+ token.meta.gpInlineSource = {
1183
+ token: sourceToken,
1184
+ occurrence: occurrenceAt(state.src, sourceToken, sourceStart)
1185
+ };
1186
+ return matched;
1187
+ });
1188
+ const originalRender = md.renderer.rules[tokenType];
1189
+ md.renderer.rules[tokenType] = (tokens, index, options, env, renderer) => {
1190
+ const token = tokens[index];
1191
+ if (token.attrs) {
1192
+ token.attrs = token.attrs.filter(([name]) => !RESERVED_ATTRS.has(name.toLowerCase()));
1193
+ }
1194
+ const source = token.meta?.gpInlineSource;
1195
+ if (source) {
1196
+ token.attrSet(SOURCE_TOKEN_ATTR, source.token);
1197
+ token.attrSet(SOURCE_OCCURRENCE_ATTR, String(source.occurrence));
1198
+ }
1199
+ return originalRender ? originalRender(tokens, index, options, env, renderer) : renderer.renderToken(tokens, index, options);
1200
+ };
1201
+ }
1202
+ }
1203
+
532
1204
  // src/lib/markdown/renderer.ts
533
1205
  function hasDefaultExport(plugin2) {
534
1206
  return !!plugin2 && typeof plugin2 === "object" && "default" in plugin2;
@@ -553,10 +1225,13 @@ function createMarkdownRenderer(customPlugins) {
553
1225
  md.use(unwrapPlugin(markdownItDeflist));
554
1226
  md.use(unwrapPlugin(markdownItSourceMap));
555
1227
  md.use(plugin);
1228
+ md.use(gpPinScope);
556
1229
  registerImageRule(md);
557
1230
  if (customPlugins && customPlugins.length > 0) {
558
1231
  applyPlugins(md, customPlugins);
559
1232
  }
1233
+ registerInlineSourceMetadata(md);
1234
+ md.core.ruler.push("source_range", sourceRangeRule);
560
1235
  return md;
561
1236
  }
562
1237
  function applyPlugins(md, plugins) {
@@ -580,16 +1255,6 @@ function collectPluginCss(plugins) {
580
1255
  `);
581
1256
  }
582
1257
 
583
- // src/lib/pagedjs-marker.ts
584
- var PAGEDJS_VERSION = "0.4.3";
585
- var PAGEDJS_POLYFILL_MARKER = "data-pagedjs-polyfill";
586
- function pagedjsPolyfillTag(version = PAGEDJS_VERSION) {
587
- return `<script ${PAGEDJS_POLYFILL_MARKER}="${version}"></script>`;
588
- }
589
- function pagedjsPolyfillTagRegex() {
590
- return /<script\b[^>]*(?:\bdata-pagedjs-polyfill\b|\bsrc=["'][^"']*paged\.polyfill[^"']*["'])[^>]*>\s*<\/script>/gi;
591
- }
592
-
593
1258
  // src/lib/markdown/assemble.ts
594
1259
  async function assembleBookHtml(opts) {
595
1260
  const title = opts.title ?? "Document";
@@ -612,6 +1277,8 @@ async function assembleBookHtml(opts) {
612
1277
  throw new Error(`Failed to read file ${file}: ${errorMsg}`);
613
1278
  }
614
1279
  const env = {};
1280
+ if (opts.annotateSourceChapters)
1281
+ env.sourceChapter = chapterId;
615
1282
  const rendered = md.render(content, env);
616
1283
  if (env.layoutWarnings && env.layoutWarnings.length > 0) {
617
1284
  opts.onChapterWarnings?.(chapterId, env.layoutWarnings);
@@ -635,8 +1302,10 @@ ${rendered}
635
1302
  opts.onImageRefs([...imageRefs]);
636
1303
  }
637
1304
  const inlineCss = [
638
- `/* markdown-it-paged */
639
- ${PAGED_CSS.trim()}`,
1305
+ `/* gutterpress markers */
1306
+ ${MARKER_CSS.trim()}`,
1307
+ `/* gutterpress */
1308
+ ${GUTTERPRESS_CSS.trim()}`,
640
1309
  pluginCss ? `/* user plugin css */
641
1310
  ${pluginCss.trim()}` : null,
642
1311
  projectCss ? `/* project css */
@@ -653,7 +1322,6 @@ ${projectCss.trim()}` : null
653
1322
  <style data-project-css>
654
1323
  ${inlineCss}
655
1324
  </style>
656
- ${pagedjsPolyfillTag()}
657
1325
  </head>
658
1326
  <body>
659
1327
  ${bodyContent}
@@ -661,13 +1329,9 @@ ${bodyContent}
661
1329
  </html>`;
662
1330
  }
663
1331
  export {
664
- pagedjsPolyfillTagRegex,
665
- pagedjsPolyfillTag,
666
1332
  createMarkdownRenderer,
667
1333
  collectPluginCss,
668
1334
  assembleBookHtml,
669
1335
  applyPlugins,
670
- PAGED_CSS,
671
- PAGEDJS_VERSION,
672
- PAGEDJS_POLYFILL_MARKER
1336
+ MARKER_CSS
673
1337
  };