@baiyibai-antora/extensions 1.13.0 → 1.15.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 +32 -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 +29 -18
  8. package/dist/extensions/autoterm/index.d.ts.map +1 -1
  9. package/dist/extensions/autoterm/index.js +37 -197
  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 +36 -0
  20. package/dist/extensions/cite-it/csl.d.ts.map +1 -0
  21. package/dist/extensions/cite-it/csl.js +172 -0
  22. package/dist/extensions/cite-it/csl.js.map +1 -0
  23. package/dist/extensions/cite-it/index.d.ts +301 -0
  24. package/dist/extensions/cite-it/index.d.ts.map +1 -0
  25. package/dist/extensions/cite-it/index.js +1191 -0
  26. package/dist/extensions/cite-it/index.js.map +1 -0
  27. package/dist/extensions/cite-it/macro.d.ts +109 -0
  28. package/dist/extensions/cite-it/macro.d.ts.map +1 -0
  29. package/dist/extensions/cite-it/macro.js +262 -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 +92 -0
  44. package/dist/lib/asciidoc-indexterms.d.ts.map +1 -0
  45. package/dist/lib/asciidoc-indexterms.js +280 -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,1191 @@
1
+ "use strict";
2
+ /**
3
+ * @baiyibai-antora/extensions - cite-it
4
+ *
5
+ * Resolves `cite:[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_category>.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 `cite:[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_category` (see
62
+ * mergeCategoriesConfig): naming a category already in the scaffold
63
+ * extends it (its `ref_files`/`ref_force` append to the scaffold's),
64
+ * and naming a new category adds one 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. category "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_category itself, in which case its own entry extends this
92
+ * # one rather than replacing it.
93
+ * categories:
94
+ * - ref_category: standards
95
+ * ref_files: ['cite-it-data:ROOT:partial$standards.bib']
96
+ * - ref_category: 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 category, 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" category (above) with one
122
+ * # more file, appended after the scaffold's own ref_files.
123
+ * - ref_category: literature
124
+ * ref_files: ['literature-local.bib']
125
+ * # A category the scaffold does not define at all: local to this
126
+ * # component only.
127
+ * - ref_category: 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 `cite:[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 category = typeof r['ref_category'] === 'string' ? r['ref_category']
285
+ : typeof r['refCategory'] === 'string' ? r['refCategory'] : '';
286
+ if (!category)
287
+ continue;
288
+ result.push({
289
+ ref_category: category,
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_category, 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 category named at both levels extends (ref_files/ref_force
300
+ // concatenate, playbook first) rather than replaces, and a category named
301
+ // only at the component level is added as a wholly local one.
302
+ function mergeCategoriesConfig(scaffold, local) {
303
+ const byCategory = new Map();
304
+ for (const cat of scaffold)
305
+ byCategory.set(cat.ref_category, { ...cat });
306
+ for (const cat of local) {
307
+ const existing = byCategory.get(cat.ref_category);
308
+ byCategory.set(cat.ref_category, existing ? {
309
+ ref_category: cat.ref_category,
310
+ ref_files: [...existing.ref_files, ...cat.ref_files],
311
+ ref_force: [...existing.ref_force, ...cat.ref_force],
312
+ } : cat);
313
+ }
314
+ return [...byCategory.values()].map((cat) => ({
315
+ ref_category: cat.ref_category,
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 cite:[key] usage: a component-version that is not scanned
375
+ // ---------------------------------------------------------------------------
376
+ /**
377
+ * Checks a SKIPPED (non-participating) component-version's own pages for
378
+ * `cite:[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 `cite:[` 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('cite:['))
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 cite:[${[...found].join('], cite:[')}] 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
+ blanket_pubstate: false,
448
+ categories: [],
449
+ };
450
+ const extByCompVer = new Map();
451
+ // Per-component-version scan context, captured during contentClassified
452
+ // and reused by the navigationBuilt handler below to compute true
453
+ // first-appearance citation numbers for PDF/assembled output, avoiding
454
+ // recomputing config resolution twice for the same component-version.
455
+ const assemblyScanContexts = new Map();
456
+ // component@version -> citation key -> owning category key. Needed at
457
+ // navigationBuilt time to turn findBibliographyScopes' per-CATEGORY
458
+ // page map into the per-KEY page map rewriteCiteMarkers actually needs.
459
+ const keyCategories = new Map();
460
+ // Populated later, at navigationBuilt time (see below), but declared here
461
+ // so the loadAsciiDoc wrapper registered immediately below can close over
462
+ // them: real content is not known yet when the wrapper is REGISTERED, only
463
+ // by the time it is actually CALLED, which is guaranteed to be later.
464
+ const citationNumbers = new Map();
465
+ const keyScopesByComp = new Map();
466
+ // Populated inside the if-block below, but declared here so
467
+ // ensureAssemblyDataComputed's own navigation self-heal (defined later
468
+ // in this same function) can reach both: the real, unwrapped loader,
469
+ // and this wrapper's own reference, needed to temporarily swap the
470
+ // wrapper back out and reinstall it. See that self-heal's own docblock
471
+ // for why.
472
+ let realLoadAsciiDoc;
473
+ let loadAsciiDocWrapper;
474
+ // The loadAsciiDoc wrapper is registered here, synchronously, at
475
+ // register() time itself, deliberately BEFORE any lifecycle event fires
476
+ // at all. `@antora/assembler`'s own assembleContent reads
477
+ // `context.getFunctions().loadAsciiDoc` exactly ONCE, synchronously, at
478
+ // the top of its own navigationBuilt listener, not lazily at the point it
479
+ // actually calls loadAsciiDoc, confirmed directly by reading
480
+ // assemble-content.js. If cite-it's own replaceFunctions call happened
481
+ // inside ITS OWN navigationBuilt listener instead, as an earlier version
482
+ // of this code did, whichever extension's navigationBuilt listener runs
483
+ // first would win: a playbook that lists cite-it after
484
+ // `@antora/pdf-extension` would silently get unrewritten cite:[key]
485
+ // literal text in PDF output, no error, no warning, since
486
+ // assembleContent's own loadAsciiDoc reference would already be frozen to
487
+ // the original, unwrapped function before cite-it's replaceFunctions call
488
+ // ever ran. Registering here, at register() time, fires before every
489
+ // playbookBuilt/contentClassified/navigationBuilt listener regardless of
490
+ // where cite-it appears in the extensions list, so this no longer depends
491
+ // on registration order at all.
492
+ if (ctx.getFunctions && ctx.replaceFunctions && ctx.require) {
493
+ // Captured ONCE, right here, BEFORE replaceFunctions below installs the
494
+ // wrapper: reading getFunctions().loadAsciiDoc lazily INSIDE the
495
+ // wrapper itself, instead, would read back the wrapper's own already-
496
+ // installed replacement rather than the original, an infinite
497
+ // recursion confirmed directly (a real "Maximum call stack size
498
+ // exceeded" crash against a live build, not a hypothetical). The
499
+ // fallback to `@antora/asciidoc-loader`'s own export, for the case
500
+ // getFunctions() does not carry a real loadAsciiDoc yet this early in
501
+ // the lifecycle, mirrors exactly what `@antora/assembler`'s own
502
+ // assemble-content.js does for the same reason.
503
+ const realLoadAsciiDocFn = (ctx.getFunctions().loadAsciiDoc ??
504
+ ctx.require('@antora/asciidoc-loader').loadAsciiDoc);
505
+ realLoadAsciiDoc = realLoadAsciiDocFn;
506
+ // Named, not inline, so ensureAssemblyDataComputed's own navigation
507
+ // self-heal can reinstall this SAME reference after temporarily
508
+ // swapping it out; see that function's own docblock for why.
509
+ loadAsciiDocWrapper = (...args) => {
510
+ const [origFile, catalog, asciidocConfig] = args;
511
+ let file = origFile;
512
+ const isAssembled = !!(asciidocConfig?.attributes && 'loader-assembler' in asciidocConfig.attributes);
513
+ let restoreForeignPages;
514
+ if (isAssembled && file?.contents && file.src?.component && file.src?.version) {
515
+ const compKey = `${file.src.component}@${file.src.version}`;
516
+ // @antora/assembler resolves the active profile as
517
+ // assemblyConfig.profile, defaulting to the export's own target
518
+ // backend (e.g. 'pdf') when no profile is configured explicitly
519
+ // — never bare `undefined` in an ordinary PDF build — then bakes
520
+ // it into every assembled document's own real attributes as
521
+ // 'assembler-profile' (assemble-content.js: intrinsicAttributes),
522
+ // right here in this same asciidocConfig. Passed through so
523
+ // ensureAssemblyDataComputed can look up navFiles under the
524
+ // SAME profile key assembler itself resolved, instead of always
525
+ // checking the unnamed default profile slot.
526
+ const profileName = asciidocConfig?.attributes?.['assembler-profile'];
527
+ ensureAssemblyDataComputed(compKey, profileName);
528
+ const numbers = citationNumbers.get(compKey);
529
+ if (numbers && numbers.size > 0) {
530
+ const keyScopes = keyScopesByComp.get(compKey) ?? null;
531
+ const before = file.contents.toString('utf8');
532
+ const after = (0, assembly_numbering_1.rewriteCiteMarkers)(before, numbers, true, keyScopes);
533
+ if (after !== before) {
534
+ file = Object.assign(Object.create(Object.getPrototypeOf(file)), file, { contents: Buffer.from(after, 'utf8') });
535
+ }
536
+ }
537
+ restoreForeignPages = rewriteForeignCitedPages(file.src, profileName);
538
+ }
539
+ // realLoadAsciiDoc is always assigned by this point: this closure is
540
+ // only ever reachable after the synchronous assignment a few lines
541
+ // above it in this same if-block has already run.
542
+ try {
543
+ return realLoadAsciiDoc(file, catalog, asciidocConfig ?? {});
544
+ }
545
+ finally {
546
+ restoreForeignPages?.();
547
+ }
548
+ };
549
+ ctx.replaceFunctions({ loadAsciiDoc: loadAsciiDocWrapper });
550
+ }
551
+ else {
552
+ logger.warn('cite-it: replaceFunctions/getFunctions/require unavailable on this extension context; PDF/assembled numbering will not be applied (ordinary HTML citations are unaffected)');
553
+ }
554
+ ctx.once('contentAggregated', ({ contentAggregate }) => {
555
+ for (const item of (contentAggregate ?? [])) {
556
+ const ciExt = item.ext?.['cite_it'];
557
+ if (ciExt && typeof ciExt === 'object') {
558
+ extByCompVer.set(`${item.name}@${item.version}`, ciExt);
559
+ }
560
+ }
561
+ });
562
+ ctx.once('playbookBuilt', async ({ playbook }) => {
563
+ const extEntry = findOwnExtEntry(playbook?.antora?.extensions ?? []);
564
+ const raw = (extEntry.cite_it ?? extEntry.citeIt ?? {});
565
+ cfg = {
566
+ enabled: toBool(pick(raw, ['enabled'], cfg.enabled), cfg.enabled),
567
+ quiet: toBool(pick(raw, ['quiet'], cfg.quiet), cfg.quiet),
568
+ style: String(pick(raw, ['style'], cfg.style)),
569
+ output_partial: String(pick(raw, ['output_partial', 'outputPartial'], cfg.output_partial)),
570
+ suppress_events: toBool(pick(raw, ['suppress_events', 'suppressEvents'], cfg.suppress_events), cfg.suppress_events),
571
+ write_to_disk: toBool(pick(raw, ['write_to_disk', 'writeToDisk'], cfg.write_to_disk), cfg.write_to_disk),
572
+ blanket_pubstate: toBool(pick(raw, ['blanket_pubstate', 'blanketPubstate'], cfg.blanket_pubstate), cfg.blanket_pubstate),
573
+ categories: Array.isArray(raw['categories']) ? parseCategoriesConfig(raw['categories']) : cfg.categories,
574
+ };
575
+ quietMode = cfg.quiet;
576
+ logger.debug(`cite-it config: ${JSON.stringify(cfg)}`);
577
+ });
578
+ ctx.once('contentClassified', async ({ contentCatalog }) => {
579
+ if (!cfg.enabled) {
580
+ logger.debug('cite-it disabled (skipped)');
581
+ return;
582
+ }
583
+ if (!contentCatalog) {
584
+ logger.error('contentCatalog is missing');
585
+ return;
586
+ }
587
+ logEvent('Executing cite-it');
588
+ const componentVersions = (0, index_1.getComponentVersions)(contentCatalog);
589
+ for (const [component, versions] of componentVersions) {
590
+ for (const version of versions) {
591
+ const compKey = `${component}@${version}`;
592
+ // No opt-in gate: a component-version participates automatically
593
+ // once the playbook-level scaffold (or its own ext.cite_it, if
594
+ // present) gives it at least one category. ext.cite_it in
595
+ // antora.yml is now purely an OPTIONAL override, exactly like
596
+ // enabled/style/output_partial/write_to_disk below, never a
597
+ // requirement to be scanned at all. See the module docblock for
598
+ // the earlier opt-in-gate design this replaced and why.
599
+ const compExt = extByCompVer.get(compKey) ?? {};
600
+ if (!toBool(compExt['enabled'], cfg.enabled)) {
601
+ logger.debug(`cite-it: ${compKey} - skipped (enabled: false)`);
602
+ checkOrphanedCiteUsage(compKey, 'skipped (enabled: false)', contentCatalog, component, version, logger);
603
+ continue;
604
+ }
605
+ // The playbook-level categories scaffold needs no counterpart here
606
+ // at all: a component's own categories[] is entirely optional,
607
+ // additive to the scaffold by ref_category. See
608
+ // mergeCategoriesConfig for the actual merge.
609
+ const categories = mergeCategoriesConfig(cfg.categories, parseCategoriesConfig(compExt['categories']));
610
+ if (categories.length === 0) {
611
+ // Without the opt-in gate, this branch is reached by every
612
+ // component-version site-wide that has nothing to do with
613
+ // cite-it at all, not just ones that tried to configure
614
+ // something and got it wrong; warning for all of them would
615
+ // flood the build log with noise about components that never
616
+ // showed any intent to use cite-it. Warn only when this
617
+ // component-version's own antora.yml actually carries an
618
+ // ext.cite_it block (real, if misconfigured, intent), debug
619
+ // otherwise, matching every other "nothing to do here" skip
620
+ // above.
621
+ const hasOwnExt = extByCompVer.has(compKey);
622
+ const message = `cite-it: ${compKey} - skipped (no categories: none in the playbook-level scaffold or this component's own ext.cite_it.categories)`;
623
+ if (hasOwnExt)
624
+ logger.warn(message);
625
+ else
626
+ logger.debug(message);
627
+ checkOrphanedCiteUsage(compKey, 'skipped (no categories)', contentCatalog, component, version, logger);
628
+ continue;
629
+ }
630
+ let files = contentCatalog.getFiles().filter((f) => f.src.component === component && f.src.version === version);
631
+ const compStyle = typeof compExt['style'] === 'string' ? compExt['style'] : cfg.style;
632
+ const compOutPartial = typeof compExt['output_partial'] === 'string' ? compExt['output_partial'] : cfg.output_partial;
633
+ const compWriteToDisk = toBool(compExt['write_to_disk'] ?? compExt['writeToDisk'], cfg.write_to_disk);
634
+ const compBlanketPubstate = toBool(compExt['blanket_pubstate'] ?? compExt['blanketPubstate'], cfg.blanket_pubstate);
635
+ const module = files.find((f) => f.src.family === 'page')?.src.module ?? 'ROOT';
636
+ // A page's own include::partial$<category-partial>[] is resolved
637
+ // as part of the scan below (collectIncludeText walks every
638
+ // include:: directive it finds, including this one, even though
639
+ // its own resolved text is never itself scanned for citations).
640
+ // Each category partial must already exist in the catalog before
641
+ // that resolution is attempted: a real ContentCatalog's
642
+ // resolveResource can memoize a failed lookup, and a resolution
643
+ // attempted before the partial exists would otherwise permanently
644
+ // poison the SAME lookup Antora Assembler needs to perform for
645
+ // real, later, during assembly. Injecting an empty placeholder for
646
+ // EVERY category now, before the scan below and before any real
647
+ // .bib work, means every resolveResource call for any of these
648
+ // resource IDs, whether at scan time or assembly time, finds a
649
+ // real file, first attempt onward - independent of whether this
650
+ // component-version turns out to have any real citation usage at
651
+ // all, which the scan below has not yet determined at this point.
652
+ const categoryPartials = categories.map((cat) => ({
653
+ category: cat.ref_category,
654
+ relative: categoryPartialName(compOutPartial, cat.ref_category),
655
+ }));
656
+ for (const cp of categoryPartials) {
657
+ (0, index_2.injectPartial)(contentCatalog, files, component, version, module, cp.relative, '', false, logger);
658
+ }
659
+ // Re-fetch: the placeholders just added above are now the real
660
+ // targets of any include:: resolution attempted during the scan
661
+ // below, and the later, real injectPartial calls, with the actual
662
+ // content, need their own `existing` lookups to find them too.
663
+ // Both need a file list that reflects the placeholders, not the
664
+ // stale snapshot from before they existed.
665
+ files = contentCatalog.getFiles().filter((f) => f.src.component === component && f.src.version === version);
666
+ // ---------------------------------------------------------------
667
+ // 1. Scan pages (+ resolved includes) for cite:[key] usage,
668
+ // through a real Asciidoctor conversion (macro.ts's
669
+ // scanCiteKeys), not a hand-rolled regex. Runs BEFORE ever
670
+ // resolving or reading a single .bib file: the playbook-level
671
+ // scaffold applies to every component-version site-wide with
672
+ // no opt-in step (see the module docblock), so an asset-only
673
+ // component-version with no relation to citations at all -
674
+ // vale rule sets, PDF theme/font collections, and the like -
675
+ // still inherits it and used to hit real .bib resolution
676
+ // unconditionally, warning "not found" for files it never had
677
+ // any reason to carry. Scanning first and skipping entirely
678
+ // when nothing was found (see below) means a component-version
679
+ // with zero real cite:[] usage anywhere never even looks for
680
+ // its category files in the first place, rather than looking
681
+ // and then discovering there was nothing to find. Placeholder
682
+ // injection above still runs unconditionally either way: it
683
+ // does no .bib work and carries no warning of its own, and the
684
+ // resolveResource-poisoning hazard it guards against applies
685
+ // regardless of whether this component-version turns out to
686
+ // have real citation usage.
687
+ // ---------------------------------------------------------------
688
+ const componentAttrs = (contentCatalog.getComponents?.() ?? [])
689
+ .find((c) => c.name === component)?.versions
690
+ .find((v) => (v.version ?? v.name) === version)?.asciidoc?.attributes ?? {};
691
+ const scanFiles = files.filter((f) => f.src.family === 'page');
692
+ const includeVisited = new Set();
693
+ for (const f of scanFiles) {
694
+ const key = `${f.src.component}@${f.src.version}:${f.src.module}:${f.src.family}$${f.src.relative}`;
695
+ includeVisited.add(key);
696
+ }
697
+ const citedKeys = new Set();
698
+ for (const f of scanFiles) {
699
+ const raw = f.contents.toString('utf8');
700
+ const headerAttrs = (0, asciidoc_conditionals_1.pageHeaderAttributes)(raw);
701
+ const isDefined = (attr) => {
702
+ if (headerAttrs.has(attr))
703
+ return headerAttrs.get(attr) === true;
704
+ const v = componentAttrs[attr];
705
+ return v !== undefined && v !== null && v !== false;
706
+ };
707
+ let text = (0, index_1.stripNonContent)(raw, isDefined, []);
708
+ const extra = (0, index_1.collectIncludeText)(text, contentCatalog, component, version, f.src.module ?? 'ROOT', includeVisited, [], undefined, [], f.src.path ?? '');
709
+ if (extra)
710
+ text = `${text}\n${(0, index_1.stripNonContent)(extra, isDefined, [])}`;
711
+ // Same concatenation-then-convert hazard sanitizeForScan already
712
+ // fixed for assembly-numbering.ts's own scan (63a6538): a page
713
+ // and its resolved include target each carrying a `=`-level-0
714
+ // title makes the combined text an invalid multi-document
715
+ // concatenation before it ever reaches scanCiteKeys' own real
716
+ // processor.convert() call. This call site concatenates the same
717
+ // way but was missed in that fix; confirmed live, "Executing
718
+ // cite-it" is where the level-0/incomplete-row errors actually
719
+ // fired, not assembly-numbering.ts's own computeCitationNumbers.
720
+ for (const key of (0, macro_1.scanCiteKeys)((0, asciidoc_indexterms_1.sanitizeForScan)(text)))
721
+ citedKeys.add(key);
722
+ }
723
+ // A category's own ref_force list lists a key in the generated
724
+ // bibliography even when no page cites it at all (see the
725
+ // CiteCategoryConfig field docblock), so zero cited keys does not
726
+ // always mean zero real work; only skip when NEITHER a real
727
+ // citation nor a forced entry gives this component-version
728
+ // anything to generate. citedKeys.size === 0 already means the
729
+ // scan above found nothing to report as orphaned either, so no
730
+ // separate checkOrphanedCiteUsage call is needed on this path.
731
+ const anyForced = categories.some((cat) => cat.ref_force.length > 0);
732
+ if (citedKeys.size === 0 && !anyForced) {
733
+ const hasOwnExt = extByCompVer.has(compKey);
734
+ const message = `cite-it: ${compKey} - skipped (no cite:[key] usage anywhere and no ref_force entries in any category)`;
735
+ if (hasOwnExt)
736
+ logger.warn(message);
737
+ else
738
+ logger.debug(message);
739
+ continue;
740
+ }
741
+ // ---------------------------------------------------------------
742
+ // 2. Resolve, parse, and merge each category's .bib files.
743
+ // Files listed earlier in a category's own files[] are the
744
+ // shared base; later files override by citation key, mirroring
745
+ // autoterm's mergeTerminology semantics exactly.
746
+ // ---------------------------------------------------------------
747
+ const combinedEntries = new Map();
748
+ const keyCategory = new Map();
749
+ const categoryKeys = new Map();
750
+ for (const cat of categories) {
751
+ const bibFiles = resolveBibFiles(cat.ref_files, contentCatalog, component, version, module, logger);
752
+ let catEntries = new Map();
753
+ for (const bf of bibFiles) {
754
+ const { entries, warnings } = (0, registry_1.parseCiteFile)(bf.contents.toString('utf8'), `${compKey}:${bf.src.path ?? bf.src.relative ?? ''}`);
755
+ for (const w of warnings)
756
+ logger.warn(`cite-it: ${w}`);
757
+ const { merged } = (0, registry_1.mergeCiteRegistry)(catEntries, entries);
758
+ catEntries = merged;
759
+ }
760
+ const keys = new Set();
761
+ for (const [key, entry] of catEntries) {
762
+ if (keyCategory.has(key) && keyCategory.get(key) !== cat.ref_category) {
763
+ logger.warn(`cite-it: ${compKey} - citation key "${key}" appears in both category "${keyCategory.get(key)}" and "${cat.ref_category}"; the later category's membership wins`);
764
+ }
765
+ keyCategory.set(key, cat.ref_category);
766
+ combinedEntries.set(key, entry);
767
+ keys.add(key);
768
+ }
769
+ categoryKeys.set(cat.ref_category, keys);
770
+ }
771
+ // A key claimed by more than one category was added to every
772
+ // claiming category's own Set above, since each category's loop
773
+ // only knows about categories processed before it. keyCategory now
774
+ // holds each conflicted key's final, later-wins owner; remove the
775
+ // key from every OTHER category's Set so only its real owner lists
776
+ // it, matching the warning already logged above.
777
+ for (const [cat, keys] of categoryKeys) {
778
+ for (const key of [...keys]) {
779
+ if (keyCategory.get(key) !== cat)
780
+ keys.delete(key);
781
+ }
782
+ }
783
+ // Each category's own ref_force list reassigns a key's category
784
+ // membership outright, the same mechanism as the natural conflict
785
+ // resolution above: strip the key from its previous category's Set,
786
+ // then add it to THIS category's Set, the one whose own ref_force
787
+ // names it. A key's rendered row lives in exactly one category at a
788
+ // time, so this reassignment, not an additive union, is what "force
789
+ // this citation under this heading" actually means. Nesting
790
+ // ref_force inside its own category means the target is never
791
+ // ambiguous: there is no separate back-reference field to get wrong.
792
+ //
793
+ // A key with no entry anywhere warns rather than silently rendering
794
+ // nowhere: a visible mistake beats a citation named in ref_force
795
+ // deliberately just vanishing over a typo.
796
+ for (const cat of categories) {
797
+ for (const key of cat.ref_force) {
798
+ if (!combinedEntries.has(key)) {
799
+ logger.warn(`cite-it: ${compKey} - ref_force names unknown key "${key}" in category "${cat.ref_category}"; not found in any configured category's data`);
800
+ continue;
801
+ }
802
+ const previousCategory = keyCategory.get(key);
803
+ if (previousCategory && previousCategory !== cat.ref_category) {
804
+ categoryKeys.get(previousCategory)?.delete(key);
805
+ }
806
+ keyCategory.set(key, cat.ref_category);
807
+ categoryKeys.get(cat.ref_category)?.add(key);
808
+ }
809
+ }
810
+ if (!cfg.suppress_events) {
811
+ logEvent(`${compKey} - ${combinedEntries.size} bibliography entr${combinedEntries.size === 1 ? 'y' : 'ies'} loaded across ${categories.length} categor${categories.length === 1 ? 'y' : 'ies'}`);
812
+ }
813
+ for (const key of citedKeys) {
814
+ if (!combinedEntries.has(key)) {
815
+ logger.warn(`cite-it: ${compKey} - cite:[${key}] cites an unknown key; not found in any configured category`);
816
+ }
817
+ }
818
+ // ---------------------------------------------------------------
819
+ // 3. Populate the shared cache the cite:[key] macro reads at
820
+ // real conversion time (macro.ts). Formatted only for keys
821
+ // actually used (cited, or ref_force), same set the
822
+ // generated partial lists.
823
+ // ---------------------------------------------------------------
824
+ const refForceKeys = new Set(categories.flatMap((cat) => cat.ref_force));
825
+ const usedKeysByCategory = new Map();
826
+ const allUsedKeys = [];
827
+ for (const cat of categories) {
828
+ const keys = [...(categoryKeys.get(cat.ref_category) ?? new Set())]
829
+ .filter((k) => citedKeys.has(k) || refForceKeys.has(k))
830
+ .sort((a, b) => a.localeCompare(b));
831
+ usedKeysByCategory.set(cat.ref_category, keys);
832
+ allUsedKeys.push(...keys);
833
+ }
834
+ // A key's entry carrying a supersededby field (registry.ts) warns
835
+ // once here, at generation time, scoped to the keys a category's
836
+ // partial actually lists (cited or ref_force'd), not every entry
837
+ // the registry happens to hold: an unused entry going stale is not
838
+ // this component-version's problem to flag. Deliberately not
839
+ // raised again at each cite:[key] use site (macro.ts): one clear
840
+ // signal per generated partial, not one per occurrence in prose.
841
+ for (const [category, keys] of usedKeysByCategory) {
842
+ for (const key of keys) {
843
+ const supersededBy = combinedEntries.get(key)?.['supersededby'];
844
+ if (typeof supersededBy === 'string' && supersededBy) {
845
+ logger.warn(`cite-it: ${compKey} - citation "${key}" in category "${category}" carries supersededby "${supersededBy}"; a newer revision is available`);
846
+ }
847
+ }
848
+ }
849
+ const formattedByKey = (0, csl_1.formatBibliographyEntries)(compStyle, combinedEntries, allUsedKeys, compBlanketPubstate);
850
+ const cacheData = { entriesByKey: combinedEntries, formattedByKey, blanketPubstate: compBlanketPubstate };
851
+ macro_1.citeCache.set(compKey, cacheData);
852
+ assemblyScanContexts.set(compKey, {
853
+ contentCatalog: contentCatalog,
854
+ component,
855
+ version,
856
+ componentAttrs,
857
+ module,
858
+ categoryPartials,
859
+ });
860
+ keyCategories.set(compKey, keyCategory);
861
+ // ---------------------------------------------------------------
862
+ // 4. Generate and inject each category's own bibliography
863
+ // partial. A component author routes different categories to
864
+ // different pages simply by choosing which page includes
865
+ // which category's partial.
866
+ // ---------------------------------------------------------------
867
+ for (const cp of categoryPartials) {
868
+ const cat = categories.find((c) => c.ref_category === cp.category);
869
+ const keys = usedKeysByCategory.get(cat.ref_category) ?? [];
870
+ const content = generateCategoryPartial(keys, formattedByKey);
871
+ (0, index_2.injectPartial)(contentCatalog, files, component, version, module, cp.relative, content, compWriteToDisk, logger);
872
+ }
873
+ }
874
+ }
875
+ logEvent('cite-it finished');
876
+ });
877
+ // ---------------------------------------------------------------------
878
+ // PDF/assembled numbering. See assembly-numbering.ts's own module
879
+ // docblock for the full reasoning behind the numbering mechanism itself.
880
+ //
881
+ // Computed LAZILY, memoized per component-version, on the first assembled
882
+ // loadAsciiDoc call the wrapper above sees for that component-version,
883
+ // rather than eagerly in a navigationBuilt listener as an earlier version
884
+ // of this code did (the earlier version genuinely raced against
885
+ // `@antora/assembler`'s own navigationBuilt listener and lost against a
886
+ // real playbook where `@antora/pdf-extension` preceded cite-it in
887
+ // `antora.extensions`, leaving every cite:[key] unrewritten with no
888
+ // error). Computing lazily on first actual use, inside this same wrapper
889
+ // invocation, sidesteps that race entirely regardless of extension order.
890
+ //
891
+ // A DIFFERENT bug surfaced later, unrelated to that race: this function
892
+ // used to always resolve navigation as `componentVersion?.navigation`,
893
+ // falling back to a hardcoded, always-unnamed assembler-profile lookup
894
+ // (`assemblerProfiles.get(compKey).get(undefined)`) for any navFiles
895
+ // override. Verified directly against @antora/assembler's own source
896
+ // (assemble-content.js/build-navigation.js): `componentVersion.navigation`
897
+ // is only ever assigned by @antora/navigation-builder for a component
898
+ // version that owns at least one real `nav`-family file — it stays
899
+ // permanently undefined otherwise, by design, not a timing artifact.
900
+ // Assembler itself resolves the active profile as `assemblyConfig.profile`,
901
+ // which DEFAULTS TO THE TARGET BACKEND NAME (e.g. 'pdf') whenever no
902
+ // profile is configured explicitly, never bare `undefined` in an ordinary
903
+ // PDF build, and looks up that component-version's navFiles override
904
+ // under THAT key first, falling back to the unnamed slot only as a second
905
+ // choice (`componentVersionProfiles?.get(baseModel.profile) ??
906
+ // componentVersionProfiles?.get()`). A hardcoded `.get(undefined)` lookup
907
+ // here therefore misses the real override in the common case. Assembler
908
+ // also bakes the resolved profile name into every assembled document's
909
+ // own real attributes as `assembler-profile` (assemble-content.js's
910
+ // intrinsicAttributes, merged into what the wrapper above already
911
+ // receives as `asciidocConfig.attributes`) specifically so a consumer
912
+ // like this one can read back the SAME profile it resolved, rather than
913
+ // needing to reimplement its resolution logic; `profileName` below is
914
+ // that value, threaded through from the wrapper.
915
+ //
916
+ // A THIRD gap surfaced later still, confirmed live against a real Antora
917
+ // build: a playbook can register @antora/pdf-extension programmatically,
918
+ // at normal register()-phase time, instead of through the playbook's
919
+ // own antora.extensions list, with no ordering race either way. With no
920
+ // ext.assembler block anywhere (assemblerProfiles legitimately empty)
921
+ // and real, unmodified Antora navigation driving assembly correctly,
922
+ // `componentVersion.navigation` still read empty here. Assembler's own
923
+ // no-profile resolution
924
+ // (assemble-content.js's generateSelectAssemblerProfile) never actually
925
+ // reads componentVersion.navigation FIRST — it reads
926
+ // `navigationCatalog?.getNavigation(componentVersion.name,
927
+ // componentVersion.version) ?? componentVersion.navigation`, the
928
+ // NavigationCatalog instance @antora/navigation-builder's own
929
+ // buildNavigation returns and generate-site.js stores as the
930
+ // navigationCatalog context variable, alongside (not only through)
931
+ // componentVersion.navigation. This function only ever checked the
932
+ // fallback half of that chain; matching assembler's own real
933
+ // precedence, primary source first, is the fix, not a guess: it can
934
+ // only find more, never less, than the fallback-only lookup already
935
+ // did.
936
+ //
937
+ // A FOURTH gap: even navigationCatalog can read empty. Confirmed live,
938
+ // not a codebase bug: componentVersion.navigation itself reads undefined
939
+ // at this exact read point despite a real nav-family file existing in
940
+ // the content catalog, confirmed as the SAME object reference two
941
+ // independent ways (this function's own
942
+ // manual lookup and contentCatalog.getComponentVersion()) — not a stale
943
+ // copy, not the wrong object. Rather than depend further on exactly
944
+ // when/how something else populates it, self-heal: call
945
+ // @antora/navigation-builder's own buildNavigation directly (exposed via
946
+ // getFunctions(), the same mechanism buildAlternateNavigation below
947
+ // already uses), which re-derives navigation from the catalog's own
948
+ // nav-family files and reassigns componentVersion.navigation as a
949
+ // genuine side effect, discarding buildNavigation's own returned
950
+ // NavigationCatalog (a fresh, separate instance every call) rather than
951
+ // touching the site's real one.
952
+ // ---------------------------------------------------------------------
953
+ function ensureAssemblyDataComputed(compKey, profileName) {
954
+ if (citationNumbers.has(compKey))
955
+ return; // already computed, or in progress
956
+ const scanCtx = assemblyScanContexts.get(compKey);
957
+ if (!scanCtx)
958
+ return;
959
+ citationNumbers.set(compKey, new Map()); // reentrancy guard, replaced below
960
+ // scanCtx.contentCatalog is typed against assembly-numbering.ts's own
961
+ // narrower ContentCatalog interface (no getComponents); the stored
962
+ // runtime object is the same real catalog assemblyScanContexts.set(...)
963
+ // captured, just narrowed at that point too, so this cast back to the
964
+ // wider local interface is safe.
965
+ const contentCatalog = scanCtx.contentCatalog;
966
+ const components = contentCatalog.getComponents?.() ?? [];
967
+ const contextVars = ctx.getVariables?.();
968
+ const assemblerProfiles = contextVars?.assemblerProfiles;
969
+ const navigationCatalog = contextVars?.navigationCatalog;
970
+ const componentVersion = components
971
+ .find((c) => c.name === scanCtx.component)?.versions
972
+ .find((v) => (v.version ?? v.name) === scanCtx.version);
973
+ let liveNavigation = navigationCatalog?.getNavigation(scanCtx.component, scanCtx.version);
974
+ if ((!liveNavigation || !liveNavigation.length) && ctx.getFunctions) {
975
+ // Self-heal: confirmed live, not a codebase bug, that
976
+ // componentVersion.navigation, and
977
+ // navigationCatalog's own record of it, can BOTH read empty here
978
+ // even though a real nav-family file exists in the content catalog
979
+ // and the SAME object reference is confirmed live two independent
980
+ // ways (this manual lookup and contentCatalog.getComponentVersion())
981
+ // — ruling out a stale copy or wrong-object bug on cite-it's own
982
+ // side. buildNavigation is self-contained and idempotent: it
983
+ // re-derives navigation straight from the catalog's own nav-family
984
+ // files and re-assigns componentVersion.navigation directly,
985
+ // discarding its own returned NavigationCatalog (a fresh, separate
986
+ // instance every call) rather than touching the site's real one.
987
+ // Same getFunctions() mechanism buildAlternateNavigation below
988
+ // already uses.
989
+ //
990
+ // buildNavigation's own internal implementation reads its loadAsciiDoc
991
+ // through this SAME live, shared function table (getFunctions'
992
+ // $unsafe accessor, by design, so it picks up any real replacement -
993
+ // including this extension's own wrapper). This self-heal fires from
994
+ // INSIDE that wrapper, mid-conversion of some other real page, so
995
+ // without intervention buildNavigation's own internal parse of each
996
+ // nav.adoc would recurse straight back through the wrapper, nested
997
+ // inside an already-in-progress loadAsciiDoc call. Sidestepped by
998
+ // swapping the live table back to the real, unwrapped loader for the
999
+ // duration of this one synchronous call only, then always restoring
1000
+ // the wrapper again, success or failure.
1001
+ try {
1002
+ const buildNav = ctx.getFunctions().buildNavigation;
1003
+ if (typeof buildNav === 'function' && realLoadAsciiDoc) {
1004
+ ctx.replaceFunctions?.({ loadAsciiDoc: realLoadAsciiDoc });
1005
+ try {
1006
+ buildNav(contentCatalog, componentVersion?.asciidoc);
1007
+ }
1008
+ finally {
1009
+ if (loadAsciiDocWrapper)
1010
+ ctx.replaceFunctions?.({ loadAsciiDoc: loadAsciiDocWrapper });
1011
+ }
1012
+ liveNavigation = componentVersion?.navigation;
1013
+ }
1014
+ }
1015
+ catch (err) {
1016
+ logger.warn(`cite-it: ${compKey} - buildNavigation fallback failed: ${String(err)}`);
1017
+ }
1018
+ }
1019
+ const defaultNavigation = (liveNavigation ??
1020
+ componentVersion?.navigation ??
1021
+ []);
1022
+ let navigation = defaultNavigation;
1023
+ // Same two-step lookup assembler itself performs: the resolved active
1024
+ // profile first (never bare undefined for an ordinary export whose
1025
+ // config omits `profile:` explicitly, see the docblock above), the
1026
+ // unnamed default slot only as a fallback.
1027
+ const componentVersionProfiles = assemblerProfiles?.get(`${scanCtx.version}@${scanCtx.component}`);
1028
+ const profileData = componentVersionProfiles?.get(profileName) ?? componentVersionProfiles?.get(undefined);
1029
+ if (profileData?.navFiles?.length && ctx.require && ctx.getFunctions) {
1030
+ try {
1031
+ // Mirrors @antora/assembler's own resolution exactly
1032
+ // (assemble-content.js's generateSelectAssemblerProfile):
1033
+ // buildAlternateNavigation from the shared generator functions if
1034
+ // some other extension already replaced it, otherwise straight
1035
+ // from the real @antora/navigation-builder module.
1036
+ const fxns = ctx.getFunctions();
1037
+ const buildAlternateNavigation = fxns.buildAlternateNavigation ??
1038
+ ctx.require('@antora/navigation-builder').buildAlternateNavigation;
1039
+ navigation = (buildAlternateNavigation(contentCatalog, componentVersion, profileData.navFiles, {}) ?? []);
1040
+ }
1041
+ catch (err) {
1042
+ logger.warn(`cite-it: ${compKey} - could not resolve the assembler profile's alternate navigation, falling back to the default navigation for numbering: ${String(err)}`);
1043
+ }
1044
+ }
1045
+ const numbers = (0, assembly_numbering_1.computeCitationNumbers)(navigation, scanCtx);
1046
+ citationNumbers.set(compKey, numbers);
1047
+ logger.debug(`cite-it: ${compKey} - computed ${numbers.size} true first-appearance citation number(s) for assembled output`);
1048
+ const categoryScopes = (0, assembly_numbering_1.findBibliographyScopes)(navigation, scanCtx);
1049
+ const keyCategory = keyCategories.get(compKey) ?? new Map();
1050
+ const keyScopes = new Map();
1051
+ const missingScopeCategories = new Set();
1052
+ for (const [key] of numbers) {
1053
+ const cat = keyCategory.get(key);
1054
+ const scope = cat ? categoryScopes.get(cat) : undefined;
1055
+ if (scope) {
1056
+ keyScopes.set(key, scope);
1057
+ }
1058
+ else if (cat) {
1059
+ missingScopeCategories.add(cat);
1060
+ }
1061
+ }
1062
+ keyScopesByComp.set(compKey, keyScopes);
1063
+ for (const cat of missingScopeCategories) {
1064
+ 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`);
1065
+ }
1066
+ // The generated bibliography partial's own cite:[key] row labels
1067
+ // need rewriting too, and specifically now, before any assembly-time
1068
+ // loadAsciiDoc call resolves an include:: of it: Antora's include
1069
+ // processor reads an included partial's content directly from the
1070
+ // catalog, never back through loadAsciiDoc, so the wrapper above never
1071
+ // gets a chance to touch it otherwise (see rewritePartialForAssembly's
1072
+ // own docblock for the full story). Safe to do here, synchronously,
1073
+ // before returning: this function itself is always called from inside
1074
+ // the wrapper, before that same invocation's real loadAsciiDoc call.
1075
+ (0, assembly_numbering_1.rewritePartialForAssembly)(scanCtx, numbers);
1076
+ }
1077
+ // Per participating component-version, the pages whose raw source
1078
+ // carries a cite:[ marker at all. Memoized on first use: the set is
1079
+ // fixed by contentClassified time, and rewriteForeignCitedPages below
1080
+ // runs once per assembled page load, site-wide.
1081
+ const citedPagesByComp = new Map();
1082
+ function rawSourceHolder(f) {
1083
+ const src = f.src;
1084
+ return src.contents !== undefined ? src : f;
1085
+ }
1086
+ // ---------------------------------------------------------------------
1087
+ // Cross-component includes in assembled output. The wrapper above
1088
+ // rewrites only the buffer @antora/assembler hands to loadAsciiDoc, the
1089
+ // page's own source. A page whose body is `include::other:ROOT:page$x.adoc[]`
1090
+ // carries no citation of its own, and Antora's include processor then
1091
+ // reads the included page (and, through it, the included bibliography
1092
+ // partial) straight from the catalog, never back through loadAsciiDoc.
1093
+ // Confirmed live: a component's own PDF rendered `[1]` correctly while a
1094
+ // sibling component wrapping the same page in an include:: shipped the
1095
+ // literal `cite:[key]` text, both inline and as the bibliography's own
1096
+ // row label, since Ruby asciidoctor-pdf has no citeit macro.
1097
+ //
1098
+ // For the duration of one real loadAsciiDoc call, every cited page of
1099
+ // every OTHER participating component-version is rewritten in the
1100
+ // catalog with its OWN component's numbers (the same numbering the
1101
+ // included partial, already rewritten by rewritePartialForAssembly, now
1102
+ // carries), then restored in the wrapper's finally block. Computing
1103
+ // every other component's numbers here, rather than only the loaded
1104
+ // page's own, also removes an ordering dependency: a wrapper component
1105
+ // assembled BEFORE the component it includes used to see that partial
1106
+ // still unrewritten, because rewritePartialForAssembly had not yet run
1107
+ // for it.
1108
+ //
1109
+ // A link target exists only when the included page itself hosts the
1110
+ // key's bibliography anchor: the anchor then lands inside the loaded
1111
+ // page's own fragment of the assembly, so the xref names the loaded
1112
+ // page, not the source page (an unqualified `xref:<page>.adoc` resolves
1113
+ // against the including document's component, where the source page's
1114
+ // name need not exist). Any other key is rewritten to a plain, unlinked
1115
+ // number, the same known-correct-but-unlinked fallback rewriteCiteMarkers
1116
+ // already applies to a key with no scope.
1117
+ // ---------------------------------------------------------------------
1118
+ function rewriteForeignCitedPages(loadedSrc, profileName) {
1119
+ const loadedKey = `${loadedSrc.component}@${loadedSrc.version}`;
1120
+ const loadedDocname = (loadedSrc.relative ?? '').replace(/\.[^./]+$/, '');
1121
+ const restores = [];
1122
+ for (const [compKey, scanCtx] of assemblyScanContexts) {
1123
+ if (compKey === loadedKey)
1124
+ continue;
1125
+ ensureAssemblyDataComputed(compKey, profileName);
1126
+ const numbers = citationNumbers.get(compKey);
1127
+ if (!numbers || numbers.size === 0)
1128
+ continue;
1129
+ const hostScopes = keyScopesByComp.get(compKey) ?? new Map();
1130
+ let pages = citedPagesByComp.get(compKey);
1131
+ if (!pages) {
1132
+ pages = scanCtx.contentCatalog.getFiles().filter((f) => f.src.component === scanCtx.component &&
1133
+ f.src.version === scanCtx.version &&
1134
+ f.src.family === 'page' &&
1135
+ Buffer.from(rawSourceHolder(f).contents).toString('utf8').includes('cite:['));
1136
+ citedPagesByComp.set(compKey, pages);
1137
+ }
1138
+ for (const page of pages) {
1139
+ const pageDocname = (page.src.relative ?? '').replace(/\.[^./]+$/, '');
1140
+ const scopes = new Map();
1141
+ for (const [key, host] of hostScopes) {
1142
+ if (host === pageDocname)
1143
+ scopes.set(key, loadedDocname);
1144
+ }
1145
+ const holder = rawSourceHolder(page);
1146
+ const original = holder.contents;
1147
+ const before = Buffer.from(original).toString('utf8');
1148
+ const after = (0, assembly_numbering_1.rewriteCiteMarkers)(before, numbers, true, scopes);
1149
+ if (after === before)
1150
+ continue;
1151
+ holder.contents = Buffer.from(after, 'utf8');
1152
+ restores.push(() => { holder.contents = original; });
1153
+ }
1154
+ }
1155
+ if (restores.length === 0)
1156
+ return undefined;
1157
+ return () => { for (const restore of restores)
1158
+ restore(); };
1159
+ }
1160
+ }
1161
+ /**
1162
+ * One category's own generated partial content: the bibliography list
1163
+ * only, no heading. A component author decides where THIS category
1164
+ * appears by choosing which page includes `partial$<categoryPartialName
1165
+ * (...)>`, independent of every other category, and supplies that page's
1166
+ * own heading directly as ordinary authored AsciiDoc immediately above
1167
+ * the include line. Returns '' for an empty category (caller skips
1168
+ * injecting it at all).
1169
+ */
1170
+ function generateCategoryPartial(keys, formattedByKey) {
1171
+ if (keys.length === 0)
1172
+ return '';
1173
+ // The cite-it-bibliography role is a real theming hook, not
1174
+ // decoration: dlist_role_theming.rb (the PDF theme's own converter
1175
+ // extension, since asciidoctor-pdf's dlist rendering has no per-role
1176
+ // lookup of its own) reads role_cite-it-bibliography_* theme keys for
1177
+ // this exact role to control term boldness and text alignment for PDF
1178
+ // output, and it renders as an ordinary CSS-targetable class in HTML.
1179
+ const lines = ['[horizontal.cite-it-bibliography]'];
1180
+ for (const key of keys) {
1181
+ const text = formattedByKey.get(key) ?? `(no formatted text for "${key}")`;
1182
+ // The [[cite-<key>]] anchor is what an inline cite:[key] citation
1183
+ // links to for assembled/PDF output (rewriteCiteMarkers, asLink:
1184
+ // true), see assembly-numbering.ts. Harmless, invisible in ordinary
1185
+ // HTML rendering.
1186
+ lines.push(`[[cite-${key}]]cite:[${key}]:: ${text}`);
1187
+ }
1188
+ lines.push('');
1189
+ return lines.join('\n');
1190
+ }
1191
+ //# sourceMappingURL=index.js.map