@openuji/speculator 0.3.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/dist/node.cjs CHANGED
@@ -32,21 +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,
37
38
  RespecXrefResolver: () => RespecXrefResolver,
38
39
  Speculator: () => Speculator2,
39
40
  SpeculatorError: () => SpeculatorError,
41
+ StatsTracker: () => StatsTracker,
40
42
  browserFileLoader: () => browserFileLoader,
41
43
  createFallbackFileLoader: () => createFallbackFileLoader,
42
44
  createMarkdownRenderer: () => createMarkdownRenderer,
45
+ fromRespecConfig: () => fromRespecConfig,
46
+ getChangedOutputAreas: () => getChangedOutputAreas,
43
47
  getDefaultFileLoader: () => getDefaultFileLoader,
44
48
  nodeFileLoader: () => nodeFileLoader,
45
49
  parseMarkdown: () => parseMarkdown
46
50
  });
47
51
  module.exports = __toCommonJS(node_exports);
48
52
 
49
- // src/utils/file-loader.ts
53
+ // src/utils/file-loader/node.ts
50
54
  var nodeFileLoader = async (path) => {
51
55
  const fs = await import("fs/promises");
52
56
  const { fileURLToPath } = await import("url");
@@ -57,6 +61,8 @@ var nodeFileLoader = async (path) => {
57
61
  throw new Error(`Failed to load file: ${path}. ${error instanceof Error ? error.message : "Unknown error"}`);
58
62
  }
59
63
  };
64
+
65
+ // src/utils/file-loader/browser.ts
60
66
  var browserFileLoader = async (path) => {
61
67
  try {
62
68
  const response = await fetch(path);
@@ -68,6 +74,8 @@ var browserFileLoader = async (path) => {
68
74
  throw new Error(`Failed to fetch file: ${path}. ${error instanceof Error ? error.message : "Network error"}`);
69
75
  }
70
76
  };
77
+
78
+ // src/utils/file-loader/index.ts
71
79
  function createFallbackFileLoader(loaders) {
72
80
  return async (path) => {
73
81
  let lastError;
@@ -101,6 +109,138 @@ var SpeculatorError = class extends Error {
101
109
  this.name = "SpeculatorError";
102
110
  }
103
111
  };
112
+ function fromRespecConfig(respec) {
113
+ return { ...respec };
114
+ }
115
+
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;
134
+ }
135
+ }
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
+ };
104
244
 
105
245
  // src/pipeline/postprocess.ts
106
246
  var Postprocessor = class {
@@ -108,70 +248,106 @@ var Postprocessor = class {
108
248
  this.passes = passes;
109
249
  }
110
250
  /**
111
- * Run the configured passes on the given root element.
112
- *
113
- * @param root The document root to process.
251
+ * Run the configured passes.
114
252
  * @param areas Optional list of output areas to run. If omitted, all passes
115
253
  * are executed.
116
254
  * @param options Configuration options for the passes.
117
255
  */
118
- async run(root, areas, options = {}) {
119
- const warnings = [];
120
- const outputs = {};
256
+ async run(config, areas) {
257
+ const ctx = { outputs: {}, warnings: [], config };
121
258
  const active = areas ? this.passes.filter((p) => areas.includes(p.area)) : this.passes;
122
- for (const pass of active) {
123
- const current = outputs[pass.area];
124
- const result = await pass.run(root, current, options);
125
- if (result.data !== void 0) {
126
- outputs[pass.area] = result.data;
127
- }
128
- if (result.warnings && result.warnings.length) {
129
- warnings.push(...result.warnings);
130
- }
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
+ };
276
+ }
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);
131
295
  }
132
- return { outputs, warnings };
133
296
  }
134
297
  };
135
298
 
136
299
  // src/processors/include-processor.ts
