@openuji/speculator 0.3.0 → 0.3.2

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