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.
- 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-xxegv0t0.js} +10 -7
- package/dist/{build-san7fv2z.js → build-w0vb6mgq.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-46ycxe6r.js +18 -0
- package/dist/{cli-k4bd06sd.js → cli-5czby0dd.js} +4986 -5889
- package/dist/cli-c41yr7he.js +241 -0
- package/dist/{cli-e5zhb0xs.js → cli-f4rcbt2t.js} +17 -24
- package/dist/{cli-najycadg.js → cli-gk3wsdpb.js} +69 -253
- package/dist/{index-yzrh708h.js → cli-k1065rkg.js} +2 -5
- package/dist/{cli-506tg37g.js → cli-revgt4pr.js} +2 -1
- package/dist/{cli-wchtvxvw.js → cli-v5mp7a6q.js} +14 -1
- package/dist/cli-yp3p0sf2.js +2343 -0
- package/dist/cli.js +20 -18
- package/dist/{doctor-hrk0kxxz.js → doctor-ctgn1dnt.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-eng4xh2q.js +41 -0
- package/dist/engine-q5skhgms.js +40 -0
- package/dist/gutterpress-agent-1ctgfz92.js +576 -0
- package/dist/gutterpress-viewer-cem7dmr5.js +2349 -0
- package/dist/index-a4kr77td.js +1907 -0
- package/dist/{cli-yzrh708h.js → index-mdefp0y5.js} +1 -1
- package/dist/index-mvehxh0e.js +708 -0
- package/dist/{index-bynn850m.js → index-s416qpdv.js} +3049 -4015
- package/dist/{index-wchtvxvw.js → index-v5mp7a6q.js} +14 -1
- 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-gsh6qwrq.js} +10 -7
- package/dist/{manifest.schema-z61rzw44.json → manifest.schema-zxgxnbg7.json} +21 -0
- package/dist/{new-8p38wavc.js → new-nkhycqy4.js} +12 -8
- package/dist/{plugin-ees6nhkc.js → plugin-129wcs93.js} +10 -7
- package/dist/{preflight-1q6c2edh.js → preflight-41hcejg9.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-fnqb2y4v.js +1015 -0
- package/dist/{preview-y5a2zen1.js → preview-pftsvr9z.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-dnjvb4jg.js} +10 -7
- package/dist/render.d.ts +3 -4
- package/dist/render.js +728 -64
- package/dist/{repair-zgq7q2g6.js → repair-a3enjttn.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-ph1xffqy.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
package/dist/render.js
CHANGED
|
@@ -1,7 +1,49 @@
|
|
|
1
|
-
// src/lib/markdown/
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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 (!
|
|
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("
|
|
333
|
-
cls.push("
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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") || "
|
|
407
|
-
|
|
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") || "
|
|
416
|
-
|
|
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
|
|
422
|
-
|
|
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
|
-
.
|
|
426
|
-
.
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
.
|
|
433
|
-
|
|
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:
|
|
438
|
-
margin-left:
|
|
439
|
-
margin-right:
|
|
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
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
`/*
|
|
639
|
-
${
|
|
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
|
-
|
|
671
|
-
PAGEDJS_VERSION,
|
|
672
|
-
PAGEDJS_POLYFILL_MARKER
|
|
1336
|
+
MARKER_CSS
|
|
673
1337
|
};
|