137
300
  var IncludeProcessor = class {
138
- constructor(baseUrl, fileLoader, formatProcessor) {
301
+ constructor(baseUrl, fileLoader, formatRegistry) {
139
302
  this.baseUrl = baseUrl;
140
303
  this.fileLoader = fileLoader;
141
- this.formatProcessor = formatProcessor;
304
+ this.formatRegistry = formatRegistry;
305
+ }
306
+ matches(element) {
307
+ return element.hasAttribute("data-include");
142
308
  }
143
- async process(element, stats, warnings) {
309
+ async process(element, tracker, warnings) {
144
310
  const includePath = element.getAttribute("data-include");
145
311
  const includeFormat = element.getAttribute("data-include-format") || "text";
146
312
  if (!includePath) {
147
313
  warnings.push("data-include attribute is empty");
148
- return;
314
+ element.removeAttribute("data-include");
315
+ element.removeAttribute("data-include-format");
316
+ return { content: null };
149
317
  }
150
318
  try {
151
319
  const fullPath = this.resolveFilePath(includePath);
152
320
  const content = await this.fileLoader(fullPath);
153
- const processedContent = this.formatProcessor.processContent(content, includeFormat);
154
- element.innerHTML = processedContent;
155
- stats.filesIncluded++;
321
+ const processedContent = this.formatRegistry.processContent(content, includeFormat);
322
+ tracker.incrementFiles();
156
323
  if (includeFormat === "markdown") {
157
- stats.markdownBlocks++;
324
+ tracker.incrementMarkdownBlocks();
158
325
  }
326
+ element.removeAttribute("data-include");
327
+ element.removeAttribute("data-include-format");
328
+ return { content: processedContent };
159
329
  } catch (error) {
160
330
  const errorMsg = `Failed to load: ${includePath}`;
161
- warnings.push(errorMsg);
162
- element.innerHTML = `<p class="error">${errorMsg}</p>`;
163
- throw new SpeculatorError(errorMsg, element, includePath);
331
+ element.removeAttribute("data-include");
332
+ element.removeAttribute("data-include-format");
333
+ return { content: null, error: errorMsg };
164
334
  }
165
- element.removeAttribute("data-include");
166
- element.removeAttribute("data-include-format");
167
335
  }
168
336
  resolveFilePath(path) {
169
337
  const filePath = new URL(path, this.baseUrl || "file:///").toString();
170
- console.log(`Resolved file path: ${filePath}`);
338
+ logger.debug(`Resolved file path: ${filePath}`);
171
339
  return filePath;
172
340
  }
173
341
  };
174
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
+
175
351
  // src/markdown/index.ts
176
352
  var import_markdown_it = __toESM(require("markdown-it"), 1);
177
353
 
@@ -252,6 +428,72 @@ function respecCitePlugin(md) {
252
428
  md.inline.ruler.before("link", NAME, tokenize);
253
429
  }
254
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
+
255
497
  // src/markdown/index.ts
256
498
  function createMarkdownRenderer(options = {}) {
257
499
  const md = new import_markdown_it.default({
@@ -289,6 +531,10 @@ function createMarkdownRenderer(options = {}) {
289
531
  md.use(respecConceptPlugin);
290
532
  md.use(respecIdlPlugin);
291
533
  md.use(respecCitePlugin);
534
+ if (options.mermaid) {
535
+ const config = options.mermaid === true ? {} : options.mermaid;
536
+ md.use(respecMermaidPlugin, config);
537
+ }
292
538
  for (const extension of options.extensions ?? []) {
293
539
  if (Array.isArray(extension)) {
294
540
  const [plugin, pluginOptions] = extension;
@@ -305,56 +551,88 @@ function parseMarkdown(markdown, options = {}, env) {
305
551
  return md.render(markdown, env);
306
552
  } catch (error) {
307
553
  console.error("Markdown parsing error:", error);
308
- return `<p class="error">Error parsing markdown: ${error instanceof Error ? error.message : "Unknown error"}</p>`;
554
+ return renderError(
555
+ `Error parsing markdown: ${error instanceof Error ? error.message : "Unknown error"}`
556
+ );
309
557
  }
310
558
  }
311
559
  function slugify(content) {
312
560
  return content.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-");
313
561
  }
314
562
 
315
- // src/utils/strip-ident.ts
316
- function stripIndent(content) {
317
- const lines = content.replace(/\r\n?/g, "\n").split("\n");
318
- const indents = lines.filter((line) => line.trim().length > 0).map((line) => line.match(/^\s*/)?.[0].length || 0);
319
- const minIndent = indents.length > 0 ? Math.min(...indents) : 0;
320
- return lines.map((line) => line.slice(minIndent)).join("\n");
321
- }
322
-
323
- // src/processors/format-processor.ts
324
- var FormatProcessor = class {
325
- constructor(markdownOptions = {}) {
326
- this.markdownOptions = markdownOptions;
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);
327
595
  }
328
596
  /**
329
597
  * Convert content based on the specified format.
330
598
  */
331
599
  processContent(content, format) {
332
- switch (format) {
333
- case "markdown":
334
- return parseMarkdown(content, this.markdownOptions);
335
- case "text":
336
- case "html":
337
- default:
338
- return content;
600
+ const strategy = this.strategies.get(format);
601
+ if (!strategy) {
602
+ throw new Error(`Unsupported format: ${format}`);
339
603
  }
604
+ return strategy.convert(content);
340
605
  }
341
- /**
342
- * Process an element with a data-format attribute.
343
- */
344
- process(element, stats, warnings) {
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) {
345
617
  const format = element.getAttribute("data-format");
618
+ let result = {};
346
619
  if (format === "markdown" && element.innerHTML.trim()) {
347
620
  try {
348
621
  const markdownContent = stripIndent(element.innerHTML).trim();
349
- element.innerHTML = this.processContent(markdownContent, format);
350
- stats.markdownBlocks++;
622
+ result.content = this.registry.processContent(markdownContent, format);
623
+ tracker.incrementMarkdownBlocks();
624
+ } catch (error) {
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);
351
630
  } catch (error) {
352
- const errorMsg = `Failed to process markdown: ${error instanceof Error ? error.message : "Unknown error"}`;
353
- warnings.push(errorMsg);
354
- element.innerHTML = `<p class="error">${errorMsg}</p>`;
631
+ result.error = error instanceof Error ? error.message : String(error);
355
632
  }
356
633
  }
357
634
  element.removeAttribute("data-format");
635
+ return result;
358
636
  }
359
637
  };
360
638
 
@@ -498,18 +776,27 @@ function resolveIdlLinks(root, index, warnings, suppressClass) {
498
776
  }
499
777
  });
500
778
  }
501
- var idlPass = {
502
- area: "idl",
503
- async run(root, _data, options) {
779
+ var IdlPass = class {
780
+ constructor(root) {
781
+ this.root = root;
782
+ this.area = "idl";
783
+ }
784
+ async execute(_data, config) {
504
785
  const warnings = [];
505
- const suppressClass = options.diagnostics?.suppressClass ?? "no-link-warnings";
506
- const index = buildIdlIndex(root, warnings);
507
- resolveIdlLinks(root, index, warnings, suppressClass);
786
+ const suppressClass = config.postprocess?.diagnostics?.suppressClass ?? "no-link-warnings";
787
+ const index = buildIdlIndex(this.root, warnings);
788
+ resolveIdlLinks(this.root, index, warnings, suppressClass);
508
789
  return { warnings };
509
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();
796
+ }
510
797
  };
511
798
 
512
- // src/pipeline/passes/xref.ts
799
+ // src/xref/local-map.ts
513
800
  function uniqueId2(doc, base) {
514
801
  let id = base;
515
802
  let i = 2;
@@ -522,20 +809,6 @@ function norm2(term) {
522
809
  function slugify2(text) {
523
810
  return text.trim().toLowerCase().replace(/[^\w]+/g, "-").replace(/^-+|-+$/g, "");
524
811
  }
525
- function isSuppressed(node, suppressClass) {
526
- return !!node.closest(`.${suppressClass}`);
527
- }
528
- function getCiteSpecs(node) {
529
- let el = node;
530
- while (el) {
531
- const cite = el.getAttribute("data-cite");
532
- if (cite) {
533
- return cite.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
534
- }
535
- el = el.parentElement;
536
- }
537
- return void 0;
538
- }
539
812
  function buildLocalMap(root) {
540
813
  const map = /* @__PURE__ */ new Map();
541
814
  const doc = root.ownerDocument;
@@ -580,136 +853,159 @@ function buildLocalMap(root) {
580
853
  });
581
854
  return map;
582
855
  }
583
- var xrefPass = {
584
- area: "xref",
585
- async run(root, _data, options) {
586
- const suppressClass = options.diagnostics?.suppressClass ?? "no-link-warnings";
587
- const warnings = [];
588
- const localMap = buildLocalMap(root);
589
- const xrefAnchors = Array.from(root.querySelectorAll("a[data-xref]"));
590
- const resolverConfigs = Array.isArray(options.xref) ? options.xref : options.xref ? [options.xref] : [];
591
- const unresolved = /* @__PURE__ */ new Map();
592
- for (const a of xrefAnchors) {
593
- if (isSuppressed(a, suppressClass)) continue;
594
- const term = a.getAttribute("data-xref") || "";
595
- const key = norm2(term);
596
- const hit = localMap.get(key);
597
- if (hit) {
598
- a.setAttribute("href", hit.href);
599
- continue;
600
- }
601
- const specsOverride = getCiteSpecs(a);
602
- const mapKey = `${key}|${(specsOverride || []).join(",")}`;
603
- let entry = unresolved.get(mapKey);
604
- if (!entry) {
605
- entry = { term, anchors: [], results: [] };
606
- if (specsOverride) entry.specsOverride = specsOverride;
607
- unresolved.set(mapKey, entry);
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;
868
+ } else {
869
+ specs = cfg.specs;
608
870
  }
609
- entry.anchors.push(a);
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);
610
877
  }
611
- for (const cfg of resolverConfigs) {
612
- const queries = [];
613
- const idMap = /* @__PURE__ */ new Map();
614
- for (const [key, entry] of unresolved.entries()) {
615
- let specs;
616
- if (entry.specsOverride) {
617
- const allowed = cfg.specs ? entry.specsOverride.filter((s) => cfg.specs.includes(s)) : entry.specsOverride;
618
- specs = allowed;
619
- } else {
620
- specs = cfg.specs;
621
- }
622
- if (specs && specs.length === 0) continue;
623
- const id = `${key}|${queries.length}`;
624
- const q = { id, term: entry.term };
625
- if (specs && specs.length) q.specs = specs;
626
- queries.push(q);
627
- idMap.set(id, entry);
628
- }
629
- if (!queries.length) continue;
630
- try {
631
- const resMap = await cfg.resolver.resolveBatch(queries);
632
- for (const [id, hits] of resMap.entries()) {
633
- const entry = idMap.get(id);
634
- if (entry) entry.results.push(...hits);
635
- }
636
- } catch (err) {
637
- warnings.push(
638
- `Xref resolver failed: ${err instanceof Error ? err.message : String(err)}`
639
- );
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);
640
884
  }
885
+ } catch (err) {
886
+ warnings.push(
887
+ `Xref resolver failed: ${err instanceof Error ? err.message : String(err)}`
888
+ );
641
889
  }
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);
904
+ }
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
+ );
642
924
  const defaultPriority = resolverConfigs.flatMap((cfg) => cfg.specs || []);
643
- for (const entry of unresolved.values()) {
644
- const hits = entry.results;
645
- let chosen;
646
- let ambiguous = false;
647
- const preferred = entry.specsOverride && entry.specsOverride.length ? entry.specsOverride : defaultPriority;
648
- if (hits.length > 0) {
649
- if (preferred && preferred.length) {
650
- const remaining = new Set(hits);
651
- for (const spec of preferred) {
652
- const matches = hits.filter((h) => h.cite === spec);
653
- matches.forEach((m) => remaining.delete(m));
654
- if (matches.length === 1) {
655
- chosen = matches[0];
656
- break;
657
- }
658
- if (matches.length > 1) {
659
- ambiguous = true;
660
- break;
661
- }
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();
933
+ }
934
+ };
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);
955
+ }
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;
662
974
  }
663
- if (!chosen && !ambiguous) {
664
- const leftovers = Array.from(remaining);
665
- if (leftovers.length === 1) {
666
- chosen = leftovers[0];
667
- } else if (leftovers.length > 1) {
668
- ambiguous = true;
669
- }
975
+ if (matches.length > 1) {
976
+ ambiguous = true;
977
+ break;
670
978
  }
671
- } else if (hits.length === 1) {
672
- chosen = hits[0];
673
- } else {
674
- ambiguous = true;
675
979
  }
676
- }
677
- if (chosen) {
678
- for (const a of entry.anchors) {
679
- a.setAttribute("href", chosen.href);
680
- if (chosen.cite) a.setAttribute("data-cite", chosen.cite);
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
+ }
681
987
  }
682
- } else if (ambiguous) {
683
- warnings.push(`Ambiguous xref: "${entry.term}"`);
988
+ } else if (hits.length === 1) {
989
+ chosen = hits[0];
684
990
  } else {
685
- warnings.push(`No matching xref: "${entry.term}"`);
991
+ ambiguous = true;
686
992
  }
