@baiyibai-antora/extensions 1.7.0 → 1.8.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.
- package/CHANGELOG.adoc +6 -0
- package/dist/extensions/autoterm/index.d.ts +210 -0
- package/dist/extensions/autoterm/index.d.ts.map +1 -0
- package/dist/extensions/autoterm/index.js +770 -0
- package/dist/extensions/autoterm/index.js.map +1 -0
- package/dist/extensions/trademark-it/index.d.ts +1 -0
- package/dist/extensions/trademark-it/index.d.ts.map +1 -1
- package/dist/extensions/trademark-it/index.js +1 -0
- package/dist/extensions/trademark-it/index.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @baiyibai-antora/extensions - autoterm
|
|
4
|
+
*
|
|
5
|
+
* Scans the Antora content catalog for AsciiDoc index term markup and injects
|
|
6
|
+
* two generated AsciiDoc partials into each participating component-version:
|
|
7
|
+
*
|
|
8
|
+
* modules/ROOT/partials/generated-terminology.adoc
|
|
9
|
+
* modules/ROOT/partials/generated-index.adoc
|
|
10
|
+
*
|
|
11
|
+
* A component participates by containing a page that includes the partial:
|
|
12
|
+
*
|
|
13
|
+
* include::partial$generated-terminology.adoc[]
|
|
14
|
+
* include::partial$generated-index.adoc[]
|
|
15
|
+
*
|
|
16
|
+
* The including page's position in nav.adoc controls where the section
|
|
17
|
+
* appears. The terminology page carries :!sectnums: to render unnumbered.
|
|
18
|
+
* The index page carries :assembly-style: index so the Antora Assembler
|
|
19
|
+
* preserves the [index] section and stock asciidoctor-pdf renders the
|
|
20
|
+
* page-numbered index; the generated index partial holds the web-linked
|
|
21
|
+
* index guarded by ifndef::loader-assembler[] so it renders only outside
|
|
22
|
+
* the PDF build.
|
|
23
|
+
*
|
|
24
|
+
* Definitions come from a shared table (the autoterm-data component's
|
|
25
|
+
* partial) optionally overridden per component by a local partial named
|
|
26
|
+
* trademark-local-style: modules/ROOT/partials/terminology-local.adoc.
|
|
27
|
+
* Local rows replace shared rows for the same term.
|
|
28
|
+
*
|
|
29
|
+
* Configuration in antora-playbook.yml:
|
|
30
|
+
*
|
|
31
|
+
* antora:
|
|
32
|
+
* extensions:
|
|
33
|
+
* - require: '@baiyibai-antora/extensions/autoterm'
|
|
34
|
+
* autoterm:
|
|
35
|
+
* enabled: true
|
|
36
|
+
* # Antora resource ID or filename of the shared definitions partial.
|
|
37
|
+
* terminology_file: 'ROOT:partial$terminology-table.adoc'
|
|
38
|
+
* # Filename of a component's local override partial.
|
|
39
|
+
* local_file: 'terminology-local.adoc'
|
|
40
|
+
* # Filenames of the generated partials.
|
|
41
|
+
* terminology_partial: 'generated-terminology.adoc'
|
|
42
|
+
* index_partial: 'generated-index.adoc'
|
|
43
|
+
* # AsciiDoc cols attribute for each generated table.
|
|
44
|
+
* terminology_cols: '15,85'
|
|
45
|
+
* index_cols: '30,70'
|
|
46
|
+
* # 'combined' welds each letter into its group's first row (default);
|
|
47
|
+
* # 'separate' emits the letter as its own row (may orphan at a page
|
|
48
|
+
* # bottom in the PDF).
|
|
49
|
+
* letter_style: 'combined'
|
|
50
|
+
* # Attribution line rendered above the Terminology content outside
|
|
51
|
+
* # the PDF build. AsciiDoc inline markup allowed; empty disables.
|
|
52
|
+
* attribution: 'Generated by https://gitlab.com/baiyibai-antora/baiyibai-extensions[autoterm].'
|
|
53
|
+
*
|
|
54
|
+
* The including page overrides presentation per component through header
|
|
55
|
+
* attributes: :autoterm-terminology-cols:, :autoterm-index-cols:,
|
|
56
|
+
* :autoterm-letter-style:.
|
|
57
|
+
*
|
|
58
|
+
* The generated partials live in the content catalog and regenerate on every
|
|
59
|
+
* build. Setting write_to_disk: true additionally writes them into the
|
|
60
|
+
* component's local worktree; components loaded from a remote source skip
|
|
61
|
+
* the disk write, mirroring trademark-it.
|
|
62
|
+
*
|
|
63
|
+
* Per-component configuration in a component's antora.yml under
|
|
64
|
+
* ext.autoterm overrides local_file, terminology_cols, index_cols,
|
|
65
|
+
* letter_style, write_to_disk, and enabled for that component-version.
|
|
66
|
+
* Unlike trademark-it, the block is optional: participation comes from the
|
|
67
|
+
* include, not from the block.
|
|
68
|
+
*/
|
|
69
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
70
|
+
if (k2 === undefined) k2 = k;
|
|
71
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
72
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
73
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
74
|
+
}
|
|
75
|
+
Object.defineProperty(o, k2, desc);
|
|
76
|
+
}) : (function(o, m, k, k2) {
|
|
77
|
+
if (k2 === undefined) k2 = k;
|
|
78
|
+
o[k2] = m[k];
|
|
79
|
+
}));
|
|
80
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
81
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
82
|
+
}) : function(o, v) {
|
|
83
|
+
o["default"] = v;
|
|
84
|
+
});
|
|
85
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
86
|
+
var ownKeys = function(o) {
|
|
87
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
88
|
+
var ar = [];
|
|
89
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
90
|
+
return ar;
|
|
91
|
+
};
|
|
92
|
+
return ownKeys(o);
|
|
93
|
+
};
|
|
94
|
+
return function (mod) {
|
|
95
|
+
if (mod && mod.__esModule) return mod;
|
|
96
|
+
var result = {};
|
|
97
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
98
|
+
__setModuleDefault(result, mod);
|
|
99
|
+
return result;
|
|
100
|
+
};
|
|
101
|
+
})();
|
|
102
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
103
|
+
exports.parseTerminologyTable = parseTerminologyTable;
|
|
104
|
+
exports.mergeTerminology = mergeTerminology;
|
|
105
|
+
exports.stripVerbatimBlocks = stripVerbatimBlocks;
|
|
106
|
+
exports.scanIndexterms = scanIndexterms;
|
|
107
|
+
exports.buildTerminologyEntries = buildTerminologyEntries;
|
|
108
|
+
exports.generateTerminologyPartial = generateTerminologyPartial;
|
|
109
|
+
exports.generateIndexPartial = generateIndexPartial;
|
|
110
|
+
exports.pageTitle = pageTitle;
|
|
111
|
+
exports.pageAttribute = pageAttribute;
|
|
112
|
+
exports.register = register;
|
|
113
|
+
const log_time_1 = require("../../lib/log-time");
|
|
114
|
+
const index_1 = require("../trademark-it/index");
|
|
115
|
+
const fs = __importStar(require("node:fs"));
|
|
116
|
+
const path = __importStar(require("node:path"));
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Logging helpers (mirrors trademark-it style)
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
let quietMode = true;
|
|
121
|
+
function logEvent(message) {
|
|
122
|
+
console.log(`[${(0, log_time_1.nowLocal)()}] \x1b[94mEVENT\x1b[0m (autoterm): \x1b[94m${message}\x1b[0m`);
|
|
123
|
+
}
|
|
124
|
+
function makeLogger(hostLogger) {
|
|
125
|
+
return {
|
|
126
|
+
debug: (m) => { if (!quietMode) {
|
|
127
|
+
if (hostLogger?.debug)
|
|
128
|
+
hostLogger.debug(m);
|
|
129
|
+
else
|
|
130
|
+
console.debug(m);
|
|
131
|
+
} },
|
|
132
|
+
info: (m) => { if (!quietMode) {
|
|
133
|
+
if (hostLogger?.info)
|
|
134
|
+
hostLogger.info(m);
|
|
135
|
+
else
|
|
136
|
+
console.log(m);
|
|
137
|
+
} },
|
|
138
|
+
warn: (m) => { if (hostLogger?.warn)
|
|
139
|
+
hostLogger.warn(m);
|
|
140
|
+
else
|
|
141
|
+
console.warn(m); },
|
|
142
|
+
error: (m) => { if (hostLogger?.error)
|
|
143
|
+
hostLogger.error(m);
|
|
144
|
+
else
|
|
145
|
+
console.error(m); },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Config helpers (mirrors trademark-it style)
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
function toBool(v, def) {
|
|
152
|
+
if (v === true)
|
|
153
|
+
return true;
|
|
154
|
+
if (v === false)
|
|
155
|
+
return false;
|
|
156
|
+
if (typeof v === 'string') {
|
|
157
|
+
const s = v.trim().toLowerCase();
|
|
158
|
+
if (['true', '1', 'yes', 'on'].includes(s))
|
|
159
|
+
return true;
|
|
160
|
+
if (['false', '0', 'no', 'off'].includes(s))
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return def;
|
|
164
|
+
}
|
|
165
|
+
function pick(obj, keys, def) {
|
|
166
|
+
for (const k of keys) {
|
|
167
|
+
if (obj && obj[k] !== undefined)
|
|
168
|
+
return obj[k];
|
|
169
|
+
}
|
|
170
|
+
return def;
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Definitions table parser
|
|
174
|
+
//
|
|
175
|
+
// Parses the shared/local terminology table: term in the first column,
|
|
176
|
+
// abbreviation in the second, definition in the third. Cell layout follows
|
|
177
|
+
// the trademark table style: header row inline, data cells one per line.
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
function parseTerminologyTable(asciidocText) {
|
|
180
|
+
const definitions = new Map();
|
|
181
|
+
const tableMatch = asciidocText.match(/\|={3,}([\s\S]*?)\|={3,}/);
|
|
182
|
+
if (!tableMatch)
|
|
183
|
+
return definitions;
|
|
184
|
+
const tableBody = tableMatch[1];
|
|
185
|
+
let colCount = 3;
|
|
186
|
+
const colsMatch = asciidocText.match(/\[[^\]]*cols\s*=\s*["']([^"']+)["']/);
|
|
187
|
+
if (colsMatch)
|
|
188
|
+
colCount = colsMatch[1].split(',').length;
|
|
189
|
+
const cells = [];
|
|
190
|
+
for (const raw of tableBody.split('\n')) {
|
|
191
|
+
const line = raw.trim();
|
|
192
|
+
if (!line || line === '|===')
|
|
193
|
+
continue;
|
|
194
|
+
if (!line.startsWith('|')) {
|
|
195
|
+
// Continuation of the previous cell (wrapped definition text).
|
|
196
|
+
if (cells.length > 0)
|
|
197
|
+
cells[cells.length - 1] += ` ${line}`;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const parts = line.split('|').map((p) => p.trim()).filter((p) => p !== '');
|
|
201
|
+
if (parts.length > 1) {
|
|
202
|
+
cells.push(...parts);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
cells.push(line.slice(1).trim());
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const rows = [];
|
|
209
|
+
for (let i = 0; i + colCount - 1 < cells.length; i += colCount) {
|
|
210
|
+
rows.push(cells.slice(i, i + colCount));
|
|
211
|
+
}
|
|
212
|
+
// Skip the header row.
|
|
213
|
+
for (const row of rows.slice(1)) {
|
|
214
|
+
const fullTerm = (row[0] ?? '').trim();
|
|
215
|
+
if (!fullTerm)
|
|
216
|
+
continue;
|
|
217
|
+
definitions.set(fullTerm, {
|
|
218
|
+
fullTerm,
|
|
219
|
+
abbreviation: (row[1] ?? '').trim(),
|
|
220
|
+
definition: (row[2] ?? '').trim(),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return definitions;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Merge a component-local definitions map over the shared map. A local entry
|
|
227
|
+
* replaces the shared entry whose full term matches exactly, mirroring the
|
|
228
|
+
* exact-key lookup the definitions serve; remaining local entries are added.
|
|
229
|
+
*/
|
|
230
|
+
function mergeTerminology(shared, local) {
|
|
231
|
+
const merged = new Map(shared);
|
|
232
|
+
const overridden = [];
|
|
233
|
+
for (const [key, entry] of local) {
|
|
234
|
+
if (merged.has(key))
|
|
235
|
+
overridden.push(key);
|
|
236
|
+
merged.set(key, entry);
|
|
237
|
+
}
|
|
238
|
+
return { merged, overridden };
|
|
239
|
+
}
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Indexterm scanner
|
|
242
|
+
//
|
|
243
|
+
// Extracts index terms from raw AsciiDoc source: ((visible)), (((concealed))),
|
|
244
|
+
// indexterm:[...], and indexterm2:[...]. Comments and suppressed conditionals
|
|
245
|
+
// are stripped first. An inner parenthetical names an abbreviation:
|
|
246
|
+
// ((Full Term (Abbr) )) — the trailing space before the closing marks keeps
|
|
247
|
+
// the stock PDF index intact, but the scanner accepts both spellings.
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
/**
|
|
250
|
+
* Blank the content of listing and literal blocks (---- and .... fences).
|
|
251
|
+
* Asciidoctor does not process index terms in verbatim context, so the
|
|
252
|
+
* scanner must not either; documentation showing ((...)) syntax inside a
|
|
253
|
+
* listing stays inert. Line count is preserved.
|
|
254
|
+
*/
|
|
255
|
+
function stripVerbatimBlocks(text) {
|
|
256
|
+
const out = [];
|
|
257
|
+
let fence = null;
|
|
258
|
+
for (const line of text.split('\n')) {
|
|
259
|
+
const trimmed = line.trimEnd();
|
|
260
|
+
if (fence) {
|
|
261
|
+
if (trimmed === fence)
|
|
262
|
+
fence = null;
|
|
263
|
+
out.push('');
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (/^-{4,}$/.test(trimmed) || /^\.{4,}$/.test(trimmed)) {
|
|
267
|
+
fence = trimmed;
|
|
268
|
+
out.push('');
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
out.push(line);
|
|
272
|
+
}
|
|
273
|
+
return out.join('\n');
|
|
274
|
+
}
|
|
275
|
+
function scanIndexterms(sourceText, ignoreConditionals = []) {
|
|
276
|
+
// Inline passthroughs display index term syntax without indexing it
|
|
277
|
+
// (+((Full Term))+), matching Asciidoctor's behavior; remove them and
|
|
278
|
+
// verbatim block content before scanning.
|
|
279
|
+
const text = stripVerbatimBlocks((0, index_1.stripNonContent)(sourceText, ignoreConditionals))
|
|
280
|
+
.replace(/\+\({2,3}[\s\S]+?\){2,3}\+/g, ' ');
|
|
281
|
+
const found = [];
|
|
282
|
+
const record = (raw) => {
|
|
283
|
+
const inner = raw.trim();
|
|
284
|
+
if (!inner)
|
|
285
|
+
return;
|
|
286
|
+
const abbrMatch = inner.match(/^(.*?)\s+\(([^()]+)\)$/);
|
|
287
|
+
if (abbrMatch) {
|
|
288
|
+
found.push({ full: abbrMatch[1].trim(), abbr: abbrMatch[2].trim() });
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
found.push({ full: inner, abbr: null });
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
// Concealed terms first so the visible-form regex does not half-match them.
|
|
295
|
+
const concealedRe = /\(\(\(([\s\S]+?)\)\)\)/g;
|
|
296
|
+
let scrubbed = text.replace(concealedRe, (_, inner) => {
|
|
297
|
+
record(inner.split(',')[0]);
|
|
298
|
+
return ' ';
|
|
299
|
+
});
|
|
300
|
+
// Visible flow terms. The lazy match stops at the first )); when the
|
|
301
|
+
// captured text holds an unclosed ( and the next character is ), extend by
|
|
302
|
+
// one so ((Full Term (Abbr))) yields "Full Term (Abbr)".
|
|
303
|
+
const visibleRe = /\(\(([\s\S]+?)\)\)/g;
|
|
304
|
+
let m;
|
|
305
|
+
while ((m = visibleRe.exec(scrubbed)) !== null) {
|
|
306
|
+
let inner = m[1];
|
|
307
|
+
const opens = (inner.match(/\(/g) ?? []).length;
|
|
308
|
+
const closes = (inner.match(/\)/g) ?? []).length;
|
|
309
|
+
if (opens > closes && scrubbed[visibleRe.lastIndex] === ')') {
|
|
310
|
+
inner += ')';
|
|
311
|
+
visibleRe.lastIndex += 1;
|
|
312
|
+
}
|
|
313
|
+
record(inner);
|
|
314
|
+
}
|
|
315
|
+
// Macro forms.
|
|
316
|
+
const macroRe = /indexterm2?:\[([^\]]+)\]/g;
|
|
317
|
+
while ((m = macroRe.exec(text)) !== null) {
|
|
318
|
+
record(m[1].split(',')[0]);
|
|
319
|
+
}
|
|
320
|
+
return found;
|
|
321
|
+
}
|
|
322
|
+
function escapeCell(s) {
|
|
323
|
+
return s.replace(/\|/g, '\\|');
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Build the terminology entries from the scanned terms and the merged
|
|
327
|
+
* definitions, applying the ledger rules: a term with an abbreviation always
|
|
328
|
+
* appears; a term without one appears only when it has a definition; the
|
|
329
|
+
* entry label is the abbreviation or the full term; the body is
|
|
330
|
+
* "Full Term – definition" for abbreviated terms, the definition alone
|
|
331
|
+
* otherwise; duplicate full terms keep the last-scanned occurrence.
|
|
332
|
+
*/
|
|
333
|
+
function buildTerminologyEntries(terms, definitions) {
|
|
334
|
+
const byFull = new Map();
|
|
335
|
+
for (const t of terms)
|
|
336
|
+
byFull.set(t.full, t);
|
|
337
|
+
const entries = [];
|
|
338
|
+
for (const t of byFull.values()) {
|
|
339
|
+
const definition = definitions.get(t.full)?.definition ?? '';
|
|
340
|
+
if (!t.abbr && !definition)
|
|
341
|
+
continue;
|
|
342
|
+
const label = t.abbr ?? t.full;
|
|
343
|
+
const body = t.abbr
|
|
344
|
+
? (definition ? `${t.full} – ${definition}` : t.full)
|
|
345
|
+
: definition;
|
|
346
|
+
const sortKey = label.toLowerCase();
|
|
347
|
+
entries.push({ label, body, sortKey, letter: (sortKey[0] ?? '').toUpperCase() });
|
|
348
|
+
}
|
|
349
|
+
entries.sort((a, b) => a.sortKey.localeCompare(b.sortKey, undefined, { numeric: true, sensitivity: 'base' }));
|
|
350
|
+
return entries;
|
|
351
|
+
}
|
|
352
|
+
function generateTerminologyPartial(entries, cols = '15,85', letterStyle = 'combined', attribution = '') {
|
|
353
|
+
if (entries.length === 0) {
|
|
354
|
+
return '// No index terms detected in this component.\n';
|
|
355
|
+
}
|
|
356
|
+
const lines = ['// Auto-generated by autoterm. Do not edit.'];
|
|
357
|
+
// The attribution renders only outside the PDF build, where a generated
|
|
358
|
+
// section without explanation reads strangely.
|
|
359
|
+
if (attribution) {
|
|
360
|
+
lines.push('ifndef::loader-assembler[]');
|
|
361
|
+
lines.push('[.autoterm-attribution]');
|
|
362
|
+
lines.push(attribution);
|
|
363
|
+
lines.push('endif::[]');
|
|
364
|
+
lines.push('');
|
|
365
|
+
}
|
|
366
|
+
lines.push(`[.autoterm-terminology,cols="${cols}",frame=none,grid=none]`, '|===');
|
|
367
|
+
let currentLetter = '';
|
|
368
|
+
for (const entry of entries) {
|
|
369
|
+
const newLetter = entry.letter !== currentLetter;
|
|
370
|
+
currentLetter = entry.letter;
|
|
371
|
+
if (newLetter && letterStyle === 'separate') {
|
|
372
|
+
lines.push(`|*${entry.letter}*`);
|
|
373
|
+
lines.push('|');
|
|
374
|
+
lines.push('');
|
|
375
|
+
}
|
|
376
|
+
if (newLetter && letterStyle === 'combined') {
|
|
377
|
+
lines.push(`|*${entry.letter}* +`);
|
|
378
|
+
lines.push(escapeCell(entry.label));
|
|
379
|
+
lines.push(`|{empty} +`);
|
|
380
|
+
lines.push(escapeCell(entry.body));
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
lines.push(`|${escapeCell(entry.label)}`);
|
|
384
|
+
lines.push(`|${escapeCell(entry.body)}`);
|
|
385
|
+
}
|
|
386
|
+
lines.push('');
|
|
387
|
+
}
|
|
388
|
+
lines.push('|===');
|
|
389
|
+
lines.push('');
|
|
390
|
+
return lines.join('\n');
|
|
391
|
+
}
|
|
392
|
+
function generateIndexPartial(occurrences, cols = '30,70') {
|
|
393
|
+
if (occurrences.length === 0) {
|
|
394
|
+
return '// No index terms detected in this component.\n';
|
|
395
|
+
}
|
|
396
|
+
const sorted = [...occurrences].sort((a, b) => a.sortKey.localeCompare(b.sortKey, undefined, { numeric: true, sensitivity: 'base' }));
|
|
397
|
+
const lines = [
|
|
398
|
+
'// Auto-generated by autoterm. Do not edit.',
|
|
399
|
+
'ifndef::loader-assembler[]',
|
|
400
|
+
`[.autoterm-index,cols="${cols}",frame=none,grid=none]`,
|
|
401
|
+
'|===',
|
|
402
|
+
];
|
|
403
|
+
let currentLetter = '';
|
|
404
|
+
for (const occ of sorted) {
|
|
405
|
+
const xrefs = occ.pages
|
|
406
|
+
.map((p) => `xref:${p.module === 'ROOT' ? '' : `${p.module}:`}${p.relative}[${p.title}]`)
|
|
407
|
+
.join(', ');
|
|
408
|
+
if (occ.letter !== currentLetter) {
|
|
409
|
+
currentLetter = occ.letter;
|
|
410
|
+
lines.push(`|*${occ.letter}* +`);
|
|
411
|
+
lines.push(escapeCell(occ.display));
|
|
412
|
+
lines.push('a|{empty} +');
|
|
413
|
+
lines.push(xrefs);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
lines.push(`|${escapeCell(occ.display)}`);
|
|
417
|
+
lines.push(`a|${xrefs}`);
|
|
418
|
+
}
|
|
419
|
+
lines.push('');
|
|
420
|
+
}
|
|
421
|
+
lines.push('|===');
|
|
422
|
+
lines.push('endif::[]');
|
|
423
|
+
lines.push('');
|
|
424
|
+
return lines.join('\n');
|
|
425
|
+
}
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
// Page helpers
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
/** Extract the page title from raw AsciiDoc source. */
|
|
430
|
+
function pageTitle(sourceText) {
|
|
431
|
+
const m = sourceText.match(/^=\s+(.+)$/m);
|
|
432
|
+
return m ? m[1].trim() : '';
|
|
433
|
+
}
|
|
434
|
+
/** Read a :name: value from a raw page header, or null. */
|
|
435
|
+
function pageAttribute(sourceText, name) {
|
|
436
|
+
const re = new RegExp(`^:${name}:\\s*(.*)$`, 'm');
|
|
437
|
+
const m = sourceText.match(re);
|
|
438
|
+
return m ? m[1].trim() : null;
|
|
439
|
+
}
|
|
440
|
+
function includesPartial(sourceText, partialName) {
|
|
441
|
+
const re = new RegExp(`^include::[^\\[]*partial\\$${partialName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\[`, 'm');
|
|
442
|
+
return re.test(sourceText);
|
|
443
|
+
}
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
// Antora extension entry point
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
function register(context) {
|
|
448
|
+
const ctx = context ?? this;
|
|
449
|
+
const logger = makeLogger(ctx.getLogger ? ctx.getLogger('autoterm') : undefined);
|
|
450
|
+
let cfg = {
|
|
451
|
+
enabled: true,
|
|
452
|
+
quiet: true,
|
|
453
|
+
terminology_file: 'ROOT:partial$terminology-table.adoc',
|
|
454
|
+
local_file: 'terminology-local.adoc',
|
|
455
|
+
terminology_partial: 'generated-terminology.adoc',
|
|
456
|
+
index_partial: 'generated-index.adoc',
|
|
457
|
+
terminology_cols: '15,85',
|
|
458
|
+
index_cols: '30,70',
|
|
459
|
+
letter_style: 'combined',
|
|
460
|
+
attribution: 'Generated by https://gitlab.com/baiyibai-antora/baiyibai-extensions[autoterm].',
|
|
461
|
+
write_to_disk: false,
|
|
462
|
+
suppress_events: false,
|
|
463
|
+
};
|
|
464
|
+
// Per-component overrides from antora.yml ext.autoterm, captured during
|
|
465
|
+
// contentAggregated where ext is still present, mirroring trademark-it.
|
|
466
|
+
const extByCompVer = new Map();
|
|
467
|
+
ctx.once('contentAggregated', ({ contentAggregate }) => {
|
|
468
|
+
for (const item of (contentAggregate ?? [])) {
|
|
469
|
+
const atExt = item.ext?.['autoterm'];
|
|
470
|
+
if (atExt && typeof atExt === 'object') {
|
|
471
|
+
extByCompVer.set(`${item.name}@${item.version}`, atExt);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
ctx.once('playbookBuilt', async ({ playbook }) => {
|
|
476
|
+
const extEntry = (playbook?.antora?.extensions ?? []).find((ext) => String(ext.require ?? '').includes('autoterm')) ?? {};
|
|
477
|
+
const raw = (extEntry.autoterm ?? extEntry.autoTerm ?? {});
|
|
478
|
+
cfg = {
|
|
479
|
+
enabled: toBool(pick(raw, ['enabled'], cfg.enabled), cfg.enabled),
|
|
480
|
+
quiet: toBool(pick(raw, ['quiet'], cfg.quiet), cfg.quiet),
|
|
481
|
+
terminology_file: String(pick(raw, ['terminology_file', 'terminologyFile'], cfg.terminology_file)),
|
|
482
|
+
local_file: String(pick(raw, ['local_file', 'localFile'], cfg.local_file)),
|
|
483
|
+
terminology_partial: String(pick(raw, ['terminology_partial', 'terminologyPartial'], cfg.terminology_partial)),
|
|
484
|
+
index_partial: String(pick(raw, ['index_partial', 'indexPartial'], cfg.index_partial)),
|
|
485
|
+
terminology_cols: String(pick(raw, ['terminology_cols', 'terminologyCols'], cfg.terminology_cols)),
|
|
486
|
+
index_cols: String(pick(raw, ['index_cols', 'indexCols'], cfg.index_cols)),
|
|
487
|
+
letter_style: pick(raw, ['letter_style', 'letterStyle'], cfg.letter_style) === 'separate' ? 'separate' : 'combined',
|
|
488
|
+
attribution: String(pick(raw, ['attribution'], cfg.attribution)),
|
|
489
|
+
write_to_disk: toBool(pick(raw, ['write_to_disk', 'writeToDisk', 'writetodisk'], cfg.write_to_disk), cfg.write_to_disk),
|
|
490
|
+
suppress_events: toBool(pick(raw, ['suppress_events', 'suppressEvents'], cfg.suppress_events), cfg.suppress_events),
|
|
491
|
+
};
|
|
492
|
+
quietMode = cfg.quiet;
|
|
493
|
+
logger.debug(`autoterm config: ${JSON.stringify(cfg)}`);
|
|
494
|
+
});
|
|
495
|
+
ctx.once('contentClassified', async ({ playbook, contentCatalog }) => {
|
|
496
|
+
if (!cfg.enabled) {
|
|
497
|
+
logger.debug('autoterm disabled (skipped)');
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (!contentCatalog) {
|
|
501
|
+
logger.error('contentCatalog is missing');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
logEvent('Executing autoterm');
|
|
505
|
+
// -----------------------------------------------------------------------
|
|
506
|
+
// 1. Locate and parse the shared definitions table.
|
|
507
|
+
// -----------------------------------------------------------------------
|
|
508
|
+
const sharedText = resolveDefinitionsFile(cfg.terminology_file, contentCatalog, playbook, logger);
|
|
509
|
+
const sharedDefinitions = sharedText ? parseTerminologyTable(sharedText) : new Map();
|
|
510
|
+
if (!sharedText) {
|
|
511
|
+
logger.warn(`autoterm: shared definitions file "${cfg.terminology_file}" not found; entries render without definitions`);
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
logger.debug(`autoterm: parsed ${sharedDefinitions.size} shared definitions`);
|
|
515
|
+
}
|
|
516
|
+
// -----------------------------------------------------------------------
|
|
517
|
+
// 2. Per component-version: detect participation, scan, generate, inject.
|
|
518
|
+
// -----------------------------------------------------------------------
|
|
519
|
+
const componentVersions = new Map();
|
|
520
|
+
for (const f of contentCatalog.getFiles()) {
|
|
521
|
+
if (!f.src.component || !f.src.version)
|
|
522
|
+
continue;
|
|
523
|
+
const versions = componentVersions.get(f.src.component) ?? new Set();
|
|
524
|
+
versions.add(f.src.version);
|
|
525
|
+
componentVersions.set(f.src.component, versions);
|
|
526
|
+
}
|
|
527
|
+
for (const [component, versions] of componentVersions) {
|
|
528
|
+
for (const version of versions) {
|
|
529
|
+
const files = contentCatalog.getFiles().filter((f) => f.src.component === component && f.src.version === version);
|
|
530
|
+
const pages = files.filter((f) => f.src.family === 'page');
|
|
531
|
+
let terminologyHost;
|
|
532
|
+
let indexHost;
|
|
533
|
+
for (const p of pages) {
|
|
534
|
+
const text = p.contents.toString('utf8');
|
|
535
|
+
if (!terminologyHost && includesPartial(text, cfg.terminology_partial))
|
|
536
|
+
terminologyHost = p;
|
|
537
|
+
if (!indexHost && includesPartial(text, cfg.index_partial))
|
|
538
|
+
indexHost = p;
|
|
539
|
+
}
|
|
540
|
+
if (!terminologyHost && !indexHost) {
|
|
541
|
+
logger.debug(`autoterm: ${component}@${version} — skipped (no page includes a generated partial)`);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
// Per-component overrides. The block is optional: participation comes
|
|
545
|
+
// from the include, not from the block.
|
|
546
|
+
const compExt = extByCompVer.get(`${component}@${version}`) ?? {};
|
|
547
|
+
if (!toBool(compExt['enabled'], true)) {
|
|
548
|
+
logger.debug(`autoterm: ${component}@${version} — skipped (enabled: false)`);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const compLocalFile = typeof compExt['local_file'] === 'string' ? compExt['local_file']
|
|
552
|
+
: typeof compExt['localFile'] === 'string' ? compExt['localFile']
|
|
553
|
+
: cfg.local_file;
|
|
554
|
+
const compTermCols = typeof compExt['terminology_cols'] === 'string' ? compExt['terminology_cols'] : cfg.terminology_cols;
|
|
555
|
+
const compIndexCols = typeof compExt['index_cols'] === 'string' ? compExt['index_cols'] : cfg.index_cols;
|
|
556
|
+
const compLetterStyle = compExt['letter_style'] === 'separate' ? 'separate'
|
|
557
|
+
: compExt['letter_style'] === 'combined' ? 'combined'
|
|
558
|
+
: cfg.letter_style;
|
|
559
|
+
const compWriteToDisk = toBool(compExt['write_to_disk'] ?? compExt['writeToDisk'], cfg.write_to_disk);
|
|
560
|
+
// A contributor component's own ext.autoterm local_file naming wins
|
|
561
|
+
// for that component's local table lookup.
|
|
562
|
+
const findLocalFile = (comp, ver) => {
|
|
563
|
+
const theirExt = extByCompVer.get(`${comp}@${ver}`) ?? {};
|
|
564
|
+
const name = typeof theirExt['local_file'] === 'string' ? theirExt['local_file']
|
|
565
|
+
: comp === component ? compLocalFile
|
|
566
|
+
: cfg.local_file;
|
|
567
|
+
return contentCatalog.getFiles().find((f) => f.src.component === comp && f.src.version === ver &&
|
|
568
|
+
f.src.family === 'partial' &&
|
|
569
|
+
(f.src.relative === name || f.src.basename === name));
|
|
570
|
+
};
|
|
571
|
+
// Scan pages and partials, excluding the generated and local partials.
|
|
572
|
+
const excluded = new Set([cfg.terminology_partial, cfg.index_partial, compLocalFile]);
|
|
573
|
+
const scanFiles = files.filter((f) => (f.src.family === 'page' || f.src.family === 'partial') &&
|
|
574
|
+
!excluded.has(f.src.relative ?? '') &&
|
|
575
|
+
!excluded.has(f.src.basename ?? ''));
|
|
576
|
+
// Pre-populate the include-resolution visited set with every file in
|
|
577
|
+
// this scan plus the excluded data partials, so resolved includes are
|
|
578
|
+
// neither double-counted nor pulled from the generated partials.
|
|
579
|
+
const includeVisited = new Set();
|
|
580
|
+
for (const f of files) {
|
|
581
|
+
if (f.src.family !== 'page' && f.src.family !== 'partial')
|
|
582
|
+
continue;
|
|
583
|
+
const key = `${f.src.component}@${f.src.version}:${f.src.module}:${f.src.family}$${f.src.relative}`;
|
|
584
|
+
if (scanFiles.includes(f))
|
|
585
|
+
includeVisited.add(key);
|
|
586
|
+
if (excluded.has(f.src.relative ?? '') || excluded.has(f.src.basename ?? ''))
|
|
587
|
+
includeVisited.add(key);
|
|
588
|
+
}
|
|
589
|
+
const allTerms = [];
|
|
590
|
+
const occurrenceMap = new Map();
|
|
591
|
+
for (const f of scanFiles) {
|
|
592
|
+
let text = f.contents.toString('utf8');
|
|
593
|
+
// Resolve include:: directives on pages so terms living in
|
|
594
|
+
// transcluded content, such as a reference page carried by another
|
|
595
|
+
// component, are collected and attributed to the including page.
|
|
596
|
+
if (f.src.family === 'page') {
|
|
597
|
+
const extra = (0, index_1.collectIncludeText)(text, contentCatalog, component, version, f.src.module ?? 'ROOT', includeVisited);
|
|
598
|
+
if (extra)
|
|
599
|
+
text = `${text}\n${extra}`;
|
|
600
|
+
}
|
|
601
|
+
const terms = scanIndexterms(text);
|
|
602
|
+
allTerms.push(...terms);
|
|
603
|
+
if (f.src.family !== 'page')
|
|
604
|
+
continue;
|
|
605
|
+
const title = pageTitle(text) || (f.src.relative ?? '');
|
|
606
|
+
for (const t of terms) {
|
|
607
|
+
const display = t.abbr ? `${t.full} (${t.abbr})` : t.full;
|
|
608
|
+
const sortKey = display.toLowerCase();
|
|
609
|
+
const occ = occurrenceMap.get(display) ?? {
|
|
610
|
+
display,
|
|
611
|
+
sortKey,
|
|
612
|
+
letter: (sortKey[0] ?? '').toUpperCase(),
|
|
613
|
+
pages: [],
|
|
614
|
+
};
|
|
615
|
+
const ref = { module: f.src.module ?? 'ROOT', relative: f.src.relative ?? '', title };
|
|
616
|
+
if (!occ.pages.some((p) => p.module === ref.module && p.relative === ref.relative)) {
|
|
617
|
+
occ.pages.push(ref);
|
|
618
|
+
}
|
|
619
|
+
occurrenceMap.set(display, occ);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (!cfg.suppress_events) {
|
|
623
|
+
logEvent(`${component}@${version} — ${occurrenceMap.size} index term(s) detected`);
|
|
624
|
+
}
|
|
625
|
+
// Merge definitions. Order, lowest to highest precedence: the shared
|
|
626
|
+
// table; contributing components' local files, in component-name
|
|
627
|
+
// order, where a contributor is any component whose content was
|
|
628
|
+
// pulled into this scan through include resolution; the participating
|
|
629
|
+
// component's own local file last.
|
|
630
|
+
const contributors = new Map();
|
|
631
|
+
for (const key of includeVisited) {
|
|
632
|
+
const at = key.indexOf('@');
|
|
633
|
+
const colon = key.indexOf(':', at);
|
|
634
|
+
if (at < 0 || colon < 0)
|
|
635
|
+
continue;
|
|
636
|
+
const comp = key.slice(0, at);
|
|
637
|
+
if (comp !== component)
|
|
638
|
+
contributors.set(comp, key.slice(at + 1, colon));
|
|
639
|
+
}
|
|
640
|
+
let definitions = sharedDefinitions;
|
|
641
|
+
const mergeLocal = (comp, ver, label) => {
|
|
642
|
+
const entry = findLocalFile(comp, ver);
|
|
643
|
+
if (!entry)
|
|
644
|
+
return;
|
|
645
|
+
const local = parseTerminologyTable(entry.contents.toString('utf8'));
|
|
646
|
+
const { merged, overridden } = mergeTerminology(definitions, local);
|
|
647
|
+
definitions = merged;
|
|
648
|
+
const note = overridden.length > 0 ? ` (overriding: ${overridden.join(', ')})` : '';
|
|
649
|
+
logger.info(`autoterm: ${component}@${version} — merged ${local.size} definitions from ${label} ${cfg.local_file}${note}`);
|
|
650
|
+
};
|
|
651
|
+
for (const comp of [...contributors.keys()].sort()) {
|
|
652
|
+
mergeLocal(comp, contributors.get(comp), `contributor ${comp}'s`);
|
|
653
|
+
}
|
|
654
|
+
mergeLocal(component, version, 'own');
|
|
655
|
+
// Generate and inject.
|
|
656
|
+
if (terminologyHost) {
|
|
657
|
+
const hostText = terminologyHost.contents.toString('utf8');
|
|
658
|
+
const cols = pageAttribute(hostText, 'autoterm-terminology-cols') ?? compTermCols;
|
|
659
|
+
const style = pageAttribute(hostText, 'autoterm-letter-style') === 'separate' ? 'separate'
|
|
660
|
+
: pageAttribute(hostText, 'autoterm-letter-style') === 'combined' ? 'combined'
|
|
661
|
+
: compLetterStyle;
|
|
662
|
+
const entries = buildTerminologyEntries(allTerms, definitions);
|
|
663
|
+
const content = generateTerminologyPartial(entries, cols, style, cfg.attribution);
|
|
664
|
+
injectPartial(contentCatalog, files, component, version, cfg.terminology_partial, content, compWriteToDisk, logger);
|
|
665
|
+
}
|
|
666
|
+
if (indexHost) {
|
|
667
|
+
const hostText = indexHost.contents.toString('utf8');
|
|
668
|
+
const cols = pageAttribute(hostText, 'autoterm-index-cols') ?? compIndexCols;
|
|
669
|
+
const content = generateIndexPartial([...occurrenceMap.values()], cols);
|
|
670
|
+
injectPartial(contentCatalog, files, component, version, cfg.index_partial, content, compWriteToDisk, logger);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
logEvent('autoterm finished');
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
// ---------------------------------------------------------------------------
|
|
678
|
+
// Helpers
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
/**
|
|
681
|
+
* Locate the shared definitions file: catalog partial by resource-ID suffix,
|
|
682
|
+
* then filesystem path, then basename match — mirroring trademark-it's
|
|
683
|
+
* resolution strategies.
|
|
684
|
+
*/
|
|
685
|
+
function resolveDefinitionsFile(ref, contentCatalog, playbook, logger) {
|
|
686
|
+
const catalogFile = contentCatalog.getFiles().find((f) => {
|
|
687
|
+
if (f.src.family !== 'partial')
|
|
688
|
+
return false;
|
|
689
|
+
const rel = f.src.relative ?? '';
|
|
690
|
+
const resourceId = `${f.src.module ?? 'ROOT'}:partial$${rel}`;
|
|
691
|
+
return resourceId === ref || resourceId.endsWith(ref) || rel === ref || rel.endsWith(ref);
|
|
692
|
+
});
|
|
693
|
+
if (catalogFile) {
|
|
694
|
+
logger.debug(`autoterm: definitions resolved from catalog (${catalogFile.src.path})`);
|
|
695
|
+
return catalogFile.contents.toString('utf8');
|
|
696
|
+
}
|
|
697
|
+
const fsPath = ref.startsWith('/') ? ref : `${playbook.dir}/${ref}`;
|
|
698
|
+
if (fs.existsSync(fsPath)) {
|
|
699
|
+
logger.debug(`autoterm: definitions resolved from filesystem (${fsPath})`);
|
|
700
|
+
return fs.readFileSync(fsPath, 'utf8');
|
|
701
|
+
}
|
|
702
|
+
const basename = ref.split(/[\\/]/).pop() ?? ref;
|
|
703
|
+
const fallback = contentCatalog.getFiles().find((f) => f.src.family === 'partial' && f.src.basename === basename);
|
|
704
|
+
if (fallback) {
|
|
705
|
+
logger.debug(`autoterm: definitions resolved by basename fallback (${fallback.src.path})`);
|
|
706
|
+
return fallback.contents.toString('utf8');
|
|
707
|
+
}
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Inject a generated partial into the catalog, replacing an existing entry's
|
|
712
|
+
* contents when a file with that name was already loaded. When write_to_disk
|
|
713
|
+
* is enabled and the component has a local worktree, write the file there
|
|
714
|
+
* too; components loaded from a remote source skip the disk write,
|
|
715
|
+
* mirroring trademark-it.
|
|
716
|
+
*/
|
|
717
|
+
function injectPartial(contentCatalog, componentFiles, component, version, relative, content, writeToDisk, logger) {
|
|
718
|
+
const contents = Buffer.from(content, 'utf8');
|
|
719
|
+
const existing = componentFiles.find((f) => f.src.family === 'partial' &&
|
|
720
|
+
(f.src.relative === relative || f.src.path === `modules/ROOT/partials/${relative}`));
|
|
721
|
+
if (existing) {
|
|
722
|
+
existing.contents = contents;
|
|
723
|
+
logger.debug(`autoterm: updated partial$${relative} in ${component}@${version}`);
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
const basename = relative.split('/').pop() ?? relative;
|
|
727
|
+
try {
|
|
728
|
+
contentCatalog.addFile({
|
|
729
|
+
contents,
|
|
730
|
+
src: {
|
|
731
|
+
component,
|
|
732
|
+
version,
|
|
733
|
+
module: 'ROOT',
|
|
734
|
+
family: 'partial',
|
|
735
|
+
relative,
|
|
736
|
+
path: `modules/ROOT/partials/${relative}`,
|
|
737
|
+
basename,
|
|
738
|
+
stem: basename.replace(/\.[^.]+$/, ''),
|
|
739
|
+
extname: basename.includes('.') ? `.${basename.split('.').pop()}` : '',
|
|
740
|
+
mediaType: 'text/asciidoc',
|
|
741
|
+
origin: undefined,
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
logger.debug(`autoterm: injected partial$${relative} into ${component}@${version}`);
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
logger.warn(`autoterm: could not inject partial$${relative} into ${component}@${version}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (!writeToDisk)
|
|
751
|
+
return;
|
|
752
|
+
const originFile = componentFiles.find((f) => f.src.origin?.worktree);
|
|
753
|
+
const worktree = originFile?.src.origin?.worktree;
|
|
754
|
+
const startPath = originFile?.src.origin?.startPath ?? '';
|
|
755
|
+
if (worktree) {
|
|
756
|
+
const diskPath = path.join(worktree, startPath, 'modules', 'ROOT', 'partials', relative);
|
|
757
|
+
try {
|
|
758
|
+
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
|
|
759
|
+
fs.writeFileSync(diskPath, content, 'utf8');
|
|
760
|
+
logger.debug(`autoterm: wrote partial to disk (${diskPath})`);
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
logger.warn(`autoterm: could not write partial to disk (${diskPath}): ${String(err)}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
logger.debug(`autoterm: write_to_disk skipped for ${component}@${version} — no local worktree`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
//# sourceMappingURL=index.js.map
|