@openuji/speculator 0.2.0 → 0.3.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 +21 -1
- package/bin/speculator.mjs +76 -0
- package/dist/browser.cjs +1055 -273
- package/dist/browser.d.cts +218 -38
- package/dist/browser.d.ts +218 -38
- package/dist/browser.js +11 -1
- package/dist/chunk-H7C7MOU6.js +28 -0
- package/dist/chunk-V4GSFQE7.js +1646 -0
- package/dist/cli/export-assertions.cjs +1710 -0
- package/dist/cli/export-assertions.d.cts +25 -0
- package/dist/cli/export-assertions.d.ts +25 -0
- package/dist/cli/export-assertions.js +107 -0
- package/dist/node.cjs +1055 -273
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +16 -23
- package/package.json +13 -4
- package/dist/chunk-JAR5PGCK.js +0 -862
package/dist/node.cjs
CHANGED
|
@@ -32,20 +32,25 @@ var node_exports = {};
|
|
|
32
32
|
__export(node_exports, {
|
|
33
33
|
DOMHtmlRenderer: () => DOMHtmlRenderer,
|
|
34
34
|
FormatProcessor: () => FormatProcessor,
|
|
35
|
+
FormatRegistry: () => FormatRegistry,
|
|
35
36
|
IncludeProcessor: () => IncludeProcessor,
|
|
36
37
|
LinkedomHtmlRenderer: () => LinkedomHtmlRenderer,
|
|
38
|
+
RespecXrefResolver: () => RespecXrefResolver,
|
|
37
39
|
Speculator: () => Speculator2,
|
|
38
40
|
SpeculatorError: () => SpeculatorError,
|
|
41
|
+
StatsTracker: () => StatsTracker,
|
|
39
42
|
browserFileLoader: () => browserFileLoader,
|
|
40
43
|
createFallbackFileLoader: () => createFallbackFileLoader,
|
|
41
44
|
createMarkdownRenderer: () => createMarkdownRenderer,
|
|
45
|
+
fromRespecConfig: () => fromRespecConfig,
|
|
46
|
+
getChangedOutputAreas: () => getChangedOutputAreas,
|
|
42
47
|
getDefaultFileLoader: () => getDefaultFileLoader,
|
|
43
48
|
nodeFileLoader: () => nodeFileLoader,
|
|
44
49
|
parseMarkdown: () => parseMarkdown
|
|
45
50
|
});
|
|
46
51
|
module.exports = __toCommonJS(node_exports);
|
|
47
52
|
|
|
48
|
-
// src/utils/file-loader.ts
|
|
53
|
+
// src/utils/file-loader/node.ts
|
|
49
54
|
var nodeFileLoader = async (path) => {
|
|
50
55
|
const fs = await import("fs/promises");
|
|
51
56
|
const { fileURLToPath } = await import("url");
|
|
@@ -56,6 +61,8 @@ var nodeFileLoader = async (path) => {
|
|
|
56
61
|
throw new Error(`Failed to load file: ${path}. ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
57
62
|
}
|
|
58
63
|
};
|
|
64
|
+
|
|
65
|
+
// src/utils/file-loader/browser.ts
|
|
59
66
|
var browserFileLoader = async (path) => {
|
|
60
67
|
try {
|
|
61
68
|
const response = await fetch(path);
|
|
@@ -67,6 +74,8 @@ var browserFileLoader = async (path) => {
|
|
|
67
74
|
throw new Error(`Failed to fetch file: ${path}. ${error instanceof Error ? error.message : "Network error"}`);
|
|
68
75
|
}
|
|
69
76
|
};
|
|
77
|
+
|
|
78
|
+
// src/utils/file-loader/index.ts
|
|
70
79
|
function createFallbackFileLoader(loaders) {
|
|
71
80
|
return async (path) => {
|
|
72
81
|
let lastError;
|
|
@@ -100,58 +109,245 @@ var SpeculatorError = class extends Error {
|
|
|
100
109
|
this.name = "SpeculatorError";
|
|
101
110
|
}
|
|
102
111
|
};
|
|
112
|
+
function fromRespecConfig(respec) {
|
|
113
|
+
return { ...respec };
|
|
114
|
+
}
|
|
103
115
|
|
|
104
|
-
// src/
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
// src/utils/stats-tracker.ts
|
|
117
|
+
var StatsTracker = class {
|
|
118
|
+
constructor() {
|
|
119
|
+
this._stats = {
|
|
120
|
+
elementsProcessed: 0,
|
|
121
|
+
filesIncluded: 0,
|
|
122
|
+
markdownBlocks: 0,
|
|
123
|
+
processingTime: 0
|
|
124
|
+
};
|
|
125
|
+
this.startTime = null;
|
|
126
|
+
}
|
|
127
|
+
start() {
|
|
128
|
+
this.startTime = performance.now();
|
|
129
|
+
}
|
|
130
|
+
stop() {
|
|
131
|
+
if (this.startTime !== null) {
|
|
132
|
+
this._stats.processingTime += performance.now() - this.startTime;
|
|
133
|
+
this.startTime = null;
|
|
111
134
|
}
|
|
112
135
|
}
|
|
113
|
-
|
|
136
|
+
incrementElements(count = 1) {
|
|
137
|
+
this._stats.elementsProcessed += count;
|
|
138
|
+
}
|
|
139
|
+
incrementFiles(count = 1) {
|
|
140
|
+
this._stats.filesIncluded += count;
|
|
141
|
+
}
|
|
142
|
+
incrementMarkdownBlocks(count = 1) {
|
|
143
|
+
this._stats.markdownBlocks += count;
|
|
144
|
+
}
|
|
145
|
+
toJSON() {
|
|
146
|
+
return { ...this._stats };
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/renderers/sections-renderer.ts
|
|
151
|
+
var SectionsRenderer = class {
|
|
152
|
+
constructor(speculator) {
|
|
153
|
+
this.speculator = speculator;
|
|
154
|
+
}
|
|
155
|
+
async render(sections = [], tracker = new StatsTracker()) {
|
|
156
|
+
const warnings = [];
|
|
157
|
+
const processed = [];
|
|
158
|
+
for (const section of sections) {
|
|
159
|
+
try {
|
|
160
|
+
if (section.hasAttribute("data-include") || section.hasAttribute("data-format") || section.querySelector("[data-include],[data-format]")) {
|
|
161
|
+
const res = await this.speculator.processElement(section, tracker);
|
|
162
|
+
processed.push(res.element);
|
|
163
|
+
warnings.push(...res.warnings);
|
|
164
|
+
} else {
|
|
165
|
+
processed.push(section);
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
warnings.push(
|
|
169
|
+
`Failed to process element: ${e instanceof Error ? e.message : "Unknown error"}`
|
|
170
|
+
);
|
|
171
|
+
processed.push(section);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { sections: processed, warnings, stats: tracker.toJSON() };
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// src/renderers/header-renderer.ts
|
|
179
|
+
var HeaderRenderer = class {
|
|
180
|
+
render(header) {
|
|
181
|
+
const result = {};
|
|
182
|
+
if (header) result.header = header;
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/renderers/sotd-renderer.ts
|
|
188
|
+
var SotdRenderer = class {
|
|
189
|
+
render(sotd) {
|
|
190
|
+
const result = {};
|
|
191
|
+
if (sotd) result.sotd = sotd;
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// src/document-builder.ts
|
|
197
|
+
var DocumentBuilder = class {
|
|
198
|
+
constructor(speculator, htmlRenderer) {
|
|
199
|
+
this.htmlRenderer = htmlRenderer;
|
|
200
|
+
this.headerRenderer = new HeaderRenderer();
|
|
201
|
+
this.sotdRenderer = new SotdRenderer();
|
|
202
|
+
this.sectionsRenderer = new SectionsRenderer(speculator);
|
|
203
|
+
}
|
|
204
|
+
async build(config) {
|
|
205
|
+
const tracker = new StatsTracker();
|
|
206
|
+
const sections = config.sections || [];
|
|
207
|
+
const header = config.header;
|
|
208
|
+
const sotd = config.sotd;
|
|
209
|
+
const baseDoc = (header || sotd || sections[0])?.ownerDocument;
|
|
210
|
+
const preContainer = baseDoc ? baseDoc.createElement("div") : this.htmlRenderer.parse("");
|
|
211
|
+
if (header) preContainer.appendChild(header);
|
|
212
|
+
if (sotd) preContainer.appendChild(sotd);
|
|
213
|
+
for (const section of sections) preContainer.appendChild(section);
|
|
214
|
+
const hooks = config.preProcess;
|
|
215
|
+
if (hooks) {
|
|
216
|
+
for (const hook of hooks) {
|
|
217
|
+
await hook(preContainer);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const children = Array.from(preContainer.children);
|
|
221
|
+
const updatedSections = [];
|
|
222
|
+
for (const child of children) {
|
|
223
|
+
if (child === header || child === sotd) continue;
|
|
224
|
+
updatedSections.push(child);
|
|
225
|
+
}
|
|
226
|
+
const { sections: processedSections, warnings, stats } = await this.sectionsRenderer.render(updatedSections, tracker);
|
|
227
|
+
const { header: renderedHeader } = this.headerRenderer.render(
|
|
228
|
+
header?.cloneNode(true)
|
|
229
|
+
);
|
|
230
|
+
const { sotd: renderedSotd } = this.sotdRenderer.render(
|
|
231
|
+
sotd?.cloneNode(true)
|
|
232
|
+
);
|
|
233
|
+
const finalBaseDoc = (renderedHeader || renderedSotd || processedSections[0])?.ownerDocument;
|
|
234
|
+
const container = finalBaseDoc ? finalBaseDoc.createElement("div") : this.htmlRenderer.parse("");
|
|
235
|
+
if (renderedHeader) container.appendChild(renderedHeader);
|
|
236
|
+
if (renderedSotd) container.appendChild(renderedSotd);
|
|
237
|
+
for (const section of processedSections) container.appendChild(section);
|
|
238
|
+
const result = { container, stats, warnings };
|
|
239
|
+
if (renderedHeader) result.header = renderedHeader;
|
|
240
|
+
if (renderedSotd) result.sotd = renderedSotd;
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/pipeline/postprocess.ts
|
|
246
|
+
var Postprocessor = class {
|
|
247
|
+
constructor(passes) {
|
|
248
|
+
this.passes = passes;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Run the configured passes.
|
|
252
|
+
* @param areas Optional list of output areas to run. If omitted, all passes
|
|
253
|
+
* are executed.
|
|
254
|
+
* @param options Configuration options for the passes.
|
|
255
|
+
*/
|
|
256
|
+
async run(config, areas) {
|
|
257
|
+
const ctx = { outputs: {}, warnings: [], config };
|
|
258
|
+
const active = areas ? this.passes.filter((p) => areas.includes(p.area)) : this.passes;
|
|
259
|
+
const composed = compose(active);
|
|
260
|
+
await composed(ctx);
|
|
261
|
+
return { outputs: ctx.outputs, warnings: ctx.warnings };
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
function compose(passes) {
|
|
265
|
+
return function run(ctx) {
|
|
266
|
+
let index = -1;
|
|
267
|
+
async function dispatch(i) {
|
|
268
|
+
if (i <= index) return;
|
|
269
|
+
index = i;
|
|
270
|
+
const pass = passes[i];
|
|
271
|
+
if (!pass) return;
|
|
272
|
+
await pass.run(ctx, () => dispatch(i + 1));
|
|
273
|
+
}
|
|
274
|
+
return dispatch(0);
|
|
275
|
+
};
|
|
114
276
|
}
|
|
115
277
|
|
|
278
|
+
// src/pipeline/pipeline-runner.ts
|
|
279
|
+
var PipelineRunner = class {
|
|
280
|
+
constructor(passFactory) {
|
|
281
|
+
this.passFactory = passFactory;
|
|
282
|
+
}
|
|
283
|
+
run(container, config, areas) {
|
|
284
|
+
const passes = this.passFactory(container);
|
|
285
|
+
const processor = new Postprocessor(passes);
|
|
286
|
+
return processor.run(config, areas);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/utils/logger.ts
|
|
291
|
+
var logger = {
|
|
292
|
+
debug: (...args) => {
|
|
293
|
+
if (process.env.NODE_ENV !== "production") {
|
|
294
|
+
console.debug(...args);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
116
299
|
// src/processors/include-processor.ts
|
|
117
300
|
var IncludeProcessor = class {
|
|
118
|
-
constructor(baseUrl, fileLoader,
|
|
301
|
+
constructor(baseUrl, fileLoader, formatRegistry) {
|
|
119
302
|
this.baseUrl = baseUrl;
|
|
120
303
|
this.fileLoader = fileLoader;
|
|
121
|
-
this.
|
|
304
|
+
this.formatRegistry = formatRegistry;
|
|
305
|
+
}
|
|
306
|
+
matches(element) {
|
|
307
|
+
return element.hasAttribute("data-include");
|
|
122
308
|
}
|
|
123
|
-
async process(element,
|
|
309
|
+
async process(element, tracker, warnings) {
|
|
124
310
|
const includePath = element.getAttribute("data-include");
|
|
125
311
|
const includeFormat = element.getAttribute("data-include-format") || "text";
|
|
126
312
|
if (!includePath) {
|
|
127
313
|
warnings.push("data-include attribute is empty");
|
|
128
|
-
|
|
314
|
+
element.removeAttribute("data-include");
|
|
315
|
+
element.removeAttribute("data-include-format");
|
|
316
|
+
return { content: null };
|
|
129
317
|
}
|
|
130
318
|
try {
|
|
131
319
|
const fullPath = this.resolveFilePath(includePath);
|
|
132
320
|
const content = await this.fileLoader(fullPath);
|
|
133
|
-
const processedContent = this.
|
|
134
|
-
|
|
135
|
-
stats.filesIncluded++;
|
|
321
|
+
const processedContent = this.formatRegistry.processContent(content, includeFormat);
|
|
322
|
+
tracker.incrementFiles();
|
|
136
323
|
if (includeFormat === "markdown") {
|
|
137
|
-
|
|
324
|
+
tracker.incrementMarkdownBlocks();
|
|
138
325
|
}
|
|
326
|
+
element.removeAttribute("data-include");
|
|
327
|
+
element.removeAttribute("data-include-format");
|
|
328
|
+
return { content: processedContent };
|
|
139
329
|
} catch (error) {
|
|
140
330
|
const errorMsg = `Failed to load: ${includePath}`;
|
|
141
|
-
|
|
142
|
-
element.
|
|
143
|
-
|
|
331
|
+
element.removeAttribute("data-include");
|
|
332
|
+
element.removeAttribute("data-include-format");
|
|
333
|
+
return { content: null, error: errorMsg };
|
|
144
334
|
}
|
|
145
|
-
element.removeAttribute("data-include");
|
|
146
|
-
element.removeAttribute("data-include-format");
|
|
147
335
|
}
|
|
148
336
|
resolveFilePath(path) {
|
|
149
337
|
const filePath = new URL(path, this.baseUrl || "file:///").toString();
|
|
150
|
-
|
|
338
|
+
logger.debug(`Resolved file path: ${filePath}`);
|
|
151
339
|
return filePath;
|
|
152
340
|
}
|
|
153
341
|
};
|
|
154
342
|
|
|
343
|
+
// src/utils/strip-ident.ts
|
|
344
|
+
function stripIndent(content) {
|
|
345
|
+
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
346
|
+
const indents = lines.filter((line) => line.trim().length > 0).map((line) => line.match(/^\s*/)?.[0].length || 0);
|
|
347
|
+
const minIndent = indents.length > 0 ? Math.min(...indents) : 0;
|
|
348
|
+
return lines.map((line) => line.slice(minIndent)).join("\n");
|
|
349
|
+
}
|
|
350
|
+
|
|
155
351
|
// src/markdown/index.ts
|
|
156
352
|
var import_markdown_it = __toESM(require("markdown-it"), 1);
|
|
157
353
|
|
|
@@ -232,6 +428,72 @@ function respecCitePlugin(md) {
|
|
|
232
428
|
md.inline.ruler.before("link", NAME, tokenize);
|
|
233
429
|
}
|
|
234
430
|
|
|
431
|
+
// src/markdown/plugins/mermaid.ts
|
|
432
|
+
function ensureDom() {
|
|
433
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (typeof require !== "function") {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const { DOMParser: DOMParser3 } = require("linkedom");
|
|
440
|
+
const { document: doc } = new DOMParser3().parseFromString("<html></html>", "text/html");
|
|
441
|
+
globalThis.window = doc.defaultView;
|
|
442
|
+
globalThis.document = doc;
|
|
443
|
+
}
|
|
444
|
+
function getMermaidAPI() {
|
|
445
|
+
const globalMermaid = globalThis.mermaid;
|
|
446
|
+
if (globalMermaid?.mermaidAPI) {
|
|
447
|
+
return globalMermaid.mermaidAPI;
|
|
448
|
+
}
|
|
449
|
+
if (globalThis.mermaidAPI) {
|
|
450
|
+
return globalThis.mermaidAPI;
|
|
451
|
+
}
|
|
452
|
+
if (typeof require === "function") {
|
|
453
|
+
try {
|
|
454
|
+
return require("mermaid").mermaidAPI;
|
|
455
|
+
} catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
function respecMermaidPlugin(md, config = {}) {
|
|
462
|
+
const defaultFence = md.renderer.rules.fence ?? ((tokens, idx) => `<pre><code>${md.utils.escapeHtml(tokens[idx].content)}</code></pre>
|
|
463
|
+
`);
|
|
464
|
+
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
|
|
465
|
+
const token = tokens[idx];
|
|
466
|
+
if (token.info.trim() !== "mermaid") {
|
|
467
|
+
return defaultFence(tokens, idx, options, env, self);
|
|
468
|
+
}
|
|
469
|
+
const mermaidAPI = getMermaidAPI();
|
|
470
|
+
if (!mermaidAPI) {
|
|
471
|
+
const code = md.utils.escapeHtml(token.content);
|
|
472
|
+
return `<pre><code>${code}</code></pre>
|
|
473
|
+
`;
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
ensureDom();
|
|
477
|
+
mermaidAPI.initialize(config);
|
|
478
|
+
const id = `mermaid-${idx}`;
|
|
479
|
+
const svg = mermaidAPI.render(id, token.content);
|
|
480
|
+
return `<div class="mermaid">${svg}</div>`;
|
|
481
|
+
} catch {
|
|
482
|
+
const code = md.utils.escapeHtml(token.content);
|
|
483
|
+
return `<pre><code>${code}</code></pre>
|
|
484
|
+
`;
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/utils/render.ts
|
|
490
|
+
function insertContent(element, content) {
|
|
491
|
+
element.innerHTML = content;
|
|
492
|
+
}
|
|
493
|
+
function renderError(message) {
|
|
494
|
+
return `<p class="error">${message}</p>`;
|
|
495
|
+
}
|
|
496
|
+
|
|
235
497
|
// src/markdown/index.ts
|
|
236
498
|
function createMarkdownRenderer(options = {}) {
|
|
237
499
|
const md = new import_markdown_it.default({
|
|
@@ -269,6 +531,10 @@ function createMarkdownRenderer(options = {}) {
|
|
|
269
531
|
md.use(respecConceptPlugin);
|
|
270
532
|
md.use(respecIdlPlugin);
|
|
271
533
|
md.use(respecCitePlugin);
|
|
534
|
+
if (options.mermaid) {
|
|
535
|
+
const config = options.mermaid === true ? {} : options.mermaid;
|
|
536
|
+
md.use(respecMermaidPlugin, config);
|
|
537
|
+
}
|
|
272
538
|
for (const extension of options.extensions ?? []) {
|
|
273
539
|
if (Array.isArray(extension)) {
|
|
274
540
|
const [plugin, pluginOptions] = extension;
|
|
@@ -285,56 +551,88 @@ function parseMarkdown(markdown, options = {}, env) {
|
|
|
285
551
|
return md.render(markdown, env);
|
|
286
552
|
} catch (error) {
|
|
287
553
|
console.error("Markdown parsing error:", error);
|
|
288
|
-
return
|
|
554
|
+
return renderError(
|
|
555
|
+
`Error parsing markdown: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
556
|
+
);
|
|
289
557
|
}
|
|
290
558
|
}
|
|
291
559
|
function slugify(content) {
|
|
292
560
|
return content.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-");
|
|
293
561
|
}
|
|
294
562
|
|
|
295
|
-
// src/
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
var
|
|
305
|
-
|
|
306
|
-
|
|
563
|
+
// src/format-registry.ts
|
|
564
|
+
var MarkdownStrategy = class {
|
|
565
|
+
constructor(options = {}) {
|
|
566
|
+
this.options = options;
|
|
567
|
+
}
|
|
568
|
+
convert(content) {
|
|
569
|
+
return parseMarkdown(content, this.options);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
var PassthroughStrategy = class {
|
|
573
|
+
convert(content) {
|
|
574
|
+
return content;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
var FormatRegistry = class {
|
|
578
|
+
constructor(markdownOptions = {}, customStrategies = {}) {
|
|
579
|
+
const markdown = new MarkdownStrategy(markdownOptions);
|
|
580
|
+
const passthrough = new PassthroughStrategy();
|
|
581
|
+
this.strategies = /* @__PURE__ */ new Map([
|
|
582
|
+
["markdown", markdown],
|
|
583
|
+
["text", passthrough],
|
|
584
|
+
["html", passthrough]
|
|
585
|
+
]);
|
|
586
|
+
for (const [format, strategy] of Object.entries(customStrategies)) {
|
|
587
|
+
this.strategies.set(format, strategy);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Register a new strategy for a given format.
|
|
592
|
+
*/
|
|
593
|
+
register(format, strategy) {
|
|
594
|
+
this.strategies.set(format, strategy);
|
|
307
595
|
}
|
|
308
596
|
/**
|
|
309
597
|
* Convert content based on the specified format.
|
|
310
598
|
*/
|
|
311
599
|
processContent(content, format) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
case "text":
|
|
316
|
-
case "html":
|
|
317
|
-
default:
|
|
318
|
-
return content;
|
|
600
|
+
const strategy = this.strategies.get(format);
|
|
601
|
+
if (!strategy) {
|
|
602
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
319
603
|
}
|
|
604
|
+
return strategy.convert(content);
|
|
320
605
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// src/processors/format-processor.ts
|
|
609
|
+
var FormatProcessor = class {
|
|
610
|
+
constructor(registry = new FormatRegistry()) {
|
|
611
|
+
this.registry = registry;
|
|
612
|
+
}
|
|
613
|
+
matches(element) {
|
|
614
|
+
return element.hasAttribute("data-format");
|
|
615
|
+
}
|
|
616
|
+
process(element, tracker, _warnings) {
|
|
325
617
|
const format = element.getAttribute("data-format");
|
|
618
|
+
let result = {};
|
|
326
619
|
if (format === "markdown" && element.innerHTML.trim()) {
|
|
327
620
|
try {
|
|
328
621
|
const markdownContent = stripIndent(element.innerHTML).trim();
|
|
329
|
-
|
|
330
|
-
|
|
622
|
+
result.content = this.registry.processContent(markdownContent, format);
|
|
623
|
+
tracker.incrementMarkdownBlocks();
|
|
331
624
|
} catch (error) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
625
|
+
result.error = `Failed to process markdown: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
try {
|
|
629
|
+
result.content = this.registry.processContent(element.innerHTML, format);
|
|
630
|
+
} catch (error) {
|
|
631
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
335
632
|
}
|
|
336
633
|
}
|
|
337
634
|
element.removeAttribute("data-format");
|
|
635
|
+
return result;
|
|
338
636
|
}
|
|
339
637
|
};
|
|
340
638
|
|
|
@@ -478,17 +776,27 @@ function resolveIdlLinks(root, index, warnings, suppressClass) {
|
|
|
478
776
|
}
|
|
479
777
|
});
|
|
480
778
|
}
|
|
481
|
-
var
|
|
482
|
-
|
|
779
|
+
var IdlPass = class {
|
|
780
|
+
constructor(root) {
|
|
781
|
+
this.root = root;
|
|
782
|
+
this.area = "idl";
|
|
783
|
+
}
|
|
784
|
+
async execute(_data, config) {
|
|
483
785
|
const warnings = [];
|
|
484
|
-
const suppressClass =
|
|
485
|
-
const index = buildIdlIndex(root, warnings);
|
|
486
|
-
resolveIdlLinks(root, index, warnings, suppressClass);
|
|
487
|
-
return warnings;
|
|
786
|
+
const suppressClass = config.postprocess?.diagnostics?.suppressClass ?? "no-link-warnings";
|
|
787
|
+
const index = buildIdlIndex(this.root, warnings);
|
|
788
|
+
resolveIdlLinks(this.root, index, warnings, suppressClass);
|
|
789
|
+
return { warnings };
|
|
790
|
+
}
|
|
791
|
+
async run(ctx, next) {
|
|
792
|
+
const current = ctx.outputs[this.area];
|
|
793
|
+
const { warnings } = await this.execute(current, ctx.config);
|
|
794
|
+
if (warnings && warnings.length) ctx.warnings.push(...warnings);
|
|
795
|
+
await next();
|
|
488
796
|
}
|
|
489
797
|
};
|
|
490
798
|
|
|
491
|
-
// src/
|
|
799
|
+
// src/xref/local-map.ts
|
|
492
800
|
function uniqueId2(doc, base) {
|
|
493
801
|
let id = base;
|
|
494
802
|
let i = 2;
|
|
@@ -501,9 +809,6 @@ function norm2(term) {
|
|
|
501
809
|
function slugify2(text) {
|
|
502
810
|
return text.trim().toLowerCase().replace(/[^\w]+/g, "-").replace(/^-+|-+$/g, "");
|
|
503
811
|
}
|
|
504
|
-
function isSuppressed(node, suppressClass) {
|
|
505
|
-
return !!node.closest(`.${suppressClass}`);
|
|
506
|
-
}
|
|
507
812
|
function buildLocalMap(root) {
|
|
508
813
|
const map = /* @__PURE__ */ new Map();
|
|
509
814
|
const doc = root.ownerDocument;
|
|
@@ -548,72 +853,159 @@ function buildLocalMap(root) {
|
|
|
548
853
|
});
|
|
549
854
|
return map;
|
|
550
855
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
const
|
|
558
|
-
for (const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (hit) {
|
|
564
|
-
a.setAttribute("href", hit.href);
|
|
856
|
+
|
|
857
|
+
// src/xref/resolve.ts
|
|
858
|
+
async function resolveQueries(resolverConfigs, unresolved) {
|
|
859
|
+
const warnings = [];
|
|
860
|
+
for (const cfg of resolverConfigs) {
|
|
861
|
+
const queries = [];
|
|
862
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
863
|
+
for (const [key, entry] of unresolved.entries()) {
|
|
864
|
+
let specs;
|
|
865
|
+
if (entry.specsOverride) {
|
|
866
|
+
const allowed = cfg.specs ? entry.specsOverride.filter((s) => cfg.specs.includes(s)) : entry.specsOverride;
|
|
867
|
+
specs = allowed;
|
|
565
868
|
} else {
|
|
566
|
-
|
|
567
|
-
bucket.push(a);
|
|
568
|
-
unresolved.set(key, bucket);
|
|
869
|
+
specs = cfg.specs;
|
|
569
870
|
}
|
|
871
|
+
if (specs && specs.length === 0) continue;
|
|
872
|
+
const id = `${key}|${queries.length}`;
|
|
873
|
+
const q = { id, term: entry.term };
|
|
874
|
+
if (specs && specs.length) q.specs = specs;
|
|
875
|
+
queries.push(q);
|
|
876
|
+
idMap.set(id, entry);
|
|
570
877
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
unresolved.delete(key);
|
|
583
|
-
}
|
|
584
|
-
}).catch((err) => {
|
|
585
|
-
warnings.push(`Xref resolver failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
586
|
-
});
|
|
878
|
+
if (!queries.length) continue;
|
|
879
|
+
try {
|
|
880
|
+
const resMap = await cfg.resolver.resolveBatch(queries);
|
|
881
|
+
for (const [id, hits] of resMap.entries()) {
|
|
882
|
+
const entry = idMap.get(id);
|
|
883
|
+
if (entry) entry.results.push(...hits);
|
|
884
|
+
}
|
|
885
|
+
} catch (err) {
|
|
886
|
+
warnings.push(
|
|
887
|
+
`Xref resolver failed: ${err instanceof Error ? err.message : String(err)}`
|
|
888
|
+
);
|
|
587
889
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
890
|
+
}
|
|
891
|
+
return { unresolved, warnings };
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/pipeline/passes/xref.ts
|
|
895
|
+
function isSuppressed(node, suppressClass) {
|
|
896
|
+
return !!node.closest(`.${suppressClass}`);
|
|
897
|
+
}
|
|
898
|
+
function getCiteSpecs(node) {
|
|
899
|
+
let el = node;
|
|
900
|
+
while (el) {
|
|
901
|
+
const cite = el.getAttribute("data-cite");
|
|
902
|
+
if (cite) {
|
|
903
|
+
return cite.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
|
|
591
904
|
}
|
|
592
|
-
|
|
905
|
+
el = el.parentElement;
|
|
906
|
+
}
|
|
907
|
+
return void 0;
|
|
908
|
+
}
|
|
909
|
+
var XrefPass = class {
|
|
910
|
+
constructor(root) {
|
|
911
|
+
this.root = root;
|
|
912
|
+
this.area = "xref";
|
|
913
|
+
}
|
|
914
|
+
async execute(_data, config) {
|
|
915
|
+
const suppressClass = config.postprocess?.diagnostics?.suppressClass ?? "no-link-warnings";
|
|
916
|
+
const localMap = buildLocalMap(this.root);
|
|
917
|
+
const xrefAnchors = Array.from(this.root.querySelectorAll("a[data-xref]"));
|
|
918
|
+
const resolverConfigs = Array.isArray(config.postprocess?.xref) ? config.postprocess.xref : config.postprocess?.xref ? [config.postprocess.xref] : [];
|
|
919
|
+
const unresolved = collectUnresolvedAnchors(xrefAnchors, localMap, suppressClass);
|
|
920
|
+
const { unresolved: resolvedEntries, warnings: resolveWarnings } = await resolveQueries(
|
|
921
|
+
resolverConfigs,
|
|
922
|
+
unresolved
|
|
923
|
+
);
|
|
924
|
+
const defaultPriority = resolverConfigs.flatMap((cfg) => cfg.specs || []);
|
|
925
|
+
const applyWarnings = applyXrefResults(resolvedEntries, defaultPriority);
|
|
926
|
+
return { warnings: [...resolveWarnings, ...applyWarnings] };
|
|
927
|
+
}
|
|
928
|
+
async run(ctx, next) {
|
|
929
|
+
const current = ctx.outputs[this.area];
|
|
930
|
+
const { warnings } = await this.execute(current, ctx.config);
|
|
931
|
+
if (warnings && warnings.length) ctx.warnings.push(...warnings);
|
|
932
|
+
await next();
|
|
593
933
|
}
|
|
594
934
|
};
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
935
|
+
function collectUnresolvedAnchors(anchors, localMap, suppressClass) {
|
|
936
|
+
const unresolved = /* @__PURE__ */ new Map();
|
|
937
|
+
for (const a of anchors) {
|
|
938
|
+
if (isSuppressed(a, suppressClass)) continue;
|
|
939
|
+
const term = a.getAttribute("data-xref") || "";
|
|
940
|
+
const key = norm2(term);
|
|
941
|
+
const hit = localMap.get(key);
|
|
942
|
+
if (hit) {
|
|
943
|
+
a.setAttribute("href", hit.href);
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const specsOverride = getCiteSpecs(a);
|
|
947
|
+
const mapKey = `${key}|${(specsOverride || []).join(",")}`;
|
|
948
|
+
let entry = unresolved.get(mapKey);
|
|
949
|
+
if (!entry) {
|
|
950
|
+
entry = { term, anchors: [], results: [] };
|
|
951
|
+
if (specsOverride) entry.specsOverride = specsOverride;
|
|
952
|
+
unresolved.set(mapKey, entry);
|
|
953
|
+
}
|
|
954
|
+
entry.anchors.push(a);
|
|
614
955
|
}
|
|
615
|
-
return
|
|
956
|
+
return unresolved;
|
|
957
|
+
}
|
|
958
|
+
function applyXrefResults(unresolved, defaultPriority) {
|
|
959
|
+
const warnings = [];
|
|
960
|
+
for (const entry of unresolved.values()) {
|
|
961
|
+
const hits = entry.results;
|
|
962
|
+
let chosen;
|
|
963
|
+
let ambiguous = false;
|
|
964
|
+
const preferred = entry.specsOverride && entry.specsOverride.length ? entry.specsOverride : defaultPriority;
|
|
965
|
+
if (hits.length > 0) {
|
|
966
|
+
if (preferred && preferred.length) {
|
|
967
|
+
const remaining = new Set(hits);
|
|
968
|
+
for (const spec of preferred) {
|
|
969
|
+
const matches = hits.filter((h) => h.cite === spec);
|
|
970
|
+
matches.forEach((m) => remaining.delete(m));
|
|
971
|
+
if (matches.length === 1) {
|
|
972
|
+
chosen = matches[0];
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
if (matches.length > 1) {
|
|
976
|
+
ambiguous = true;
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (!chosen && !ambiguous) {
|
|
981
|
+
const leftovers = Array.from(remaining);
|
|
982
|
+
if (leftovers.length === 1) {
|
|
983
|
+
chosen = leftovers[0];
|
|
984
|
+
} else if (leftovers.length > 1) {
|
|
985
|
+
ambiguous = true;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
} else if (hits.length === 1) {
|
|
989
|
+
chosen = hits[0];
|
|
990
|
+
} else {
|
|
991
|
+
ambiguous = true;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (chosen) {
|
|
995
|
+
for (const a of entry.anchors) {
|
|
996
|
+
a.setAttribute("href", chosen.href);
|
|
997
|
+
if (chosen.cite) a.setAttribute("data-cite", chosen.cite);
|
|
998
|
+
}
|
|
999
|
+
} else if (ambiguous) {
|
|
1000
|
+
warnings.push(`Ambiguous xref: "${entry.term}"`);
|
|
1001
|
+
} else {
|
|
1002
|
+
warnings.push(`No matching xref: "${entry.term}"`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return warnings;
|
|
616
1006
|
}
|
|
1007
|
+
|
|
1008
|
+
// src/renderers/references-renderer.ts
|
|
617
1009
|
function formatEntry(id, e) {
|
|
618
1010
|
const parts = [];
|
|
619
1011
|
parts.push(`<span class="ref-id">[${id}]</span>`);
|
|
@@ -629,86 +1021,111 @@ function formatEntry(id, e) {
|
|
|
629
1021
|
function idForRef(id) {
|
|
630
1022
|
return `bib-${id.toLowerCase()}`;
|
|
631
1023
|
}
|
|
632
|
-
var
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const
|
|
649
|
-
const renderList = (sec, ids) => {
|
|
1024
|
+
var ReferencesRenderer = class {
|
|
1025
|
+
constructor(doc) {
|
|
1026
|
+
this.doc = doc;
|
|
1027
|
+
}
|
|
1028
|
+
render(data) {
|
|
1029
|
+
const section = this.doc.createElement("section");
|
|
1030
|
+
section.id = "references";
|
|
1031
|
+
const normSec = this.doc.createElement("section");
|
|
1032
|
+
normSec.id = "normative-references";
|
|
1033
|
+
normSec.innerHTML = "<h3>Normative references</h3><ul></ul>";
|
|
1034
|
+
const infoSec = this.doc.createElement("section");
|
|
1035
|
+
infoSec.id = "informative-references";
|
|
1036
|
+
infoSec.innerHTML = "<h3>Informative references</h3><ul></ul>";
|
|
1037
|
+
section.innerHTML = "<h2>References</h2>";
|
|
1038
|
+
section.appendChild(normSec);
|
|
1039
|
+
section.appendChild(infoSec);
|
|
1040
|
+
const renderList = (sec, items) => {
|
|
650
1041
|
const ul = sec.querySelector("ul");
|
|
651
1042
|
ul.innerHTML = "";
|
|
652
|
-
|
|
653
|
-
const li =
|
|
1043
|
+
items.sort((a, b) => a.id.localeCompare(b.id)).forEach(({ id, entry }) => {
|
|
1044
|
+
const li = this.doc.createElement("li");
|
|
654
1045
|
li.id = idForRef(id);
|
|
655
|
-
const entry = biblio[id];
|
|
656
1046
|
if (entry) {
|
|
657
1047
|
li.innerHTML = formatEntry(id, entry);
|
|
658
1048
|
} else {
|
|
659
1049
|
li.setAttribute("data-spec", id);
|
|
660
1050
|
li.innerHTML = `<span class="ref-id">[${id}]</span> <span class="ref-missing">\u2014 unresolved reference</span>`;
|
|
661
|
-
warnings.push(`Unresolved reference: "${id}"`);
|
|
662
1051
|
}
|
|
663
1052
|
ul.appendChild(li);
|
|
664
1053
|
});
|
|
665
1054
|
};
|
|
666
|
-
renderList(normSec, normative);
|
|
667
|
-
renderList(infoSec, informative);
|
|
1055
|
+
renderList(normSec, data.normative);
|
|
1056
|
+
renderList(infoSec, data.informative);
|
|
1057
|
+
return section.outerHTML;
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
// src/pipeline/passes/references.ts
|
|
1062
|
+
var ReferencesPass = class {
|
|
1063
|
+
constructor(root) {
|
|
1064
|
+
this.root = root;
|
|
1065
|
+
this.area = "references";
|
|
1066
|
+
}
|
|
1067
|
+
async execute(_data, config) {
|
|
1068
|
+
const warnings = [];
|
|
1069
|
+
const biblio = config.postprocess?.biblio?.entries ?? {};
|
|
1070
|
+
const cites = Array.from(this.root.querySelectorAll("a[data-spec]"));
|
|
1071
|
+
if (!cites.length) return { data: { html: "", citeUpdates: [] }, warnings };
|
|
1072
|
+
const normativeIds = /* @__PURE__ */ new Set();
|
|
1073
|
+
const informativeIds = /* @__PURE__ */ new Set();
|
|
1074
|
+
for (const a of cites) {
|
|
1075
|
+
const id = (a.getAttribute("data-spec") || "").trim();
|
|
1076
|
+
const norm3 = (a.getAttribute("data-normative") || "false") === "true";
|
|
1077
|
+
(norm3 ? normativeIds : informativeIds).add(id);
|
|
1078
|
+
}
|
|
1079
|
+
for (const id of normativeIds) informativeIds.delete(id);
|
|
1080
|
+
const normative = [];
|
|
1081
|
+
const informative = [];
|
|
1082
|
+
Array.from(normativeIds).sort((a, b) => a.localeCompare(b)).forEach((id) => {
|
|
1083
|
+
const entry = biblio[id];
|
|
1084
|
+
if (!entry) warnings.push(`Unresolved reference: "${id}"`);
|
|
1085
|
+
normative.push({ id, entry });
|
|
1086
|
+
});
|
|
1087
|
+
Array.from(informativeIds).sort((a, b) => a.localeCompare(b)).forEach((id) => {
|
|
1088
|
+
const entry = biblio[id];
|
|
1089
|
+
if (!entry) warnings.push(`Unresolved reference: "${id}"`);
|
|
1090
|
+
informative.push({ id, entry });
|
|
1091
|
+
});
|
|
1092
|
+
const renderer = new ReferencesRenderer(this.root.ownerDocument);
|
|
1093
|
+
const html = renderer.render({ normative, informative });
|
|
1094
|
+
const citeUpdates = [];
|
|
668
1095
|
for (const a of cites) {
|
|
669
1096
|
const id = (a.getAttribute("data-spec") || "").trim();
|
|
670
1097
|
const targetId = idForRef(id);
|
|
671
|
-
|
|
672
|
-
a.setAttribute("data-cite-ref", targetId);
|
|
1098
|
+
citeUpdates.push({ element: a, href: `#${targetId}` });
|
|
673
1099
|
}
|
|
674
|
-
return warnings;
|
|
1100
|
+
return { data: { html, citeUpdates }, warnings };
|
|
1101
|
+
}
|
|
1102
|
+
async run(ctx, next) {
|
|
1103
|
+
const current = ctx.outputs[this.area];
|
|
1104
|
+
const { data, warnings } = await this.execute(current, ctx.config);
|
|
1105
|
+
if (data !== void 0) ctx.outputs[this.area] = data;
|
|
1106
|
+
if (warnings && warnings.length) ctx.warnings.push(...warnings);
|
|
1107
|
+
await next();
|
|
675
1108
|
}
|
|
676
1109
|
};
|
|
677
1110
|
|
|
678
1111
|
// src/pipeline/passes/boilerplate.ts
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
h2.textContent = title;
|
|
684
|
-
sec.appendChild(h2);
|
|
685
|
-
if (content) {
|
|
686
|
-
const p = doc.createElement("p");
|
|
687
|
-
p.textContent = content;
|
|
688
|
-
sec.appendChild(p);
|
|
1112
|
+
var BoilerplatePass = class {
|
|
1113
|
+
constructor(root) {
|
|
1114
|
+
this.root = root;
|
|
1115
|
+
this.area = "boilerplate";
|
|
689
1116
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
async run(root, options) {
|
|
694
|
-
const bp = options.boilerplate;
|
|
695
|
-
if (!bp) return [];
|
|
696
|
-
const doc = root.ownerDocument;
|
|
1117
|
+
async execute(_data, config) {
|
|
1118
|
+
const bp = config.postprocess?.boilerplate;
|
|
1119
|
+
if (!bp) return { data: { sections: [], ref: null }, warnings: [] };
|
|
697
1120
|
const mountMode = bp.mount || "end";
|
|
698
1121
|
let ref = null;
|
|
699
1122
|
if (mountMode === "before-references") {
|
|
700
|
-
ref = root.querySelector("#references");
|
|
1123
|
+
ref = this.root.querySelector("#references");
|
|
701
1124
|
} else if (mountMode === "after-toc") {
|
|
702
|
-
const toc = root.querySelector("#toc");
|
|
1125
|
+
const toc = this.root.querySelector("#toc");
|
|
703
1126
|
ref = toc ? toc.nextSibling : null;
|
|
704
1127
|
}
|
|
705
|
-
const
|
|
706
|
-
if (ref) {
|
|
707
|
-
root.insertBefore(section, ref);
|
|
708
|
-
} else {
|
|
709
|
-
root.appendChild(section);
|
|
710
|
-
}
|
|
711
|
-
};
|
|
1128
|
+
const sections = [];
|
|
712
1129
|
const defs = [
|
|
713
1130
|
{ key: "conformance", title: "Conformance" },
|
|
714
1131
|
{ key: "security", title: "Security" },
|
|
@@ -719,52 +1136,138 @@ var boilerplatePass = {
|
|
|
719
1136
|
if (!opt) continue;
|
|
720
1137
|
const cfg = typeof opt === "object" ? opt : {};
|
|
721
1138
|
const id = cfg.id || key;
|
|
722
|
-
if (root.querySelector(`#${id}`)) continue;
|
|
1139
|
+
if (this.root.querySelector(`#${id}`)) continue;
|
|
723
1140
|
const title = cfg.title || defaultTitle;
|
|
724
1141
|
const content = cfg.content;
|
|
725
|
-
const
|
|
726
|
-
|
|
1142
|
+
const descriptor = { id, title };
|
|
1143
|
+
if (content !== void 0) descriptor.content = content;
|
|
1144
|
+
sections.push(descriptor);
|
|
1145
|
+
}
|
|
1146
|
+
return { data: { sections, ref }, warnings: [] };
|
|
1147
|
+
}
|
|
1148
|
+
async run(ctx, next) {
|
|
1149
|
+
const current = ctx.outputs[this.area];
|
|
1150
|
+
const { data, warnings } = await this.execute(current, ctx.config);
|
|
1151
|
+
if (data !== void 0) ctx.outputs[this.area] = data;
|
|
1152
|
+
if (warnings && warnings.length) ctx.warnings.push(...warnings);
|
|
1153
|
+
await next();
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
// src/renderers/boilerplate-renderer.ts
|
|
1158
|
+
function createSection(doc, id, title, content) {
|
|
1159
|
+
const sec = doc.createElement("section");
|
|
1160
|
+
sec.id = id;
|
|
1161
|
+
const h2 = doc.createElement("h2");
|
|
1162
|
+
h2.textContent = title;
|
|
1163
|
+
sec.appendChild(h2);
|
|
1164
|
+
if (content) {
|
|
1165
|
+
const p = doc.createElement("p");
|
|
1166
|
+
p.textContent = content;
|
|
1167
|
+
sec.appendChild(p);
|
|
1168
|
+
}
|
|
1169
|
+
return sec;
|
|
1170
|
+
}
|
|
1171
|
+
var BoilerplateRenderer = class {
|
|
1172
|
+
constructor(doc) {
|
|
1173
|
+
this.doc = doc;
|
|
1174
|
+
}
|
|
1175
|
+
render(descriptors) {
|
|
1176
|
+
return descriptors.map((d) => createSection(this.doc, d.id, d.title, d.content));
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
// src/renderers/toc-renderer.ts
|
|
1181
|
+
var TocRenderer = class {
|
|
1182
|
+
constructor(doc) {
|
|
1183
|
+
this.doc = doc;
|
|
1184
|
+
}
|
|
1185
|
+
render(items = []) {
|
|
1186
|
+
if (!items.length) return { toc: "" };
|
|
1187
|
+
const minDepth = Math.min(...items.map((i) => i.depth));
|
|
1188
|
+
const rootOl = this.doc.createElement("ol");
|
|
1189
|
+
rootOl.setAttribute("role", "list");
|
|
1190
|
+
const olStack = [rootOl];
|
|
1191
|
+
let currentDepth = 1;
|
|
1192
|
+
for (const item of items) {
|
|
1193
|
+
const targetDepth = item.depth - minDepth + 1;
|
|
1194
|
+
while (targetDepth > currentDepth) {
|
|
1195
|
+
const newOl = this.doc.createElement("ol");
|
|
1196
|
+
newOl.setAttribute("role", "list");
|
|
1197
|
+
const parentOl = olStack[olStack.length - 1];
|
|
1198
|
+
let parentLi = parentOl.lastElementChild;
|
|
1199
|
+
if (!parentLi) {
|
|
1200
|
+
parentLi = this.doc.createElement("li");
|
|
1201
|
+
parentOl.appendChild(parentLi);
|
|
1202
|
+
}
|
|
1203
|
+
parentLi.appendChild(newOl);
|
|
1204
|
+
olStack.push(newOl);
|
|
1205
|
+
currentDepth++;
|
|
1206
|
+
}
|
|
1207
|
+
while (targetDepth < currentDepth) {
|
|
1208
|
+
olStack.pop();
|
|
1209
|
+
currentDepth--;
|
|
1210
|
+
}
|
|
1211
|
+
const currentOl = olStack[olStack.length - 1];
|
|
1212
|
+
const li = this.doc.createElement("li");
|
|
1213
|
+
li.setAttribute("data-depth", String(item.depth));
|
|
1214
|
+
const a = this.doc.createElement("a");
|
|
1215
|
+
a.href = `#${item.id}`;
|
|
1216
|
+
a.textContent = item.text;
|
|
1217
|
+
li.appendChild(a);
|
|
1218
|
+
currentOl.appendChild(li);
|
|
727
1219
|
}
|
|
728
|
-
return
|
|
1220
|
+
return { toc: rootOl.outerHTML };
|
|
729
1221
|
}
|
|
730
1222
|
};
|
|
731
1223
|
|
|
732
1224
|
// src/pipeline/passes/toc.ts
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
return [];
|
|
1225
|
+
function collectTocItems(root) {
|
|
1226
|
+
const headings = Array.from(root.querySelectorAll("h2, h3, h4"));
|
|
1227
|
+
const items = [];
|
|
1228
|
+
for (const h of headings) {
|
|
1229
|
+
if (!h.id) continue;
|
|
1230
|
+
const depth = h.tagName.toLowerCase() === "h3" ? 2 : h.tagName.toLowerCase() === "h4" ? 3 : 1;
|
|
1231
|
+
items.push({ id: h.id, text: h.textContent || "", depth });
|
|
1232
|
+
}
|
|
1233
|
+
return items;
|
|
1234
|
+
}
|
|
1235
|
+
var TocPass = class {
|
|
1236
|
+
constructor(root) {
|
|
1237
|
+
this.root = root;
|
|
1238
|
+
this.area = "toc";
|
|
1239
|
+
}
|
|
1240
|
+
async execute(_data, config) {
|
|
1241
|
+
const { toc } = config.postprocess || {};
|
|
1242
|
+
if (toc?.enabled === false) return { data: "", warnings: [] };
|
|
1243
|
+
const items = collectTocItems(this.root);
|
|
1244
|
+
if (!items.length) return { data: "", warnings: [] };
|
|
1245
|
+
const renderer = new TocRenderer(this.root.ownerDocument);
|
|
1246
|
+
const { toc: tocHtml } = renderer.render(items);
|
|
1247
|
+
return { data: tocHtml, warnings: [] };
|
|
1248
|
+
}
|
|
1249
|
+
async run(ctx, next) {
|
|
1250
|
+
const current = ctx.outputs[this.area];
|
|
1251
|
+
const { data, warnings } = await this.execute(current, ctx.config);
|
|
1252
|
+
if (data !== void 0) ctx.outputs[this.area] = data;
|
|
1253
|
+
if (warnings && warnings.length) ctx.warnings.push(...warnings);
|
|
1254
|
+
await next();
|
|
756
1255
|
}
|
|
757
1256
|
};
|
|
758
1257
|
|
|
759
1258
|
// src/pipeline/passes/diagnostics.ts
|
|
760
|
-
var
|
|
761
|
-
|
|
1259
|
+
var DiagnosticsPass = class {
|
|
1260
|
+
constructor(root) {
|
|
1261
|
+
this.root = root;
|
|
1262
|
+
this.area = "diagnostics";
|
|
1263
|
+
}
|
|
1264
|
+
async execute(_data, config) {
|
|
762
1265
|
const warnings = [];
|
|
763
|
-
const suppressClass =
|
|
764
|
-
const idsAndLinks =
|
|
1266
|
+
const suppressClass = config.postprocess?.diagnostics?.suppressClass ?? "no-link-warnings";
|
|
1267
|
+
const idsAndLinks = config.postprocess?.diagnostics?.idsAndLinks ?? true;
|
|
765
1268
|
if (idsAndLinks) {
|
|
766
1269
|
const seen = /* @__PURE__ */ new Map();
|
|
767
|
-
root.querySelectorAll("[id]").forEach((el) => {
|
|
1270
|
+
this.root.querySelectorAll("[id]").forEach((el) => {
|
|
768
1271
|
const id = el.id;
|
|
769
1272
|
if (!id) return;
|
|
770
1273
|
if (seen.has(id)) {
|
|
@@ -775,7 +1278,7 @@ var diagnosticsPass = {
|
|
|
775
1278
|
seen.set(id, el);
|
|
776
1279
|
}
|
|
777
1280
|
});
|
|
778
|
-
const anchors = root.querySelectorAll("a");
|
|
1281
|
+
const anchors = this.root.querySelectorAll("a");
|
|
779
1282
|
anchors.forEach((a) => {
|
|
780
1283
|
if (a.closest(`.${suppressClass}`)) return;
|
|
781
1284
|
const hasHref = a.hasAttribute("href") && a.getAttribute("href") !== "";
|
|
@@ -786,44 +1289,206 @@ var diagnosticsPass = {
|
|
|
786
1289
|
}
|
|
787
1290
|
});
|
|
788
1291
|
}
|
|
789
|
-
return warnings;
|
|
1292
|
+
return { warnings };
|
|
1293
|
+
}
|
|
1294
|
+
async run(ctx, next) {
|
|
1295
|
+
const current = ctx.outputs[this.area];
|
|
1296
|
+
const { warnings } = await this.execute(current, ctx.config);
|
|
1297
|
+
if (warnings && warnings.length) ctx.warnings.push(...warnings);
|
|
1298
|
+
await next();
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
// src/pipeline/passes/assertions.ts
|
|
1303
|
+
function getSpecAndVersionFromBaseUrl(baseUrl) {
|
|
1304
|
+
if (!baseUrl) return {};
|
|
1305
|
+
try {
|
|
1306
|
+
const url = new URL(baseUrl);
|
|
1307
|
+
const parts = url.pathname.replace(/\/+$/, "").split("/").filter(Boolean);
|
|
1308
|
+
const n = parts.length;
|
|
1309
|
+
if (n >= 2) {
|
|
1310
|
+
const version = parts[n - 1];
|
|
1311
|
+
const spec = parts[n - 2];
|
|
1312
|
+
return { spec, version };
|
|
1313
|
+
}
|
|
1314
|
+
} catch {
|
|
1315
|
+
}
|
|
1316
|
+
return {};
|
|
1317
|
+
}
|
|
1318
|
+
function toStandardId(prefix, major, seq) {
|
|
1319
|
+
const n = String(seq).padStart(3, "0");
|
|
1320
|
+
return `${prefix}-${major}-${n}`;
|
|
1321
|
+
}
|
|
1322
|
+
function closestBlock(el) {
|
|
1323
|
+
return el.closest("p, li, dd, dt, td, th, blockquote") || null;
|
|
1324
|
+
}
|
|
1325
|
+
function normText(el) {
|
|
1326
|
+
const text = (el.textContent || "").replace(/\s+/g, " ").trim();
|
|
1327
|
+
return text.length > 200 ? `${text.slice(0, 197)}...` : text;
|
|
1328
|
+
}
|
|
1329
|
+
function ensureUniqueId(root, desired) {
|
|
1330
|
+
let id = desired;
|
|
1331
|
+
let i = 1;
|
|
1332
|
+
const doc = root.ownerDocument;
|
|
1333
|
+
while (doc && typeof doc.getElementById === "function" && doc.getElementById(id)) {
|
|
1334
|
+
id = `${desired}-${i++}`;
|
|
1335
|
+
}
|
|
1336
|
+
return id;
|
|
1337
|
+
}
|
|
1338
|
+
var AssertionsPass = class {
|
|
1339
|
+
constructor(root) {
|
|
1340
|
+
this.root = root;
|
|
1341
|
+
this.area = "assertions";
|
|
1342
|
+
}
|
|
1343
|
+
async execute(_data, config) {
|
|
1344
|
+
const warnings = [];
|
|
1345
|
+
const { spec: specOpt, version: versionOpt } = config.postprocess?.assertions || {};
|
|
1346
|
+
const { spec: specFromPath, version: versionFromPath } = getSpecAndVersionFromBaseUrl(config.baseUrl);
|
|
1347
|
+
const spec = (specOpt || specFromPath || "SPEC").toUpperCase();
|
|
1348
|
+
const version = (versionOpt || versionFromPath || "0").toString();
|
|
1349
|
+
const majorMatch = version.match(/^(\d+)/);
|
|
1350
|
+
const major = majorMatch ? majorMatch[1] : "0";
|
|
1351
|
+
const map = /* @__PURE__ */ new Map();
|
|
1352
|
+
const markers = Array.from(this.root.querySelectorAll("em.rfc2119"));
|
|
1353
|
+
for (const em of markers) {
|
|
1354
|
+
const block = closestBlock(em);
|
|
1355
|
+
if (!block) continue;
|
|
1356
|
+
const text = (em.textContent || "").trim().toUpperCase();
|
|
1357
|
+
let type = void 0;
|
|
1358
|
+
if (text === "MUST NOT") type = "MUST NOT";
|
|
1359
|
+
else if (text === "MUST") type = "MUST";
|
|
1360
|
+
else if (text === "SHOULD") type = "SHOULD";
|
|
1361
|
+
else if (text === "MAY") type = "MAY";
|
|
1362
|
+
if (!type) continue;
|
|
1363
|
+
const entry = map.get(block) || { keywords: [], markers: [] };
|
|
1364
|
+
entry.keywords.push(type);
|
|
1365
|
+
entry.markers.push(em);
|
|
1366
|
+
map.set(block, entry);
|
|
1367
|
+
}
|
|
1368
|
+
if (!map.size) return { data: [], warnings };
|
|
1369
|
+
const selectors = "p, li, dd, dt, td, th, blockquote";
|
|
1370
|
+
const blocksInOrder = Array.from(this.root.querySelectorAll(selectors)).filter((el) => map.has(el));
|
|
1371
|
+
const items = [];
|
|
1372
|
+
let seq = 1;
|
|
1373
|
+
for (const block of blocksInOrder) {
|
|
1374
|
+
const { keywords } = map.get(block);
|
|
1375
|
+
if (keywords.length > 1) {
|
|
1376
|
+
warnings.push(
|
|
1377
|
+
`Multiple normative keywords (${keywords.join(", ")}) in block: "${normText(block)}"`
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
const type = keywords[0];
|
|
1381
|
+
const standardId = toStandardId(spec, major, seq++);
|
|
1382
|
+
let anchorId = block.id;
|
|
1383
|
+
if (!anchorId) {
|
|
1384
|
+
anchorId = ensureUniqueId(this.root, standardId);
|
|
1385
|
+
block.id = anchorId;
|
|
1386
|
+
}
|
|
1387
|
+
block.setAttribute("data-assertion-id", standardId);
|
|
1388
|
+
items.push({ id: standardId, anchorId, type, snippet: normText(block) });
|
|
1389
|
+
}
|
|
1390
|
+
return { data: items, warnings };
|
|
1391
|
+
}
|
|
1392
|
+
async run(ctx, next) {
|
|
1393
|
+
const current = ctx.outputs[this.area];
|
|
1394
|
+
const { data, warnings } = await this.execute(current, ctx.config);
|
|
1395
|
+
if (data !== void 0) ctx.outputs[this.area] = data;
|
|
1396
|
+
if (warnings && warnings.length) ctx.warnings.push(...warnings);
|
|
1397
|
+
await next();
|
|
790
1398
|
}
|
|
791
1399
|
};
|
|
792
1400
|
|
|
1401
|
+
// src/utils/output-areas.ts
|
|
1402
|
+
var CONFIG_TO_OUTPUT_MAP = [
|
|
1403
|
+
{
|
|
1404
|
+
fields: ["sections"],
|
|
1405
|
+
outputs: ["idl", "xref", "references", "boilerplate", "toc", "diagnostics", "assertions"]
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
fields: ["header", "sotd", "pubrules", "legal"],
|
|
1409
|
+
outputs: ["boilerplate"]
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
fields: ["lint"],
|
|
1413
|
+
outputs: ["diagnostics"]
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
fields: ["xref", "mdn"],
|
|
1417
|
+
outputs: ["xref"]
|
|
1418
|
+
}
|
|
1419
|
+
];
|
|
1420
|
+
function fieldChanged(oldVal, newVal) {
|
|
1421
|
+
if (Array.isArray(oldVal) && Array.isArray(newVal)) {
|
|
1422
|
+
if (oldVal === newVal) return false;
|
|
1423
|
+
if (oldVal.length !== newVal.length) return true;
|
|
1424
|
+
for (let i = 0; i < oldVal.length; i++) {
|
|
1425
|
+
if (oldVal[i] !== newVal[i]) return true;
|
|
1426
|
+
}
|
|
1427
|
+
return false;
|
|
1428
|
+
}
|
|
1429
|
+
return oldVal !== newVal;
|
|
1430
|
+
}
|
|
1431
|
+
function getChangedOutputAreas(oldConfig, newConfig) {
|
|
1432
|
+
if (!oldConfig) {
|
|
1433
|
+
const all = /* @__PURE__ */ new Set();
|
|
1434
|
+
for (const mapping of CONFIG_TO_OUTPUT_MAP) {
|
|
1435
|
+
for (const out of mapping.outputs) all.add(out);
|
|
1436
|
+
}
|
|
1437
|
+
return Array.from(all);
|
|
1438
|
+
}
|
|
1439
|
+
const areas = /* @__PURE__ */ new Set();
|
|
1440
|
+
for (const { fields, outputs } of CONFIG_TO_OUTPUT_MAP) {
|
|
1441
|
+
if (fields.some((f) => fieldChanged(oldConfig[f], newConfig[f]))) {
|
|
1442
|
+
outputs.forEach((o) => areas.add(o));
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return Array.from(areas);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
793
1448
|
// src/speculator.ts
|
|
794
1449
|
var Speculator = class {
|
|
795
1450
|
constructor(options = {}) {
|
|
796
1451
|
const baseUrl = options.baseUrl;
|
|
797
1452
|
const fileLoader = options.fileLoader || getDefaultFileLoader();
|
|
798
1453
|
const markdownOptions = options.markdownOptions || {};
|
|
799
|
-
this.
|
|
800
|
-
this.
|
|
801
|
-
this.
|
|
1454
|
+
this.baseConfig = { postprocess: options.postprocess || {} };
|
|
1455
|
+
this.formatRegistry = options.formatRegistry || new FormatRegistry(markdownOptions);
|
|
1456
|
+
this.formatProcessor = options.formatProcessor || new FormatProcessor(this.formatRegistry);
|
|
1457
|
+
this.includeProcessor = options.includeProcessor || new IncludeProcessor(baseUrl, fileLoader, this.formatRegistry);
|
|
802
1458
|
this.htmlRenderer = options.htmlRenderer || new DOMHtmlRenderer();
|
|
1459
|
+
this.documentBuilder = new DocumentBuilder(this, this.htmlRenderer);
|
|
1460
|
+
this.processors = [this.includeProcessor, this.formatProcessor];
|
|
1461
|
+
const passes = options.passes;
|
|
1462
|
+
if (Array.isArray(passes)) {
|
|
1463
|
+
this.passFactory = () => passes;
|
|
1464
|
+
} else if (typeof passes === "function") {
|
|
1465
|
+
this.passFactory = passes;
|
|
1466
|
+
} else {
|
|
1467
|
+
this.passFactory = (container) => {
|
|
1468
|
+
return [
|
|
1469
|
+
new IdlPass(container),
|
|
1470
|
+
new XrefPass(container),
|
|
1471
|
+
new ReferencesPass(container),
|
|
1472
|
+
new BoilerplatePass(container),
|
|
1473
|
+
new TocPass(container),
|
|
1474
|
+
new AssertionsPass(container),
|
|
1475
|
+
new DiagnosticsPass(container)
|
|
1476
|
+
];
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
this.pipelineRunner = new PipelineRunner(this.passFactory);
|
|
803
1480
|
}
|
|
804
1481
|
/**
|
|
805
1482
|
* Process a single DOM element
|
|
806
1483
|
*/
|
|
807
|
-
async processElement(element) {
|
|
808
|
-
|
|
809
|
-
const stats = {
|
|
810
|
-
elementsProcessed: 0,
|
|
811
|
-
filesIncluded: 0,
|
|
812
|
-
markdownBlocks: 0,
|
|
813
|
-
processingTime: 0
|
|
814
|
-
};
|
|
1484
|
+
async processElement(element, tracker = new StatsTracker()) {
|
|
1485
|
+
tracker.start();
|
|
815
1486
|
const warnings = [];
|
|
816
1487
|
const clonedElement = element.cloneNode(true);
|
|
817
1488
|
try {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}
|
|
821
|
-
if (clonedElement.hasAttribute("data-format")) {
|
|
822
|
-
this.formatProcessor.process(clonedElement, stats, warnings);
|
|
823
|
-
}
|
|
824
|
-
stats.elementsProcessed = 1;
|
|
825
|
-
stats.processingTime = performance.now() - startTime;
|
|
826
|
-
return { element: clonedElement, warnings, stats };
|
|
1489
|
+
await this.processNode(clonedElement, tracker, warnings);
|
|
1490
|
+
tracker.stop();
|
|
1491
|
+
return { element: clonedElement, warnings, stats: tracker.toJSON() };
|
|
827
1492
|
} catch (error) {
|
|
828
1493
|
throw new SpeculatorError(
|
|
829
1494
|
`Failed to process element: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
@@ -831,66 +1496,126 @@ var Speculator = class {
|
|
|
831
1496
|
);
|
|
832
1497
|
}
|
|
833
1498
|
}
|
|
1499
|
+
async processNode(node, tracker, warnings) {
|
|
1500
|
+
let matched = false;
|
|
1501
|
+
for (const processor of this.processors) {
|
|
1502
|
+
if (!processor.matches(node)) continue;
|
|
1503
|
+
matched = true;
|
|
1504
|
+
const { content, error } = await processor.process(node, tracker, warnings);
|
|
1505
|
+
if (error) {
|
|
1506
|
+
warnings.push(error);
|
|
1507
|
+
insertContent(node, renderError(error));
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
if (content !== void 0 && content !== null) {
|
|
1511
|
+
let renderedContent = content;
|
|
1512
|
+
if (processor instanceof FormatProcessor) {
|
|
1513
|
+
const rendered = this.htmlRenderer.parse(content);
|
|
1514
|
+
renderedContent = this.htmlRenderer.serialize(rendered);
|
|
1515
|
+
}
|
|
1516
|
+
insertContent(node, renderedContent);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (matched) tracker.incrementElements();
|
|
1520
|
+
const children = Array.from(node.children);
|
|
1521
|
+
for (const child of children) {
|
|
1522
|
+
await this.processNode(child, tracker, warnings);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
834
1525
|
/**
|
|
835
|
-
* Process an entire document
|
|
1526
|
+
* Process an entire document described by a SpeculatorConfig
|
|
836
1527
|
*/
|
|
837
|
-
async renderDocument(
|
|
1528
|
+
async renderDocument(spec, configOrOutputs = {}) {
|
|
838
1529
|
const startTime = performance.now();
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
elementsProcessed: 0,
|
|
844
|
-
filesIncluded: 0,
|
|
845
|
-
markdownBlocks: 0,
|
|
846
|
-
processingTime: 0
|
|
1530
|
+
const config = {
|
|
1531
|
+
...this.baseConfig,
|
|
1532
|
+
...spec,
|
|
1533
|
+
postprocess: { ...this.baseConfig.postprocess || {}, ...spec.postprocess || {} }
|
|
847
1534
|
};
|
|
848
|
-
const
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1535
|
+
const { container, header, sotd, stats, warnings: sectionWarnings } = await this.documentBuilder.build(config);
|
|
1536
|
+
const allWarnings = [...sectionWarnings];
|
|
1537
|
+
let areas = getChangedOutputAreas(this.prevConfig, config);
|
|
1538
|
+
this.prevConfig = config;
|
|
1539
|
+
if (Array.isArray(configOrOutputs)) {
|
|
1540
|
+
areas = areas.filter((a) => configOrOutputs.includes(a));
|
|
1541
|
+
}
|
|
1542
|
+
let toc;
|
|
1543
|
+
let boilerplate;
|
|
1544
|
+
let references;
|
|
1545
|
+
let pipelineOutputs = {};
|
|
1546
|
+
try {
|
|
1547
|
+
if (areas.length) {
|
|
1548
|
+
const result = await this.pipelineRunner.run(container, config, areas);
|
|
1549
|
+
pipelineOutputs = result.outputs;
|
|
857
1550
|
allWarnings.push(...result.warnings);
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1551
|
+
if (result.outputs.toc) {
|
|
1552
|
+
toc = result.outputs.toc;
|
|
1553
|
+
}
|
|
1554
|
+
const bpOut = result.outputs.boilerplate;
|
|
1555
|
+
if (bpOut && bpOut.sections.length) {
|
|
1556
|
+
const renderer = new BoilerplateRenderer(container.ownerDocument);
|
|
1557
|
+
const nodes = renderer.render(bpOut.sections);
|
|
1558
|
+
boilerplate = nodes.map((el) => el.outerHTML);
|
|
1559
|
+
}
|
|
1560
|
+
const refOut = result.outputs.references;
|
|
1561
|
+
if (refOut && refOut.html) {
|
|
1562
|
+
refOut.citeUpdates.forEach(
|
|
1563
|
+
({ element, href }) => element.setAttribute("href", href)
|
|
1564
|
+
);
|
|
1565
|
+
references = refOut.html;
|
|
1566
|
+
}
|
|
863
1567
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
section.parentNode.replaceChild(processedElements[index], section);
|
|
1568
|
+
const hooks = config.postProcess ? Array.isArray(config.postProcess) ? config.postProcess : [config.postProcess] : [];
|
|
1569
|
+
for (const hook of hooks) {
|
|
1570
|
+
await hook(container, pipelineOutputs);
|
|
868
1571
|
}
|
|
869
|
-
});
|
|
870
|
-
const passes = [
|
|
871
|
-
idlPass,
|
|
872
|
-
xrefPass,
|
|
873
|
-
referencesPass,
|
|
874
|
-
boilerplatePass,
|
|
875
|
-
tocPass,
|
|
876
|
-
diagnosticsPass
|
|
877
|
-
];
|
|
878
|
-
try {
|
|
879
|
-
const { warnings } = await postprocess(container, passes, this.postprocessOptions || {});
|
|
880
|
-
allWarnings.push(...warnings);
|
|
881
1572
|
} catch (e) {
|
|
882
|
-
allWarnings.push(
|
|
1573
|
+
allWarnings.push(
|
|
1574
|
+
`Postprocess failed: ${e instanceof Error ? e.message : "Unknown error"}`
|
|
1575
|
+
);
|
|
883
1576
|
}
|
|
884
|
-
|
|
885
|
-
|
|
1577
|
+
stats.processingTime = performance.now() - startTime;
|
|
1578
|
+
const finalSections = Array.from(container.children).filter(
|
|
1579
|
+
(el) => el !== header && el !== sotd
|
|
1580
|
+
);
|
|
1581
|
+
return {
|
|
1582
|
+
sections: finalSections,
|
|
1583
|
+
warnings: allWarnings,
|
|
1584
|
+
toc,
|
|
1585
|
+
stats,
|
|
1586
|
+
...header ? { header } : {},
|
|
1587
|
+
...sotd ? { sotd } : {},
|
|
1588
|
+
...boilerplate ? { boilerplate } : {},
|
|
1589
|
+
...references ? { references } : {}
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
async renderSections(inputHtml) {
|
|
1593
|
+
const container = this.htmlRenderer.parse(inputHtml);
|
|
1594
|
+
const sections = Array.from(container.children);
|
|
1595
|
+
return this.renderDocument({ sections });
|
|
886
1596
|
}
|
|
887
1597
|
/**
|
|
888
1598
|
* Process HTML string and return processed HTML
|
|
889
1599
|
*/
|
|
890
|
-
async renderHTML(
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
1600
|
+
async renderHTML(inputHtml) {
|
|
1601
|
+
const result = await this.renderSections(inputHtml);
|
|
1602
|
+
const container = this.htmlRenderer.parse("<div></div>");
|
|
1603
|
+
const doc = container.ownerDocument;
|
|
1604
|
+
const root = doc.createElement("div");
|
|
1605
|
+
for (const section of result.sections) {
|
|
1606
|
+
root.appendChild(section);
|
|
1607
|
+
}
|
|
1608
|
+
const htmlSections = this.htmlRenderer.serialize(root);
|
|
1609
|
+
return {
|
|
1610
|
+
sections: htmlSections,
|
|
1611
|
+
toc: result.toc,
|
|
1612
|
+
warnings: result.warnings,
|
|
1613
|
+
stats: result.stats,
|
|
1614
|
+
...result.header ? { header: this.htmlRenderer.serialize(result.header) } : {},
|
|
1615
|
+
...result.sotd ? { sotd: this.htmlRenderer.serialize(result.sotd) } : {},
|
|
1616
|
+
...result.boilerplate ? { boilerplate: result.boilerplate } : {},
|
|
1617
|
+
...result.references ? { references: result.references } : {}
|
|
1618
|
+
};
|
|
894
1619
|
}
|
|
895
1620
|
};
|
|
896
1621
|
|
|
@@ -907,6 +1632,58 @@ var LinkedomHtmlRenderer = class {
|
|
|
907
1632
|
}
|
|
908
1633
|
};
|
|
909
1634
|
|
|
1635
|
+
// src/utils/respec-xref-resolver.ts
|
|
1636
|
+
function groupQueriesBySpec(queries) {
|
|
1637
|
+
const bySpec = /* @__PURE__ */ new Map();
|
|
1638
|
+
for (const q of queries) {
|
|
1639
|
+
const key = (q.specs || []).slice().sort().join(",");
|
|
1640
|
+
const arr = bySpec.get(key) || [];
|
|
1641
|
+
arr.push(q);
|
|
1642
|
+
bySpec.set(key, arr);
|
|
1643
|
+
}
|
|
1644
|
+
return bySpec;
|
|
1645
|
+
}
|
|
1646
|
+
function buildXrefURL(endpoint, specKey, qs) {
|
|
1647
|
+
const params = new URLSearchParams();
|
|
1648
|
+
for (const q of qs) {
|
|
1649
|
+
params.append("terms", q.term);
|
|
1650
|
+
}
|
|
1651
|
+
if (specKey) params.set("cite", specKey);
|
|
1652
|
+
return `${endpoint}?${params.toString()}`;
|
|
1653
|
+
}
|
|
1654
|
+
function mapXrefResults(data, q) {
|
|
1655
|
+
const list = data[q.term.toLowerCase()] || [];
|
|
1656
|
+
return list.map((item) => ({
|
|
1657
|
+
href: item.uri || item.url || item.href,
|
|
1658
|
+
text: item.title || item.term || item.text,
|
|
1659
|
+
cite: item.spec || item.shortname
|
|
1660
|
+
})).filter((r) => !!r.href);
|
|
1661
|
+
}
|
|
1662
|
+
var RespecXrefResolver = class {
|
|
1663
|
+
constructor(fetchFn = fetch, endpoint = "https://respec.org/xref") {
|
|
1664
|
+
this.fetchFn = fetchFn;
|
|
1665
|
+
this.endpoint = endpoint;
|
|
1666
|
+
}
|
|
1667
|
+
async resolveBatch(queries) {
|
|
1668
|
+
const results = /* @__PURE__ */ new Map();
|
|
1669
|
+
for (const [specKey, qs] of groupQueriesBySpec(queries)) {
|
|
1670
|
+
const url = buildXrefURL(this.endpoint, specKey, qs);
|
|
1671
|
+
try {
|
|
1672
|
+
const resp = await this.fetchFn(url);
|
|
1673
|
+
const data = await resp.json();
|
|
1674
|
+
for (const q of qs) {
|
|
1675
|
+
results.set(q.id || q.term, mapXrefResults(data, q));
|
|
1676
|
+
}
|
|
1677
|
+
} catch {
|
|
1678
|
+
for (const q of qs) {
|
|
1679
|
+
results.set(q.id || q.term, []);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return results;
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
|
|
910
1687
|
// src/node.ts
|
|
911
1688
|
var Speculator2 = class extends Speculator {
|
|
912
1689
|
constructor(options = {}) {
|
|
@@ -917,13 +1694,18 @@ var Speculator2 = class extends Speculator {
|
|
|
917
1694
|
0 && (module.exports = {
|
|
918
1695
|
DOMHtmlRenderer,
|
|
919
1696
|
FormatProcessor,
|
|
1697
|
+
FormatRegistry,
|
|
920
1698
|
IncludeProcessor,
|
|
921
1699
|
LinkedomHtmlRenderer,
|
|
1700
|
+
RespecXrefResolver,
|
|
922
1701
|
Speculator,
|
|
923
1702
|
SpeculatorError,
|
|
1703
|
+
StatsTracker,
|
|
924
1704
|
browserFileLoader,
|
|
925
1705
|
createFallbackFileLoader,
|
|
926
1706
|
createMarkdownRenderer,
|
|
1707
|
+
fromRespecConfig,
|
|
1708
|
+
getChangedOutputAreas,
|
|
927
1709
|
getDefaultFileLoader,
|
|
928
1710
|
nodeFileLoader,
|
|
929
1711
|
parseMarkdown
|