gutterpress 0.11.21 → 0.11.22-alpha.1

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 (51) hide show
  1. package/README.md +11 -0
  2. package/dist/api/index.d.ts +5 -1
  3. package/dist/api/index.js +67 -46
  4. package/dist/{build-0neyk5h6.js → build-se85j9ec.js} +16 -10
  5. package/dist/cli-5553w972.js +75 -0
  6. package/dist/{cli-b57adna7.js → cli-8hccw62p.js} +9 -201
  7. package/dist/cli-9vy4b6ec.js +2089 -0
  8. package/dist/{cli-sqkccnm7.js → cli-d8xjns66.js} +1 -102
  9. package/dist/{cli-j0drzb6f.js → cli-fbwqv7p4.js} +720 -5428
  10. package/dist/cli-gc9av5hs.js +103 -0
  11. package/dist/cli-gmm3bdeq.js +196 -0
  12. package/dist/cli-h3phdstf.js +200 -0
  13. package/dist/{index-qf7y4d85.js → cli-habkc5ng.js} +12 -258
  14. package/dist/cli-pfrg1r9x.js +3073 -0
  15. package/dist/{cli-j79451n6.js → cli-yejbb9f8.js} +5 -76
  16. package/dist/cli.js +15 -13
  17. package/dist/{doctor-fptq8tvn.js → doctor-bv2t82zd.js} +7 -4
  18. package/dist/{engine-x07q927h.js → engine-h4ehmrvf.js} +4 -3
  19. package/dist/{engine-0b8bt05n.js → engine-qwkyhrej.js} +3 -2
  20. package/dist/{ext-gp624t5c.js → ext-a8ndp142.js} +21 -14
  21. package/dist/extension-restore-926mv2qk.js +16 -0
  22. package/dist/extension-restore-q1f7njec.js +20 -0
  23. package/dist/index-38e18a7j.js +3244 -0
  24. package/dist/{index-mr72vt8n.js → index-5h9a38jn.js} +1 -213
  25. package/dist/{cli-5qr3z0qa.js → index-93pqkvty.js} +63 -207
  26. package/dist/index-dhknsvfz.js +213 -0
  27. package/dist/index-dm7hx46j.js +2208 -0
  28. package/dist/{index-t7r8g75c.js → index-gmfh6n05.js} +4 -2
  29. package/dist/index-r7jcsk6h.js +200 -0
  30. package/dist/{index-mr7zs8mv.js → index-tkjvs1gf.js} +4774 -9354
  31. package/dist/index.js +70 -49
  32. package/dist/lib/extension-manifest.d.ts +22 -1
  33. package/dist/lib/markdown/plugins.d.ts +20 -0
  34. package/dist/lib/markdown/two-versions.d.ts +32 -0
  35. package/dist/lib/publish/plugin-provider.d.ts +16 -0
  36. package/dist/lib/publish/run-publish.d.ts +13 -5
  37. package/dist/lib/publish/types.d.ts +34 -2
  38. package/dist/lib/text-diff.d.ts +43 -0
  39. package/dist/lib/two-versions-markers.d.ts +26 -0
  40. package/dist/lib/version-changes.d.ts +87 -0
  41. package/dist/{new-dre6aw05.js → new-nadp2hvb.js} +23 -16
  42. package/dist/plugins-gy4q9vaa.js +26 -0
  43. package/dist/plugins-yeh4shd7.js +24 -0
  44. package/dist/{preflight-1k1zkg1p.js → preflight-c6q25xqa.js} +18 -11
  45. package/dist/{preview-dtrksvxf.js → preview-y8chv4cb.js} +19 -13
  46. package/dist/{publish-g1ytnrmr.js → publish-3qwvxsxa.js} +40 -23
  47. package/dist/render.js +120 -0
  48. package/dist/{source-provider-pzmz0br9.js → source-provider-2r74a7wp.js} +2 -1
  49. package/dist/{source-provider-qbthnmqw.js → source-provider-qbfknhyy.js} +2 -1
  50. package/dist/{validate-yrrq8639.js → validate-w108kk87.js} +16 -10
  51. package/package.json +1 -1