687
993
  }
688
- return { warnings };
689
- }
690
- };
691
-
692
- // src/pipeline/passes/references.ts
693
- function ensureSection(root, id, title) {
694
- let section = root.querySelector(`#${id}`);
695
- if (!section) {
696
- section = root.ownerDocument.createElement("section");
697
- section.id = id;
698
- section.innerHTML = `<h2>${title}</h2>`;
699
- root.appendChild(section);
700
- }
701
- return section;
702
- }
703
- function ensureSubsection(parent, id, title) {
704
- let sec = parent.querySelector(`#${id}`);
705
- if (!sec) {
706
- sec = parent.ownerDocument.createElement("section");
707
- sec.id = id;
708
- sec.innerHTML = `<h3>${title}</h3><ul></ul>`;
709
- parent.appendChild(sec);
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
+ }
710
1004
  }
711
- return sec;
1005
+ return warnings;
712
1006
  }
1007
+
1008
+ // src/renderers/references-renderer.ts
713
1009
  function formatEntry(id, e) {
714
1010
  const parts = [];
715
1011
  parts.push(`<span class="ref-id">[${id}]</span>`);
@@ -725,88 +1021,111 @@ function formatEntry(id, e) {
725
1021
  function idForRef(id) {
726
1022
  return `bib-${id.toLowerCase()}`;
727
1023
  }
728
- var referencesPass = {
729
- area: "references",
730
- async run(root, _data, options) {
731
- const warnings = [];
732
- const biblio = options.biblio?.entries ?? {};
733
- const cites = Array.from(root.querySelectorAll("a[data-spec]"));
734
- if (!cites.length) return { warnings };
735
- const normative = /* @__PURE__ */ new Set();
736
- const informative = /* @__PURE__ */ new Set();
737
- for (const a of cites) {
738
- const id = (a.getAttribute("data-spec") || "").trim();
739
- const norm3 = (a.getAttribute("data-normative") || "false") === "true";
740
- (norm3 ? normative : informative).add(id);
741
- }
742
- for (const id of normative) informative.delete(id);
743
- const refs = ensureSection(root, "references", "References");
744
- const normSec = ensureSubsection(refs, "normative-references", "Normative references");
745
- const infoSec = ensureSubsection(refs, "informative-references", "Informative references");
746
- 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) => {
747
1041
  const ul = sec.querySelector("ul");
748
1042
  ul.innerHTML = "";
749
- Array.from(ids).sort((a, b) => a.localeCompare(b)).forEach((id) => {
750
- const li = root.ownerDocument.createElement("li");
1043
+ items.sort((a, b) => a.id.localeCompare(b.id)).forEach(({ id, entry }) => {
1044
+ const li = this.doc.createElement("li");
751
1045
  li.id = idForRef(id);
752
- const entry = biblio[id];
753
1046
  if (entry) {
754
1047
  li.innerHTML = formatEntry(id, entry);
755
1048
  } else {
756
1049
  li.setAttribute("data-spec", id);
757
1050
  li.innerHTML = `<span class="ref-id">[${id}]</span> <span class="ref-missing">\u2014 unresolved reference</span>`;
758
- warnings.push(`Unresolved reference: "${id}"`);
759
1051
  }
760
1052
  ul.appendChild(li);
761
1053
  });
762
1054
  };
763
- renderList(normSec, normative);
764
- 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 = [];
765
1095
  for (const a of cites) {
766
1096
  const id = (a.getAttribute("data-spec") || "").trim();
767
1097
  const targetId = idForRef(id);
768
- a.setAttribute("href", `#${targetId}`);
769
- a.setAttribute("data-cite-ref", targetId);
1098
+ citeUpdates.push({ element: a, href: `#${targetId}` });
770
1099
  }
771
- 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();
772
1108
  }
773
1109
  };
774
1110
 
775
1111
  // src/pipeline/passes/boilerplate.ts
776
- function createSection(doc, id, title, content) {
777
- const sec = doc.createElement("section");
778
- sec.id = id;
779
- const h2 = doc.createElement("h2");
780
- h2.textContent = title;
781
- sec.appendChild(h2);
782
- if (content) {
783
- const p = doc.createElement("p");
784
- p.textContent = content;
785
- sec.appendChild(p);
1112
+ var BoilerplatePass = class {
1113
+ constructor(root) {
1114
+ this.root = root;
1115
+ this.area = "boilerplate";
786
1116
  }
787
- return sec;
788
- }
789
- var boilerplatePass = {
790
- area: "boilerplate",
791
- async run(root, _data, options) {
792
- const bp = options.boilerplate;
793
- if (!bp) return { warnings: [] };
794
- const doc = root.ownerDocument;
1117
+ async execute(_data, config) {
1118
+ const bp = config.postprocess?.boilerplate;
1119
+ if (!bp) return { data: { sections: [], ref: null }, warnings: [] };
795
1120
  const mountMode = bp.mount || "end";
796
1121
  let ref = null;
797
1122
  if (mountMode === "before-references") {
798
- ref = root.querySelector("#references");
1123
+ ref = this.root.querySelector("#references");
799
1124
  } else if (mountMode === "after-toc") {
800
- const toc = root.querySelector("#toc");
1125
+ const toc = this.root.querySelector("#toc");
801
1126
  ref = toc ? toc.nextSibling : null;
802
1127
  }
803
- const insert = (section) => {
804
- if (ref) {
805
- root.insertBefore(section, ref);
806
- } else {
807
- root.appendChild(section);
808
- }
809
- };
1128
+ const sections = [];
810
1129
  const defs = [
811
1130
  { key: "conformance", title: "Conformance" },
812
1131
  { key: "security", title: "Security" },
@@ -817,54 +1136,138 @@ var boilerplatePass = {
817
1136
  if (!opt) continue;
818
1137
  const cfg = typeof opt === "object" ? opt : {};
819
1138
  const id = cfg.id || key;
820
- if (root.querySelector(`#${id}`)) continue;
1139
+ if (this.root.querySelector(`#${id}`)) continue;
821
1140
  const title = cfg.title || defaultTitle;
822
1141
  const content = cfg.content;
823
- const sec = createSection(doc, id, title, content);
824
- insert(sec);
1142
+ const descriptor = { id, title };
1143
+ if (content !== void 0) descriptor.content = content;
1144
+ sections.push(descriptor);
825
1145
  }
826
- return { warnings: [] };
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();
827
1154
  }
828
1155
  };
829
1156
 
830
- // src/pipeline/passes/toc.ts
831
- var tocPass = {
832
- area: "toc",
833
- async run(root, _data, options) {
834
- const { toc } = options;
835
- if (toc?.enabled === false) return { warnings: [] };
836
- const selector = toc?.selector ?? "#toc";
837
- const mount = root.querySelector(selector);
838
- if (!mount) return { warnings: [] };
839
- const headings = Array.from(root.querySelectorAll("h2, h3"));
840
- if (!headings.length) return { warnings: [] };
841
- const doc = root.ownerDocument;
842
- const ol = doc.createElement("ol");
843
- ol.setAttribute("role", "list");
844
- for (const h of headings) {
845
- if (!h.id) continue;
846
- const li = doc.createElement("li");
847
- const depth = h.tagName.toLowerCase() === "h3" ? 2 : 1;
848
- li.setAttribute("data-depth", String(depth));
849
- li.innerHTML = `<a href="#${h.id}">${h.textContent || ""}</a>`;
850
- ol.appendChild(li);
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);
851
1219
  }
852
- mount.innerHTML = "";
853
- mount.appendChild(ol);
854
- return { warnings: [] };
1220
+ return { toc: rootOl.outerHTML };
1221
+ }
1222
+ };
1223
+
1224
+ // src/pipeline/passes/toc.ts
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();
855
1255
  }
856
1256
  };
857
1257
 
858
1258
  // src/pipeline/passes/diagnostics.ts
859
- var diagnosticsPass = {
860
- area: "diagnostics",
861
- async run(root, _data, options) {
1259
+ var DiagnosticsPass = class {
1260
+ constructor(root) {
1261
+ this.root = root;
1262
+ this.area = "diagnostics";
1263
+ }
1264
+ async execute(_data, config) {
862
1265
  const warnings = [];
863
- const suppressClass = options.diagnostics?.suppressClass ?? "no-link-warnings";
864
- const idsAndLinks = options.diagnostics?.idsAndLinks ?? true;
1266
+ const suppressClass = config.postprocess?.diagnostics?.suppressClass ?? "no-link-warnings";
1267
+ const idsAndLinks = config.postprocess?.diagnostics?.idsAndLinks ?? true;
865
1268
  if (idsAndLinks) {
866
1269
  const seen = /* @__PURE__ */ new Map();
867
- root.querySelectorAll("[id]").forEach((el) => {
1270
+ this.root.querySelectorAll("[id]").forEach((el) => {
868
1271
  const id = el.id;
869
1272
  if (!id) return;
870
1273
  if (seen.has(id)) {
@@ -875,7 +1278,7 @@ var diagnosticsPass = {
875
1278
  seen.set(id, el);
876
1279
  }
877
1280
  });
878
- const anchors = root.querySelectorAll("a");
1281
+ const anchors = this.root.querySelectorAll("a");
879
1282
  anchors.forEach((a) => {
880
1283
  if (a.closest(`.${suppressClass}`)) return;
881
1284
  const hasHref = a.hasAttribute("href") && a.getAttribute("href") !== "";
@@ -888,50 +1291,204 @@ var diagnosticsPass = {
888
1291
  }
889
1292
  return { warnings };
890
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
+ }
891
1300
  };
892
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();
1398
+ }
1399
+ };
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
+
893
1448
  // src/speculator.ts
894
1449
  var Speculator = class {
895
1450
  constructor(options = {}) {
896
1451
  const baseUrl = options.baseUrl;
897
1452
  const fileLoader = options.fileLoader || getDefaultFileLoader();
898
1453
  const markdownOptions = options.markdownOptions || {};
899
- this.postprocessOptions = options.postprocess || {};
900
- this.formatProcessor = options.formatProcessor || new FormatProcessor(markdownOptions);
901
- this.includeProcessor = options.includeProcessor || new IncludeProcessor(baseUrl, fileLoader, this.formatProcessor);
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);
902
1458
  this.htmlRenderer = options.htmlRenderer || new DOMHtmlRenderer();
903
- this.postprocessor = new Postprocessor([
904
- idlPass,
905
- xrefPass,
906
- referencesPass,
907
- boilerplatePass,
908
- tocPass,
909
- diagnosticsPass
910
- ]);
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);
911
1480
  }
912
1481
  /**
913
1482
  * Process a single DOM element
914
1483
  */
915
- async processElement(element) {
916
- const startTime = performance.now();
917
- const stats = {
918
- elementsProcessed: 0,
919
- filesIncluded: 0,
920
- markdownBlocks: 0,
921
- processingTime: 0
922
- };
1484
+ async processElement(element, tracker = new StatsTracker()) {
1485
+ tracker.start();
923
1486
  const warnings = [];
924
1487
  const clonedElement = element.cloneNode(true);
925
1488
  try {
926
- if (clonedElement.hasAttribute("data-include")) {
927
- await this.includeProcessor.process(clonedElement, stats, warnings);
928
- }
929
- if (clonedElement.hasAttribute("data-format")) {
930
- this.formatProcessor.process(clonedElement, stats, warnings);
931
- }
932
- stats.elementsProcessed = 1;
933
- stats.processingTime = performance.now() - startTime;
934
- return { element: clonedElement, warnings, stats };
1489
+ await this.processNode(clonedElement, tracker, warnings);
1490
+ tracker.stop();
1491
+ return { element: clonedElement, warnings, stats: tracker.toJSON() };
935
1492
  } catch (error) {
936
1493
  throw new SpeculatorError(
937
1494
  `Failed to process element: ${error instanceof Error ? error.message : "Unknown error"}`,
@@ -939,71 +1496,126 @@ var Speculator = class {
939
1496
  );
940
1497
  }
941
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
+ }
942
1525
  /**
943
- * Process an entire document
1526
+ * Process an entire document described by a SpeculatorConfig
944
1527
  */
945
- async renderDocument(container) {
1528
+ async renderDocument(spec, configOrOutputs = {}) {
946
1529
  const startTime = performance.now();
947
- const sections = container.querySelectorAll(
948
- "section[data-include], section[data-format], *[data-include], *[data-format]"
949
- );
950
- const allStats = {
951
- elementsProcessed: 0,
952
- filesIncluded: 0,
953
- markdownBlocks: 0,
954
- processingTime: 0
1530
+ const config = {
1531
+ ...this.baseConfig,
1532
+ ...spec,
1533
+ postprocess: { ...this.baseConfig.postprocess || {}, ...spec.postprocess || {} }
955
1534
  };
956
- const allWarnings = [];
957
- const processedElements = [];
958
- for (const section of Array.from(sections)) {
959
- try {
960
- const result = await this.processElement(section);
961
- processedElements.push(result.element);
962
- allStats.elementsProcessed += result.stats.elementsProcessed;
963
- allStats.filesIncluded += result.stats.filesIncluded;
964
- allStats.markdownBlocks += result.stats.markdownBlocks;
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;
965
1550
  allWarnings.push(...result.warnings);
966
- } catch (error) {
967
- allWarnings.push(
968
- `Failed to process element: ${error instanceof Error ? error.message : "Unknown error"}`
969
- );
970
- processedElements.push(section);
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
+ }
971
1567
  }
972
- }
973
- sections.forEach((section, index) => {
974
- if (processedElements[index] && section.parentNode) {
975
- 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);
976
1571
  }
977
- });
978
- try {
979
- const areas = [
980
- "idl",
981
- "xref",
982
- "references",
983
- "boilerplate",
984
- "toc",
985
- "diagnostics"
986
- ];
987
- const { warnings } = await this.postprocessor.run(
988
- container,
989
- areas,
990
- this.postprocessOptions || {}
991
- );
992
- allWarnings.push(...warnings);
993
1572
  } catch (e) {
994
- allWarnings.push(`Postprocess failed: ${e instanceof Error ? e.message : "Unknown error"}`);
1573
+ allWarnings.push(
1574
+ `Postprocess failed: ${e instanceof Error ? e.message : "Unknown error"}`
1575
+ );
995
1576
  }
996
- allStats.processingTime = performance.now() - startTime;
997
- return { element: container, warnings: allWarnings, stats: allStats };
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 });
998
1596
  }
999
1597
  /**
1000
1598
  * Process HTML string and return processed HTML
1001
1599
  */
1002
1600
  async renderHTML(inputHtml) {
1003
- const container = this.htmlRenderer.parse(inputHtml);
1004
- const result = await this.renderDocument(container);
1005
- const html = this.htmlRenderer.serialize(result.element);
1006
- return { html, warnings: result.warnings, stats: result.stats };
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
+ };
1007
1619
  }
1008
1620
  };
1009
1621
 
@@ -1082,14 +1694,18 @@ var Speculator2 = class extends Speculator {
1082
1694
  0 && (module.exports = {
1083
1695
  DOMHtmlRenderer,
1084
1696
  FormatProcessor,
1697
+ FormatRegistry,
1085
1698
  IncludeProcessor,
1086
1699
  LinkedomHtmlRenderer,
1087
1700
  RespecXrefResolver,
1088
1701
  Speculator,
1089
1702
  SpeculatorError,
1703
+ StatsTracker,
1090
1704
  browserFileLoader,
1091
1705
  createFallbackFileLoader,
1092
1706
  createMarkdownRenderer,
1707
+ fromRespecConfig,
1708
+ getChangedOutputAreas,
1093
1709
  getDefaultFileLoader,
1094
1710
  nodeFileLoader,
1095
1711
  parseMarkdown