@baiyibai-antora/extensions 1.12.0 → 1.14.0

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.
Files changed (53) hide show
  1. package/CHANGELOG.adoc +25 -0
  2. package/README.adoc +17 -1
  3. package/dist/extensions/attribute-distiller/index.d.ts +6 -0
  4. package/dist/extensions/attribute-distiller/index.d.ts.map +1 -1
  5. package/dist/extensions/attribute-distiller/index.js +92 -16
  6. package/dist/extensions/attribute-distiller/index.js.map +1 -1
  7. package/dist/extensions/autoterm/index.d.ts +34 -25
  8. package/dist/extensions/autoterm/index.d.ts.map +1 -1
  9. package/dist/extensions/autoterm/index.js +52 -215
  10. package/dist/extensions/autoterm/index.js.map +1 -1
  11. package/dist/extensions/cite-it/asciidoc.d.ts +29 -0
  12. package/dist/extensions/cite-it/asciidoc.d.ts.map +1 -0
  13. package/dist/extensions/cite-it/asciidoc.js +32 -0
  14. package/dist/extensions/cite-it/asciidoc.js.map +1 -0
  15. package/dist/extensions/cite-it/assembly-numbering.d.ts +202 -0
  16. package/dist/extensions/cite-it/assembly-numbering.d.ts.map +1 -0
  17. package/dist/extensions/cite-it/assembly-numbering.js +385 -0
  18. package/dist/extensions/cite-it/assembly-numbering.js.map +1 -0
  19. package/dist/extensions/cite-it/csl.d.ts +45 -0
  20. package/dist/extensions/cite-it/csl.d.ts.map +1 -0
  21. package/dist/extensions/cite-it/csl.js +139 -0
  22. package/dist/extensions/cite-it/csl.js.map +1 -0
  23. package/dist/extensions/cite-it/index.d.ts +300 -0
  24. package/dist/extensions/cite-it/index.d.ts.map +1 -0
  25. package/dist/extensions/cite-it/index.js +1098 -0
  26. package/dist/extensions/cite-it/index.js.map +1 -0
  27. package/dist/extensions/cite-it/macro.d.ts +104 -0
  28. package/dist/extensions/cite-it/macro.d.ts.map +1 -0
  29. package/dist/extensions/cite-it/macro.js +245 -0
  30. package/dist/extensions/cite-it/macro.js.map +1 -0
  31. package/dist/extensions/cite-it/registry.d.ts +64 -0
  32. package/dist/extensions/cite-it/registry.d.ts.map +1 -0
  33. package/dist/extensions/cite-it/registry.js +157 -0
  34. package/dist/extensions/cite-it/registry.js.map +1 -0
  35. package/dist/extensions/trademark-it/index.d.ts +91 -22
  36. package/dist/extensions/trademark-it/index.d.ts.map +1 -1
  37. package/dist/extensions/trademark-it/index.js +239 -242
  38. package/dist/extensions/trademark-it/index.js.map +1 -1
  39. package/dist/lib/asciidoc-conditionals.d.ts +59 -0
  40. package/dist/lib/asciidoc-conditionals.d.ts.map +1 -0
  41. package/dist/lib/asciidoc-conditionals.js +0 -0
  42. package/dist/lib/asciidoc-conditionals.js.map +1 -0
  43. package/dist/lib/asciidoc-indexterms.d.ts +126 -0
  44. package/dist/lib/asciidoc-indexterms.d.ts.map +1 -0
  45. package/dist/lib/asciidoc-indexterms.js +261 -0
  46. package/dist/lib/asciidoc-indexterms.js.map +1 -0
  47. package/dist/lib/asciidoc-table.d.ts +32 -0
  48. package/dist/lib/asciidoc-table.d.ts.map +1 -0
  49. package/dist/lib/asciidoc-table.js +55 -0
  50. package/dist/lib/asciidoc-table.js.map +1 -0
  51. package/package.json +19 -4
  52. package/resources/cite-it/ieee-bracket.csl +519 -0
  53. package/resources/cite-it/locale-en-US.xml +864 -0
