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