@@ -0,0 +1,3073 @@
1
+ import {
2
+ PACKAGE_VERSION,
3
+ debug
4
+ } from "./cli-5553w972.js";
5
+ import {
6
+ isExactNpmVersion,
7
+ isValidNpmPackageName,
8
+ packageResolutionTargets,
9
+ resolvePackageEntry,
10
+ resolveVendoredPluginInstallRoot,
11
+ safePackageTarget,
12
+ vendoredNpmPluginPackageDir
13
+ } from "./cli-h3phdstf.js";
14
+
15
+ // src/lib/markdown/plugins.ts
16
+ import { existsSync as existsSync2, readFileSync, statSync, unlinkSync } from "node:fs";
17
+ import { link, unlink } from "node:fs/promises";
18
+ import { isBuiltin } from "node:module";
19
+ import { resolve as resolve2, join, dirname, basename, extname, isAbsolute, relative, sep } from "node:path";
20
+ import { pathToFileURL } from "node:url";
21
+
22
+ // src/lib/style-declarations.ts
23
+ import { existsSync } from "node:fs";
24
+ import { resolve } from "node:path";
25
+ function resolveDeclaredStyles(rawStyles, baseDir, subject) {
26
+ if (!rawStyles || rawStyles.length === 0)
27
+ return;
28
+ return rawStyles.map((rel) => {
29
+ const abs = resolve(baseDir, rel);
30
+ if (!existsSync(abs)) {
31
+ throw new Error(`${subject} declares stylesheet "${rel}" but no file exists at ${abs}.`);
32
+ }
33
+ return abs;
34
+ });
35
+ }
36
+
37
+ // src/lib/extension-manifest.ts
38
+ import { readFile } from "node:fs/promises";
39
+ import path from "node:path";
40
+ import { satisfies, validRange } from "semver";
41
+ var EXTENSION_MANIFEST_FILENAME = "package.json";
42
+ var GUTTERPRESS_KEY = "gutterpress";
43
+ function object(value) {
44
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
45
+ }
46
+ function str(value) {
47
+ return typeof value === "string" ? value : undefined;
48
+ }
49
+ function authorName(value) {
50
+ return str(value) ?? str(object(value)?.name);
51
+ }
52
+ function metaFromPackageJson(raw) {
53
+ const pkg = object(raw);
54
+ if (!pkg)
55
+ return {};
56
+ const gp = object(pkg[GUTTERPRESS_KEY]) ?? {};
57
+ const keywords = Array.isArray(pkg.keywords) ? pkg.keywords.filter((k) => typeof k === "string") : undefined;
58
+ return {
59
+ ...str(pkg.name) ? { name: str(pkg.name) } : {},
60
+ ...authorName(pkg.author) ? { author: authorName(pkg.author) } : {},
61
+ ...str(pkg.description) ? { description: str(pkg.description) } : {},
62
+ ...keywords ? { keywords } : {},
63
+ ...str(pkg.main) ? { main: str(pkg.main) } : {},
64
+ ...str(object(pkg.engines)?.gutterpress) ? { gutterpressRange: str(object(pkg.engines)?.gutterpress) } : {},
65
+ ..."preview" in gp ? { preview: str(gp.preview) ?? null } : {},
66
+ ...Array.isArray(gp.styles) ? { styles: declaredList(gp.styles) } : {},
67
+ ...str(gp.tokensFile) ? { tokensFile: str(gp.tokensFile) } : {},
68
+ ...str(gp.markdown) ? { markdown: str(gp.markdown) } : {},
69
+ ...str(gp.components) ? { components: str(gp.components) } : {},
70
+ ...str(gp.snippets) ? { snippets: str(gp.snippets) } : {},
71
+ ...gp.publish === true ? { publish: true } : {},
72
+ ...gp.engineStyles !== undefined ? { engineStyles: gp.engineStyles } : {}
73
+ };
74
+ }
75
+ async function readExtensionMeta(dir) {
76
+ try {
77
+ return metaFromPackageJson(JSON.parse(await readFile(path.join(dir, EXTENSION_MANIFEST_FILENAME), "utf8")));
78
+ } catch {
79
+ return {};
80
+ }
81
+ }
82
+ function declaredList(value) {
83
+ return Array.isArray(value) ? value.filter((s) => typeof s === "string" && s.trim().length > 0) : [];
84
+ }
85
+ function extensionStyleList(meta) {
86
+ return declaredList(meta.styles);
87
+ }
88
+ function extensionEntry(meta) {
89
+ return meta.markdown?.trim() || (meta.publish ? undefined : meta.main?.trim()) || undefined;
90
+ }
91
+ function extensionCarries(meta) {
92
+ return {
93
+ markdown: !!extensionEntry(meta),
94
+ styles: extensionStyleList(meta).length > 0,
95
+ snippets: !!meta.snippets?.trim(),
96
+ components: !!meta.components?.trim(),
97
+ publish: meta.publish === true
98
+ };
99
+ }
100
+ function assertPublishOnly(meta, ref) {
101
+ if (!meta.publish)
102
+ return;
103
+ const carries = extensionCarries(meta);
104
+ const also = ["markdown", "styles", "components", "snippets"].filter((k) => carries[k]);
105
+ if (also.length > 0) {
106
+ throw new Error(`Publishing plugin "${ref}" also declares ${also.join(", ")} — a package is a ` + "publisher or a markdown plugin, not both.");
107
+ }
108
+ }
109
+ function pathEscapesFolder(rel) {
110
+ return path.isAbsolute(rel) || rel.split(/[\\/]/).includes("..");
111
+ }
112
+ function assertSupportsThisGutterpress(meta, ref) {
113
+ const range = meta.gutterpressRange;
114
+ if (range === undefined)
115
+ return;
116
+ if (!validRange(range)) {
117
+ throw new Error(`Extension "${ref}" has an invalid engines.gutterpress range "${range}".`);
118
+ }
119
+ if (!satisfies(PACKAGE_VERSION, range, { includePrerelease: true })) {
120
+ throw new Error(`Extension "${ref}" needs Gutterpress ${range} (its package.json engines.gutterpress); ` + `this is Gutterpress ${PACKAGE_VERSION}. Update Gutterpress, or pin an older version of the extension.`);
121
+ }
122
+ }
123
+ function assertExtensionContained(meta) {
124
+ if (meta.engineStyles !== undefined) {
125
+ throw new Error("The extension's metadata declares `engineStyles`, which was removed — " + "move its entries to the end of `styles`.");
126
+ }
127
+ const entry = extensionEntry(meta);
128
+ const declared = [
129
+ ...extensionStyleList(meta),
130
+ ...entry ? [entry] : [],
131
+ ...meta.publish && meta.main?.trim() ? [meta.main.trim()] : [],
132
+ ...meta.components ? [meta.components] : [],
133
+ ...meta.snippets ? [meta.snippets] : [],
134
+ ...meta.tokensFile ? [meta.tokensFile] : []
135
+ ];
136
+ for (const rel of declared) {
137
+ if (pathEscapesFolder(rel)) {
138
+ throw new Error(`Extension metadata declares "${rel}" outside its own folder; an extension must be self-contained.`);
139
+ }
140
+ }
141
+ }
142
+ function resolveExtension(dir, meta, subject) {
143
+ const entry = extensionEntry(meta);
144
+ const styles = resolveDeclaredStyles(extensionStyleList(meta), dir, subject);
145
+ const [markdown] = resolveDeclaredStyles(entry ? [entry] : undefined, dir, subject) ?? [];
146
+ const [components] = resolveDeclaredStyles(meta.components ? [meta.components] : undefined, dir, subject) ?? [];
147
+ const [snippets] = resolveDeclaredStyles(meta.snippets ? [meta.snippets] : undefined, dir, subject) ?? [];
148
+ return {
149
+ ...markdown ? { markdown } : {},
150
+ ...styles ? { styles } : {},
151
+ ...meta.tokensFile?.trim() ? { tokensFile: meta.tokensFile.trim() } : {},
152
+ ...components ? { components } : {},
153
+ ...snippets ? { snippets } : {}
154
+ };
155
+ }
156
+
157
+ // src/lib/markdown/renderer.ts
158
+ import MarkdownIt from "markdown-it";
159
+ import markdownItAttrs from "markdown-it-attrs";
160
+ import markdownItFootnote from "markdown-it-footnote";
161
+
162
+ // src/lib/markdown/markers.js
163
+ function unwrapAttrBraces(token) {
164
+ if (typeof token !== "string")
165
+ return token;
166
+ let t = token.trim();
167
+ if (t.startsWith("{"))
168
+ t = t.slice(1).trim();
169
+ if (t.endsWith("}"))
170
+ t = t.slice(0, -1).trim();
171
+ return t || token;
172
+ }
173
+ function isBareToken(token) {
174
+ return token && !token.includes("=") && !token.startsWith(".") && !token.startsWith("#");
175
+ }
176
+ var KNOWN_KINDS = [
177
+ "chapter",
178
+ "spread",
179
+ "page",
180
+ "section",
181
+ "continue",
182
+ "page-break",
183
+ "column-break",
184
+ "end-section"
185
+ ];
186
+ var BARE_TOKEN_RE = /^[A-Za-z0-9_][A-Za-z0-9_.:-]*$/;
187
+ function editDistance(a, b) {
188
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
189
+ for (let i = 1;i <= a.length; i++) {
190
+ const cur = [i];
191
+ for (let j = 1;j <= b.length; j++) {
192
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
193
+ }
194
+ prev = cur;
195
+ }
196
+ return prev[b.length];
197
+ }
198
+ function nearestKind(word) {
199
+ const w = word.toLowerCase();
200
+ if (w.length < 3)
201
+ return null;
202
+ for (const kind of KNOWN_KINDS) {
203
+ if (editDistance(w, kind) <= 1)
204
+ return kind;
205
+ }
206
+ return null;
207
+ }
208
+ function escapeHtml(s) {
209
+ return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
210
+ }
211
+ function escapeAttr(s) {
212
+ return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
213
+ }
214
+ function parseMarkerLine(line, declaredWords) {
215
+ const trimmed = line.trim();
216
+ if (!trimmed.startsWith("@"))
217
+ return null;
218
+ const tokens = [];
219
+ let buf = "";
220
+ let quote = null;
221
+ for (let i = 0;i < trimmed.length; i++) {
222
+ const ch = trimmed[i];
223
+ if (quote) {
224
+ if (ch === quote)
225
+ quote = null;
226
+ else
227
+ buf += ch;
228
+ continue;
229
+ }
230
+ if (ch === '"' || ch === "'") {
231
+ quote = ch;
232
+ continue;
233
+ }
234
+ if (/\s/.test(ch)) {
235
+ if (buf)
236
+ tokens.push(buf);
237
+ buf = "";
238
+ continue;
239
+ }
240
+ buf += ch;
241
+ }
242
+ if (buf)
243
+ tokens.push(buf);
244
+ for (let i = 1;i < tokens.length; i++)
245
+ tokens[i] = unwrapAttrBraces(tokens[i]);
246
+ const head = tokens[0];
247
+ const kind = head.slice(1);
248
+ if (!KNOWN_KINDS.includes(kind) && !(declaredWords && declaredWords.has(kind)))
249
+ return null;
250
+ if (kind === "continue" || kind.startsWith("end-")) {
251
+ return { kind, name: null, attrs: {} };
252
+ }
253
+ const isBreak = kind === "page-break" || kind === "column-break";
254
+ const body = isBreak ? tokens.slice(1).filter((token) => !isBareToken(token)) : tokens.slice(1);
255
+ const hasExplicitAttrsOrShorthand = body.some((token) => token.includes("=") || token.startsWith(".") || token.startsWith("#"));
256
+ const bareTokens = body.filter(isBareToken);
257
+ let name = null;
258
+ let nameIndex = -1;
259
+ const firstBareTokenIndex = body.findIndex((token) => isBareToken(token));
260
+ if (hasExplicitAttrsOrShorthand && bareTokens.length) {
261
+ name = bareTokens[0];
262
+ nameIndex = firstBareTokenIndex;
263
+ } else if (bareTokens.length === 1) {
264
+ name = bareTokens[0];
265
+ nameIndex = firstBareTokenIndex;
266
+ }
267
+ const attrs = {};
268
+ const classes = [];
269
+ let sawExplicitAttr = false;
270
+ let hasAmbiguousBareToken = false;
271
+ const unknownTokens = [];
272
+ for (const t of body) {
273
+ if (t.startsWith(".") || t.startsWith("#")) {
274
+ if (!BARE_TOKEN_RE.test(t.slice(1).trim()))
275
+ unknownTokens.push(t);
276
+ continue;
277
+ }
278
+ if (t.indexOf("=") > 0)
279
+ continue;
280
+ if (!BARE_TOKEN_RE.test(t))
281
+ unknownTokens.push(t);
282
+ }
283
+ for (let idx = 0;idx < body.length; idx++) {
284
+ if (idx === nameIndex) {
285
+ if (sawExplicitAttr && isBareToken(body[idx]))
286
+ hasAmbiguousBareToken = true;
287
+ continue;
288
+ }
289
+ const t = body[idx];
290
+ if (t.startsWith(".")) {
291
+ const c = t.slice(1).trim();
292
+ if (c)
293
+ classes.push(c);
294
+ continue;
295
+ }
296
+ if (t.startsWith("#")) {
297
+ const id = t.slice(1).trim();
298
+ if (id)
299
+ attrs.id = id;
300
+ continue;
301
+ }
302
+ const eq = t.indexOf("=");
303
+ if (eq > 0) {
304
+ sawExplicitAttr = true;
305
+ const key = t.slice(0, eq).trim();
306
+ const val = t.slice(eq + 1).trim();
307
+ if (!key)
308
+ continue;
309
+ if (key === "class") {
310
+ val.split(/[,\s]+/).filter(Boolean).forEach((c) => classes.push(c));
311
+ } else {
312
+ attrs[key] = val;
313
+ }
314
+ continue;
315
+ }
316
+ if (sawExplicitAttr && isBareToken(t))
317
+ hasAmbiguousBareToken = true;
318
+ classes.push(t);
319
+ }
320
+ if (classes.length)
321
+ attrs.class = classes.join(" ");
322
+ if (isBreak)
323
+ return { kind, name: null, attrs };
324
+ const marker = { kind, name, attrs };
325
+ if (hasAmbiguousBareToken)
326
+ marker.__ambiguousBareToken = true;
327
+ if (unknownTokens.length)
328
+ marker.__unknownTokens = unknownTokens;
329
+ if (bareTokens.length > 1 && !hasAmbiguousBareToken && !unknownTokens.length) {
330
+ marker.__extraBareTokens = { tokens: bareTokens, named: name !== null };
331
+ }
332
+ return marker;
333
+ }
334
+ function warn(env, line, type, message, marker) {
335
+ if (!env.layoutWarnings)
336
+ env.layoutWarnings = [];
337
+ env.layoutWarnings.push({ line, type, message, marker });
338
+ }
339
+ var COMPONENT_SEVERITIES = ["error", "warning", "info"];
340
+ function blockEnd(tokens, i) {
341
+ if (tokens[i].nesting !== 1)
342
+ return i;
343
+ let depth = 0;
344
+ for (let j = i;j < tokens.length; j++) {
345
+ depth += tokens[j].nesting;
346
+ if (depth === 0)
347
+ return j;
348
+ }
349
+ return tokens.length - 1;
350
+ }
351
+ function inlineText(tokens, from, to) {
352
+ const parts = [];
353
+ for (let k = from;k <= to; k++)
354
+ if (tokens[k].type === "inline")
355
+ parts.push(tokens[k].content);
356
+ return parts.join(`
357
+ `);
358
+ }
359
+ function soleImage(inline) {
360
+ const parts = (inline && inline.children || []).filter((c) => c.type !== "softbreak" && !(c.type === "text" && !c.content.trim()));
361
+ return parts.length === 1 && parts[0].type === "image" ? parts[0] : null;
362
+ }
363
+ function toComponentBlocks(tokens, fallbackLine) {
364
+ const blocks = [];
365
+ for (let i = 0;i < tokens.length; i++) {
366
+ const tok = tokens[i];
367
+ const end = blockEnd(tokens, i);
368
+ const line = tok.map ? tok.map[0] + 1 : tok.meta && tok.meta.line || fallbackLine;
369
+ switch (tok.type) {
370
+ case "heading_open":
371
+ blocks.push({ type: "heading", level: Number(tok.tag.slice(1)), text: inlineText(tokens, i, end), line });
372
+ break;
373
+ case "paragraph_open": {
374
+ const image = soleImage(tokens[i + 1]);
375
+ blocks.push(image ? { type: "image", alt: image.content, src: image.attrGet("src") || "", line } : { type: "paragraph", text: inlineText(tokens, i, end), line });
376
+ break;
377
+ }
378
+ case "bullet_list_open":
379
+ case "ordered_list_open": {
380
+ const items = [];
381
+ for (let k = i + 1;k < end; k++) {
382
+ if (tokens[k].type !== "list_item_open" || tokens[k].level !== tok.level + 1)
383
+ continue;
384
+ const itemEnd = blockEnd(tokens, k);
385
+ const first = tokens.slice(k + 1, itemEnd).find((t) => t.type === "inline");
386
+ items.push(first ? first.content : "");
387
+ k = itemEnd;
388
+ }
389
+ blocks.push({ type: "list", ordered: tok.type === "ordered_list_open", items, line });
390
+ break;
391
+ }
392
+ case "blockquote_open":
393
+ blocks.push({ type: "quote", text: inlineText(tokens, i, end), line });
394
+ break;
395
+ case "fence":
396
+ case "code_block":
397
+ blocks.push({ type: "code", lang: (tok.info || "").trim().split(/\s+/)[0] || "", text: tok.content, line });
398
+ break;
399
+ case "table_open":
400
+ blocks.push({ type: "table", line });
401
+ break;
402
+ case "hr":
403
+ blocks.push({ type: "rule", line });
404
+ break;
405
+ case "html_block":
406
+ blocks.push({ type: "html", text: tok.content, line });
407
+ break;
408
+ case "layout_component_open":
409
+ blocks.push({ type: "component", name: tok.meta.component, line });
410
+ break;
411
+ default:
412
+ blocks.push({ type: tok.type.replace(/_open$/, ""), line });
413
+ }
414
+ i = end;
415
+ }
416
+ return blocks;
417
+ }
418
+ function runComponentValidate(env, decl, meta, tokens, lines) {
419
+ const name = meta.component;
420
+ const line = meta.line || 0;
421
+ const report = (at, type, message, severity) => {
422
+ if (!env.layoutWarnings)
423
+ env.layoutWarnings = [];
424
+ env.layoutWarnings.push(severity ? { line: at, type, message, severity } : { line: at, type, message });
425
+ };
426
+ const failed = (why) => report(line, "component_validate_failed", `@${name}: plugin "${decl.validateOwner}"'s validate() ${why}`);
427
+ let lastLine = line;
428
+ for (const t of tokens)
429
+ if (t.map && t.map[1] > lastLine)
430
+ lastLine = t.map[1];
431
+ const component = {
432
+ name,
433
+ variant: meta.variant || null,
434
+ attrs: { ...meta.attrs },
435
+ line,
436
+ text: lines.slice(line, lastLine).join(`
437
+ `),
438
+ blocks: toComponentBlocks(tokens, line)
439
+ };
440
+ let result;
441
+ try {
442
+ result = decl.validate(component);
443
+ } catch (error) {
444
+ failed(`threw: ${error instanceof Error ? error.message : String(error)}`);
445
+ return;
446
+ }
447
+ if (result === undefined || result === null)
448
+ return;
449
+ if (typeof result === "string")
450
+ result = [result];
451
+ if (!Array.isArray(result)) {
452
+ failed("must return a message, an array of problems, or nothing (and must not be async).");
453
+ return;
454
+ }
455
+ for (const entry of result) {
456
+ const problem = typeof entry === "string" ? { message: entry } : entry;
457
+ if (!problem || typeof problem.message !== "string" || !problem.message.trim()) {
458
+ failed("returned a problem without a message.");
459
+ continue;
460
+ }
461
+ const at = Number.isInteger(problem.line) && problem.line > 0 ? problem.line : line;
462
+ const severity = COMPONENT_SEVERITIES.includes(problem.severity) ? problem.severity : undefined;
463
+ report(at, "component_invalid", `@${name}: ${problem.message}`, severity);
464
+ }
465
+ }
466
+ function proseEscapeHint(kind) {
467
+ return ` If this line is prose that happens to begin with "@${kind}" — a wrapped sentence, for` + ` instance — escape it as \\@${kind} or write it as \`@${kind}\` in backticks, and it stays text.`;
468
+ }
469
+ function addClasses(token, baseClass, extraClass) {
470
+ const cls = [];
471
+ if (baseClass)
472
+ cls.push(baseClass);
473
+ if (extraClass)
474
+ cls.push(extraClass);
475
+ const merged = cls.join(" ").trim();
476
+ if (merged)
477
+ token.attrSet("class", merged);
478
+ }
479
+ function attachDataAttrs(token, kind, name, attrs) {
480
+ if (name) {
481
+ if (kind === "chapter")
482
+ token.attrSet("data-chapter-label", name);
483
+ else if (kind === "spread")
484
+ token.attrSet("data-spread", name);
485
+ else if (kind === "page")
486
+ token.attrSet("data-page", name);
487
+ else if (kind === "section")
488
+ token.attrSet("data-section", name);
489
+ else
490
+ token.attrSet(`data-${kind}`, name);
491
+ }
492
+ if (attrs.template)
493
+ token.attrSet("data-template", attrs.template);
494
+ if (attrs.region)
495
+ token.attrSet("data-region", attrs.region);
496
+ if (attrs.id)
497
+ token.attrSet("id", attrs.id);
498
+ for (const [k, v] of Object.entries(attrs)) {
499
+ if (!v)
500
+ continue;
501
+ if (k === "class" || k === "id" || k === "template" || k === "region")
502
+ continue;
503
+ token.attrSet(k.startsWith("data-") ? k : `data-${k}`, v);
504
+ }
505
+ }
506
+ function attachBreak(token, baseClass, attrs = {}) {
507
+ addClasses(token, baseClass, attrs.class);
508
+ token.attrSet("aria-hidden", "true");
509
+ attachDataAttrs(token, "break", null, attrs);
510
+ }
511
+ var DECLARED_NAME_RE = /^[a-z][a-z0-9-]*$/;
512
+ function validateDeclaredMarkerName(name, pluginName) {
513
+ if (!DECLARED_NAME_RE.test(name)) {
514
+ throw new Error(`Plugin "${pluginName}" declares marker "@${name}" with an invalid name — marker names must be ` + `lower-case letters, digits and hyphens, starting with a letter (like core's own "page-break").`);
515
+ }
516
+ if (name.startsWith("end-")) {
517
+ throw new Error(`Plugin "${pluginName}" declares marker "@${name}", but names starting with "end-" are reserved ` + `for the auto-derived closer of another declared marker (a container named "${name.slice(4)}" ` + `would collide with it). Rename this marker.`);
518
+ }
519
+ }
520
+ var TAG_NAME_RE = /^[a-z][a-z0-9-]*$/;
521
+ function resolveContainerShape(name, pluginName, decl) {
522
+ if (decl.section !== undefined && typeof decl.section !== "boolean") {
523
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`section\` that is not a boolean.`);
524
+ }
525
+ if (decl.section) {
526
+ for (const field of ["tag", "label", "autoCloseAt", "selfClosing"]) {
527
+ if (decl[field] !== undefined) {
528
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" sets \`section: true\` and \`${field}\` — a section ` + `marker is a core @section, so \`${field}\` does not apply. Remove one of them.`);
529
+ }
530
+ }
531
+ }
532
+ const tag = decl.tag !== undefined ? decl.tag : "div";
533
+ if (typeof tag !== "string" || !TAG_NAME_RE.test(tag)) {
534
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has an invalid \`tag\` (${JSON.stringify(decl.tag)}) — ` + `must be a lower-case HTML tag name.`);
535
+ }
536
+ const classBase = decl.class !== undefined ? decl.class : "";
537
+ if (typeof classBase !== "string") {
538
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`class\` that is not a string.`);
539
+ }
540
+ let variants;
541
+ if (decl.variants !== undefined) {
542
+ if (typeof decl.variants !== "object" || decl.variants === null || Array.isArray(decl.variants)) {
543
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has \`variants\` that is not a plain object.`);
544
+ }
545
+ variants = {};
546
+ for (const [variantName, variantClass] of Object.entries(decl.variants)) {
547
+ if (typeof variantClass !== "string") {
548
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" variant "${variantName}" is not a string.`);
549
+ }
550
+ variants[variantName] = variantClass;
551
+ }
552
+ }
553
+ let label;
554
+ if (decl.label !== undefined) {
555
+ if (typeof decl.label !== "object" || decl.label === null || Array.isArray(decl.label)) {
556
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`label\` that is not an object.`);
557
+ }
558
+ if (typeof decl.label.class !== "string" || !decl.label.class) {
559
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`label.class\` that is not a non-empty string.`);
560
+ }
561
+ const match = typeof decl.label.from === "string" ? /^attr:([A-Za-z_][A-Za-z0-9_-]*)$/.exec(decl.label.from) : null;
562
+ if (!match) {
563
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`label.from\` (${JSON.stringify(decl.label.from)}) that is not "attr:<name>" — that is the only supported form today.`);
564
+ }
565
+ const labelTag = decl.label.tag !== undefined ? decl.label.tag : "div";
566
+ if (typeof labelTag !== "string" || !TAG_NAME_RE.test(labelTag)) {
567
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has an invalid \`label.tag\` (${JSON.stringify(decl.label.tag)}).`);
568
+ }
569
+ label = { class: decl.label.class, attr: match[1], tag: labelTag };
570
+ }
571
+ let autoCloseAtEof = false;
572
+ if (decl.autoCloseAt !== undefined) {
573
+ if (!Array.isArray(decl.autoCloseAt)) {
574
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has an \`autoCloseAt\` that is not an array.`);
575
+ }
576
+ for (const value of decl.autoCloseAt) {
577
+ if (value !== "eof") {
578
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" declares an unsupported \`autoCloseAt\` value ` + `${JSON.stringify(value)} — only "eof" is currently supported.`);
579
+ }
580
+ }
581
+ autoCloseAtEof = decl.autoCloseAt.includes("eof");
582
+ }
583
+ if (decl.selfClosing !== undefined && typeof decl.selfClosing !== "boolean") {
584
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`selfClosing\` that is not a boolean.`);
585
+ }
586
+ if (decl.selfClosing && decl.autoCloseAt !== undefined) {
587
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" sets \`selfClosing: true\` and \`autoCloseAt\` — a ` + `self-closing marker has no open container to auto-close. Remove one of them.`);
588
+ }
589
+ if (decl.validate !== undefined && typeof decl.validate !== "function") {
590
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`validate\` that is not a function.`);
591
+ }
592
+ const check = decl.validate ? { validate: decl.validate, validateOwner: pluginName } : {};
593
+ return {
594
+ tag,
595
+ classBase,
596
+ variants,
597
+ label,
598
+ autoCloseAtEof,
599
+ ...decl.section ? { section: true } : {},
600
+ ...decl.selfClosing ? { selfClosing: true } : {},
601
+ ...check
602
+ };
603
+ }
604
+ function presetVariantOf(name, pluginName, rawDecl) {
605
+ const preset = rawDecl.preset !== undefined ? rawDecl.preset : {};
606
+ if (typeof preset !== "object" || preset === null || Array.isArray(preset)) {
607
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`preset\` that is not a plain object.`);
608
+ }
609
+ if (preset.variant !== undefined && typeof preset.variant !== "string") {
610
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`preset.variant\` that is not a string.`);
611
+ }
612
+ return preset.variant;
613
+ }
614
+ function resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf) {
615
+ const pluginName = originOf.get(name);
616
+ if (typeof rawDecl !== "object" || rawDecl === null || Array.isArray(rawDecl)) {
617
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" is not a plain object.`);
618
+ }
619
+ if (rawDecl.snippet !== undefined && (typeof rawDecl.snippet !== "string" || !rawDecl.snippet.trim())) {
620
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`snippet\` that is not a non-empty path string.`);
621
+ }
622
+ if (rawDecl.deprecated !== undefined) {
623
+ if (typeof rawDecl.deprecated !== "string" || !rawDecl.deprecated) {
624
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has a \`deprecated\` value that is not a non-empty string.`);
625
+ }
626
+ return { name, deprecated: rawDecl.deprecated };
627
+ }
628
+ if (rawDecl.alias !== undefined) {
629
+ if (typeof rawDecl.alias !== "string" || !rawDecl.alias) {
630
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" has an \`alias\` value that is not a non-empty string.`);
631
+ }
632
+ const targetRaw = rawRegistry.get(rawDecl.alias);
633
+ if (!targetRaw) {
634
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" aliases unknown marker "@${rawDecl.alias}" — no ` + `loaded plugin declares it.`);
635
+ }
636
+ if (targetRaw.deprecated !== undefined) {
637
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" aliases "@${rawDecl.alias}", which is deprecated: ` + `${targetRaw.deprecated}`);
638
+ }
639
+ if (targetRaw.alias !== undefined) {
640
+ throw new Error(`Plugin "${pluginName}"'s marker "@${name}" aliases "@${rawDecl.alias}", which is itself an alias. ` + `Chained aliases are not supported — alias the base marker directly.`);
641
+ }
642
+ const container = resolveContainerShape(rawDecl.alias, originOf.get(rawDecl.alias), targetRaw);
643
+ return { name, baseKind: rawDecl.alias, presetVariant: presetVariantOf(name, pluginName, rawDecl), ...container };
644
+ }
645
+ const presetVariant = presetVariantOf(name, pluginName, rawDecl);
646
+ return {
647
+ name,
648
+ baseKind: name,
649
+ ...presetVariant !== undefined ? { presetVariant } : {},
650
+ ...resolveContainerShape(name, pluginName, rawDecl)
651
+ };
652
+ }
653
+ var CORE_MARKER_OWNER = "Gutterpress";
654
+ var CORE_DECLARED_MARKERS = {
655
+ columns: {
656
+ class: "gp-columns",
657
+ preset: { variant: "2" },
658
+ variants: { 1: "gp-columns-1", 2: "gp-columns-2", 3: "gp-columns-3", 4: "gp-columns-4", 5: "gp-columns-5" }
659
+ },
660
+ page: { variants: { blank: "gp-page-blank", "full-bleed": "gp-page-full-bleed" } }
661
+ };
662
+ var LOCATION_KINDS = ["page", "chapter", "section"];
663
+ var LOCATION_KEYS = ["variants", "label", "snippet"];
664
+ function isLocationDeclaration(name, rawDecl) {
665
+ return LOCATION_KINDS.includes(name) && rawDecl !== null && typeof rawDecl === "object" && !Array.isArray(rawDecl) && Object.hasOwn(rawDecl, "variants") && Object.keys(rawDecl).every((k) => LOCATION_KEYS.includes(k));
666
+ }
667
+ function addLocationVariants(kind, rawDecl, pluginName, into) {
668
+ const { variants, label, snippet } = rawDecl;
669
+ const where = `Plugin "${pluginName}" declares @${kind} variants`;
670
+ if (variants === null || typeof variants !== "object" || Array.isArray(variants)) {
671
+ throw new Error(`${where} that are not a plain object of { word: 'class' }.`);
672
+ }
673
+ if (label !== undefined && typeof label !== "string")
674
+ throw new Error(`${where} with a \`label\` that is not a string.`);
675
+ if (snippet !== undefined && typeof snippet !== "string")
676
+ throw new Error(`${where} with a \`snippet\` that is not a string.`);
677
+ for (const [word, cls] of Object.entries(variants)) {
678
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(word)) {
679
+ throw new Error(`${where} with an invalid word "${word}" — use letters, digits, hyphens and underscores.`);
680
+ }
681
+ if (typeof cls !== "string" || !cls.trim()) {
682
+ throw new Error(`${where}: variant "${word}" needs a non-empty class string.`);
683
+ }
684
+ const taken = into[kind].get(word);
685
+ if (taken) {
686
+ throw new Error(`@${kind} variant "${word}" is declared by both plugin "${taken.owner}" and plugin "${pluginName}". ` + `Variant words must be unique across every loaded plugin — rename one of them.`);
687
+ }
688
+ into[kind].set(word, { class: cls.trim(), owner: pluginName, ...label ? { label } : {}, ...snippet ? { snippet } : {} });
689
+ }
690
+ }
691
+ function buildDeclaredMarkerRegistry(sources) {
692
+ const rawRegistry = new Map;
693
+ const originOf = new Map;
694
+ const locationVariants = { page: new Map, chapter: new Map, section: new Map };
695
+ for (const { pluginName, markers } of [{ pluginName: CORE_MARKER_OWNER, markers: CORE_DECLARED_MARKERS }, ...sources]) {
696
+ if (!markers)
697
+ continue;
698
+ for (const [name, rawDecl] of Object.entries(markers)) {
699
+ validateDeclaredMarkerName(name, pluginName);
700
+ if (isLocationDeclaration(name, rawDecl)) {
701
+ addLocationVariants(name, rawDecl, pluginName, locationVariants);
702
+ continue;
703
+ }
704
+ if (KNOWN_KINDS.includes(name) || pluginName !== CORE_MARKER_OWNER && name in CORE_DECLARED_MARKERS) {
705
+ throw new Error(`Plugin "${pluginName}" declares marker "@${name}", which is a core Gutterpress marker name ` + `(${[...KNOWN_KINDS, ...Object.keys(CORE_DECLARED_MARKERS).filter((k) => !KNOWN_KINDS.includes(k))].map((k) => `@${k}`).join(", ")}). ` + `Core marker names cannot be shadowed — ` + `rename the plugin's marker.`);
706
+ }
707
+ if (rawRegistry.has(name)) {
708
+ throw new Error(`Marker "@${name}" is declared by both plugin "${originOf.get(name)}" and plugin "${pluginName}". ` + `Declared marker names must be unique across every loaded plugin — rename one of them.`);
709
+ }
710
+ rawRegistry.set(name, rawDecl);
711
+ originOf.set(name, pluginName);
712
+ }
713
+ }
714
+ const resolved = new Map;
715
+ for (const [name, rawDecl] of rawRegistry) {
716
+ resolved.set(name, resolveMarkerDeclaration(name, rawDecl, rawRegistry, originOf));
717
+ }
718
+ resolved.locationVariants = locationVariants;
719
+ return resolved;
720
+ }
721
+ function declaredKindWords(declaredMarkers) {
722
+ if (!declaredMarkers)
723
+ return null;
724
+ const words = new Set;
725
+ for (const name of declaredMarkers.keys()) {
726
+ words.add(name);
727
+ words.add(`end-${name}`);
728
+ }
729
+ return words;
730
+ }
731
+ function nearestDeclaredMarkerName(word, names) {
732
+ const w = word.toLowerCase();
733
+ const threshold = Math.max(2, Math.floor(w.length / 3));
734
+ let best = null;
735
+ let bestDistance = Infinity;
736
+ for (const known of names) {
737
+ const d = editDistance(w, known);
738
+ if (d < bestDistance) {
739
+ bestDistance = d;
740
+ best = known;
741
+ }
742
+ }
743
+ return best !== null && bestDistance > 0 && bestDistance <= threshold ? best : null;
744
+ }
745
+ function scanForUnknownDeclaredMarkers(state, declaredMarkers) {
746
+ if (!declaredMarkers)
747
+ return;
748
+ for (let tokenIndex = 0;tokenIndex < state.tokens.length; tokenIndex++) {
749
+ const tok = state.tokens[tokenIndex];
750
+ if (tok.type !== "inline" || !tok.map)
751
+ continue;
752
+ const opener = state.tokens[tokenIndex - 1];
753
+ if (!opener || opener.type !== "paragraph_open")
754
+ continue;
755
+ const lines = tok.content.split(`
756
+ `);
757
+ for (let i = 0;i < lines.length; i++) {
758
+ const m = /^@([A-Za-z][A-Za-z0-9-]*)(?=\s|$)/.exec(lines[i].trim());
759
+ if (!m)
760
+ continue;
761
+ const word = m[1];
762
+ if (KNOWN_KINDS.includes(word) || declaredMarkers.has(word))
763
+ continue;
764
+ const suggestion = nearestDeclaredMarkerName(word, declaredMarkers.keys());
765
+ if (!suggestion)
766
+ continue;
767
+ warn(state.env, tok.map[0] + i + 1, "unknown_marker", `Unknown marker "@${word}". Did you mean "@${suggestion}"?`, null);
768
+ }
769
+ }
770
+ }
771
+ function variantClassFor(decl, word) {
772
+ return word && decl.variants && Object.hasOwn(decl.variants, word) ? decl.variants[word] : "";
773
+ }
774
+ function nearVariant(word, variants) {
775
+ return variants.find((v) => v.toLowerCase() === word.toLowerCase()) || (word.length >= 3 ? nearestDeclaredMarkerName(word, variants) : null);
776
+ }
777
+ function warnUnknownVariant(env, meta, decl) {
778
+ const word = meta.name;
779
+ if (!word)
780
+ return;
781
+ const variants = Object.keys(decl.variants || {});
782
+ if (variants.includes(word))
783
+ return;
784
+ const near = nearVariant(word, variants);
785
+ warn(env, meta.__line, "unknown_variant", `"${word}" is not a variant of @${meta.kind}, so it adds no styling. ` + (variants.length ? `Variants: ${variants.join(", ")}.` : `@${meta.kind} has no variants.`) + (near ? ` Did you mean "${near}"?` : ""), meta);
786
+ }
787
+ function warnNearMissVariant(env, meta, kind, variants) {
788
+ const words = [...variants.keys()];
789
+ const near = nearVariant(meta.name, words);
790
+ if (!near)
791
+ return;
792
+ warn(env, meta.__line, "unknown_variant", `"${meta.name}" is not a variant of @${kind}, so it is only a ${kind} name and adds no styling. ` + `Variants: ${words.join(", ")}. Did you mean "${near}"?`, meta);
793
+ }
794
+ function plugin(md, pluginOptions = {}) {
795
+ const options = {
796
+ preferPagesInSpreads: false,
797
+ ...pluginOptions
798
+ };
799
+ const declaredMarkers = options.declaredMarkers instanceof Map && options.declaredMarkers.size > 0 ? options.declaredMarkers : buildDeclaredMarkerRegistry([]);
800
+ const declaredWords = declaredKindWords(declaredMarkers);
801
+ const locationVariants = declaredMarkers.locationVariants || buildDeclaredMarkerRegistry([]).locationVariants;
802
+ function markerBlock(state, startLine, endLine, silent) {
803
+ const pos = state.bMarks[startLine] + state.tShift[startLine];
804
+ const max = state.eMarks[startLine];
805
+ const line = state.src.slice(pos, max);
806
+ const parsed = parseMarkerLine(line, declaredWords);
807
+ if (!parsed)
808
+ return false;
809
+ if (silent)
810
+ return true;
811
+ state.env.__layoutMarkersUsed = true;
812
+ const token = state.push("layout_marker", "", 0);
813
+ token.meta = parsed;
814
+ token.meta.__line = startLine + 1;
815
+ if (parsed.__ambiguousBareToken) {
816
+ delete parsed.__ambiguousBareToken;
817
+ 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);
818
+ }
819
+ if (parsed.__unknownTokens) {
820
+ const unknown = parsed.__unknownTokens;
821
+ delete parsed.__unknownTokens;
822
+ for (const t of unknown) {
823
+ 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.` + proseEscapeHint(parsed.kind), parsed);
824
+ }
825
+ }
826
+ if (parsed.__extraBareTokens) {
827
+ const { tokens, named } = parsed.__extraBareTokens;
828
+ delete parsed.__extraBareTokens;
829
+ const list = tokens.map((t) => `"${t}"`).join(", ");
830
+ 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(" .")}.` + proseEscapeHint(parsed.kind), parsed);
831
+ }
832
+ state.line = startLine + 1;
833
+ return true;
834
+ }
835
+ md.block.ruler.before("paragraph", "layout_marker", markerBlock, {
836
+ alt: ["paragraph", "reference", "blockquote", "list"]
837
+ });
838
+ const SCOPE_CLOSE_ORDER = ["section", "page", "spread", "chapter"];
839
+ function scanForMistypedMarkers(state) {
840
+ for (let tokenIndex = 0;tokenIndex < state.tokens.length; tokenIndex++) {
841
+ const tok = state.tokens[tokenIndex];
842
+ if (tok.type !== "inline" || !tok.map)
843
+ continue;
844
+ const opener = state.tokens[tokenIndex - 1];
845
+ if (!opener || opener.type !== "paragraph_open")
846
+ continue;
847
+ const lines = tok.content.split(`
848
+ `);
849
+ for (let i = 0;i < lines.length; i++) {
850
+ const m = /^@([A-Za-z][A-Za-z0-9-]*)(?=\s|$)/.exec(lines[i].trim());
851
+ if (!m)
852
+ continue;
853
+ if (KNOWN_KINDS.includes(m[1]))
854
+ continue;
855
+ const suggestion = nearestKind(m[1]);
856
+ if (!suggestion)
857
+ continue;
858
+ 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);
859
+ }
860
+ }
861
+ }
862
+ md.core.ruler.after("block", "layout_transform", function(state) {
863
+ scanForUnknownDeclaredMarkers(state, declaredMarkers);
864
+ if (!state.env.__layoutMarkersUsed)
865
+ return;
866
+ scanForMistypedMarkers(state);
867
+ const out = [];
868
+ const declaredFrames = [];
869
+ function declaredFrameIndex(kind) {
870
+ for (let i = declaredFrames.length - 1;i >= 0; i--) {
871
+ if (declaredFrames[i].kind === kind)
872
+ return i;
873
+ }
874
+ return -1;
875
+ }
876
+ function closeDeclaredFrame(kind) {
877
+ const at = declaredFrameIndex(kind);
878
+ if (at === -1)
879
+ return false;
880
+ drainDeclaredFrames(at);
881
+ return true;
882
+ }
883
+ function sectionAbove(length) {
884
+ const section = stack.get("section");
885
+ return !!section && section.inColumns && declaredFrameIndex("columns") === length - 1;
886
+ }
887
+ function sectionHostDepth() {
888
+ const section = stack.get("section");
889
+ if (section && !section.inColumns)
890
+ return 0;
891
+ return declaredFrameIndex("columns") + 1;
892
+ }
893
+ function drainDeclaredFrames(keep = 0) {
894
+ while (declaredFrames.length > keep) {
895
+ if (sectionAbove(declaredFrames.length)) {
896
+ stack.close("section");
897
+ continue;
898
+ }
899
+ const frame = declaredFrames.pop();
900
+ out.push(new state.Token("layout_component_close", frame.tag, -1));
901
+ }
902
+ }
903
+ function drainDeclaredFramesAtEof() {
904
+ while (declaredFrames.length) {
905
+ if (sectionAbove(declaredFrames.length)) {
906
+ stack.close("section");
907
+ continue;
908
+ }
909
+ const frame = declaredFrames.pop();
910
+ if (!frame.decl.autoCloseAtEof) {
911
+ warn(state.env, frame.line || 0, "declared_marker_eof_close", `An open @${frame.kind} reached end-of-document; closing it automatically. Add ` + `@end-${frame.kind} to close it explicitly, or declare autoCloseAt: ["eof"] on this ` + `marker if running to end-of-document is expected.`, null);
912
+ }
913
+ out.push(new state.Token("layout_component_close", frame.tag, -1));
914
+ }
915
+ }
916
+ function openDeclaredMarker(meta, decl) {
917
+ const t = new state.Token("layout_component_open", decl.tag, 1);
918
+ const variant = meta.name || decl.presetVariant || null;
919
+ warnUnknownVariant(state.env, meta, decl);
920
+ t.meta = {
921
+ line: meta.__line,
922
+ component: meta.kind,
923
+ kind: decl.baseKind,
924
+ variant,
925
+ attrs: meta.attrs || {},
926
+ labelled: false
927
+ };
928
+ const variantClass = variantClassFor(decl, variant);
929
+ const baseClass = [decl.classBase, variantClass].filter(Boolean).join(" ");
930
+ addClasses(t, baseClass, meta.attrs && meta.attrs.class ? meta.attrs.class : "");
931
+ attachDataAttrs(t, decl.baseKind, variant, meta.attrs || {});
932
+ out.push(t);
933
+ if (!decl.selfClosing)
934
+ declaredFrames.push({ kind: decl.baseKind, tag: decl.tag, decl, line: meta.__line });
935
+ if (decl.label) {
936
+ const value = meta.attrs && meta.attrs[decl.label.attr];
937
+ if (value) {
938
+ const labelToken = new state.Token("html_block", "", 0);
939
+ labelToken.content = `<${decl.label.tag} class="${escapeAttr(decl.label.class)}">${escapeHtml(value)}</${decl.label.tag}>
940
+ `;
941
+ out.push(labelToken);
942
+ t.meta.labelled = true;
943
+ }
944
+ }
945
+ if (decl.selfClosing)
946
+ out.push(new state.Token("layout_component_close", decl.tag, -1));
947
+ }
948
+ const stack = {
949
+ frames: [],
950
+ get(kind) {
951
+ return this.frames.find((f) => f.kind === kind) || null;
952
+ },
953
+ has(kind) {
954
+ return this.frames.some((f) => f.kind === kind);
955
+ },
956
+ open(frame, openToken) {
957
+ out.push(openToken);
958
+ this.frames.push(frame);
959
+ },
960
+ _pop(kind) {
961
+ const at = this.frames.findIndex((f) => f.kind === kind);
962
+ if (at === -1)
963
+ return;
964
+ this.frames.splice(at, 1);
965
+ out.push(new state.Token(`layout_${kind}_close`, "div", -1));
966
+ },
967
+ close(kind) {
968
+ if (!this.has(kind))
969
+ return;
970
+ for (const inner of SCOPE_CLOSE_ORDER) {
971
+ if (inner === kind)
972
+ break;
973
+ this._pop(inner);
974
+ }
975
+ this._pop(kind);
976
+ },
977
+ closeAll() {
978
+ for (const kind of SCOPE_CLOSE_ORDER)
979
+ this._pop(kind);
980
+ }
981
+ };
982
+ function openChapter(meta) {
983
+ const t = new state.Token("layout_chapter_open", "div", 1);
984
+ t.meta = { line: meta.__line };
985
+ const variant = locationVariants.chapter.get(meta.name);
986
+ const label = variant ? "" : meta.name;
987
+ const classes = [variant?.class, meta.attrs?.class].filter(Boolean).join(" ");
988
+ addClasses(t, "chapter", classes);
989
+ attachDataAttrs(t, "chapter", label, meta.attrs || {});
990
+ const explicit = (classes.match(/(?:^|\s)(chapter-\d+)(?=\s|$)/) || [])[1] || "";
991
+ const fromAttr = meta.attrs?.ch ? `chapter-${meta.attrs.ch}` : "";
992
+ stack.open({
993
+ kind: "chapter",
994
+ label: label || "",
995
+ counterClass: explicit || fromAttr,
996
+ openerEmitted: false
997
+ }, t);
998
+ }
999
+ function openSpread(meta) {
1000
+ const t = new state.Token("layout_spread_open", "div", 1);
1001
+ t.meta = { line: meta.__line };
1002
+ addClasses(t, "spread", meta.attrs && meta.attrs.class ? meta.attrs.class : "");
1003
+ attachDataAttrs(t, "spread", meta.name, meta.attrs || {});
1004
+ stack.open({
1005
+ kind: "spread",
1006
+ classes: meta.attrs && meta.attrs.class || "",
1007
+ noPagesYet: true,
1008
+ sawAnyPage: false
1009
+ }, t);
1010
+ }
1011
+ function openPage(meta) {
1012
+ const chapter = stack.get("chapter");
1013
+ const t = new state.Token("layout_page_open", "div", 1);
1014
+ t.meta = { line: meta.__line };
1015
+ const explicit = meta.attrs && meta.attrs.class ? meta.attrs.class : "";
1016
+ const counterClass = chapter ? chapter.counterClass : "";
1017
+ const tokens = explicit ? explicit.split(/\s+/) : [];
1018
+ const withCounter = counterClass && !tokens.includes(counterClass) ? explicit ? `${explicit} ${counterClass}` : counterClass : explicit;
1019
+ const variant = locationVariants.page.get(meta.name);
1020
+ if (!variant && meta.name)
1021
+ warnNearMissVariant(state.env, meta, "page", locationVariants.page);
1022
+ const merged = [variant?.class, withCounter].filter(Boolean).join(" ");
1023
+ addClasses(t, "page", merged);
1024
+ attachDataAttrs(t, "page", meta.name, meta.attrs || {});
1025
+ const label = chapter ? chapter.label : "";
1026
+ if (label)
1027
+ t.attrSet("data-chapter-label", label);
1028
+ stack.open({ kind: "page", classes: explicit }, t);
1029
+ if (label && !chapter.openerEmitted) {
1030
+ const opener = new state.Token("html_block", "", 0);
1031
+ opener.content = `<div class="chapter-opener" data-chapter-label="${escapeAttr(label)}">${escapeHtml(label)}</div>
1032
+ `;
1033
+ out.push(opener);
1034
+ chapter.openerEmitted = true;
1035
+ }
1036
+ const spread = stack.get("spread");
1037
+ if (spread) {
1038
+ spread.sawAnyPage = true;
1039
+ spread.noPagesYet = false;
1040
+ } else if (options.preferPagesInSpreads) {
1041
+ warn(state.env, meta.__line || 0, "page_outside_spread", "@page used outside of a spread; allowed, but spreads are recommended for deliberate grouping.", meta);
1042
+ }
1043
+ }
1044
+ function sectionMetaFor(meta, decl) {
1045
+ const attrs = meta.attrs || {};
1046
+ const variant = meta.name || decl.presetVariant || null;
1047
+ warnUnknownVariant(state.env, meta, decl);
1048
+ const variantClass = variantClassFor(decl, variant);
1049
+ const cls = [decl.classBase, variantClass, attrs.class].filter(Boolean).join(" ");
1050
+ return {
1051
+ kind: "section",
1052
+ name: null,
1053
+ attrs: cls ? { ...attrs, class: cls } : attrs,
1054
+ __line: meta.__line,
1055
+ __component: { component: meta.kind, kind: decl.baseKind, variant, attrs }
1056
+ };
1057
+ }
1058
+ function openSection(meta, inColumns = false) {
1059
+ const t = new state.Token("layout_section_open", "div", 1);
1060
+ const component = meta.__component || null;
1061
+ const variant = component ? null : locationVariants.section.get(meta.name);
1062
+ addClasses(t, "section", [variant?.class, meta.attrs && meta.attrs.class].filter(Boolean).join(" "));
1063
+ if (component)
1064
+ attachDataAttrs(t, component.kind, component.variant, meta.attrs || {});
1065
+ else
1066
+ attachDataAttrs(t, "section", meta.name, meta.attrs || {});
1067
+ t.meta = {
1068
+ line: meta.__line,
1069
+ ...component || {},
1070
+ ...meta.__continued ? { continued: true } : {}
1071
+ };
1072
+ stack.open({
1073
+ kind: "section",
1074
+ classes: meta.attrs && meta.attrs.class || "",
1075
+ sawContent: false,
1076
+ ...inColumns ? { inColumns: true } : {},
1077
+ meta: { name: meta.name || null, attrs: { ...meta.attrs || {} }, component },
1078
+ openToken: t
1079
+ }, t);
1080
+ }
1081
+ function warnIfEmptyDecoratedSection(closingKind, closingLine) {
1082
+ const sec = stack.get("section");
1083
+ if (!sec || sec.sawContent)
1084
+ return;
1085
+ const attrs = sec.meta && sec.meta.attrs || {};
1086
+ const decorations = [];
1087
+ for (const c of (attrs.class || "").split(/\s+/).filter(Boolean))
1088
+ decorations.push(`.${c}`);
1089
+ for (const [k, v] of Object.entries(attrs)) {
1090
+ if (k === "class")
1091
+ continue;
1092
+ decorations.push(k === "id" ? `#${v}` : `${k}=${v}`);
1093
+ }
1094
+ if (!decorations.length)
1095
+ return;
1096
+ const openLine = sec.openToken && sec.openToken.meta && Number.isFinite(sec.openToken.meta.line) ? sec.openToken.meta.line : closingLine;
1097
+ warn(state.env, openLine, "empty_section", `This @section (${decorations.join(" ")}) was closed by the @${closingKind} on line ${closingLine} with no content between the two markers, so its styling applies to an empty element and nothing prints the layout it asked for. Delete one of the two markers, or move the content that belongs inside the section between them.`, null);
1098
+ }
1099
+ for (let i = 0;i < state.tokens.length; i++) {
1100
+ const tok = state.tokens[i];
1101
+ if (tok.type !== "layout_marker") {
1102
+ const openSectionFrame = stack.get("section");
1103
+ if (openSectionFrame)
1104
+ openSectionFrame.sawContent = true;
1105
+ out.push(tok);
1106
+ continue;
1107
+ }
1108
+ let meta = tok.meta || {};
1109
+ let kind = meta.kind;
1110
+ const line = meta.__line || 0;
1111
+ if (declaredMarkers) {
1112
+ const closing = kind.startsWith("end-");
1113
+ const sectionDecl = declaredMarkers.get(closing ? kind.slice(4) : kind);
1114
+ if (sectionDecl && sectionDecl.section) {
1115
+ if (closing) {
1116
+ kind = "end-section";
1117
+ } else {
1118
+ meta = sectionMetaFor(meta, sectionDecl);
1119
+ kind = "section";
1120
+ }
1121
+ }
1122
+ }
1123
+ if (kind === "chapter") {
1124
+ drainDeclaredFrames();
1125
+ stack.close("chapter");
1126
+ openChapter(meta);
1127
+ continue;
1128
+ }
1129
+ if (kind === "spread") {
1130
+ if (stack.has("spread")) {
1131
+ warn(state.env, line, "nested_spread", "@spread encountered while another spread is open; closing the previous spread automatically.", meta);
1132
+ }
1133
+ drainDeclaredFrames();
1134
+ stack.close("spread");
1135
+ openSpread(meta);
1136
+ continue;
1137
+ }
1138
+ if (kind === "page") {
1139
+ drainDeclaredFrames();
1140
+ stack.close("page");
1141
+ openPage(meta);
1142
+ continue;
1143
+ }
1144
+ if (kind === "section") {
1145
+ const keep = sectionHostDepth();
1146
+ drainDeclaredFrames(keep);
1147
+ warnIfEmptyDecoratedSection("section", line);
1148
+ stack.close("section");
1149
+ const spread = stack.get("spread");
1150
+ if (spread && spread.noPagesYet) {
1151
+ 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);
1152
+ }
1153
+ if (meta.name && !locationVariants.section.has(meta.name)) {
1154
+ warnNearMissVariant(state.env, meta, "section", locationVariants.section);
1155
+ }
1156
+ openSection(meta, keep > 0);
1157
+ continue;
1158
+ }
1159
+ if (kind === "continue") {
1160
+ const section = stack.get("section");
1161
+ if (!section || !section.meta) {
1162
+ warn(state.env, line, "continue_without_section", "@continue used without an open @section; ignoring marker.", meta);
1163
+ continue;
1164
+ }
1165
+ const keep = sectionHostDepth();
1166
+ drainDeclaredFrames(keep);
1167
+ const contMeta = {
1168
+ name: section.meta.name,
1169
+ attrs: { ...section.meta.attrs || {} },
1170
+ __continued: true
1171
+ };
1172
+ delete contMeta.attrs.id;
1173
+ if (section.meta.component)
1174
+ contMeta.__component = section.meta.component;
1175
+ contMeta.__line = line;
1176
+ const cls = (contMeta.attrs.class || "").split(/\s+/).filter(Boolean);
1177
+ if (!cls.includes("gp-continued"))
1178
+ cls.push("gp-continued");
1179
+ contMeta.attrs.class = cls.join(" ");
1180
+ stack.close("section");
1181
+ openSection(contMeta, keep > 0);
1182
+ continue;
1183
+ }
1184
+ if (kind === "page-break") {
1185
+ const t = new state.Token("layout_page_break", "div", 0);
1186
+ t.meta = { line };
1187
+ attachBreak(t, "gp-page-break", meta.attrs);
1188
+ out.push(t);
1189
+ continue;
1190
+ }
1191
+ if (kind === "column-break") {
1192
+ const t = new state.Token("layout_column_break", "div", 0);
1193
+ t.meta = { line };
1194
+ attachBreak(t, "gp-column-break", meta.attrs);
1195
+ out.push(t);
1196
+ continue;
1197
+ }
1198
+ if (kind === "end-section") {
1199
+ drainDeclaredFrames(sectionHostDepth());
1200
+ warnIfEmptyDecoratedSection("end-section", line);
1201
+ stack.close("section");
1202
+ continue;
1203
+ }
1204
+ if (declaredMarkers) {
1205
+ const openDecl = declaredMarkers.get(kind);
1206
+ if (openDecl) {
1207
+ if (openDecl.deprecated) {
1208
+ warn(state.env, line, "deprecated_marker", `@${kind} is deprecated: ${openDecl.deprecated}`, meta);
1209
+ continue;
1210
+ }
1211
+ closeDeclaredFrame(openDecl.baseKind);
1212
+ openDeclaredMarker(meta, openDecl);
1213
+ continue;
1214
+ }
1215
+ if (kind.startsWith("end-")) {
1216
+ const closeDecl = declaredMarkers.get(kind.slice(4));
1217
+ if (closeDecl) {
1218
+ if (closeDecl.deprecated) {
1219
+ warn(state.env, line, "deprecated_marker", `@${kind} is deprecated: ${closeDecl.deprecated}`, meta);
1220
+ continue;
1221
+ }
1222
+ if (!closeDeclaredFrame(closeDecl.baseKind)) {
1223
+ warn(state.env, line, "declared_marker_close_without_open", closeDecl.selfClosing ? `@${kind} is not a marker: @${closeDecl.baseKind} is self-closing and has nothing to close; ignoring marker.` : `@${kind} used without an open @${closeDecl.baseKind}; ignoring marker.`, meta);
1224
+ }
1225
+ continue;
1226
+ }
1227
+ }
1228
+ }
1229
+ }
1230
+ const eofSpread = stack.get("spread");
1231
+ if (eofSpread && !eofSpread.sawAnyPage) {
1232
+ warn(state.env, 0, "spread_eof_close", "An open @spread reached end-of-document; closing it automatically.", null);
1233
+ }
1234
+ drainDeclaredFramesAtEof();
1235
+ stack.closeAll();
1236
+ state.tokens = out;
1237
+ });
1238
+ if (declaredMarkers && [...declaredMarkers.values()].some((d) => d.validate)) {
1239
+ md.core.ruler.push("gp_component_validate", function(state) {
1240
+ let lines = null;
1241
+ const tokens = state.tokens;
1242
+ for (let i = 0;i < tokens.length; i++) {
1243
+ const tok = tokens[i];
1244
+ const isSection = tok.type === "layout_section_open" && tok.meta && tok.meta.component && !tok.meta.continued;
1245
+ if (tok.type !== "layout_component_open" && !isSection)
1246
+ continue;
1247
+ const decl = declaredMarkers.get(tok.meta.component);
1248
+ if (!decl || !decl.validate)
1249
+ continue;
1250
+ if (!lines)
1251
+ lines = state.src.split(`
1252
+ `);
1253
+ const from = i + (tok.meta.labelled ? 2 : 1);
1254
+ runComponentValidate(state.env, decl, tok.meta, tokens.slice(from, blockEnd(tokens, i)), lines);
1255
+ }
1256
+ });
1257
+ }
1258
+ const renderBreak = (tokens, idx, options2, env, self) => `<div${self.renderAttrs(tokens[idx])}></div>
1259
+ `;
1260
+ md.renderer.rules.layout_page_break = renderBreak;
1261
+ md.renderer.rules.layout_column_break = renderBreak;
1262
+ md.renderer.rules.layout_marker = () => "";
1263
+ }
1264
+ var NO_MARGIN_BOXES = [
1265
+ "top-left-corner",
1266
+ "top-left",
1267
+ "top-center",
1268
+ "top-right",
1269
+ "top-right-corner",
1270
+ "bottom-left-corner",
1271
+ "bottom-left",
1272
+ "bottom-center",
1273
+ "bottom-right",
1274
+ "bottom-right-corner",
1275
+ "left-top",
1276
+ "left-middle",
1277
+ "left-bottom",
1278
+ "right-top",
1279
+ "right-middle",
1280
+ "right-bottom"
1281
+ ].map((box) => ` @${box} { content: none; }`).join(`
1282
+ `);
1283
+ var MARKER_CSS = `
1284
+ /* The UA default of 8px body margin is a screen affordance with no meaning
1285
+ in paged media, and engines disagree about it: a polyfill that treats the
1286
+ page div as the page box drops it, native print keeps it. Left in place it
1287
+ insets EVERY page's content by 8px per side, and -- measured, 300dpi,
1288
+ 6x4in sheet -- it is what stops a full-width block from reaching the
1289
+ paper: it lands at 0.080..5.917in of a 6in sheet instead of
1290
+ 0.000..6.000in, because width:100% resolves against the BODY content box,
1291
+ not the page's. Zeroing it here (first in the cascade) makes the two
1292
+ agree. Authors who want a body margin still just declare one. */
1293
+ body { margin: 0; }
1294
+
1295
+ .gp-page-break { break-before: page; }
1296
+ .page { break-before: page; }
1297
+ .spread { break-before: page; }
1298
+ :where(.page, .spread) { position: relative; display: flow-root; box-sizing: border-box; min-height: calc(var(--gp-content-h, 1px) - 1px); }
1299
+ .gp-column-break { break-after: column; height: 0; font-size: 0; line-height: 0; visibility: hidden; }
1300
+
1301
+ :where(h1,h2,h3,h4,h5,h6) { break-after: avoid; }
1302
+ /* A lead-in paragraph ("**ROLL A DIE!**" above a roll table, "You will need:"
1303
+ above a list) stays with the table or list it introduces, the way a heading
1304
+ stays with what follows it -- otherwise the label can end a column or page
1305
+ with its table starting on the next (#370). Standard break-after; the
1306
+ :has() selector is Chromium's own. :where() keeps specificity at zero so an
1307
+ author rule such as p { break-after: auto } wins. Deliberately table/ul/ol only,
1308
+ also when a @section wraps it (the second rule): the lead-in then keeps with
1309
+ the section that opens with the table or list. */
1310
+ :where(p):has(+ :where(table, ul, ol)) { break-after: avoid; }
1311
+ :where(p):has(+ :where(.section) > :where(table, ul, ol):first-child) { break-after: avoid; }
1312
+ :where(img, svg, video) { max-width: 100%; }
1313
+ :where(p > img:only-child, figure > img) { width: fit-content; max-width: 100%; height: auto; vertical-align: bottom; }
1314
+ :where(p) > :where(img:not([class])) { max-height: calc(var(--gp-content-h) - 4px); object-fit: contain; }
1315
+ :where(figure) { break-inside: avoid; }
1316
+
1317
+ /* Core page variants — \`@page blank\` and \`@page full-bleed\` (CORE_DECLARED_MARKERS).
1318
+ Each is the \`.page\` the marker already emits plus one class; the class
1319
+ assigns the page to a NAMED page, and the named \`@page\` block below is
1320
+ plain CSS Paged Media. Authors never write these names.
1321
+
1322
+ SPEC GAP FILLED: none. Named pages (\`@page <name>\`, \`page:\`) and margin
1323
+ box \`content: none\` are standard and implemented in Chromium; this is
1324
+ author vocabulary that expands to them (permanent, like @columns), not a
1325
+ shim, so nothing here is deleted when Chrome catches up. Both pages are
1326
+ named \`gp-\`, which the shrink-to-fit audit (engine/compiler/build.ts)
1327
+ leaves out of its per-page limits, so they cannot loosen it for other pages.
1328
+
1329
+ The folio still COUNTS on these pages (only what is drawn is cleared), and
1330
+ a book that assigns \`page:\` to every \`.page\` in its own unlayered CSS
1331
+ overrides the assignment, as any unlayered rule overrides this layer. */
1332
+ @page gp-page-blank {
1333
+ ${NO_MARGIN_BOXES}
1334
+ }
1335
+ .page.gp-page-blank { page: gp-page-blank; }
1336
+
1337
+ /* full-bleed: zero page margin, so the content box IS the sheet. The margin
1338
+ boxes are cleared too — with zero margins they would land on the trim line. */
1339
+ @page gp-page-full-bleed {
1340
+ margin: 0;
1341
+ ${NO_MARGIN_BOXES}
1342
+ }
1343
+ .page.gp-page-full-bleed { page: gp-page-full-bleed; }
1344
+
1345
+ `;
1346
+
1347
+ // src/lib/markdown/gutterpress-css.ts
1348
+ var GUTTERPRESS_CSS = `
1349
+ /* gp-* author image/block vocabulary. One vocabulary, gp-* only — the
1350
+ pre-vocabulary utility names (.center/.float-left/.float-right/
1351
+ .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename
1352
+ the classes in their markdown (see the migration note). Source ORDER is
1353
+ the contract — see the doctrine comment above. */
1354
+
1355
+ /* flow positions */
1356
+ .gp-left {
1357
+ float: left;
1358
+ margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;
1359
+ max-width: 50%;
1360
+ }
1361
+ .gp-right {
1362
+ float: right;
1363
+ margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);
1364
+ max-width: 50%;
1365
+ }
1366
+ .gp-center {
1367
+ display: block;
1368
+ float: none;
1369
+ margin-left: auto;
1370
+ margin-right: auto;
1371
+ max-width: 100%;
1372
+ }
1373
+ .gp-full {
1374
+ display: block;
1375
+ float: none;
1376
+ width: 100%;
1377
+ max-width: 100%;
1378
+ }
1379
+ @page gp-full-bleed { margin-left: 0; margin-right: 0; }
1380
+ .gp-bleed {
1381
+ display: block;
1382
+ float: none;
1383
+ break-before: page;
1384
+ page: gp-full-bleed;
1385
+ max-width: none;
1386
+ width: 100%;
1387
+ margin-left: 0;
1388
+ margin-right: 0;
1389
+ }
1390
+
1391
+ /* sizes — AFTER the flow positions so max-width:100% lifts the floats' 50%
1392
+ cap at equal specificity */
1393
+ .gp-size-10 { width: 10%; max-width: 100%; }
1394
+ .gp-size-20 { width: 20%; max-width: 100%; }
1395
+ .gp-size-30 { width: 30%; max-width: 100%; }
1396
+ .gp-size-40 { width: 40%; max-width: 100%; }
1397
+ .gp-size-50 { width: 50%; max-width: 100%; }
1398
+ .gp-size-60 { width: 60%; max-width: 100%; }
1399
+ .gp-size-70 { width: 70%; max-width: 100%; }
1400
+ .gp-size-80 { width: 80%; max-width: 100%; }
1401
+ .gp-size-90 { width: 90%; max-width: 100%; }
1402
+
1403
+ /* float clearance presets — consumed by var(--gp-gap) in the float rules
1404
+ above and by .gp-shape's shape-margin below; --gp-gap itself is
1405
+ author-settable CSS */
1406
+ .gp-tight { --gp-gap: 0.5em; }
1407
+ .gp-loose { --gp-gap: 2em; }
1408
+
1409
+ /* column runs — plain CSS Multi-column, emitted by the core @columns marker
1410
+ (markers.js): @columns [1-5] ... @end-columns is the ONE way to lay
1411
+ content out in columns, and @column-break moves to the next column.
1412
+ The classes below are the CSS that marker emits, not author vocabulary.
1413
+
1414
+ Permanent vocabulary, not a shim: Chromium implements multicol natively
1415
+ and these rules are the standard properties verbatim, so there is no
1416
+ spec gap here to remove later. Deliberately minimal — column-fill is
1417
+ NOT set (the CSS initial balance applies).
1418
+ Themes style a run through two tokens instead of writing column rules:
1419
+ --gp-column-gap default 1.5em
1420
+ --gp-column-rule default none (any column-rule shorthand value)
1421
+ Both hooks sit at zero specificity (:where), so any author rule wins.
1422
+ .gp-columns is two columns on its own; a count class (bare @columns
1423
+ emits gp-columns-2) wins because it comes later at the same specificity. */
1424
+ :where(.gp-columns, .gp-columns-1, .gp-columns-2, .gp-columns-3, .gp-columns-4, .gp-columns-5) {
1425
+ column-gap: var(--gp-column-gap, 1.5em);
1426
+ column-rule: var(--gp-column-rule, none);
1427
+ }
1428
+ .gp-columns { columns: 2; }
1429
+ .gp-columns-1 { columns: 1; }
1430
+ .gp-columns-2 { columns: 2; }
1431
+ .gp-columns-3 { columns: 3; }
1432
+ .gp-columns-4 { columns: 4; }
1433
+ .gp-columns-5 { columns: 5; }
1434
+
1435
+ /* fragmentation controls — generic pagination utilities for any element,
1436
+ independent of the column vocabulary above (a book reached for
1437
+ these just as often outside a multicol run: keeping a card whole,
1438
+ forcing a section to start a fresh page). Standard properties only —
1439
+ this project is Chromium-only (CLAUDE.md), so there are no legacy
1440
+ page-break-* twins to also emit. One name each. */
1441
+ .gp-no-break { break-inside: avoid; }
1442
+ .gp-break-before { break-before: page; }
1443
+
1444
+ /* shape wrap — text follows the image's alpha silhouette instead of its
1445
+ rectangular box. shape-outside only applies to floats, so this is inert
1446
+ without .gp-left/.gp-right (and under .gp-pin, which un-floats). The
1447
+ shape URL cannot be written in CSS (url() contexts can't read attr()),
1448
+ so the image renderer rule (images.ts) mirrors the src into an inline
1449
+ --gp-shape:url(...) custom property whenever it sees this class --
1450
+ authors only ever type the class. threshold 0.2 ignores near-transparent
1451
+ anti-aliasing halos; shape-margin shares the float-gap vocabulary. */
1452
+ img.gp-shape {
1453
+ shape-outside: var(--gp-shape);
1454
+ shape-image-threshold: 0.2;
1455
+ shape-margin: var(--gp-gap, 1em);
1456
+ }
1457
+
1458
+ /* pin — within the nearest positioned ancestor (.page/.spread, rule above).
1459
+ The four insets are the pin box: 0 (the text block's edge) unless an
1460
+ engine set --gp-pin-<edge> on the page root for an edge a pin uses (the
1461
+ art-safe line). Non-auto insets and the explicit centers are
1462
+ load-bearing; see doctrine comment. */
1463
+ .gp-pin {
1464
+ position: absolute;
1465
+ top: var(--gp-pin-top, 0px);
1466
+ right: var(--gp-pin-right, 0px);
1467
+ bottom: var(--gp-pin-bottom, 0px);
1468
+ left: var(--gp-pin-left, 0px);
1469
+ align-self: center;
1470
+ justify-self: center;
1471
+ margin: 0;
1472
+ max-width: 100%;
1473
+ }
1474
+
1475
+ /* pin edge modifiers — AFTER .gp-pin to beat its center defaults;
1476
+ justify-self is inert on in-flow floats, so gp-left/gp-right safely do
1477
+ double duty as flow float + pin edge */
1478
+ .gp-top { align-self: start; }
1479
+ .gp-bottom { align-self: end; }
1480
+ .gp-left { justify-self: start; }
1481
+ .gp-right { justify-self: end; }
1482
+
1483
+ /* the escape hatch — the paper's edge, past the printer's safe area: the
1484
+ pin box loses its art-safe insets. Set on the pin itself, so it beats the
1485
+ values the page root hands down. */
1486
+ .gp-pin.gp-flush {
1487
+ --gp-pin-top: 0px;
1488
+ --gp-pin-right: 0px;
1489
+ --gp-pin-bottom: 0px;
1490
+ --gp-pin-left: 0px;
1491
+ }
1492
+
1493
+ /* wrapper-margin neutralization (same pattern and rationale as the
1494
+ .gp-bleed paragraph-margin note in the doctrine comment; for pin, the
1495
+ emptied paragraph would otherwise leave a phantom margin gap in flow) */
1496
+ :where(p:has(> img.gp-bleed:only-child)) { margin: 0; }
1497
+ :where(p:has(> img.gp-pin:only-child)) { margin: 0; }
1498
+
1499
+ /* depth — a named ladder for z-index, so books stop hand-tuning bare
1500
+ integers. A real book measured 21 z-index declarations using only four
1501
+ distinct values (-1, 0, 1, 2), each written literally at its use site.
1502
+ The custom properties are the author-settable surface (a book needing a
1503
+ deeper stack raises them once); the classes are the shorthand.
1504
+
1505
+ NOT named "layer": CSS Paged Media 3 §3.1 already defines "page layers"
1506
+ (page background, canvas, borders, contents, margin boxes) and those are
1507
+ parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a
1508
+ different concept would collide with the spec vocabulary this project
1509
+ tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the
1510
+ ladder avoids those words too.
1511
+
1512
+ .gp-behind is the one that earns its place: it puts a pinned image UNDER
1513
+ the page's text, which is otherwise impossible to express without a bare
1514
+ negative z-index. "Above" needs no class — an out-of-flow pin already
1515
+ paints above in-flow content.
1516
+
1517
+ Two things silently defeat .gp-behind, neither visible at the use site:
1518
+ - a stacking context on the .page/.spread ancestor (z-index, isolation,
1519
+ opacity, filter, transform on it traps the negative layer inside).
1520
+ Core keeps .page/.spread at 'position: relative; z-index: auto'
1521
+ precisely so they are not stacking contexts.
1522
+ - a clipping ancestor (overflow other than visible) — but only where
1523
+ the art actually overhangs that ancestor's clip box on a clipped
1524
+ axis: the overhang is cut off, the same mechanism that clips a
1525
+ .gp-bleed plate back to the wrapper's width. Clipping never reorders
1526
+ layers — within-bounds art under a clipping .page prints whole and
1527
+ still behind (measured; see the build audit's comment in
1528
+ engine/compiler/build.ts), and a static wrapper's overflow never
1529
+ binds an abspos .gp-pin at all.
1530
+ The build-time engine.layer.trapped audit reports both against the live
1531
+ ancestor chain. printsafe/page-containment is only an early source hint for
1532
+ declarations written directly on .page/.spread. */
1533
+ :root {
1534
+ --gp-z-behind: -1;
1535
+ --gp-z-base: 0;
1536
+ --gp-z-raised: 1;
1537
+ --gp-z-front: 2;
1538
+ }
1539
+ .gp-behind { z-index: var(--gp-z-behind); }
1540
+ .gp-base { z-index: var(--gp-z-base); }
1541
+ .gp-raised { z-index: var(--gp-z-raised); }
1542
+ .gp-front { z-index: var(--gp-z-front); }
1543
+
1544
+ /* GFM-style alert/callout boxes (#237) — the DOM gfm-alerts.ts emits from
1545
+ "> [!TYPE]" blockquote syntax when a project opts into the bundled
1546
+ gutterpress-gfm-alerts feature (BUILTIN_OPTIONAL_PLUGINS, renderer.ts).
1547
+ The plugin is optional and inert by default; these rules are equally
1548
+ inert wherever the classes never appear, exactly like every other
1549
+ utility above, so shipping them costs nothing to a book that has not
1550
+ turned the feature on.
1551
+
1552
+ This is authored-COMPONENT vocabulary, not the @marker structural family
1553
+ (markers.js/MARKER_CSS) — CLAUDE.md §6 splits the two core CSS blocks by
1554
+ ROLE, and an author-facing box an author invokes via inline syntax is the
1555
+ same role as every other gp-* utility here, not page/chapter/section DOM.
1556
+
1557
+ Deliberately minimal (CLAUDE.md "thin over capable" / the issue's own
1558
+ "a rule and a label"): a left border plus a bold label, :where()'d to
1559
+ zero specificity so a theme's own (unlayered) rule for any of these
1560
+ classes wins outright regardless of selector weight — see the
1561
+ cascade-layers note atop this file. Five semantic accent colors (the same
1562
+ NOTE/TIP/IMPORTANT/WARNING/CAUTION hues GitHub's own alerts use — a
1563
+ long-established, non-proprietary convention, not DC branding) are
1564
+ exposed as :root custom properties, the same author-settable-surface
1565
+ pattern as the --gp-z-* ladder above: a book retints every alert of one
1566
+ type by overriding a single property, no specificity fight required.
1567
+ --gp-alert-color cascades from the type modifier down to .gp-alert-title
1568
+ through ordinary CSS inheritance (the title is always a DOM descendant of
1569
+ the box), so it is set in exactly five places, not ten. */
1570
+ :root {
1571
+ --gp-alert-note-color: #0969da;
1572
+ --gp-alert-tip-color: #1a7f37;
1573
+ --gp-alert-important-color: #8250df;
1574
+ --gp-alert-warning-color: #9a6700;
1575
+ --gp-alert-caution-color: #cf222e;
1576
+ }
1577
+ :where(.gp-alert) {
1578
+ margin: 1em 0;
1579
+ padding: 0.5em 1em;
1580
+ border-left: 0.25em solid var(--gp-alert-color, currentColor);
1581
+ }
1582
+ :where(.gp-alert-title) {
1583
+ margin: 0 0 0.5em;
1584
+ font-weight: bold;
1585
+ color: var(--gp-alert-color, inherit);
1586
+ }
1587
+ :where(.gp-alert-note) { --gp-alert-color: var(--gp-alert-note-color); }
1588
+ :where(.gp-alert-tip) { --gp-alert-color: var(--gp-alert-tip-color); }
1589
+ :where(.gp-alert-important) { --gp-alert-color: var(--gp-alert-important-color); }
1590
+ :where(.gp-alert-warning) { --gp-alert-color: var(--gp-alert-warning-color); }
1591
+ :where(.gp-alert-caution) { --gp-alert-color: var(--gp-alert-caution-color); }
1592
+ `;
1593
+ var GP_CLASSES = new Set([
1594
+ "gp-left",
1595
+ "gp-right",
1596
+ "gp-center",
1597
+ "gp-full",
1598
+ "gp-bleed",
1599
+ "gp-size-10",
1600
+ "gp-size-20",
1601
+ "gp-size-30",
1602
+ "gp-size-40",
1603
+ "gp-size-50",
1604
+ "gp-size-60",
1605
+ "gp-size-70",
1606
+ "gp-size-80",
1607
+ "gp-size-90",
1608
+ "gp-tight",
1609
+ "gp-loose",
1610
+ "gp-columns",
1611
+ "gp-columns-1",
1612
+ "gp-columns-2",
1613
+ "gp-columns-3",
1614
+ "gp-columns-4",
1615
+ "gp-columns-5",
1616
+ "gp-page-blank",
1617
+ "gp-page-full-bleed",
1618
+ "gp-no-break",
1619
+ "gp-break-before",
1620
+ "gp-shape",
1621
+ "gp-pin",
1622
+ "gp-top",
1623
+ "gp-bottom",
1624
+ "gp-flush",
1625
+ "gp-behind",
1626
+ "gp-base",
1627
+ "gp-raised",
1628
+ "gp-front",
1629
+ "gp-alert",
1630
+ "gp-alert-title",
1631
+ "gp-alert-note",
1632
+ "gp-alert-tip",
1633
+ "gp-alert-important",
1634
+ "gp-alert-warning",
1635
+ "gp-alert-caution",
1636
+ "gp-page-break",
1637
+ "gp-column-break",
1638
+ "gp-continued"
1639
+ ]);
1640
+
1641
+ // src/lib/markdown/gp-pin-scope.js
1642
+ function warn2(env, line, type, message, severity) {
1643
+ if (!env.layoutWarnings)
1644
+ env.layoutWarnings = [];
1645
+ env.layoutWarnings.push(severity ? { line, type, message, severity } : { line, type, message });
1646
+ }
1647
+ function editDistance2(a, b) {
1648
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
1649
+ for (let i = 1;i <= a.length; i++) {
1650
+ const cur = [i];
1651
+ for (let j = 1;j <= b.length; j++) {
1652
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
1653
+ }
1654
+ prev = cur;
1655
+ }
1656
+ return prev[b.length];
1657
+ }
1658
+ function nearestGpClass(cls) {
1659
+ const threshold = Math.max(2, Math.floor(cls.length / 3));
1660
+ let best = null;
1661
+ let bestDistance = Infinity;
1662
+ for (const known of GP_CLASSES) {
1663
+ const d = editDistance2(cls, known);
1664
+ if (d < bestDistance) {
1665
+ bestDistance = d;
1666
+ best = known;
1667
+ }
1668
+ }
1669
+ return best !== null && bestDistance > 0 && bestDistance <= threshold ? best : null;
1670
+ }
1671
+ function hasPinClass(token) {
1672
+ const cls = token.attrGet && token.attrGet("class");
1673
+ return typeof cls === "string" && cls.split(/\s+/).includes("gp-pin");
1674
+ }
1675
+ var PIN_EDGES = ["gp-top", "gp-right", "gp-bottom", "gp-left"];
1676
+ var FLUSH_ART_MSG = "`.gp-flush` puts this image on the paper's edge, past the printer's safe area. Printers trim up to 0.125in there, so keep it for art that is meant to bleed off the page.";
1677
+ function checkFlushArt(token, env, line) {
1678
+ if (token.type !== "image")
1679
+ return;
1680
+ const cls = token.attrGet && token.attrGet("class");
1681
+ if (typeof cls !== "string")
1682
+ return;
1683
+ const classes = cls.split(/\s+/);
1684
+ if (!classes.includes("gp-pin") || !classes.includes("gp-flush"))
1685
+ return;
1686
+ if (!classes.some((c) => PIN_EDGES.includes(c)))
1687
+ return;
1688
+ warn2(env, line, "flush_art", FLUSH_ART_MSG, "info");
1689
+ }
1690
+ function elementNameFor(token) {
1691
+ switch (token.type) {
1692
+ case "layout_chapter_open":
1693
+ return "@chapter";
1694
+ case "layout_spread_open":
1695
+ return "@spread";
1696
+ case "layout_page_open":
1697
+ return "@page";
1698
+ case "layout_section_open":
1699
+ return "@section";
1700
+ case "image":
1701
+ return "an image";
1702
+ case "heading_open":
1703
+ return "a heading";
1704
+ case "paragraph_open":
1705
+ return "a paragraph";
1706
+ case "link_open":
1707
+ return "a link";
1708
+ default:
1709
+ return token.tag ? `a <${token.tag}>` : "an element";
1710
+ }
1711
+ }
1712
+ function checkUnknownGpClasses(token, env, line) {
1713
+ const cls = token.attrGet && token.attrGet("class");
1714
+ if (typeof cls !== "string" || !cls)
1715
+ return;
1716
+ for (const c of cls.split(/\s+/)) {
1717
+ if (!c.startsWith("gp-") || GP_CLASSES.has(c))
1718
+ continue;
1719
+ const suggestion = nearestGpClass(c);
1720
+ const base = `Unknown class "${c}" on ${elementNameFor(token)}.`;
1721
+ warn2(env, line, "unknown_gp_class", suggestion ? `${base} Did you mean "${suggestion}"?` : base);
1722
+ }
1723
+ }
1724
+ function gpPinScope(md) {
1725
+ 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.";
1726
+ md.core.ruler.push("gp_pin_scope_check", function(state) {
1727
+ let depth = 0;
1728
+ for (const token of state.tokens) {
1729
+ if (token.type === "layout_page_open" || token.type === "layout_spread_open") {
1730
+ depth += 1;
1731
+ } else if (token.type === "layout_page_close" || token.type === "layout_spread_close") {
1732
+ depth -= 1;
1733
+ } else if (depth === 0) {
1734
+ if (token.type === "inline" && token.children) {
1735
+ for (const child of token.children) {
1736
+ if (child.type === "image" && hasPinClass(child)) {
1737
+ warn2(state.env, (token.map?.[0] ?? 0) + 1, "pin_outside_page", PIN_OUTSIDE_PAGE_MSG);
1738
+ }
1739
+ }
1740
+ } else if (token.nesting === 1 && hasPinClass(token)) {
1741
+ warn2(state.env, (token.map?.[0] ?? 0) + 1, "pin_outside_page", PIN_OUTSIDE_PAGE_MSG);
1742
+ }
1743
+ }
1744
+ const line = (token.map?.[0] ?? 0) + 1;
1745
+ checkUnknownGpClasses(token, state.env, line);
1746
+ if (token.type === "inline" && token.children) {
1747
+ for (const child of token.children) {
1748
+ checkUnknownGpClasses(child, state.env, line);
1749
+ checkFlushArt(child, state.env, line);
1750
+ }
1751
+ }
1752
+ }
1753
+ });
1754
+ }
1755
+
1756
+ // src/lib/slug.ts
1757
+ function slugify(name, fallback = "") {
1758
+ return name.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || fallback;
1759
+ }
1760
+ function prettify(slug) {
1761
+ const spaced = slug.replace(/[-_]+/g, " ").trim();
1762
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
1763
+ }
1764
+
1765
+ // src/lib/markdown/block-names.ts
1766
+ var BLOCK_OPEN = /^layout_(chapter|spread|page|section|component)_open$/;
1767
+ var BLOCK_CLOSE = /^layout_(chapter|spread|page|section|component)_close$/;
1768
+ var FOLDED = {
1769
+ ß: "ss",
1770
+ æ: "ae",
1771
+ œ: "oe",
1772
+ ø: "o",
1773
+ đ: "d",
1774
+ ð: "d",
1775
+ þ: "th",
1776
+ ł: "l",
1777
+ ı: "i"
1778
+ };
1779
+ function nameSlug(text) {
1780
+ return slugify(text.replace(/['’]/g, "").replace(/[ßæœøđðþłı]/gi, (c) => FOLDED[c.toLowerCase()] ?? c));
1781
+ }
1782
+ function nameFromSrc(src) {
1783
+ if (/^data:/i.test(src))
1784
+ return "";
1785
+ const path2 = src.split(/[?#]/, 1)[0] ?? "";
1786
+ const base = path2.split(/[\\/]/).pop() ?? "";
1787
+ let decoded = base;
1788
+ try {
1789
+ decoded = decodeURIComponent(base);
1790
+ } catch {}
1791
+ return nameSlug(decoded.replace(/\.[^.]*$/, ""));
1792
+ }
1793
+ function takeExplicitName(token) {
1794
+ const data = token.attrGet("data-name");
1795
+ const plain = token.attrGet("name");
1796
+ if (plain !== null && token.attrs)
1797
+ token.attrs = token.attrs.filter(([k]) => k !== "name");
1798
+ if (data)
1799
+ return data;
1800
+ if (plain) {
1801
+ token.attrSet("data-name", plain);
1802
+ return plain;
1803
+ }
1804
+ return "";
1805
+ }
1806
+ function inlineText2(inline) {
1807
+ if (!inline || inline.type !== "inline")
1808
+ return "";
1809
+ const parts = [];
1810
+ for (const child of inline.children ?? []) {
1811
+ if (child.type === "text" || child.type === "text_special" || child.type === "code_inline" || child.type === "image") {
1812
+ parts.push(child.content);
1813
+ } else if (child.type === "softbreak" || child.type === "hardbreak") {
1814
+ parts.push(" ");
1815
+ }
1816
+ }
1817
+ return parts.join("");
1818
+ }
1819
+ function claim(used, slug) {
1820
+ let name = slug;
1821
+ for (let n = 2;used.has(name); n++)
1822
+ name = `${slug}-${n}`;
1823
+ used.add(name);
1824
+ return name;
1825
+ }
1826
+ function nameBlocksAndHeadings(tokens) {
1827
+ const blocks = [];
1828
+ const open = [];
1829
+ const headings = [];
1830
+ const headingSlugs = new Map;
1831
+ for (let i = 0;i < tokens.length; i++) {
1832
+ const tok = tokens[i];
1833
+ const opened = BLOCK_OPEN.exec(tok.type);
1834
+ if (opened) {
1835
+ const block = { token: tok, type: opened[1], heading: "" };
1836
+ blocks.push(block);
1837
+ open.push(block);
1838
+ continue;
1839
+ }
1840
+ const closed = BLOCK_CLOSE.exec(tok.type);
1841
+ if (closed) {
1842
+ for (let j = open.length - 1;j >= 0; j--) {
1843
+ if (open[j].type === closed[1]) {
1844
+ open.splice(j, 1);
1845
+ break;
1846
+ }
1847
+ }
1848
+ continue;
1849
+ }
1850
+ if (tok.type === "heading_open") {
1851
+ const slug = nameSlug(inlineText2(tokens[i + 1]));
1852
+ headings.push(tok);
1853
+ headingSlugs.set(tok, slug);
1854
+ const owner = open[open.length - 1];
1855
+ if (owner && !owner.heading)
1856
+ owner.heading = slug;
1857
+ }
1858
+ }
1859
+ const usedBlocks = new Set;
1860
+ const usedHeadings = new Set;
1861
+ const unnamedBlocks = new Set;
1862
+ for (const block of blocks) {
1863
+ const explicit = block.token.attrGet("data-name");
1864
+ if (explicit)
1865
+ usedBlocks.add(explicit);
1866
+ else
1867
+ unnamedBlocks.add(block);
1868
+ }
1869
+ const unnamedHeadings = [];
1870
+ for (const heading of headings) {
1871
+ const explicit = takeExplicitName(heading);
1872
+ if (explicit)
1873
+ usedHeadings.add(explicit);
1874
+ else
1875
+ unnamedHeadings.push(heading);
1876
+ }
1877
+ let previousSection = null;
1878
+ for (const block of blocks) {
1879
+ const { token } = block;
1880
+ if (block.type === "section") {
1881
+ if (token.meta?.continued && previousSection) {
1882
+ const inherited = previousSection.token.attrGet("data-name");
1883
+ if (inherited)
1884
+ token.attrSet("data-name", inherited);
1885
+ previousSection = block;
1886
+ continue;
1887
+ }
1888
+ previousSection = block;
1889
+ }
1890
+ if (!unnamedBlocks.has(block))
1891
+ continue;
1892
+ const slug = nameSlug(token.attrGet("data-label") ?? "") || block.heading;
1893
+ if (slug)
1894
+ token.attrSet("data-name", claim(usedBlocks, slug));
1895
+ }
1896
+ for (const heading of unnamedHeadings) {
1897
+ const slug = headingSlugs.get(heading);
1898
+ if (slug)
1899
+ heading.attrSet("data-name", claim(usedHeadings, slug));
1900
+ }
1901
+ }
1902
+ function gpBlockNames(md) {
1903
+ md.core.ruler.push("gp_block_names", (state) => nameBlocksAndHeadings(state.tokens));
1904
+ }
1905
+
1906
+ // src/lib/markdown/table-align.ts
1907
+ var ALIGN = /^text-align:(left|center|right)$/;
1908
+ function gpTableAlign(md) {
1909
+ md.core.ruler.after("block", "gp_table_align", (state) => {
1910
+ for (const token of state.tokens) {
1911
+ if (token.type !== "th_open" && token.type !== "td_open")
1912
+ continue;
1913
+ const match = ALIGN.exec(token.attrGet("style") ?? "");
1914
+ if (!match)
1915
+ continue;
1916
+ token.attrs = (token.attrs ?? []).filter(([key]) => key !== "style");
1917
+ token.attrSet("data-align", match[1]);
1918
+ }
1919
+ });
1920
+ }
1921
+ var TABLE_ALIGN_CSS = `
1922
+ th[data-align="left"], td[data-align="left"] { text-align: left; }
1923
+ th[data-align="center"], td[data-align="center"] { text-align: center; }
1924
+ th[data-align="right"], td[data-align="right"] { text-align: right; }
1925
+ `;
1926
+
1927
+ // src/lib/two-versions-markers.ts
1928
+ var YOUR_VERSION_LABEL = "your version";
1929
+ var ONLINE_VERSION_LABEL = "online version";
1930
+ var TWO_VERSIONS_OPEN = `<<<<<<< ${YOUR_VERSION_LABEL}`;
1931
+ var TWO_VERSIONS_SEPARATOR = "=======";
1932
+ var TWO_VERSIONS_CLOSE = `>>>>>>> ${ONLINE_VERSION_LABEL}`;
1933
+
1934
+ // src/lib/markdown/two-versions.ts
1935
+ var TWO_VERSIONS_CSS = `
1936
+ .gp-two-versions {
1937
+ margin: 1em 0;
1938
+ border: 2px dashed #b7791f;
1939
+ border-radius: 6px;
1940
+ break-inside: avoid;
1941
+ }
1942
+ .gp-two-versions > .gp-version {
1943
+ padding: 0.5em 0.9em;
1944
+ }
1945
+ .gp-two-versions > .gp-version + .gp-version {
1946
+ border-top: 2px dashed #b7791f;
1947
+ }
1948
+ .gp-two-versions > .gp-version > .gp-version-text {
1949
+ margin: 0;
1950
+ white-space: pre-wrap;
1951
+ overflow-wrap: anywhere;
1952
+ font-size: 0.85em;
1953
+ }
1954
+ .gp-two-versions > .gp-version::before {
1955
+ content: attr(data-label);
1956
+ display: block;
1957
+ margin-bottom: 0.35em;
1958
+ font-family: system-ui, sans-serif;
1959
+ font-size: 0.7em;
1960
+ font-weight: 700;
1961
+ letter-spacing: 0.06em;
1962
+ text-transform: uppercase;
1963
+ color: #b7791f;
1964
+ }
1965
+ `;
1966
+ function lineText(state, line) {
1967
+ return state.src.slice(state.bMarks[line] + state.tShift[line], state.eMarks[line]);
1968
+ }
1969
+ var MARKER_LINE = /^@[a-z]/;
1970
+ function hasMarkerLine(state, from, to) {
1971
+ for (let line = from;line < to; line++) {
1972
+ if (MARKER_LINE.test(lineText(state, line)))
1973
+ return true;
1974
+ }
1975
+ return false;
1976
+ }
1977
+ function pushVerbatim(state, from, to) {
1978
+ const raw = state.getLines(from, to, 0, false);
1979
+ const token = state.push("html_block", "", 0);
1980
+ token.map = [from, to];
1981
+ token.block = true;
1982
+ token.content = `<pre class="gp-version-text">${state.md.utils.escapeHtml(raw)}</pre>
1983
+ `;
1984
+ }
1985
+ function pushVersion(state, which, label, from, to) {
1986
+ const open = state.push("two_version_open", "div", 1);
1987
+ open.attrSet("class", `gp-version gp-version-${which}`);
1988
+ open.attrSet("data-label", label);
1989
+ open.map = [from, to];
1990
+ open.block = true;
1991
+ if (hasMarkerLine(state, from, to)) {
1992
+ pushVerbatim(state, from, to);
1993
+ } else {
1994
+ state.parentType = "container";
1995
+ state.lineMax = to;
1996
+ state.md.block.tokenize(state, from, to);
1997
+ }
1998
+ const close = state.push("two_version_close", "div", -1);
1999
+ close.block = true;
2000
+ }
2001
+ function twoVersions(state, startLine, endLine, silent) {
2002
+ if (state.sCount[startLine] - state.blkIndent >= 4)
2003
+ return false;
2004
+ if (state.src.charCodeAt(state.bMarks[startLine] + state.tShift[startLine]) !== 60)
2005
+ return false;
2006
+ if (lineText(state, startLine) !== TWO_VERSIONS_OPEN)
2007
+ return false;
2008
+ let separator = -1;
2009
+ let closer = -1;
2010
+ for (let line = startLine + 1;line < endLine; line++) {
2011
+ const text = lineText(state, line);
2012
+ if (separator < 0 && text === TWO_VERSIONS_SEPARATOR)
2013
+ separator = line;
2014
+ else if (text === TWO_VERSIONS_CLOSE) {
2015
+ closer = line;
2016
+ break;
2017
+ } else if (text === TWO_VERSIONS_OPEN)
2018
+ return false;
2019
+ }
2020
+ if (separator < 0 || closer < 0)
2021
+ return false;
2022
+ if (silent)
2023
+ return true;
2024
+ const oldParent = state.parentType;
2025
+ const oldLineMax = state.lineMax;
2026
+ const open = state.push("two_versions_open", "div", 1);
2027
+ open.attrSet("class", "gp-two-versions");
2028
+ open.map = [startLine, closer + 1];
2029
+ open.block = true;
2030
+ pushVersion(state, "yours", "Your version", startLine + 1, separator);
2031
+ pushVersion(state, "online", "Online version", separator + 1, closer);
2032
+ const close = state.push("two_versions_close", "div", -1);
2033
+ close.block = true;
2034
+ state.parentType = oldParent;
2035
+ state.lineMax = oldLineMax;
2036
+ state.line = closer + 1;
2037
+ return true;
2038
+ }
2039
+ function gpTwoVersions(md) {
2040
+ md.block.ruler.before("html_block", "two_versions", twoVersions, {
2041
+ alt: ["paragraph", "reference", "blockquote", "list"]
2042
+ });
2043
+ }
2044
+
2045
+ // src/lib/markdown/renderer.ts
2046
+ import markdownItSourceMap from "markdown-it-source-map";
2047
+ import markdownItDeflist from "markdown-it-deflist";
2048
+ import markdownItMark from "markdown-it-mark";
2049
+ import markdownItSub from "markdown-it-sub";
2050
+ import markdownItSup from "markdown-it-sup";
2051
+ import markdownItAbbr from "markdown-it-abbr";
2052
+
2053
+ // src/lib/markdown/gfm-alerts.ts
2054
+ var ALERT_LABELS = {
2055
+ note: "Note",
2056
+ tip: "Tip",
2057
+ important: "Important",
2058
+ warning: "Warning",
2059
+ caution: "Caution"
2060
+ };
2061
+ var ALERT_MARKER = /^\[!(note|tip|important|warning|caution)\]\s*$/i;
2062
+ function isFiniteMap(map) {
2063
+ return Array.isArray(map) && map.length === 2 && Number.isFinite(map[0]) && Number.isFinite(map[1]);
2064
+ }
2065
+ function blockToken(state, type, tag, nesting) {
2066
+ const token = new state.Token(type, tag, nesting);
2067
+ token.block = true;
2068
+ return token;
2069
+ }
2070
+ var gfmAlertsRule = (state) => {
2071
+ const tokens = state.tokens;
2072
+ const newTokens = [];
2073
+ for (let i = 0;i < tokens.length; i++) {
2074
+ const tok = tokens[i];
2075
+ if (tok.type !== "blockquote_open") {
2076
+ newTokens.push(tok);
2077
+ continue;
2078
+ }
2079
+ let depth = 0;
2080
+ let closeIdx = -1;
2081
+ for (let j = i;j < tokens.length; j++) {
2082
+ const jt = tokens[j];
2083
+ if (jt.type === "blockquote_open")
2084
+ depth++;
2085
+ else if (jt.type === "blockquote_close") {
2086
+ depth--;
2087
+ if (depth === 0) {
2088
+ closeIdx = j;
2089
+ break;
2090
+ }
2091
+ }
2092
+ }
2093
+ const markerParaOpen = tokens[i + 1];
2094
+ const markerInline = tokens[i + 2];
2095
+ const markerParaClose = tokens[i + 3];
2096
+ const hasMarkerParagraph = closeIdx !== -1 && markerParaOpen?.type === "paragraph_open" && markerInline?.type === "inline" && markerParaClose?.type === "paragraph_close";
2097
+ if (!hasMarkerParagraph) {
2098
+ newTokens.push(tok);
2099
+ continue;
2100
+ }
2101
+ const content = markerInline.content;
2102
+ const newlineIdx = content.indexOf(`
2103
+ `);
2104
+ const firstLine = newlineIdx === -1 ? content : content.slice(0, newlineIdx);
2105
+ const match = ALERT_MARKER.exec(firstLine);
2106
+ if (!match) {
2107
+ newTokens.push(tok);
2108
+ continue;
2109
+ }
2110
+ const type = match[1].toLowerCase();
2111
+ const label = ALERT_LABELS[type];
2112
+ const markerLine = isFiniteMap(tok.map) ? tok.map[0] + 1 : undefined;
2113
+ const alertOpen = blockToken(state, "gp_alert_open", "div", 1);
2114
+ alertOpen.attrSet("class", `gp-alert gp-alert-${type}`);
2115
+ if (markerLine !== undefined)
2116
+ alertOpen.meta = { line: markerLine };
2117
+ newTokens.push(alertOpen);
2118
+ const titleOpen = blockToken(state, "gp_alert_title_open", "p", 1);
2119
+ titleOpen.attrSet("class", "gp-alert-title");
2120
+ if (markerLine !== undefined)
2121
+ titleOpen.meta = { line: markerLine };
2122
+ newTokens.push(titleOpen);
2123
+ const titleInline = new state.Token("inline", "", 0);
2124
+ titleInline.content = label;
2125
+ titleInline.children = [];
2126
+ newTokens.push(titleInline);
2127
+ newTokens.push(blockToken(state, "gp_alert_title_close", "p", -1));
2128
+ const rest = newlineIdx === -1 ? "" : content.slice(newlineIdx + 1);
2129
+ if (rest.trim() !== "") {
2130
+ markerInline.content = rest;
2131
+ if (isFiniteMap(markerParaOpen.map)) {
2132
+ markerParaOpen.map = [markerParaOpen.map[0] + 1, markerParaOpen.map[1]];
2133
+ }
2134
+ newTokens.push(markerParaOpen, markerInline, markerParaClose);
2135
+ }
2136
+ for (let j = i + 4;j < closeIdx; j++) {
2137
+ newTokens.push(tokens[j]);
2138
+ }
2139
+ newTokens.push(blockToken(state, "gp_alert_close", "div", -1));
2140
+ i = closeIdx;
2141
+ }
2142
+ state.tokens = newTokens;
2143
+ };
2144
+ function gfmAlerts(md) {
2145
+ md.core.ruler.after("block", "gp_gfm_alerts", gfmAlertsRule);
2146
+ }
2147
+
2148
+ // src/lib/markdown/images.ts
2149
+ var isSrcsetSpace = (char) => char === "\t" || char === `
2150
+ ` || char === "\f" || char === "\r" || char === " ";
2151
+ function parseSrcsetUrlCandidates(input) {
2152
+ const candidates = [];
2153
+ let position = 0;
2154
+ while (position < input.length) {
2155
+ while (position < input.length && (isSrcsetSpace(input[position]) || input[position] === ",")) {
2156
+ position++;
2157
+ }
2158
+ if (position >= input.length)
2159
+ break;
2160
+ const urlStart = position;
2161
+ while (position < input.length && !isSrcsetSpace(input[position]))
2162
+ position++;
2163
+ let urlEnd = position;
2164
+ if (input[urlEnd - 1] === ",") {
2165
+ while (urlEnd > urlStart && input[urlEnd - 1] === ",")
2166
+ urlEnd--;
2167
+ if (urlEnd > urlStart) {
2168
+ candidates.push({
2169
+ url: input.slice(urlStart, urlEnd),
2170
+ start: urlStart,
2171
+ end: urlEnd
2172
+ });
2173
+ }
2174
+ continue;
2175
+ }
2176
+ let parenDepth = 0;
2177
+ while (position < input.length) {
2178
+ const char = input[position];
2179
+ if (char === "(")
2180
+ parenDepth++;
2181
+ else if (char === ")" && parenDepth > 0)
2182
+ parenDepth--;
2183
+ else if (char === "," && parenDepth === 0) {
2184
+ position++;
2185
+ break;
2186
+ }
2187
+ position++;
2188
+ }
2189
+ if (urlEnd > urlStart) {
2190
+ candidates.push({
2191
+ url: input.slice(urlStart, urlEnd),
2192
+ start: urlStart,
2193
+ end: urlEnd
2194
+ });
2195
+ }
2196
+ }
2197
+ return candidates;
2198
+ }
2199
+ function registerImageRule(md) {
2200
+ const defaultRender = md.renderer.rules.image ?? ((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options));
2201
+ md.renderer.rules.image = (tokens, idx, options, env, self) => {
2202
+ const token = tokens[idx];
2203
+ for (const child of token.children ?? []) {
2204
+ if (child.type === "text_special" || child.type === "code_inline")
2205
+ child.type = "text";
2206
+ }
2207
+ const srcIdx = token.attrIndex("src");
2208
+ if (srcIdx >= 0 && token.attrs) {
2209
+ const src = token.attrs[srcIdx][1];
2210
+ if (src) {
2211
+ const explicit = takeExplicitName(token);
2212
+ const named = explicit || nameFromSrc(src);
2213
+ if (named)
2214
+ token.attrSet("data-name", named);
2215
+ const bucket = env ?? {};
2216
+ (bucket.imageRefs ??= []).push(src);
2217
+ const cls = token.attrGet("class");
2218
+ if (cls && cls.split(/\s+/).includes("gp-shape")) {
2219
+ const cssUrl = src.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
2220
+ const decl = `--gp-shape:url("${cssUrl}")`;
2221
+ const existing = token.attrGet("style");
2222
+ token.attrSet("style", existing ? `${decl}; ${existing}` : decl);
2223
+ }
2224
+ }
2225
+ }
2226
+ return defaultRender(tokens, idx, options, env, self);
2227
+ };
2228
+ }
2229
+ function collectHtmlImageRefs(html) {
2230
+ const refs = [];
2231
+ const tags = [];
2232
+ const collectTags = (active) => {
2233
+ for (const match of active.matchAll(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g)) {
2234
+ if (/^<(?:img|source)\b/i.test(match[0]))
2235
+ tags.push(match[0]);
2236
+ }
2237
+ };
2238
+ const protectedRegion = /<!--[\s\S]*?-->|<(script|style|pre|code|textarea)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
2239
+ let last = 0;
2240
+ for (const match of html.matchAll(protectedRegion)) {
2241
+ const index = match.index ?? 0;
2242
+ collectTags(html.slice(last, index));
2243
+ last = index + match[0].length;
2244
+ }
2245
+ collectTags(html.slice(last));
2246
+ const attr = (tag, name) => {
2247
+ const match = new RegExp(`[\\t\\n\\f\\r ]${name}[\\t\\n\\f\\r ]*=[\\t\\n\\f\\r ]*(?:"([^"]*)"|'([^']*)'|([^\\t\\n\\f\\r >]+))`, "i").exec(tag);
2248
+ return match?.[1] ?? match?.[2] ?? match?.[3];
2249
+ };
2250
+ for (const tag of tags) {
2251
+ if (!/^<img\b/i.test(tag))
2252
+ continue;
2253
+ const src = attr(tag, "src");
2254
+ if (src)
2255
+ refs.push(src);
2256
+ }
2257
+ for (const tag of tags) {
2258
+ const list = attr(tag, "srcset") ?? "";
2259
+ refs.push(...parseSrcsetUrlCandidates(list).map((candidate) => candidate.url));
2260
+ }
2261
+ return refs;
2262
+ }
2263
+
2264
+ // src/lib/markdown/source-range.ts
2265
+ var SOURCE_RANGE_ATTR = "data-source-range";
2266
+ var SOURCE_CHAPTER_ATTR = "data-chapter-src";
2267
+ var SELF_CLOSING_BLOCK_TYPES = new Set([
2268
+ "fence",
2269
+ "hr",
2270
+ "html_block",
2271
+ "layout_page_break",
2272
+ "layout_column_break"
2273
+ ]);
2274
+ function isFiniteRangeTuple(value) {
2275
+ return Array.isArray(value) && value.length === 2 && Number.isFinite(value[0]) && Number.isFinite(value[1]);
2276
+ }
2277
+ function resolveRange(token) {
2278
+ if (isFiniteRangeTuple(token.map)) {
2279
+ return [token.map[0], token.map[1]];
2280
+ }
2281
+ const meta = token.meta;
2282
+ const line = meta?.line;
2283
+ if (typeof line === "number" && Number.isFinite(line)) {
2284
+ return [line - 1, line];
2285
+ }
2286
+ return null;
2287
+ }
2288
+ function isAnnotationTarget(token) {
2289
+ if (token.nesting === 1)
2290
+ return true;
2291
+ return token.nesting === 0 && SELF_CLOSING_BLOCK_TYPES.has(token.type);
2292
+ }
2293
+ function annotateToken(token, chapter) {
2294
+ if (!isAnnotationTarget(token))
2295
+ return;
2296
+ const range = resolveRange(token);
2297
+ if (!range)
2298
+ return;
2299
+ token.attrSet(SOURCE_RANGE_ATTR, `${range[0]}:${range[1]}`);
2300
+ if (chapter)
2301
+ token.attrSet(SOURCE_CHAPTER_ATTR, chapter);
2302
+ }
2303
+ var sourceRangeRule = (state) => {
2304
+ const chapter = typeof state.env?.sourceChapter === "string" ? state.env.sourceChapter : null;
2305
+ for (const token of state.tokens) {
2306
+ annotateToken(token, chapter);
2307
+ }
2308
+ };
2309
+
2310
+ // src/lib/markdown/inline-source.ts
2311
+ var SOURCE_TOKEN_ATTR = "data-gp-source-token";
2312
+ var SOURCE_OCCURRENCE_ATTR = "data-gp-source-occurrence";
2313
+ var RESERVED_ATTRS = new Set([SOURCE_TOKEN_ATTR, SOURCE_OCCURRENCE_ATTR]);
2314
+ function isHtmlSpace(char) {
2315
+ return char === " " || char === "\t" || char === `
2316
+ ` || char === "\r" || char === "\f";
2317
+ }
2318
+ function stripReservedAttrsFromTag(html) {
2319
+ let out = "";
2320
+ let inTag = false;
2321
+ let quote = "";
2322
+ for (let i = 0;i < html.length; ) {
2323
+ const char = html[i];
2324
+ if (!inTag) {
2325
+ out += char;
2326
+ if (char === "<")
2327
+ inTag = true;
2328
+ i++;
2329
+ continue;
2330
+ }
2331
+ if (quote) {
2332
+ out += char;
2333
+ if (char === quote)
2334
+ quote = "";
2335
+ i++;
2336
+ continue;
2337
+ }
2338
+ if (char === '"' || char === "'") {
2339
+ quote = char;
2340
+ out += char;
2341
+ i++;
2342
+ continue;
2343
+ }
2344
+ if (char === ">") {
2345
+ inTag = false;
2346
+ out += char;
2347
+ i++;
2348
+ continue;
2349
+ }
2350
+ if (!isHtmlSpace(char)) {
2351
+ out += char;
2352
+ i++;
2353
+ continue;
2354
+ }
2355
+ let nameStart = i;
2356
+ while (isHtmlSpace(html[nameStart]))
2357
+ nameStart++;
2358
+ let nameEnd = nameStart;
2359
+ while (nameEnd < html.length && !isHtmlSpace(html[nameEnd]) && html[nameEnd] !== "=" && html[nameEnd] !== "/" && html[nameEnd] !== ">")
2360
+ nameEnd++;
2361
+ const name = html.slice(nameStart, nameEnd).toLowerCase();
2362
+ if (!RESERVED_ATTRS.has(name)) {
2363
+ out += html.slice(i, nameStart);
2364
+ i = nameStart;
2365
+ continue;
2366
+ }
2367
+ i = nameEnd;
2368
+ while (isHtmlSpace(html[i]))
2369
+ i++;
2370
+ if (html[i] !== "=")
2371
+ continue;
2372
+ i++;
2373
+ while (isHtmlSpace(html[i]))
2374
+ i++;
2375
+ const valueQuote = html[i];
2376
+ if (valueQuote === '"' || valueQuote === "'") {
2377
+ for (i++;i < html.length && html[i] !== valueQuote; i++) {}
2378
+ if (html[i] === valueQuote)
2379
+ i++;
2380
+ } else {
2381
+ while (i < html.length && !isHtmlSpace(html[i]) && html[i] !== ">")
2382
+ i++;
2383
+ }
2384
+ }
2385
+ return out;
2386
+ }
2387
+ function tagEnd(html, start) {
2388
+ let quote = "";
2389
+ for (let i = start + 1;i < html.length; i++) {
2390
+ const char = html[i];
2391
+ if (quote) {
2392
+ if (char === quote)
2393
+ quote = "";
2394
+ } else if (char === '"' || char === "'")
2395
+ quote = char;
2396
+ else if (char === ">")
2397
+ return i + 1;
2398
+ }
2399
+ return html.length;
2400
+ }
2401
+ function tagIdentity(tag) {
2402
+ let i = 1;
2403
+ while (isHtmlSpace(tag[i]))
2404
+ i++;
2405
+ const closing = tag[i] === "/";
2406
+ if (closing) {
2407
+ i++;
2408
+ while (isHtmlSpace(tag[i]))
2409
+ i++;
2410
+ }
2411
+ const start = i;
2412
+ while (i < tag.length) {
2413
+ const char = tag[i].toLowerCase();
2414
+ if (char >= "a" && char <= "z" || char >= "0" && char <= "9" || char === "-")
2415
+ i++;
2416
+ else
2417
+ break;
2418
+ }
2419
+ return i > start ? { name: tag.slice(start, i).toLowerCase(), closing } : null;
2420
+ }
2421
+ function findProtectedClose(html, tag, from) {
2422
+ const needle = `</${tag}`;
2423
+ for (let at = html.indexOf(needle, from);at >= 0; at = html.indexOf(needle, at + needle.length)) {
2424
+ const boundary = html[at + needle.length];
2425
+ if (boundary === ">" || boundary === "/" || isHtmlSpace(boundary))
2426
+ return at;
2427
+ }
2428
+ return -1;
2429
+ }
2430
+ function stripReservedRawHtmlAttrs(html, initialProtectedTag = "") {
2431
+ const protectedTags = new Set([
2432
+ "script",
2433
+ "style",
2434
+ "textarea",
2435
+ "title",
2436
+ "xmp",
2437
+ "iframe",
2438
+ "noembed",
2439
+ "noframes",
2440
+ "noscript",
2441
+ "plaintext"
2442
+ ]);
2443
+ const lower = html.toLowerCase();
2444
+ let out = "";
2445
+ let i = 0;
2446
+ let protectedTag = initialProtectedTag;
2447
+ while (i < html.length) {
2448
+ if (protectedTag) {
2449
+ if (protectedTag === "plaintext")
2450
+ return { html: out + html.slice(i), protectedTag };
2451
+ const closeAt = findProtectedClose(lower, protectedTag, i);
2452
+ if (closeAt < 0)
2453
+ return { html: out + html.slice(i), protectedTag };
2454
+ out += html.slice(i, closeAt);
2455
+ i = closeAt;
2456
+ protectedTag = "";
2457
+ continue;
2458
+ }
2459
+ if (html.startsWith("<!--", i)) {
2460
+ const closeAt = html.indexOf("-->", i + 4);
2461
+ const end2 = closeAt < 0 ? html.length : closeAt + 3;
2462
+ out += html.slice(i, end2);
2463
+ i = end2;
2464
+ continue;
2465
+ }
2466
+ if (html[i] !== "<") {
2467
+ out += html[i++];
2468
+ continue;
2469
+ }
2470
+ const end = tagEnd(html, i);
2471
+ const tag = html.slice(i, end);
2472
+ const identity = tagIdentity(tag);
2473
+ if (!identity) {
2474
+ out += tag;
2475
+ } else if (!identity.closing && (identity.name === "img" || identity.name === "a")) {
2476
+ out += stripReservedAttrsFromTag(tag);
2477
+ } else {
2478
+ out += tag;
2479
+ if (!identity.closing && protectedTags.has(identity.name)) {
2480
+ protectedTag = identity.name;
2481
+ }
2482
+ }
2483
+ i = end;
2484
+ }
2485
+ return { html: out, protectedTag };
2486
+ }
2487
+ function occurrenceAt(source, token, target) {
2488
+ let occurrence = 0;
2489
+ let from = 0;
2490
+ while (from < target) {
2491
+ const found = source.indexOf(token, from);
2492
+ if (found < 0 || found >= target)
2493
+ break;
2494
+ occurrence++;
2495
+ from = found + token.length;
2496
+ }
2497
+ return occurrence;
2498
+ }
2499
+ function registerInlineSourceMetadata(md) {
2500
+ md.core.ruler.push("inline_source_raw_html", (state) => {
2501
+ let protectedTag = "";
2502
+ for (const block of state.tokens) {
2503
+ if (block.type === "html_block") {
2504
+ const sanitized = stripReservedRawHtmlAttrs(block.content, protectedTag);
2505
+ block.content = sanitized.html;
2506
+ protectedTag = sanitized.protectedTag;
2507
+ continue;
2508
+ }
2509
+ if (!block.children)
2510
+ continue;
2511
+ for (const token of block.children) {
2512
+ if (token.type !== "html_inline")
2513
+ continue;
2514
+ const sanitized = stripReservedRawHtmlAttrs(token.content, protectedTag);
2515
+ token.content = sanitized.html;
2516
+ protectedTag = sanitized.protectedTag;
2517
+ }
2518
+ }
2519
+ });
2520
+ const ruler = md.inline.ruler;
2521
+ for (const [ruleName, tokenType] of [["image", "image"], ["link", "link_open"]]) {
2522
+ const entry = ruler.__rules__.find((rule) => rule.name === ruleName);
2523
+ if (!entry)
2524
+ continue;
2525
+ const original = entry.fn;
2526
+ ruler.at(ruleName, (state, silent) => {
2527
+ const sourceStart = state.pos;
2528
+ const tokenStart = state.tokens.length;
2529
+ const matched = original(state, silent);
2530
+ if (!matched || silent)
2531
+ return matched;
2532
+ const token = state.tokens.slice(tokenStart).find((candidate) => candidate.type === tokenType);
2533
+ if (!token)
2534
+ return matched;
2535
+ const sourceToken = state.src.slice(sourceStart, state.pos);
2536
+ if (sourceToken.includes(`
2537
+ `) || sourceToken.includes("\r"))
2538
+ return matched;
2539
+ token.meta ??= {};
2540
+ token.meta.gpInlineSource = {
2541
+ token: sourceToken,
2542
+ occurrence: occurrenceAt(state.src, sourceToken, sourceStart)
2543
+ };
2544
+ return matched;
2545
+ });
2546
+ const originalRender = md.renderer.rules[tokenType];
2547
+ md.renderer.rules[tokenType] = (tokens, index, options, env, renderer) => {
2548
+ const token = tokens[index];
2549
+ if (token.attrs) {
2550
+ token.attrs = token.attrs.filter(([name]) => !RESERVED_ATTRS.has(name.toLowerCase()));
2551
+ }
2552
+ const source = token.meta?.gpInlineSource;
2553
+ if (source) {
2554
+ token.attrSet(SOURCE_TOKEN_ATTR, source.token);
2555
+ token.attrSet(SOURCE_OCCURRENCE_ATTR, String(source.occurrence));
2556
+ }
2557
+ return originalRender ? originalRender(tokens, index, options, env, renderer) : renderer.renderToken(tokens, index, options);
2558
+ };
2559
+ }
2560
+ }
2561
+
2562
+ // src/lib/markdown/renderer.ts
2563
+ function hasDefaultExport(plugin2) {
2564
+ return !!plugin2 && typeof plugin2 === "object" && "default" in plugin2;
2565
+ }
2566
+ function unwrapPlugin(plugin2) {
2567
+ return hasDefaultExport(plugin2) ? plugin2.default : plugin2;
2568
+ }
2569
+ var BUILTIN_OPTIONAL_PLUGINS = {
2570
+ "markdown-it-mark": unwrapPlugin(markdownItMark),
2571
+ "markdown-it-sub": unwrapPlugin(markdownItSub),
2572
+ "markdown-it-sup": unwrapPlugin(markdownItSup),
2573
+ "markdown-it-abbr": unwrapPlugin(markdownItAbbr),
2574
+ "gutterpress-gfm-alerts": gfmAlerts
2575
+ };
2576
+ function createMarkdownRenderer(customPlugins) {
2577
+ const md = new MarkdownIt({
2578
+ html: true,
2579
+ linkify: true,
2580
+ typographer: true
2581
+ });
2582
+ md.use(unwrapPlugin(markdownItAttrs));
2583
+ md.use(unwrapPlugin(markdownItFootnote));
2584
+ md.use(unwrapPlugin(markdownItDeflist));
2585
+ md.use(unwrapPlugin(markdownItSourceMap));
2586
+ const declaredMarkerSources = (customPlugins ?? []).filter((p) => !!p.markers).map((p) => ({ pluginName: p.name, markers: p.markers }));
2587
+ const declaredMarkers = buildDeclaredMarkerRegistry(declaredMarkerSources);
2588
+ md.use(plugin, { declaredMarkers });
2589
+ md.use(gpPinScope);
2590
+ md.use(gpBlockNames);
2591
+ md.use(gpTableAlign);
2592
+ md.use(gpTwoVersions);
2593
+ registerImageRule(md);
2594
+ if (customPlugins && customPlugins.length > 0) {
2595
+ applyPlugins(md, customPlugins);
2596
+ }
2597
+ registerInlineSourceMetadata(md);
2598
+ md.core.ruler.push("source_range", sourceRangeRule);
2599
+ return md;
2600
+ }
2601
+ function applyPlugins(md, plugins) {
2602
+ for (const { name, plugin: plugin2, options, metadata } of plugins) {
2603
+ try {
2604
+ md.use(plugin2, options);
2605
+ } catch (error) {
2606
+ const errorMsg = error instanceof Error ? error.message : String(error);
2607
+ throw new Error(`Failed to apply plugin "${name}": ${errorMsg}`);
2608
+ }
2609
+ if (metadata?.name) {
2610
+ debug(`Loaded plugin: ${metadata.name} v${metadata.version ?? "?"}`);
2611
+ } else {
2612
+ debug(`Loaded plugin: ${name}`);
2613
+ }
2614
+ }
2615
+ }
2616
+ function collectPluginCss(plugins) {
2617
+ return plugins.map((p) => p.css).filter((css) => typeof css === "string" && css.length > 0).join(`
2618
+
2619
+ `);
2620
+ }
2621
+ function collectPluginStylePaths(plugins) {
2622
+ return plugins.flatMap((p) => p.styles ?? []);
2623
+ }
2624
+ function collectPluginStyleGroups(plugins) {
2625
+ const taken = new Set;
2626
+ const groups = [];
2627
+ for (const p of plugins) {
2628
+ const paths = p.styles ?? [];
2629
+ const css = typeof p.css === "string" && p.css.length > 0 ? p.css : undefined;
2630
+ if (paths.length === 0 && !css)
2631
+ continue;
2632
+ const base = `ext.${layerSlug(p.name)}`;
2633
+ let layer = base;
2634
+ for (let n = 2;taken.has(layer); n++)
2635
+ layer = `${base}-${n}`;
2636
+ taken.add(layer);
2637
+ groups.push({ name: p.name, layer, paths, ...css ? { css } : {} });
2638
+ }
2639
+ return groups;
2640
+ }
2641
+ function layerSlug(name) {
2642
+ const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
2643
+ return /^[a-z]/.test(slug) ? slug : `x${slug}`;
2644
+ }
2645
+ // src/lib/markdown/plugins.ts
2646
+ var vendoredRootsOpened = new Set;
2647
+ var bunVendoredResolverInstalled = false;
2648
+ function isInsideDir(candidate, root) {
2649
+ return candidate === root || candidate.startsWith(root.endsWith(sep) ? root : root + sep);
2650
+ }
2651
+ function resolveBareSpecifierSync(specifier, fromDir, stopRoot, condition) {
2652
+ const parts = specifier.split("/");
2653
+ const name = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
2654
+ const subpath = specifier.slice(name.length).replace(/^\//, "");
2655
+ for (let dir = fromDir;isInsideDir(dir, stopRoot); dir = dirname(dir)) {
2656
+ const packageDir = join(dir, "node_modules", ...name.split("/"));
2657
+ const packageJson = join(packageDir, "package.json");
2658
+ if (!existsSync2(packageJson)) {
2659
+ if (dir === stopRoot)
2660
+ break;
2661
+ continue;
2662
+ }
2663
+ let manifest;
2664
+ try {
2665
+ manifest = JSON.parse(readFileSync(packageJson, "utf8"));
2666
+ } catch {
2667
+ return null;
2668
+ }
2669
+ for (const target of packageResolutionTargets(manifest, subpath, condition)) {
2670
+ const rel = safePackageTarget(target.target);
2671
+ if (!rel)
2672
+ continue;
2673
+ const base = resolve2(packageDir, ...rel.split("/"));
2674
+ if (!isInsideDir(base, packageDir))
2675
+ continue;
2676
+ const candidates = target.exact ? [base] : [base, `${base}.mjs`, `${base}.js`, `${base}.cjs`, `${base}.json`, join(base, "index.mjs"), join(base, "index.js"), join(base, "index.cjs")];
2677
+ for (const candidate of candidates) {
2678
+ try {
2679
+ if (statSync(candidate).isFile())
2680
+ return candidate;
2681
+ } catch {}
2682
+ }
2683
+ }
2684
+ return null;
2685
+ }
2686
+ return null;
2687
+ }
2688
+ function installBunVendoredResolver() {
2689
+ if (bunVendoredResolverInstalled)
2690
+ return;
2691
+ bunVendoredResolverInstalled = true;
2692
+ const bun = globalThis.Bun;
2693
+ if (typeof bun?.plugin !== "function")
2694
+ return;
2695
+ bun.plugin({
2696
+ name: "gutterpress-vendored-plugins",
2697
+ setup(build) {
2698
+ build.onResolve({ filter: /^[^./]/ }, (args) => {
2699
+ if (!args.importer || isAbsolute(args.path) || /^[a-z]+:/i.test(args.path) || isBuiltin(args.path)) {
2700
+ return;
2701
+ }
2702
+ let root;
2703
+ for (const opened of vendoredRootsOpened) {
2704
+ if (isInsideDir(args.importer, opened)) {
2705
+ root = opened;
2706
+ break;
2707
+ }
2708
+ }
2709
+ if (!root)
2710
+ return;
2711
+ const resolved = resolveBareSpecifierSync(args.path, dirname(args.importer), root, args.kind === "require-call" || args.kind === "require-resolve" ? "require" : "import");
2712
+ return resolved ? { path: resolved } : undefined;
2713
+ });
2714
+ }
2715
+ });
2716
+ }
2717
+ async function vendoredInstallRoot(packageName, baseDir, version) {
2718
+ if (!version || !isValidNpmPackageName(packageName) || !isExactNpmVersion(version))
2719
+ return null;
2720
+ return resolveVendoredPluginInstallRoot(baseDir, packageName, version);
2721
+ }
2722
+ async function importVendoredEntry(installRoot, packageDir, spec) {
2723
+ let entry;
2724
+ try {
2725
+ entry = await resolvePackageEntry(packageDir);
2726
+ } catch (cause) {
2727
+ throw new Error(`the downloaded copy of ${spec} under plugins/npm/ is incomplete ` + `(${cause instanceof Error ? cause.message : String(cause)}). ` + `Reinstall it with \`gutterpress ext add ${spec}\`.`, { cause });
2728
+ }
2729
+ const entryPath = join(packageDir, ...entry.split("/"));
2730
+ vendoredRootsOpened.add(installRoot);
2731
+ installBunVendoredResolver();
2732
+ return { module: await import(pathToFileURL(entryPath).href), entryPath };
2733
+ }
2734
+ async function loadNpmPackage(packageName, baseDir, version) {
2735
+ const installRoot = await vendoredInstallRoot(packageName, baseDir, version);
2736
+ if (installRoot) {
2737
+ const packageDir = vendoredNpmPluginPackageDir(installRoot, packageName);
2738
+ const { module, entryPath } = await importVendoredEntry(installRoot, packageDir, `${packageName}@${version}`);
2739
+ return { module, moduleDir: dirname(entryPath), packageDir, entryPath };
2740
+ }
2741
+ try {
2742
+ const packageDir = join(baseDir, "node_modules", ...packageName.split("/"));
2743
+ const packagePath = join(packageDir, ...(await resolvePackageEntry(packageDir)).split("/"));
2744
+ return {
2745
+ module: await import(pathToFileURL(packagePath).href),
2746
+ moduleDir: dirname(packagePath),
2747
+ packageDir,
2748
+ entryPath: packagePath
2749
+ };
2750
+ } catch {}
2751
+ try {
2752
+ return { module: await import(packageName), moduleDir: null, packageDir: null, entryPath: null };
2753
+ } catch {}
2754
+ const looksLikeJsFilename = /\.(m?js|cjs)$/i.test(packageName);
2755
+ const suggestedPath = looksLikeJsFilename ? `./${packageName}` : `./plugins/${packageName}.js`;
2756
+ if (!looksLikeJsFilename && (!version || !isExactNpmVersion(version))) {
2757
+ throw new Error(`Extension "${packageName}${version ? `@${version}` : ""}" is not pinned to an exact version, so it can't be downloaded automatically. Run \`gutterpress ext add ${packageName}\` (or use Book settings > Features in the desktop app) to download it and pin its version in your manifest.`);
2758
+ }
2759
+ throw new Error(`Extension "${packageName}" not found. Install it with \`gutterpress ext add ${version ? `${packageName}@${version}` : packageName}\` (or Book settings > Features > Advanced in the desktop app),
2760
+ or reference a local file under \`extensions:\`:
2761
+ extensions:
2762
+ - ${suggestedPath}`);
2763
+ }
2764
+ function validateStylesExport(value, pluginRef) {
2765
+ if (value === undefined)
2766
+ return;
2767
+ if (!Array.isArray(value) || !value.every((s) => typeof s === "string")) {
2768
+ throw new Error(`Plugin "${pluginRef}" exports \`styles\` that is not an array of strings.`);
2769
+ }
2770
+ return value.length > 0 ? value : undefined;
2771
+ }
2772
+ function validateMarkersExport(value, pluginRef) {
2773
+ if (value === undefined)
2774
+ return;
2775
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2776
+ throw new Error(`Plugin "${pluginRef}" exports \`markers\` that is not a plain object.`);
2777
+ }
2778
+ const table = value;
2779
+ return Object.keys(table).length > 0 ? table : undefined;
2780
+ }
2781
+ function extractPluginExports(pluginModule, pluginRef, exportName) {
2782
+ const mod = pluginModule !== null && (typeof pluginModule === "object" || typeof pluginModule === "function") ? pluginModule : {};
2783
+ let plugin2;
2784
+ let metadata = mod.metadata;
2785
+ let css = mod.css;
2786
+ let styles = validateStylesExport(mod.styles, pluginRef);
2787
+ let markers = validateMarkersExport(mod.markers, pluginRef);
2788
+ if (exportName && typeof mod[exportName] === "function") {
2789
+ plugin2 = mod[exportName];
2790
+ } else if (exportName) {
2791
+ const available = Object.entries(mod).filter(([, value]) => typeof value === "function").map(([name]) => name).sort();
2792
+ throw new Error(`Plugin "${pluginRef}" does not export a plugin function named "${exportName}".` + (available.length > 0 ? ` Available function exports: ${available.join(", ")}.` : ""));
2793
+ } else if (typeof mod.default === "function") {
2794
+ plugin2 = mod.default;
2795
+ } else if (typeof pluginModule === "function") {
2796
+ plugin2 = pluginModule;
2797
+ } else if (typeof mod.default === "object" && mod.default !== null && typeof mod.default.default === "function") {
2798
+ const inner = mod.default;
2799
+ plugin2 = inner.default;
2800
+ metadata = inner.metadata ?? metadata;
2801
+ css = inner.css ?? css;
2802
+ styles = validateStylesExport(inner.styles, pluginRef) ?? styles;
2803
+ markers = validateMarkersExport(inner.markers, pluginRef) ?? markers;
2804
+ }
2805
+ if (typeof plugin2 !== "function") {
2806
+ throw new Error(`Plugin "${pluginRef}" does not export a valid plugin function. Expected \`export default function (md, options) { ... }\` or CommonJS \`module.exports = function (md, options) { ... }\`.`);
2807
+ }
2808
+ return { plugin: plugin2, metadata, css, styles, markers };
2809
+ }
2810
+ var pathPluginCache = new Map;
2811
+ function __resetPathPluginCacheForTests() {
2812
+ pathPluginCache.clear();
2813
+ }
2814
+ var liveShadowPaths = new Set;
2815
+ var exitCleanupRegistered = false;
2816
+ function ensureExitCleanupRegistered() {
2817
+ if (exitCleanupRegistered)
2818
+ return;
2819
+ exitCleanupRegistered = true;
2820
+ process.on("exit", () => {
2821
+ for (const shadowPath of liveShadowPaths) {
2822
+ try {
2823
+ unlinkSync(shadowPath);
2824
+ } catch {}
2825
+ }
2826
+ });
2827
+ }
2828
+ function shadowPathFor(pluginPath, mtimeMs) {
2829
+ const ext = extname(pluginPath);
2830
+ const stem = basename(pluginPath, ext);
2831
+ const token = String(mtimeMs).replace(/\./g, "-");
2832
+ return join(dirname(pluginPath), `.${stem}.gutterpress-reload-${process.pid}-${token}${ext}`);
2833
+ }
2834
+ async function loadCachedPathPluginModule(pluginPath) {
2835
+ const mtimeMs = statSync(pluginPath).mtimeMs;
2836
+ const cached = pathPluginCache.get(pluginPath);
2837
+ if (cached && cached.mtimeMs === mtimeMs) {
2838
+ return cached.module;
2839
+ }
2840
+ const shadowPath = shadowPathFor(pluginPath, mtimeMs);
2841
+ let pluginModule;
2842
+ let shadowActive = false;
2843
+ try {
2844
+ await link(pluginPath, shadowPath);
2845
+ shadowActive = true;
2846
+ ensureExitCleanupRegistered();
2847
+ liveShadowPaths.add(shadowPath);
2848
+ pluginModule = await import(pathToFileURL(shadowPath).href);
2849
+ } catch (error) {
2850
+ if (shadowActive) {
2851
+ liveShadowPaths.delete(shadowPath);
2852
+ await unlink(shadowPath).catch(() => {});
2853
+ throw error;
2854
+ }
2855
+ pluginModule = await import(pathToFileURL(pluginPath).href);
2856
+ }
2857
+ const previousShadow = cached?.shadowPath;
2858
+ pathPluginCache.set(pluginPath, {
2859
+ mtimeMs,
2860
+ module: pluginModule,
2861
+ shadowPath: shadowActive ? shadowPath : null
2862
+ });
2863
+ if (previousShadow) {
2864
+ liveShadowPaths.delete(previousShadow);
2865
+ await unlink(previousShadow).catch(() => {});
2866
+ }
2867
+ return pluginModule;
2868
+ }
2869
+ function resolvePluginStyles(rawStyles, moduleDir, pluginRef) {
2870
+ if (!rawStyles || rawStyles.length === 0)
2871
+ return;
2872
+ if (!moduleDir) {
2873
+ throw new Error(`Plugin "${pluginRef}" exports \`styles\` but its module directory could not be resolved.`);
2874
+ }
2875
+ return resolveDeclaredStyles(rawStyles, moduleDir, `Plugin "${pluginRef}"`);
2876
+ }
2877
+ function noopPlugin() {}
2878
+ function extensionMetadata(meta) {
2879
+ if (!meta.name && !meta.description && !meta.author)
2880
+ return;
2881
+ return {
2882
+ ...meta.name ? { name: meta.name } : {},
2883
+ ...meta.description ? { description: meta.description } : {},
2884
+ ...meta.author ? { author: meta.author } : {}
2885
+ };
2886
+ }
2887
+ function staleManifestHint(dir) {
2888
+ const stale = ["gutterpress.json", "theme.json"].find((name) => existsSync2(join(dir, name)));
2889
+ if (!stale)
2890
+ return "";
2891
+ return `
2892
+
2893
+ This folder still has a ${stale}. Gutterpress no longer reads it — ` + `describe the extension in package.json instead:
2894
+ {
2895
+ "name": "my-extension",
2896
+ "description": "What it does",
2897
+ "author": "You",
2898
+ "main": "plugin.js",
2899
+ "gutterpress": { "styles": ["theme.css"] }
2900
+ }
2901
+ (\`main\` only if the package carries a markdown-it plugin; \`gutterpress.styles\` only if it carries stylesheets.)`;
2902
+ }
2903
+ async function loadExtensionFromDir(extensionDir, config, pluginRef) {
2904
+ const meta = await readExtensionMeta(extensionDir);
2905
+ assertExtensionContained(meta);
2906
+ assertPublishOnly(meta, pluginRef);
2907
+ assertSupportsThisGutterpress(meta, pluginRef);
2908
+ const resolved = resolveExtension(extensionDir, meta, `Plugin "${pluginRef}"`);
2909
+ const extensionStyles = resolved.styles ?? [];
2910
+ const name = config.name ?? meta.name ?? pluginRef;
2911
+ if (!resolved.markdown && extensionStyles.length === 0 && !meta.publish) {
2912
+ throw new Error(`Extension folder "${pluginRef}" declares no markdown-it plugin (\`main\`) and no ` + "`gutterpress.styles` in its package.json — there is nothing to load. Point " + "`path` at a JS file directly for a plain plugin, or describe the package in package.json." + staleManifestHint(extensionDir));
2913
+ }
2914
+ if (!resolved.markdown) {
2915
+ return {
2916
+ name,
2917
+ plugin: noopPlugin,
2918
+ ...extensionMetadata(meta) ? { metadata: extensionMetadata(meta) } : {},
2919
+ ...extensionStyles.length > 0 ? { styles: extensionStyles } : {},
2920
+ options: config.options
2921
+ };
2922
+ }
2923
+ const pluginModule = await loadCachedPathPluginModule(resolved.markdown);
2924
+ const { plugin: plugin2, metadata, css, styles: rawStyles, markers } = extractPluginExports(pluginModule, pluginRef, config.export);
2925
+ const ownStyles = resolvePluginStyles(rawStyles, dirname(resolved.markdown), pluginRef);
2926
+ const styles = [...extensionStyles, ...ownStyles ?? []];
2927
+ return {
2928
+ name,
2929
+ plugin: plugin2,
2930
+ metadata: metadata ?? extensionMetadata(meta),
2931
+ css,
2932
+ ...styles.length > 0 ? { styles } : {},
2933
+ ...markers ? { markers } : {},
2934
+ options: config.options
2935
+ };
2936
+ }
2937
+ async function loadPlugin(config, baseDir) {
2938
+ const pluginRef = config.use;
2939
+ let pluginModule;
2940
+ let pluginName;
2941
+ let moduleDir = null;
2942
+ let packageMeta;
2943
+ let packageStyles = [];
2944
+ if (!config.path && !config.name) {
2945
+ throw new Error("Plugin manifest entry must specify either `path` or `name`. Got an empty plugin config.");
2946
+ }
2947
+ if (!config.path && !config.version && config.name && BUILTIN_OPTIONAL_PLUGINS[config.name]) {
2948
+ return {
2949
+ name: config.name,
2950
+ plugin: BUILTIN_OPTIONAL_PLUGINS[config.name],
2951
+ options: config.options
2952
+ };
2953
+ }
2954
+ if (config.path) {
2955
+ const candidatePath = resolve2(baseDir, config.path);
2956
+ if (existsSync2(candidatePath) && statSync(candidatePath).isDirectory()) {
2957
+ try {
2958
+ return await loadExtensionFromDir(candidatePath, config, pluginRef);
2959
+ } catch (error) {
2960
+ const errorMsg = error instanceof Error ? error.message : String(error);
2961
+ throw new Error(`Failed to load plugin "${pluginRef}": ${errorMsg}`);
2962
+ }
2963
+ }
2964
+ }
2965
+ try {
2966
+ if (config.path) {
2967
+ const pluginPath = resolve2(baseDir, config.path);
2968
+ if (!existsSync2(pluginPath)) {
2969
+ throw new Error(`Plugin file not found: ${pluginPath} (resolved from manifest entry path="${config.path}")`);
2970
+ }
2971
+ pluginModule = await loadCachedPathPluginModule(pluginPath);
2972
+ pluginName = config.name ?? config.path;
2973
+ moduleDir = dirname(pluginPath);
2974
+ } else {
2975
+ const installRoot = await vendoredInstallRoot(config.name, baseDir, config.version);
2976
+ if (installRoot) {
2977
+ const meta = await readExtensionMeta(vendoredNpmPluginPackageDir(installRoot, config.name));
2978
+ if (meta.publish) {
2979
+ assertPublishOnly(meta, pluginRef);
2980
+ assertSupportsThisGutterpress(meta, pluginRef);
2981
+ return {
2982
+ name: config.name,
2983
+ plugin: noopPlugin,
2984
+ ...extensionMetadata(meta) ? { metadata: extensionMetadata(meta) } : {},
2985
+ options: config.options
2986
+ };
2987
+ }
2988
+ }
2989
+ const loaded = await loadNpmPackage(config.name, baseDir, config.version);
2990
+ pluginModule = loaded.module;
2991
+ moduleDir = loaded.moduleDir;
2992
+ pluginName = config.name;
2993
+ if (loaded.packageDir) {
2994
+ const meta = await readExtensionMeta(loaded.packageDir);
2995
+ assertExtensionContained(meta);
2996
+ assertSupportsThisGutterpress(meta, pluginRef);
2997
+ const resolved = resolveExtension(loaded.packageDir, { ...meta, main: undefined }, `Plugin "${pluginRef}"`);
2998
+ if (resolved.markdown && loaded.entryPath && resolve2(resolved.markdown) !== resolve2(loaded.entryPath)) {
2999
+ throw new Error(`its package.json declares gutterpress.markdown "${meta.markdown}" but the package entry is "${relative(loaded.packageDir, loaded.entryPath)}" — an npm extension's ` + "markdown-it plugin is its package entry, so declare that file or drop the field.");
3000
+ }
3001
+ packageMeta = meta;
3002
+ packageStyles = resolved.styles ?? [];
3003
+ }
3004
+ }
3005
+ } catch (error) {
3006
+ const errorMsg = error instanceof Error ? error.message : String(error);
3007
+ throw new Error(`Failed to load plugin "${pluginRef}": ${errorMsg}`);
3008
+ }
3009
+ const { plugin: plugin2, metadata, css, styles: rawStyles, markers } = extractPluginExports(pluginModule, pluginRef, config.export);
3010
+ const ownStyles = resolvePluginStyles(rawStyles, moduleDir, pluginRef);
3011
+ const styles = packageStyles.length > 0 ? [...packageStyles, ...ownStyles ?? []] : ownStyles;
3012
+ return {
3013
+ name: pluginName,
3014
+ plugin: plugin2,
3015
+ metadata: metadata ?? (packageMeta ? extensionMetadata(packageMeta) : undefined),
3016
+ css,
3017
+ styles,
3018
+ markers,
3019
+ options: config.options
3020
+ };
3021
+ }
3022
+ async function loadPublishFunction({ dir, installRoot, ref }) {
3023
+ const meta = await readExtensionMeta(dir);
3024
+ assertExtensionContained(meta);
3025
+ assertPublishOnly(meta, ref);
3026
+ assertSupportsThisGutterpress(meta, ref);
3027
+ let mod;
3028
+ if (installRoot) {
3029
+ mod = (await importVendoredEntry(installRoot, dir, ref)).module;
3030
+ } else {
3031
+ if (!meta.main)
3032
+ throw new Error(`Publishing plugin "${ref}" declares no \`main\` in its package.json.`);
3033
+ const entry = resolve2(dir, meta.main);
3034
+ if (!existsSync2(entry)) {
3035
+ throw new Error(`Publishing plugin "${ref}" declares main "${meta.main}" but no file exists at ${entry}.`);
3036
+ }
3037
+ mod = await loadCachedPathPluginModule(entry);
3038
+ }
3039
+ const moduleExports = mod ?? {};
3040
+ const publish = moduleExports.publish ?? moduleExports.default?.publish;
3041
+ if (typeof publish !== "function") {
3042
+ throw new Error(`"${ref}" does not export a \`publish\` function.`);
3043
+ }
3044
+ return publish;
3045
+ }
3046
+ async function loadPlugins(configs, baseDir, onError) {
3047
+ const plugins = [];
3048
+ for (const config of configs) {
3049
+ if (!onError) {
3050
+ plugins.push(await loadPlugin(config, baseDir));
3051
+ continue;
3052
+ }
3053
+ try {
3054
+ plugins.push(await loadPlugin(config, baseDir));
3055
+ } catch (error) {
3056
+ onError(config.use, error instanceof Error ? error : new Error(String(error)));
3057
+ }
3058
+ }
3059
+ return plugins;
3060
+ }
3061
+ async function loadPluginsWithCss(configs, baseDir, onError) {
3062
+ if (!configs || configs.length === 0) {
3063
+ return { plugins: undefined, pluginStyles: [], pluginStylePaths: [] };
3064
+ }
3065
+ const plugins = await loadPlugins(configs, baseDir, onError);
3066
+ return {
3067
+ plugins,
3068
+ pluginStyles: collectPluginStyleGroups(plugins),
3069
+ pluginStylePaths: collectPluginStylePaths(plugins)
3070
+ };
3071
+ }
3072
+
3073
+ export { TWO_VERSIONS_OPEN, TWO_VERSIONS_CLOSE, MARKER_CSS, GUTTERPRESS_CSS, TWO_VERSIONS_CSS, TABLE_ALIGN_CSS, slugify, prettify, parseSrcsetUrlCandidates, collectHtmlImageRefs, createMarkdownRenderer, applyPlugins, collectPluginCss, collectPluginStylePaths, collectPluginStyleGroups, resolveDeclaredStyles, EXTENSION_MANIFEST_FILENAME, readExtensionMeta, extensionStyleList, extensionEntry, extensionCarries, assertExtensionContained, __resetPathPluginCacheForTests, loadPlugin, loadPublishFunction, loadPlugins, loadPluginsWithCss };