@@ -0,0 +1,1098 @@
1
+ "use strict";
2
+ /**
3
+ * @baiyibai-antora/extensions - cite-it
4
+ *
5
+ * Resolves `citeit:[key]` citations against BibTeX/BibLaTeX `.bib` files and
6
+ * generates a categorized bibliography partial. Numbering is a PDF/Antora
7
+ * Assembler-flattened-output concept only; ordinary, non-assembled HTML
8
+ * citations render a templated author/year fragment instead, carrying no
9
+ * number. See macro.ts and assembly-numbering.ts for the two rendering
10
+ * paths this splits into.
11
+ *
12
+ * A category is defined entirely by which `.bib` file(s) an entry lives in,
13
+ * never by a data field inside the entry. Each category is an ordered list
14
+ * of source files: a shared collected file first, then zero or more
15
+ * component-local override files; later files' entries override earlier
16
+ * files' entries by citation key, the same override-by-key semantics
17
+ * autoterm's `mergeTerminology` already establishes.
18
+ *
19
+ * Each category generates its OWN partial (`categoryPartialName`,
20
+ * `<output_partial>-<ref_section>.adoc`). A component author routes a
21
+ * category to whichever page they choose, simply by including that
22
+ * category's own partial there; different categories may genuinely live
23
+ * on different pages, and citations correctly cross-link to whichever
24
+ * page actually hosts the cited entry (assembly-numbering.ts's
25
+ * `findBibliographyScopes`/`rewriteCiteMarkers`), not just a single
26
+ * hardcoded bibliography page.
27
+ *
28
+ * A component-version participates automatically, with no opt-in step at
29
+ * all: once the playbook-level `categories` scaffold (below) gives a
30
+ * component-version at least one category, it is scanned and gets its
31
+ * generated partial(s), whether or not its own antora.yml mentions
32
+ * cite-it at all. `ext.cite_it` in a component-version's antora.yml is
33
+ * purely an OPTIONAL override, the same standing as `enabled`, `style`,
34
+ * `output_partial`, and `write_to_disk`: present only when a component
35
+ * needs to diverge from the playbook default, never required just to
36
+ * take part.
37
+ *
38
+ * EARLIER DESIGN, REPLACED: this extension originally required an
39
+ * explicit `ext.cite_it` block, even an empty one, mirroring
40
+ * trademark-it's own opt-in gate (heavy per-page scanning and
41
+ * generated-partial injection are real per-build cost, so a component
42
+ * shouldn't pay it just because a sibling component happens to use
43
+ * cite-it). In practice this produced a bad failure mode: a page citing
44
+ * a key with `citeit:[key]` in a component-version that forgot the
45
+ * `ext.cite_it` block either silently rendered an unrelated component's
46
+ * entry (if the key happened to collide) or a generic "unknown key"
47
+ * message that read like a typo rather than "this component-version
48
+ * never participated" - either way, with no diagnostic trail under the
49
+ * `quiet: true` every real registration uses. Removed rather than
50
+ * patched further: every component-version now participates by default,
51
+ * so there is no opt-in state left to forget. The performance tradeoff
52
+ * this accepts (every component-version with categories available now
53
+ * gets scanned, not just ones that explicitly asked to be) is a real
54
+ * cost, not a free lunch; it was decided against keeping the gate.
55
+ *
56
+ * `categories` in the PLAYBOOK-level `cite_it` config (below) is the
57
+ * real scaffold, mirroring how `trademark_file`/`terminology_file` work
58
+ * for trademark-it/autoterm: declared once, inherited by every
59
+ * component-version automatically. A component-version's own
60
+ * `ext.cite_it.categories`, if present at all, is entirely optional and
61
+ * purely additive to the scaffold, matched by `ref_section` (see
62
+ * mergeCategoriesConfig): naming a section already in the scaffold
63
+ * extends it (its `ref_files`/`ref_force` append to the scaffold's),
64
+ * and naming a new section adds a category local to that component
65
+ * alone. A component-version with
66
+ * genuinely no categories anywhere, no playbook scaffold and no local
67
+ * `ext.cite_it.categories`, is still skipped: there is nothing to scan
68
+ * for.
69
+ *
70
+ * Configuration in antora-playbook.yml:
71
+ *
72
+ * antora:
73
+ * extensions:
74
+ * - require: '@baiyibai-antora/extensions/cite-it'
75
+ * cite_it:
76
+ * enabled: true
77
+ * # Only 'ieee-bracket' is bundled and supported in this version.
78
+ * style: 'ieee-bracket'
79
+ * # Base filename for each category's own generated partial
80
+ * # (e.g. section "standards" -> generated-bibliography-
81
+ * # standards.adoc), one per category, injected into each
82
+ * # participating component-version.
83
+ * output_partial: 'generated-bibliography.adoc'
84
+ * # Write each generated partial to disk in the component's local
85
+ * # worktree too, alongside the source files, for IDE include
86
+ * # resolution. Skipped automatically for a component loaded from
87
+ * # a remote source (no local worktree to write into).
88
+ * write_to_disk: false
89
+ * # The global scaffold: every participating component-version
90
+ * # inherits these categories as-is unless it names the same
91
+ * # ref_section itself, in which case its own entry extends this
92
+ * # one rather than replacing it.
93
+ * categories:
94
+ * - ref_section: standards
95
+ * ref_files: ['cite-it-data:ROOT:partial$standards.bib']
96
+ * - ref_section: literature
97
+ * ref_files: ['cite-it-data:ROOT:partial$literature-shared.bib']
98
+ * ref_force: ['rfc2119']
99
+ *
100
+ * The generated partial itself carries no heading; a category's bibliography
101
+ * is a `[horizontal]` list only. The page including it supplies its own
102
+ * heading, ordinary authored AsciiDoc, immediately above the include line:
103
+ *
104
+ * == Standards References
105
+ *
106
+ * include::partial$generated-bibliography-standards.adoc[]
107
+ *
108
+ * Per-component configuration in antora.yml, under ext.cite_it, opts a
109
+ * component-version into cite-it and MAY extend the playbook-level
110
+ * scaffold. An empty block inherits the scaffold unchanged:
111
+ *
112
+ * ext:
113
+ * cite_it: {}
114
+ *
115
+ * A component needing a local-only addition to an existing section, or a
116
+ * wholly local category the scaffold does not define, adds only that:
117
+ *
118
+ * ext:
119
+ * cite_it:
120
+ * categories:
121
+ * # Extends the scaffold's own "literature" section (above) with one
122
+ * # more file, appended after the scaffold's own ref_files.
123
+ * - ref_section: literature
124
+ * ref_files: ['literature-local.bib']
125
+ * # A section the scaffold does not define at all: local to this
126
+ * # component only.
127
+ * - ref_section: internal-notes
128
+ * ref_files: ['internal-notes.bib']
129
+ *
130
+ * `.bib` files are resolved from the content catalog's `partial` family,
131
+ * the same family autoterm's terminology-table.adoc and trademark-it's
132
+ * trademark-table.adoc already use for shared, non-rendered reference data.
133
+ * Neither of those is independently published either: a `.bib` file's own
134
+ * confidentiality comes from which repository it lives in and who can read
135
+ * that repository, not from its Antora family. A category's `ref_files`
136
+ * list is the SAME shared-then-local-override pattern autoterm/trademark-it
137
+ * already establish:
138
+ *
139
+ * - A resource-ID reference (`component:module:partial$relative`, for
140
+ * example `cite-it-data:ROOT:partial$standards.bib` above) resolves
141
+ * against the full catalog, including a component the SAME playbook
142
+ * added as its own separate `content.sources` entry. This is the
143
+ * intended shape for a shared bibliography maintained in its own
144
+ * repository, the same pattern `baiyibai-autoterm-data` already
145
+ * establishes for autoterm's shared terminology table.
146
+ * - A bare filename (`literature-local.bib` above) resolves against the
147
+ * PARTICIPATING component-version's own `partial` family files only.
148
+ * This is the shape for a component-local override or addition, living
149
+ * alongside that component's own pages, never published on its own,
150
+ * reached only through this resolution (not through `include::`, since
151
+ * `.bib` files are read directly, not transcluded).
152
+ *
153
+ * `ref_force` names citation keys always listed in that category's
154
+ * bibliography even when no page cites them, for example a standard a
155
+ * component must disclose regardless of whether it is quoted inline. It is
156
+ * nested inside the category it targets, not a separate flat list with a
157
+ * back-reference: which heading a force-included citation renders under is
158
+ * simply which category's `ref_force` names it, independent of which
159
+ * category's `ref_files` actually defines the underlying entry. In the
160
+ * example above, `rfc2119` is force-included under the `literature` heading
161
+ * even if its own entry is defined inside the `standards` category's `.bib`
162
+ * file: a component author choosing where a force-included citation appears
163
+ * is a display decision, not a statement about which file happens to hold
164
+ * the data.
165
+ *
166
+ * A `.bib` entry's own `supersededby` field, a custom field naming the
167
+ * citation key of the newer entry replacing it, is not a real BibLaTeX
168
+ * field (see registry.ts) but is read directly from the raw parsed entry
169
+ * regardless. When a category's generated partial actually lists such a
170
+ * key (cited or `ref_force`d), cite-it warns once at generation time,
171
+ * naming the newer key, so an author sees a newer revision exists without
172
+ * reading the `.bib` file directly. Bibtex itself carries no native
173
+ * concept of an outdated reference; this is a cite-it-specific convention,
174
+ * not a standard `.bib` field any external tool is expected to recognize.
175
+ * Deliberately not raised again at each `citeit:[key]` use site in prose,
176
+ * one signal per generated partial is enough; managing `.bib` data itself,
177
+ * including deciding when an entry is superseded, stays outside cite-it's
178
+ * own scope (see the docs page's own "Scope" section).
179
+ */
180
+ Object.defineProperty(exports, "__esModule", { value: true });
181
+ exports.register = register;
182
+ const log_time_1 = require("../../lib/log-time");
183
+ const asciidoc_conditionals_1 = require("../../lib/asciidoc-conditionals");
184
+ const asciidoc_indexterms_1 = require("../../lib/asciidoc-indexterms");
185
+ const index_1 = require("../trademark-it/index");
186
+ const index_2 = require("../autoterm/index");
187
+ const registry_1 = require("./registry");
188
+ const csl_1 = require("./csl");
189
+ const macro_1 = require("./macro");
190
+ const assembly_numbering_1 = require("./assembly-numbering");
191
+ // ---------------------------------------------------------------------------
192
+ // Logging helpers (mirrors trademark-it/autoterm style)
193
+ // ---------------------------------------------------------------------------
194
+ let quietMode = true;
195
+ function logEvent(message) {
196
+ console.log(`[${(0, log_time_1.nowLocal)()}] \x1b[94mEVENT\x1b[0m (cite-it): \x1b[94m${message}\x1b[0m`);
197
+ }
198
+ function makeLogger(hostLogger) {
199
+ return {
200
+ debug: (m) => { if (!quietMode) {
201
+ if (hostLogger?.debug)
202
+ hostLogger.debug(m);
203
+ else
204
+ console.debug(m);
205
+ } },
206
+ info: (m) => { if (!quietMode) {
207
+ if (hostLogger?.info)
208
+ hostLogger.info(m);
209
+ else
210
+ console.log(m);
211
+ } },
212
+ warn: (m) => { if (hostLogger?.warn)
213
+ hostLogger.warn(m);
214
+ else
215
+ console.warn(m); },
216
+ error: (m) => { if (hostLogger?.error)
217
+ hostLogger.error(m);
218
+ else
219
+ console.error(m); },
220
+ };
221
+ }
222
+ // ---------------------------------------------------------------------------
223
+ // Config helpers (mirrors trademark-it/autoterm style)
224
+ // ---------------------------------------------------------------------------
225
+ function toBool(v, def) {
226
+ if (v === true)
227
+ return true;
228
+ if (v === false)
229
+ return false;
230
+ if (typeof v === 'string') {
231
+ const s = v.trim().toLowerCase();
232
+ if (['true', '1', 'yes', 'on'].includes(s))
233
+ return true;
234
+ if (['false', '0', 'no', 'off'].includes(s))
235
+ return false;
236
+ }
237
+ return def;
238
+ }
239
+ function toStringArray(v) {
240
+ if (Array.isArray(v))
241
+ return v.map(String);
242
+ if (typeof v === 'string' && v.trim())
243
+ return v.split(',').map((s) => s.trim()).filter(Boolean);
244
+ return [];
245
+ }
246
+ /**
247
+ * Locates this extension's own entry in `playbook.antora.extensions` by
248
+ * whether it carries `cite_it`/`citeIt` as an own key, not by matching a
249
+ * substring of its `require` string. A require-string match breaks the
250
+ * moment a consumer vendors or re-exports this package under a different
251
+ * name or path: the require string then never contains "cite-it"/
252
+ * "cite_it" at all, `.find()` returns nothing, and every configured
253
+ * setting silently falls back to hardcoded defaults, no warning, no error
254
+ * (confirmed as a real report, not a hypothetical). Checking for the
255
+ * config key itself identifies the right entry regardless of how it was
256
+ * required; the require-string check remains only as a fallback for the
257
+ * case where no entry carries the key at all. An empty `ext.cite_it: {}`
258
+ * override is still caught correctly here, since the key is present even
259
+ * with no options set under it.
260
+ */
261
+ function findOwnExtEntry(entries) {
262
+ return entries.find((ext) => 'cite_it' in ext || 'citeIt' in ext)
263
+ ?? entries.find((ext) => {
264
+ const req = String(ext.require ?? '');
265
+ return req.includes('cite-it') || req.includes('cite_it');
266
+ })
267
+ ?? {};
268
+ }
269
+ function pick(obj, keys, def) {
270
+ for (const k of keys) {
271
+ if (obj && obj[k] !== undefined)
272
+ return obj[k];
273
+ }
274
+ return def;
275
+ }
276
+ function parseCategoriesConfig(v) {
277
+ if (!Array.isArray(v))
278
+ return [];
279
+ const result = [];
280
+ for (const raw of v) {
281
+ if (!raw || typeof raw !== 'object')
282
+ continue;
283
+ const r = raw;
284
+ const section = typeof r['ref_section'] === 'string' ? r['ref_section']
285
+ : typeof r['refSection'] === 'string' ? r['refSection'] : '';
286
+ if (!section)
287
+ continue;
288
+ result.push({
289
+ ref_section: section,
290
+ ref_files: toStringArray(r['ref_files'] ?? r['refFiles']),
291
+ ref_force: toStringArray(r['ref_force'] ?? r['refForce']),
292
+ });
293
+ }
294
+ return result;
295
+ }
296
+ // The playbook-level scaffold and a component's own categories[] merge by
297
+ // ref_section, the same shared-then-local-override precedence autoterm's
298
+ // mergeTerminology and a single category's own ref_files list both already
299
+ // establish: a section named at both levels extends (ref_files/ref_force
300
+ // concatenate, playbook first) rather than replaces, and a section named
301
+ // only at the component level is added as a wholly local category.
302
+ function mergeCategoriesConfig(scaffold, local) {
303
+ const bySection = new Map();
304
+ for (const cat of scaffold)
305
+ bySection.set(cat.ref_section, { ...cat });
306
+ for (const cat of local) {
307
+ const existing = bySection.get(cat.ref_section);
308
+ bySection.set(cat.ref_section, existing ? {
309
+ ref_section: cat.ref_section,
310
+ ref_files: [...existing.ref_files, ...cat.ref_files],
311
+ ref_force: [...existing.ref_force, ...cat.ref_force],
312
+ } : cat);
313
+ }
314
+ return [...bySection.values()].map((cat) => ({
315
+ ref_section: cat.ref_section,
316
+ ref_files: cat.ref_files,
317
+ ref_force: cat.ref_force,
318
+ }));
319
+ }
320
+ /**
321
+ * One generated partial per category, not one combined partial for the
322
+ * whole component-version: the key design feature is that an author decides
323
+ * where each category appears simply by choosing which page includes
324
+ * which category's own partial, and different categories may genuinely
325
+ * live on different pages, with citations correctly cross-linking to
326
+ * whichever page actually hosts the cited entry (see assembly-
327
+ * numbering.ts's `findBibliographyScopes`/`rewriteCiteMarkers`).
328
+ * `generated-bibliography.adoc` + category `standards` becomes
329
+ * `generated-bibliography-standards.adoc`.
330
+ */
331
+ function categoryPartialName(base, categoryKey) {
332
+ const dot = base.lastIndexOf('.');
333
+ return dot < 0 ? `${base}-${categoryKey}` : `${base.slice(0, dot)}-${categoryKey}${base.slice(dot)}`;
334
+ }
335
+ // ---------------------------------------------------------------------------
336
+ // `.bib` file resolution
337
+ // ---------------------------------------------------------------------------
338
+ const RESOURCE_ID_RE = /[:$]/;
339
+ /** `.bib` files live in the `partial` family: never independently
340
+ * published, and the same family autoterm/trademark-it already use for
341
+ * shared reference data collected from another repository. A resource-ID
342
+ * reference resolves against the full catalog (including a separate
343
+ * `content.sources` entry the playbook adds for a shared data component);
344
+ * a bare filename resolves only against the participating component-
345
+ * version's own `partial` family files (a local override or addition).
346
+ */
347
+ function resolveBibFiles(refs, contentCatalog, component, version, module, logger) {
348
+ const result = [];
349
+ for (const ref of refs) {
350
+ let file;
351
+ if (RESOURCE_ID_RE.test(ref) && contentCatalog.resolveResource) {
352
+ try {
353
+ file = contentCatalog.resolveResource(ref, { component, version, module }, 'partial');
354
+ }
355
+ catch {
356
+ file = undefined;
357
+ }
358
+ }
359
+ if (!file) {
360
+ file = contentCatalog.getFiles().find((f) => f.src.component === component &&
361
+ f.src.version === version &&
362
+ f.src.family === 'partial' &&
363
+ (f.src.relative === ref || f.src.basename === ref));
364
+ }
365
+ if (!file) {
366
+ logger.warn(`cite-it: .bib file "${ref}" not found (expected family 'partial') for ${component}@${version}`);
367
+ continue;
368
+ }
369
+ result.push(file);
370
+ }
371
+ return result;
372
+ }
373
+ // ---------------------------------------------------------------------------
374
+ // Orphaned citeit:[key] usage: a component-version that is not scanned
375
+ // ---------------------------------------------------------------------------
376
+ /**
377
+ * Checks a SKIPPED (non-participating) component-version's own pages for
378
+ * `citeit:[key]` usage that will never resolve, and records any found into
379
+ * `orphanedCiteKeys` (macro.ts) for the macro to detect at render time and
380
+ * report clearly, instead of either silently borrowing an unrelated
381
+ * component's data through the cross-component fallback (if the key
382
+ * happens to collide) or rendering a generic "unknown key" message that
383
+ * reads like a typo rather than "this component-version is not scanned".
384
+ *
385
+ * There is no opt-in gate to forget any more (see the module docblock):
386
+ * a component-version only reaches here for two real reasons, explicitly
387
+ * disabled (`ext.cite_it.enabled: false`) or genuinely no categories
388
+ * anywhere, no playbook scaffold and no local `ext.cite_it.categories`.
389
+ * Both are still worth flagging if the component-version's own pages cite
390
+ * something anyway, since the citation still cannot resolve.
391
+ *
392
+ * Deliberately does NOT resolve `include::` content, unlike the real scan
393
+ * pass for a participating component (which calls `collectIncludeText`):
394
+ * a wrapper page whose entire content is `include::other-component:...[]`
395
+ * correctly has nothing of its OWN to flag here, since the included
396
+ * content belongs to, and was authored for, a genuinely participating
397
+ * component. This is exactly what keeps the demo site's own
398
+ * include::-based wrapper pages (autoterm.adoc, trademark-it.adoc,
399
+ * cite-it.adoc's own siblings) from being misreported.
400
+ *
401
+ * A cheap substring pre-check gates the real, Asciidoctor-based scan
402
+ * (`scanCiteKeys`): a component-version that never mentions `citeit:[` at
403
+ * all pays nothing beyond a plain string search per page.
404
+ */
405
+ function checkOrphanedCiteUsage(compKey, reason, contentCatalog, component, version, logger) {
406
+ const pages = contentCatalog.getFiles().filter((f) => f.src.component === component && f.src.version === version && f.src.family === 'page');
407
+ if (pages.length === 0)
408
+ return;
409
+ const componentAttrs = (contentCatalog.getComponents?.() ?? [])
410
+ .find((c) => c.name === component)?.versions
411
+ .find((v) => (v.version ?? v.name) === version)?.asciidoc?.attributes ?? {};
412
+ const found = new Set();
413
+ for (const f of pages) {
414
+ const raw = f.contents.toString('utf8');
415
+ if (!raw.includes('citeit:['))
416
+ continue;
417
+ const headerAttrs = (0, asciidoc_conditionals_1.pageHeaderAttributes)(raw);
418
+ const isDefined = (attr) => {
419
+ if (headerAttrs.has(attr))
420
+ return headerAttrs.get(attr) === true;
421
+ const v = componentAttrs[attr];
422
+ return v !== undefined && v !== null && v !== false;
423
+ };
424
+ const text = (0, index_1.stripNonContent)(raw, isDefined, []);
425
+ for (const key of (0, macro_1.scanCiteKeys)((0, asciidoc_indexterms_1.sanitizeForScan)(text)))
426
+ found.add(key);
427
+ }
428
+ if (found.size === 0)
429
+ return;
430
+ macro_1.orphanedCiteKeys.set(compKey, found);
431
+ logger.warn(`cite-it: ${compKey} - ${reason}, but citeit:[${[...found].join('], citeit:[')}] is used directly on its own page(s); ` +
432
+ 'these citations will not resolve for this component-version, regardless of the playbook-level configuration');
433
+ }
434
+ // ---------------------------------------------------------------------------
435
+ // Antora extension entry point
436
+ // ---------------------------------------------------------------------------
437
+ function register(context) {
438
+ const ctx = context ?? this;
439
+ const logger = makeLogger(ctx.getLogger ? ctx.getLogger('cite-it') : undefined);
440
+ let cfg = {
441
+ enabled: true,
442
+ quiet: true,
443
+ style: 'ieee-bracket',
444
+ output_partial: 'generated-bibliography.adoc',
445
+ suppress_events: false,
446
+ write_to_disk: false,
447
+ categories: [],
448
+ };
449
+ const extByCompVer = new Map();
450
+ // Per-component-version scan context, captured during contentClassified
451
+ // and reused by the navigationBuilt handler below to compute true
452
+ // first-appearance citation numbers for PDF/assembled output, avoiding
453
+ // recomputing config resolution twice for the same component-version.
454
+ const assemblyScanContexts = new Map();
455
+ // component@version -> citation key -> owning category key. Needed at
456
+ // navigationBuilt time to turn findBibliographyScopes' per-CATEGORY
457
+ // page map into the per-KEY page map rewriteCiteMarkers actually needs.
458
+ const keyCategories = new Map();
459
+ // Populated later, at navigationBuilt time (see below), but declared here
460
+ // so the loadAsciiDoc wrapper registered immediately below can close over
461
+ // them: real content is not known yet when the wrapper is REGISTERED, only
462
+ // by the time it is actually CALLED, which is guaranteed to be later.
463
+ const citationNumbers = new Map();
464
+ const keyScopesByComp = new Map();
465
+ // Populated inside the if-block below, but declared here so
466
+ // ensureAssemblyDataComputed's own navigation self-heal (defined later
467
+ // in this same function) can reach both: the real, unwrapped loader,
468
+ // and this wrapper's own reference, needed to temporarily swap the
469
+ // wrapper back out and reinstall it. See that self-heal's own docblock
470
+ // for why.
471
+ let realLoadAsciiDoc;
472
+ let loadAsciiDocWrapper;
473
+ // The loadAsciiDoc wrapper is registered here, synchronously, at
474
+ // register() time itself, deliberately BEFORE any lifecycle event fires
475
+ // at all. `@antora/assembler`'s own assembleContent reads
476
+ // `context.getFunctions().loadAsciiDoc` exactly ONCE, synchronously, at
477
+ // the top of its own navigationBuilt listener, not lazily at the point it
478
+ // actually calls loadAsciiDoc, confirmed directly by reading
479
+ // assemble-content.js. If cite-it's own replaceFunctions call happened
480
+ // inside ITS OWN navigationBuilt listener instead, as an earlier version
481
+ // of this code did, whichever extension's navigationBuilt listener runs
482
+ // first would win: a playbook that lists cite-it after
483
+ // `@antora/pdf-extension` would silently get unrewritten citeit:[key]
484
+ // literal text in PDF output, no error, no warning, since
485
+ // assembleContent's own loadAsciiDoc reference would already be frozen to
486
+ // the original, unwrapped function before cite-it's replaceFunctions call
487
+ // ever ran. Registering here, at register() time, fires before every
488
+ // playbookBuilt/contentClassified/navigationBuilt listener regardless of
489
+ // where cite-it appears in the extensions list, so this no longer depends
490
+ // on registration order at all.
491
+ if (ctx.getFunctions && ctx.replaceFunctions && ctx.require) {
492
+ // Captured ONCE, right here, BEFORE replaceFunctions below installs the
493
+ // wrapper: reading getFunctions().loadAsciiDoc lazily INSIDE the
494
+ // wrapper itself, instead, would read back the wrapper's own already-
495
+ // installed replacement rather than the original, an infinite
496
+ // recursion confirmed directly (a real "Maximum call stack size
497
+ // exceeded" crash against a live build, not a hypothetical). The
498
+ // fallback to `@antora/asciidoc-loader`'s own export, for the case
499
+ // getFunctions() does not carry a real loadAsciiDoc yet this early in
500
+ // the lifecycle, mirrors exactly what `@antora/assembler`'s own
501
+ // assemble-content.js does for the same reason.
502
+ const realLoadAsciiDocFn = (ctx.getFunctions().loadAsciiDoc ??
503
+ ctx.require('@antora/asciidoc-loader').loadAsciiDoc);
504
+ realLoadAsciiDoc = realLoadAsciiDocFn;
505
+ // Named, not inline, so ensureAssemblyDataComputed's own navigation
506
+ // self-heal can reinstall this SAME reference after temporarily
507
+ // swapping it out; see that function's own docblock for why.
508
+ loadAsciiDocWrapper = (...args) => {
509
+ const [origFile, catalog, asciidocConfig] = args;
510
+ let file = origFile;
511
+ const isAssembled = !!(asciidocConfig?.attributes && 'loader-assembler' in asciidocConfig.attributes);
512
+ if (isAssembled && file?.contents && file.src?.component && file.src?.version) {
513
+ const compKey = `${file.src.component}@${file.src.version}`;
514
+ // @antora/assembler resolves the active profile as
515
+ // assemblyConfig.profile, defaulting to the export's own target
516
+ // backend (e.g. 'pdf') when no profile is configured explicitly
517
+ // — never bare `undefined` in an ordinary PDF build — then bakes
518
+ // it into every assembled document's own real attributes as
519
+ // 'assembler-profile' (assemble-content.js: intrinsicAttributes),
520
+ // right here in this same asciidocConfig. Passed through so
521
+ // ensureAssemblyDataComputed can look up navFiles under the
522
+ // SAME profile key assembler itself resolved, instead of always
523
+ // checking the unnamed default profile slot.
524
+ const profileName = asciidocConfig?.attributes?.['assembler-profile'];
525
+ ensureAssemblyDataComputed(compKey, profileName);
526
+ const numbers = citationNumbers.get(compKey);
527
+ if (numbers && numbers.size > 0) {
528
+ const keyScopes = keyScopesByComp.get(compKey) ?? null;
529
+ const before = file.contents.toString('utf8');
530
+ const after = (0, assembly_numbering_1.rewriteCiteMarkers)(before, numbers, true, keyScopes);
531
+ if (after !== before) {
532
+ file = Object.assign(Object.create(Object.getPrototypeOf(file)), file, { contents: Buffer.from(after, 'utf8') });
533
+ }
534
+ }
535
+ }
536
+ // realLoadAsciiDoc is always assigned by this point: this closure is
537
+ // only ever reachable after the synchronous assignment a few lines
538
+ // above it in this same if-block has already run.
539
+ return realLoadAsciiDoc(file, catalog, asciidocConfig ?? {});
540
+ };
541
+ ctx.replaceFunctions({ loadAsciiDoc: loadAsciiDocWrapper });
542
+ }
543
+ else {
544
+ logger.warn('cite-it: replaceFunctions/getFunctions/require unavailable on this extension context; PDF/assembled numbering will not be applied (ordinary HTML citations are unaffected)');
545
+ }
546
+ ctx.once('contentAggregated', ({ contentAggregate }) => {
547
+ for (const item of (contentAggregate ?? [])) {
548
+ const ciExt = item.ext?.['cite_it'];
549
+ if (ciExt && typeof ciExt === 'object') {
550
+ extByCompVer.set(`${item.name}@${item.version}`, ciExt);
551
+ }
552
+ }
553
+ });
554
+ ctx.once('playbookBuilt', async ({ playbook }) => {
555
+ const extEntry = findOwnExtEntry(playbook?.antora?.extensions ?? []);
556
+ const raw = (extEntry.cite_it ?? extEntry.citeIt ?? {});
557
+ cfg = {
558
+ enabled: toBool(pick(raw, ['enabled'], cfg.enabled), cfg.enabled),
559
+ quiet: toBool(pick(raw, ['quiet'], cfg.quiet), cfg.quiet),
560
+ style: String(pick(raw, ['style'], cfg.style)),
561
+ output_partial: String(pick(raw, ['output_partial', 'outputPartial'], cfg.output_partial)),
562
+ suppress_events: toBool(pick(raw, ['suppress_events', 'suppressEvents'], cfg.suppress_events), cfg.suppress_events),
563
+ write_to_disk: toBool(pick(raw, ['write_to_disk', 'writeToDisk'], cfg.write_to_disk), cfg.write_to_disk),
564
+ categories: Array.isArray(raw['categories']) ? parseCategoriesConfig(raw['categories']) : cfg.categories,
565
+ };
566
+ quietMode = cfg.quiet;
567
+ logger.debug(`cite-it config: ${JSON.stringify(cfg)}`);
568
+ });
569
+ ctx.once('contentClassified', async ({ contentCatalog }) => {
570
+ if (!cfg.enabled) {
571
+ logger.debug('cite-it disabled (skipped)');
572
+ return;
573
+ }
574
+ if (!contentCatalog) {
575
+ logger.error('contentCatalog is missing');
576
+ return;
577
+ }
578
+ logEvent('Executing cite-it');
579
+ const componentVersions = (0, index_1.getComponentVersions)(contentCatalog);
580
+ for (const [component, versions] of componentVersions) {
581
+ for (const version of versions) {
582
+ const compKey = `${component}@${version}`;
583
+ // No opt-in gate: a component-version participates automatically
584
+ // once the playbook-level scaffold (or its own ext.cite_it, if
585
+ // present) gives it at least one category. ext.cite_it in
586
+ // antora.yml is now purely an OPTIONAL override, exactly like
587
+ // enabled/style/output_partial/write_to_disk below, never a
588
+ // requirement to be scanned at all. See the module docblock for
589
+ // the earlier opt-in-gate design this replaced and why.
590
+ const compExt = extByCompVer.get(compKey) ?? {};
591
+ if (!toBool(compExt['enabled'], cfg.enabled)) {
592
+ logger.debug(`cite-it: ${compKey} - skipped (enabled: false)`);
593
+ checkOrphanedCiteUsage(compKey, 'skipped (enabled: false)', contentCatalog, component, version, logger);
594
+ continue;
595
+ }
596
+ // The playbook-level categories scaffold needs no counterpart here
597
+ // at all: a component's own categories[] is entirely optional,
598
+ // additive to the scaffold by ref_section. See
599
+ // mergeCategoriesConfig for the actual merge.
600
+ const categories = mergeCategoriesConfig(cfg.categories, parseCategoriesConfig(compExt['categories']));
601
+ if (categories.length === 0) {
602
+ // Without the opt-in gate, this branch is reached by every
603
+ // component-version site-wide that has nothing to do with
604
+ // cite-it at all, not just ones that tried to configure
605
+ // something and got it wrong; warning for all of them would
606
+ // flood the build log with noise about components that never
607
+ // showed any intent to use cite-it. Warn only when this
608
+ // component-version's own antora.yml actually carries an
609
+ // ext.cite_it block (real, if misconfigured, intent), debug
610
+ // otherwise, matching every other "nothing to do here" skip
611
+ // above.
612
+ const hasOwnExt = extByCompVer.has(compKey);
613
+ const message = `cite-it: ${compKey} - skipped (no categories: none in the playbook-level scaffold or this component's own ext.cite_it.categories)`;
614
+ if (hasOwnExt)
615
+ logger.warn(message);
616
+ else
617
+ logger.debug(message);
618
+ checkOrphanedCiteUsage(compKey, 'skipped (no categories)', contentCatalog, component, version, logger);
619
+ continue;
620
+ }
621
+ let files = contentCatalog.getFiles().filter((f) => f.src.component === component && f.src.version === version);
622
+ const compStyle = typeof compExt['style'] === 'string' ? compExt['style'] : cfg.style;
623
+ const compOutPartial = typeof compExt['output_partial'] === 'string' ? compExt['output_partial'] : cfg.output_partial;
624
+ const compWriteToDisk = toBool(compExt['write_to_disk'] ?? compExt['writeToDisk'], cfg.write_to_disk);
625
+ const module = files.find((f) => f.src.family === 'page')?.src.module ?? 'ROOT';
626
+ // A page's own include::partial$<category-partial>[] is resolved
627
+ // as part of the scan below (collectIncludeText walks every
628
+ // include:: directive it finds, including this one, even though
629
+ // its own resolved text is never itself scanned for citations).
630
+ // Each category partial must already exist in the catalog before
631
+ // that resolution is attempted: a real ContentCatalog's
632
+ // resolveResource can memoize a failed lookup, and a resolution
633
+ // attempted before the partial exists would otherwise permanently
634
+ // poison the SAME lookup Antora Assembler needs to perform for
635
+ // real, later, during assembly. Injecting an empty placeholder for
636
+ // EVERY category now, before the scan below and before any real
637
+ // .bib work, means every resolveResource call for any of these
638
+ // resource IDs, whether at scan time or assembly time, finds a
639
+ // real file, first attempt onward - independent of whether this
640
+ // component-version turns out to have any real citation usage at
641
+ // all, which the scan below has not yet determined at this point.
642
+ const categoryPartials = categories.map((cat) => ({
643
+ section: cat.ref_section,
644
+ relative: categoryPartialName(compOutPartial, cat.ref_section),
645
+ }));
646
+ for (const cp of categoryPartials) {
647
+ (0, index_2.injectPartial)(contentCatalog, files, component, version, module, cp.relative, '', false, logger);
648
+ }
649
+ // Re-fetch: the placeholders just added above are now the real
650
+ // targets of any include:: resolution attempted during the scan
651
+ // below, and the later, real injectPartial calls, with the actual
652
+ // content, need their own `existing` lookups to find them too.
653
+ // Both need a file list that reflects the placeholders, not the
654
+ // stale snapshot from before they existed.
655
+ files = contentCatalog.getFiles().filter((f) => f.src.component === component && f.src.version === version);
656
+ // ---------------------------------------------------------------
657
+ // 1. Scan pages (+ resolved includes) for citeit:[key] usage,
658
+ // through a real Asciidoctor conversion (macro.ts's
659
+ // scanCiteKeys), not a hand-rolled regex. Runs BEFORE ever
660
+ // resolving or reading a single .bib file: the playbook-level
661
+ // scaffold applies to every component-version site-wide with
662
+ // no opt-in step (see the module docblock), so an asset-only
663
+ // component-version with no relation to citations at all -
664
+ // vale rule sets, PDF theme/font collections, and the like -
665
+ // still inherits it and used to hit real .bib resolution
666
+ // unconditionally, warning "not found" for files it never had
667
+ // any reason to carry. Scanning first and skipping entirely
668
+ // when nothing was found (see below) means a component-version
669
+ // with zero real citeit:[] usage anywhere never even looks for
670
+ // its category files in the first place, rather than looking
671
+ // and then discovering there was nothing to find. Placeholder
672
+ // injection above still runs unconditionally either way: it
673
+ // does no .bib work and carries no warning of its own, and the
674
+ // resolveResource-poisoning hazard it guards against applies
675
+ // regardless of whether this component-version turns out to
676
+ // have real citation usage.
677
+ // ---------------------------------------------------------------
678
+ const componentAttrs = (contentCatalog.getComponents?.() ?? [])
679
+ .find((c) => c.name === component)?.versions
680
+ .find((v) => (v.version ?? v.name) === version)?.asciidoc?.attributes ?? {};
681
+ const scanFiles = files.filter((f) => f.src.family === 'page');
682
+ const includeVisited = new Set();
683
+ for (const f of scanFiles) {
684
+ const key = `${f.src.component}@${f.src.version}:${f.src.module}:${f.src.family}$${f.src.relative}`;
685
+ includeVisited.add(key);
686
+ }
687
+ const citedKeys = new Set();
688
+ for (const f of scanFiles) {
689
+ const raw = f.contents.toString('utf8');
690
+ const headerAttrs = (0, asciidoc_conditionals_1.pageHeaderAttributes)(raw);
691
+ const isDefined = (attr) => {
692
+ if (headerAttrs.has(attr))
693
+ return headerAttrs.get(attr) === true;
694
+ const v = componentAttrs[attr];
695
+ return v !== undefined && v !== null && v !== false;
696
+ };
697
+ let text = (0, index_1.stripNonContent)(raw, isDefined, []);
698
+ const extra = (0, index_1.collectIncludeText)(text, contentCatalog, component, version, f.src.module ?? 'ROOT', includeVisited, [], undefined, [], f.src.path ?? '');
699
+ if (extra)
700
+ text = `${text}\n${(0, index_1.stripNonContent)(extra, isDefined, [])}`;
701
+ // Same concatenation-then-convert hazard sanitizeForScan already
702
+ // fixed for assembly-numbering.ts's own scan (63a6538): a page
703
+ // and its resolved include target each carrying a `=`-level-0
704
+ // title makes the combined text an invalid multi-document
705
+ // concatenation before it ever reaches scanCiteKeys' own real
706
+ // processor.convert() call. This call site concatenates the same
707
+ // way but was missed in that fix; confirmed live, "Executing
708
+ // cite-it" is where the level-0/incomplete-row errors actually
709
+ // fired, not assembly-numbering.ts's own computeCitationNumbers.
710
+ for (const key of (0, macro_1.scanCiteKeys)((0, asciidoc_indexterms_1.sanitizeForScan)(text)))
711
+ citedKeys.add(key);
712
+ }
713
+ // A category's own ref_force list lists a key in the generated
714
+ // bibliography even when no page cites it at all (see the
715
+ // CiteCategoryConfig field docblock), so zero cited keys does not
716
+ // always mean zero real work; only skip when NEITHER a real
717
+ // citation nor a forced entry gives this component-version
718
+ // anything to generate. citedKeys.size === 0 already means the
719
+ // scan above found nothing to report as orphaned either, so no
720
+ // separate checkOrphanedCiteUsage call is needed on this path.
721
+ const anyForced = categories.some((cat) => cat.ref_force.length > 0);
722
+ if (citedKeys.size === 0 && !anyForced) {
723
+ const hasOwnExt = extByCompVer.has(compKey);
724
+ const message = `cite-it: ${compKey} - skipped (no citeit:[key] usage anywhere and no ref_force entries in any category)`;
725
+ if (hasOwnExt)
726
+ logger.warn(message);
727
+ else
728
+ logger.debug(message);
729
+ continue;
730
+ }
731
+ // ---------------------------------------------------------------
732
+ // 2. Resolve, parse, and merge each category's .bib files.
733
+ // Files listed earlier in a category's own files[] are the
734
+ // shared base; later files override by citation key, mirroring
735
+ // autoterm's mergeTerminology semantics exactly.
736
+ // ---------------------------------------------------------------
737
+ const combinedEntries = new Map();
738
+ const keyCategory = new Map();
739
+ const categoryKeys = new Map();
740
+ for (const cat of categories) {
741
+ const bibFiles = resolveBibFiles(cat.ref_files, contentCatalog, component, version, module, logger);
742
+ let catEntries = new Map();
743
+ for (const bf of bibFiles) {
744
+ const { entries, warnings } = (0, registry_1.parseCiteFile)(bf.contents.toString('utf8'), `${compKey}:${bf.src.path ?? bf.src.relative ?? ''}`);
745
+ for (const w of warnings)
746
+ logger.warn(`cite-it: ${w}`);
747
+ const { merged } = (0, registry_1.mergeCiteRegistry)(catEntries, entries);
748
+ catEntries = merged;
749
+ }
750
+ const keys = new Set();
751
+ for (const [key, entry] of catEntries) {
752
+ if (keyCategory.has(key) && keyCategory.get(key) !== cat.ref_section) {
753
+ logger.warn(`cite-it: ${compKey} - citation key "${key}" appears in both category "${keyCategory.get(key)}" and "${cat.ref_section}"; the later category's membership wins`);
754
+ }
755
+ keyCategory.set(key, cat.ref_section);
756
+ combinedEntries.set(key, entry);
757
+ keys.add(key);
758
+ }
759
+ categoryKeys.set(cat.ref_section, keys);
760
+ }
761
+ // A key claimed by more than one category was added to every
762
+ // claiming category's own Set above, since each category's loop
763
+ // only knows about categories processed before it. keyCategory now
764
+ // holds each conflicted key's final, later-wins owner; remove the
765
+ // key from every OTHER category's Set so only its real owner lists
766
+ // it, matching the warning already logged above.
767
+ for (const [cat, keys] of categoryKeys) {
768
+ for (const key of [...keys]) {
769
+ if (keyCategory.get(key) !== cat)
770
+ keys.delete(key);
771
+ }
772
+ }
773
+ // Each category's own ref_force list reassigns a key's category
774
+ // membership outright, the same mechanism as the natural conflict
775
+ // resolution above: strip the key from its previous category's Set,
776
+ // then add it to THIS category's Set, the one whose own ref_force
777
+ // names it. A key's rendered row lives in exactly one category at a
778
+ // time, so this reassignment, not an additive union, is what "force
779
+ // this citation under this heading" actually means. Nesting
780
+ // ref_force inside its own category means the target is never
781
+ // ambiguous: there is no separate back-reference field to get wrong.
782
+ //
783
+ // A key with no entry anywhere warns rather than silently rendering
784
+ // nowhere: a visible mistake beats a citation named in ref_force
785
+ // deliberately just vanishing over a typo.
786
+ for (const cat of categories) {
787
+ for (const key of cat.ref_force) {
788
+ if (!combinedEntries.has(key)) {
789
+ logger.warn(`cite-it: ${compKey} - ref_force names unknown key "${key}" in category "${cat.ref_section}"; not found in any configured category's data`);
790
+ continue;
791
+ }
792
+ const previousSection = keyCategory.get(key);
793
+ if (previousSection && previousSection !== cat.ref_section) {
794
+ categoryKeys.get(previousSection)?.delete(key);
795
+ }
796
+ keyCategory.set(key, cat.ref_section);
797
+ categoryKeys.get(cat.ref_section)?.add(key);
798
+ }
799
+ }
800
+ if (!cfg.suppress_events) {
801
+ logEvent(`${compKey} - ${combinedEntries.size} bibliography entr${combinedEntries.size === 1 ? 'y' : 'ies'} loaded across ${categories.length} categor${categories.length === 1 ? 'y' : 'ies'}`);
802
+ }
803
+ for (const key of citedKeys) {
804
+ if (!combinedEntries.has(key)) {
805
+ logger.warn(`cite-it: ${compKey} - citeit:[${key}] cites an unknown key; not found in any configured category`);
806
+ }
807
+ }
808
+ // ---------------------------------------------------------------
809
+ // 3. Populate the shared cache the citeit:[key] macro reads at
810
+ // real conversion time (macro.ts). Formatted only for keys
811
+ // actually used (cited, or ref_force), same set the
812
+ // generated partial lists.
813
+ // ---------------------------------------------------------------
814
+ const refForceKeys = new Set(categories.flatMap((cat) => cat.ref_force));
815
+ const usedKeysByCategory = new Map();
816
+ const allUsedKeys = [];
817
+ for (const cat of categories) {
818
+ const keys = [...(categoryKeys.get(cat.ref_section) ?? new Set())]
819
+ .filter((k) => citedKeys.has(k) || refForceKeys.has(k))
820
+ .sort((a, b) => a.localeCompare(b));
821
+ usedKeysByCategory.set(cat.ref_section, keys);
822
+ allUsedKeys.push(...keys);
823
+ }
824
+ // A key's entry carrying a supersededby field (registry.ts) warns
825
+ // once here, at generation time, scoped to the keys a category's
826
+ // partial actually lists (cited or ref_force'd), not every entry
827
+ // the registry happens to hold: an unused entry going stale is not
828
+ // this component-version's problem to flag. Deliberately not
829
+ // raised again at each citeit:[key] use site (macro.ts): one clear
830
+ // signal per generated partial, not one per occurrence in prose.
831
+ for (const [section, keys] of usedKeysByCategory) {
832
+ for (const key of keys) {
833
+ const supersededBy = combinedEntries.get(key)?.['supersededby'];
834
+ if (typeof supersededBy === 'string' && supersededBy) {
835
+ logger.warn(`cite-it: ${compKey} - citation "${key}" in category "${section}" carries supersededby "${supersededBy}"; a newer revision is available`);
836
+ }
837
+ }
838
+ }
839
+ const formattedByKey = (0, csl_1.formatBibliographyEntries)(compStyle, combinedEntries, allUsedKeys);
840
+ const cacheData = { entriesByKey: combinedEntries, formattedByKey };
841
+ macro_1.citeCache.set(compKey, cacheData);
842
+ assemblyScanContexts.set(compKey, {
843
+ contentCatalog: contentCatalog,
844
+ component,
845
+ version,
846
+ componentAttrs,
847
+ module,
848
+ categoryPartials,
849
+ });
850
+ keyCategories.set(compKey, keyCategory);
851
+ // ---------------------------------------------------------------
852
+ // 4. Generate and inject each category's own bibliography
853
+ // partial. A component author routes different categories to
854
+ // different pages simply by choosing which page includes
855
+ // which category's partial.
856
+ // ---------------------------------------------------------------
857
+ for (const cp of categoryPartials) {
858
+ const cat = categories.find((c) => c.ref_section === cp.section);
859
+ const keys = usedKeysByCategory.get(cat.ref_section) ?? [];
860
+ const content = generateCategoryPartial(keys, formattedByKey);
861
+ (0, index_2.injectPartial)(contentCatalog, files, component, version, module, cp.relative, content, compWriteToDisk, logger);
862
+ }
863
+ }
864
+ }
865
+ logEvent('cite-it finished');
866
+ });
867
+ // ---------------------------------------------------------------------
868
+ // PDF/assembled numbering. See assembly-numbering.ts's own module
869
+ // docblock for the full reasoning behind the numbering mechanism itself.
870
+ //
871
+ // Computed LAZILY, memoized per component-version, on the first assembled
872
+ // loadAsciiDoc call the wrapper above sees for that component-version,
873
+ // rather than eagerly in a navigationBuilt listener as an earlier version
874
+ // of this code did (the earlier version genuinely raced against
875
+ // `@antora/assembler`'s own navigationBuilt listener and lost against a
876
+ // real playbook where `@antora/pdf-extension` preceded cite-it in
877
+ // `antora.extensions`, leaving every citeit:[key] unrewritten with no
878
+ // error). Computing lazily on first actual use, inside this same wrapper
879
+ // invocation, sidesteps that race entirely regardless of extension order.
880
+ //
881
+ // A DIFFERENT bug surfaced later, unrelated to that race: this function
882
+ // used to always resolve navigation as `componentVersion?.navigation`,
883
+ // falling back to a hardcoded, always-unnamed assembler-profile lookup
884
+ // (`assemblerProfiles.get(compKey).get(undefined)`) for any navFiles
885
+ // override. Verified directly against @antora/assembler's own source
886
+ // (assemble-content.js/build-navigation.js): `componentVersion.navigation`
887
+ // is only ever assigned by @antora/navigation-builder for a component
888
+ // version that owns at least one real `nav`-family file — it stays
889
+ // permanently undefined otherwise, by design, not a timing artifact.
890
+ // Assembler itself resolves the active profile as `assemblyConfig.profile`,
891
+ // which DEFAULTS TO THE TARGET BACKEND NAME (e.g. 'pdf') whenever no
892
+ // profile is configured explicitly, never bare `undefined` in an ordinary
893
+ // PDF build, and looks up that component-version's navFiles override
894
+ // under THAT key first, falling back to the unnamed slot only as a second
895
+ // choice (`componentVersionProfiles?.get(baseModel.profile) ??
896
+ // componentVersionProfiles?.get()`). A hardcoded `.get(undefined)` lookup
897
+ // here therefore misses the real override in the common case. Assembler
898
+ // also bakes the resolved profile name into every assembled document's
899
+ // own real attributes as `assembler-profile` (assemble-content.js's
900
+ // intrinsicAttributes, merged into what the wrapper above already
901
+ // receives as `asciidocConfig.attributes`) specifically so a consumer
902
+ // like this one can read back the SAME profile it resolved, rather than
903
+ // needing to reimplement its resolution logic; `profileName` below is
904
+ // that value, threaded through from the wrapper.
905
+ //
906
+ // A THIRD gap surfaced later still, confirmed live against a real Antora
907
+ // build: a playbook can register @antora/pdf-extension programmatically,
908
+ // at normal register()-phase time, instead of through the playbook's
909
+ // own antora.extensions list, with no ordering race either way. With no
910
+ // ext.assembler block anywhere (assemblerProfiles legitimately empty)
911
+ // and real, unmodified Antora navigation driving assembly correctly,
912
+ // `componentVersion.navigation` still read empty here. Assembler's own
913
+ // no-profile resolution
914
+ // (assemble-content.js's generateSelectAssemblerProfile) never actually
915
+ // reads componentVersion.navigation FIRST — it reads
916
+ // `navigationCatalog?.getNavigation(componentVersion.name,
917
+ // componentVersion.version) ?? componentVersion.navigation`, the
918
+ // NavigationCatalog instance @antora/navigation-builder's own
919
+ // buildNavigation returns and generate-site.js stores as the
920
+ // navigationCatalog context variable, alongside (not only through)
921
+ // componentVersion.navigation. This function only ever checked the
922
+ // fallback half of that chain; matching assembler's own real
923
+ // precedence, primary source first, is the fix, not a guess: it can
924
+ // only find more, never less, than the fallback-only lookup already
925
+ // did.
926
+ //
927
+ // A FOURTH gap: even navigationCatalog can read empty. Confirmed live,
928
+ // not a codebase bug: componentVersion.navigation itself reads undefined
929
+ // at this exact read point despite a real nav-family file existing in
930
+ // the content catalog, confirmed as the SAME object reference two
931
+ // independent ways (this function's own
932
+ // manual lookup and contentCatalog.getComponentVersion()) — not a stale
933
+ // copy, not the wrong object. Rather than depend further on exactly
934
+ // when/how something else populates it, self-heal: call
935
+ // @antora/navigation-builder's own buildNavigation directly (exposed via
936
+ // getFunctions(), the same mechanism buildAlternateNavigation below
937
+ // already uses), which re-derives navigation from the catalog's own
938
+ // nav-family files and reassigns componentVersion.navigation as a
939
+ // genuine side effect, discarding buildNavigation's own returned
940
+ // NavigationCatalog (a fresh, separate instance every call) rather than
941
+ // touching the site's real one.
942
+ // ---------------------------------------------------------------------
943
+ function ensureAssemblyDataComputed(compKey, profileName) {
944
+ if (citationNumbers.has(compKey))
945
+ return; // already computed, or in progress
946
+ const scanCtx = assemblyScanContexts.get(compKey);
947
+ if (!scanCtx)
948
+ return;
949
+ citationNumbers.set(compKey, new Map()); // reentrancy guard, replaced below
950
+ // scanCtx.contentCatalog is typed against assembly-numbering.ts's own
951
+ // narrower ContentCatalog interface (no getComponents); the stored
952
+ // runtime object is the same real catalog assemblyScanContexts.set(...)
953
+ // captured, just narrowed at that point too, so this cast back to the
954
+ // wider local interface is safe.
955
+ const contentCatalog = scanCtx.contentCatalog;
956
+ const components = contentCatalog.getComponents?.() ?? [];
957
+ const contextVars = ctx.getVariables?.();
958
+ const assemblerProfiles = contextVars?.assemblerProfiles;
959
+ const navigationCatalog = contextVars?.navigationCatalog;
960
+ const componentVersion = components
961
+ .find((c) => c.name === scanCtx.component)?.versions
962
+ .find((v) => (v.version ?? v.name) === scanCtx.version);
963
+ let liveNavigation = navigationCatalog?.getNavigation(scanCtx.component, scanCtx.version);
964
+ if ((!liveNavigation || !liveNavigation.length) && ctx.getFunctions) {
965
+ // Self-heal: confirmed live, not a codebase bug, that
966
+ // componentVersion.navigation, and
967
+ // navigationCatalog's own record of it, can BOTH read empty here
968
+ // even though a real nav-family file exists in the content catalog
969
+ // and the SAME object reference is confirmed live two independent
970
+ // ways (this manual lookup and contentCatalog.getComponentVersion())
971
+ // — ruling out a stale copy or wrong-object bug on cite-it's own
972
+ // side. buildNavigation is self-contained and idempotent: it
973
+ // re-derives navigation straight from the catalog's own nav-family
974
+ // files and re-assigns componentVersion.navigation directly,
975
+ // discarding its own returned NavigationCatalog (a fresh, separate
976
+ // instance every call) rather than touching the site's real one.
977
+ // Same getFunctions() mechanism buildAlternateNavigation below
978
+ // already uses.
979
+ //
980
+ // buildNavigation's own internal implementation reads its loadAsciiDoc
981
+ // through this SAME live, shared function table (getFunctions'
982
+ // $unsafe accessor, by design, so it picks up any real replacement -
983
+ // including this extension's own wrapper). This self-heal fires from
984
+ // INSIDE that wrapper, mid-conversion of some other real page, so
985
+ // without intervention buildNavigation's own internal parse of each
986
+ // nav.adoc would recurse straight back through the wrapper, nested
987
+ // inside an already-in-progress loadAsciiDoc call. Sidestepped by
988
+ // swapping the live table back to the real, unwrapped loader for the
989
+ // duration of this one synchronous call only, then always restoring
990
+ // the wrapper again, success or failure.
991
+ try {
992
+ const buildNav = ctx.getFunctions().buildNavigation;
993
+ if (typeof buildNav === 'function' && realLoadAsciiDoc) {
994
+ ctx.replaceFunctions?.({ loadAsciiDoc: realLoadAsciiDoc });
995
+ try {
996
+ buildNav(contentCatalog, componentVersion?.asciidoc);
997
+ }
998
+ finally {
999
+ if (loadAsciiDocWrapper)
1000
+ ctx.replaceFunctions?.({ loadAsciiDoc: loadAsciiDocWrapper });
1001
+ }
1002
+ liveNavigation = componentVersion?.navigation;
1003
+ }
1004
+ }
1005
+ catch (err) {
1006
+ logger.warn(`cite-it: ${compKey} - buildNavigation fallback failed: ${String(err)}`);
1007
+ }
1008
+ }
1009
+ const defaultNavigation = (liveNavigation ??
1010
+ componentVersion?.navigation ??
1011
+ []);
1012
+ let navigation = defaultNavigation;
1013
+ // Same two-step lookup assembler itself performs: the resolved active
1014
+ // profile first (never bare undefined for an ordinary export whose
1015
+ // config omits `profile:` explicitly, see the docblock above), the
1016
+ // unnamed default slot only as a fallback.
1017
+ const componentVersionProfiles = assemblerProfiles?.get(`${scanCtx.version}@${scanCtx.component}`);
1018
+ const profileData = componentVersionProfiles?.get(profileName) ?? componentVersionProfiles?.get(undefined);
1019
+ if (profileData?.navFiles?.length && ctx.require && ctx.getFunctions) {
1020
+ try {
1021
+ // Mirrors @antora/assembler's own resolution exactly
1022
+ // (assemble-content.js's generateSelectAssemblerProfile):
1023
+ // buildAlternateNavigation from the shared generator functions if
1024
+ // some other extension already replaced it, otherwise straight
1025
+ // from the real @antora/navigation-builder module.
1026
+ const fxns = ctx.getFunctions();
1027
+ const buildAlternateNavigation = fxns.buildAlternateNavigation ??
1028
+ ctx.require('@antora/navigation-builder').buildAlternateNavigation;
1029
+ navigation = (buildAlternateNavigation(contentCatalog, componentVersion, profileData.navFiles, {}) ?? []);
1030
+ }
1031
+ catch (err) {
1032
+ logger.warn(`cite-it: ${compKey} - could not resolve the assembler profile's alternate navigation, falling back to the default navigation for numbering: ${String(err)}`);
1033
+ }
1034
+ }
1035
+ const numbers = (0, assembly_numbering_1.computeCitationNumbers)(navigation, scanCtx);
1036
+ citationNumbers.set(compKey, numbers);
1037
+ logger.debug(`cite-it: ${compKey} - computed ${numbers.size} true first-appearance citation number(s) for assembled output`);
1038
+ const categoryScopes = (0, assembly_numbering_1.findBibliographyScopes)(navigation, scanCtx);
1039
+ const keyCategory = keyCategories.get(compKey) ?? new Map();
1040
+ const keyScopes = new Map();
1041
+ const missingScopeCategories = new Set();
1042
+ for (const [key] of numbers) {
1043
+ const cat = keyCategory.get(key);
1044
+ const scope = cat ? categoryScopes.get(cat) : undefined;
1045
+ if (scope) {
1046
+ keyScopes.set(key, scope);
1047
+ }
1048
+ else if (cat) {
1049
+ missingScopeCategories.add(cat);
1050
+ }
1051
+ }
1052
+ keyScopesByComp.set(compKey, keyScopes);
1053
+ for (const cat of missingScopeCategories) {
1054
+ logger.warn(`cite-it: ${compKey} - could not find the page that includes category "${cat}"'s generated partial; its citation links will render as plain numbers, not clickable links`);
1055
+ }
1056
+ // The generated bibliography partial's own citeit:[key] row labels
1057
+ // need rewriting too, and specifically now, before any assembly-time
1058
+ // loadAsciiDoc call resolves an include:: of it: Antora's include
1059
+ // processor reads an included partial's content directly from the
1060
+ // catalog, never back through loadAsciiDoc, so the wrapper above never
1061
+ // gets a chance to touch it otherwise (see rewritePartialForAssembly's
1062
+ // own docblock for the full story). Safe to do here, synchronously,
1063
+ // before returning: this function itself is always called from inside
1064
+ // the wrapper, before that same invocation's real loadAsciiDoc call.
1065
+ (0, assembly_numbering_1.rewritePartialForAssembly)(scanCtx, numbers);
1066
+ }
1067
+ }
1068
+ /**
1069
+ * One category's own generated partial content: the bibliography list
1070
+ * only, no heading. A component author decides where THIS category
1071
+ * appears by choosing which page includes `partial$<categoryPartialName
1072
+ * (...)>`, independent of every other category, and supplies that page's
1073
+ * own heading directly as ordinary authored AsciiDoc immediately above
1074
+ * the include line. Returns '' for an empty category (caller skips
1075
+ * injecting it at all).
1076
+ */
1077
+ function generateCategoryPartial(keys, formattedByKey) {
1078
+ if (keys.length === 0)
1079
+ return '';
1080
+ // The cite-it-bibliography role is a real theming hook, not
1081
+ // decoration: dlist_role_theming.rb (the PDF theme's own converter
1082
+ // extension, since asciidoctor-pdf's dlist rendering has no per-role
1083
+ // lookup of its own) reads role_cite-it-bibliography_* theme keys for
1084
+ // this exact role to control term boldness and text alignment for PDF
1085
+ // output, and it renders as an ordinary CSS-targetable class in HTML.
1086
+ const lines = ['[horizontal.cite-it-bibliography]'];
1087
+ for (const key of keys) {
1088
+ const text = formattedByKey.get(key) ?? `(no formatted text for "${key}")`;
1089
+ // The [[cite-<key>]] anchor is what an inline citeit:[key] citation
1090
+ // links to for assembled/PDF output (rewriteCiteMarkers, asLink:
1091
+ // true), see assembly-numbering.ts. Harmless, invisible in ordinary
1092
+ // HTML rendering.
1093
+ lines.push(`[[cite-${key}]]citeit:[${key}]:: ${text}`);
1094
+ }
1095
+ lines.push('');
1096
+ return lines.join('\n');
1097
+ }
1098
+ //# sourceMappingURL=index.js.map