@openuji/speculator 0.2.0 → 0.3.1

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