asciidoctor-rhrev 1.2.3 → 1.5.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.
|
@@ -2,16 +2,210 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Antora extension for revision history (rhrev) block macro.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* Provides HTML5 backend support for revision history tables.
|
|
7
7
|
* Auto-injects revision history at document start unless :rhrev: macro or manual mode.
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* Usage in antora-playbook.yml:
|
|
10
10
|
* antora:
|
|
11
11
|
* extensions:
|
|
12
12
|
* - require: ./antora-extension/antora-rhrev-asciidoc.js
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
// Mirrors Converter::ROLLUP_CONTEXTS on the Ruby gem's PDF side and
|
|
16
|
+
// ROLLUP_CONTEXTS in rhrev_html.rb: block types with no revision-history row
|
|
17
|
+
// of their own. A marked one rolls up as a bullet into its nearest
|
|
18
|
+
// enclosing section's entry instead (or the document-level -all entry, if
|
|
19
|
+
// it has no enclosing section, or its enclosing section has no id).
|
|
20
|
+
const ROLLUP_CONTEXTS = ['paragraph', 'ulist', 'olist', 'dlist', 'admonition', 'open', 'quote', 'verse', 'sidebar']
|
|
21
|
+
|
|
22
|
+
// Row-level: gets its own revision-history row when marked, requires an id.
|
|
23
|
+
// Same six contexts collectAllEntries already scanned before this port.
|
|
24
|
+
const ROW_LEVEL_CONTEXTS = ['section', 'floating_title', 'example', 'listing', 'table', 'image']
|
|
25
|
+
|
|
26
|
+
// Walks up from node to its nearest enclosing section, or null if none
|
|
27
|
+
// exists (the document preamble). Same shape as Converter#enclosing_section
|
|
28
|
+
// on the Ruby side.
|
|
29
|
+
function enclosingSection(node) {
|
|
30
|
+
let section = node.getParent ? node.getParent() : null
|
|
31
|
+
while (section && section.getContext && section.getContext() !== 'section') {
|
|
32
|
+
section = section.getParent ? section.getParent() : null
|
|
33
|
+
}
|
|
34
|
+
return section || null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Finds anchorNode's existing entry for this revision on catalog and
|
|
38
|
+
// appends another bullet onto its change text, or creates the entry if
|
|
39
|
+
// anchorNode has none yet. Same shape as Converter#append_child_change_to_entry.
|
|
40
|
+
function appendChildChangeToEntry(catalog, revision, anchorNode, changeText) {
|
|
41
|
+
const entries = catalog.entries[revision] || (catalog.entries[revision] = [])
|
|
42
|
+
const anchorId = anchorNode.getId()
|
|
43
|
+
const entry = entries.find((e) => e.anchor === anchorId)
|
|
44
|
+
if (entry) {
|
|
45
|
+
entry.change = (entry.change && String(entry.change).length) ? `${entry.change} * ${changeText}` : changeText
|
|
46
|
+
} else {
|
|
47
|
+
catalog.addEntry(revision, anchorId, changeText, {
|
|
48
|
+
title: anchorNode.getTitle ? anchorNode.getTitle() : null,
|
|
49
|
+
context: anchorNode.getContext ? anchorNode.getContext() : null
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Appends onto catalog's existing -all change text for this revision, or
|
|
55
|
+
// sets it fresh if none exists yet. Same shape as Converter#append_child_change_to_all.
|
|
56
|
+
function appendChildChangeToAll(catalog, revision, changeText) {
|
|
57
|
+
const existing = catalog.allEntries[revision]
|
|
58
|
+
const combined = (existing && String(existing).length) ? `${existing} * ${changeText}` : changeText
|
|
59
|
+
catalog.addAllEntry(revision, combined)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Gathers every marked rollup-context node in one unified, document-order
|
|
63
|
+
// traversal (findBy returns document order on its own, confirmed directly,
|
|
64
|
+
// no separate sort needed). Must run after the row-level scan in
|
|
65
|
+
// collectAllEntries, so a section's own entry, if it is also independently
|
|
66
|
+
// marked, already exists for a child's bullet to append onto instead of
|
|
67
|
+
// racing to create a duplicate.
|
|
68
|
+
function rollupChildRevisionEntries(catalog, doc, prefix) {
|
|
69
|
+
const nodes = doc.findBy((n) => ROLLUP_CONTEXTS.includes(n.getContext()))
|
|
70
|
+
nodes.forEach((node) => {
|
|
71
|
+
const attrs = node.getAttributes()
|
|
72
|
+
if (!attrs) return
|
|
73
|
+
const section = enclosingSection(node)
|
|
74
|
+
const hasSection = !!(section && section.getId())
|
|
75
|
+
Object.keys(attrs).forEach((key) => {
|
|
76
|
+
if (!key.startsWith(prefix)) return
|
|
77
|
+
if (key.includes('-all') || key.endsWith('-cover')) return
|
|
78
|
+
const revision = key.substring(prefix.length)
|
|
79
|
+
const value = String(attrs[key])
|
|
80
|
+
if (hasSection) {
|
|
81
|
+
appendChildChangeToEntry(catalog, revision, section, value)
|
|
82
|
+
} else {
|
|
83
|
+
appendChildChangeToAll(catalog, revision, value)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Table cells cannot carry an rhrev attribute of their own through normal
|
|
90
|
+
// AsciiDoc parsing: a plain (|) cell never parses its content as blocks at
|
|
91
|
+
// all, an attribute list there is just literal text. An AsciiDoc-style (a|)
|
|
92
|
+
// cell does parse its content as blocks, so [rhrev1-2="..."] attaches, but
|
|
93
|
+
// to the first nested block, not the cell, and that nested block lives in a
|
|
94
|
+
// structurally separate inner document (getInnerDocument()), unreachable
|
|
95
|
+
// from the outer document's own findBy. Confirmed directly, same gotcha as
|
|
96
|
+
// the Ruby side.
|
|
97
|
+
//
|
|
98
|
+
// Copies any rhrev-prefixed attribute from a marked nested rollup-context
|
|
99
|
+
// node onto the enclosing cell (for the revision-history entry, see
|
|
100
|
+
// rollupTableCellRevisionEntries below, which needs it on the cell), then
|
|
101
|
+
// strips it from the nested node. For the visual side, tagging the cell's
|
|
102
|
+
// own <td>/<th> needs a converter-level override (see the NOT IMPLEMENTED
|
|
103
|
+
// note further down: built, verified standalone, broke real Antora builds
|
|
104
|
+
// on a sibling package's dependency conflict, dropped). Tagging the nested
|
|
105
|
+
// node itself instead needs no override at all: it renders through the
|
|
106
|
+
// same, completely unmodified converter as any other rollup-context block,
|
|
107
|
+
// which already supports a role. So when this key matches the *current*
|
|
108
|
+
// revision's change attribute and change bars are enabled, the role is set
|
|
109
|
+
// here directly, on the node inside the cell's own inner document, before
|
|
110
|
+
// the attribute is stripped. The outer change-bar tagging treeProcessor
|
|
111
|
+
// cannot reach this on its own: it walks the outer document only, and the
|
|
112
|
+
// inner document is structurally separate, unreachable from the outer
|
|
113
|
+
// document's own findBy, confirmed directly, same gotcha as the Ruby side.
|
|
114
|
+
//
|
|
115
|
+
// Must run at treeprocessor time (see below), not lazily inside
|
|
116
|
+
// collectAllEntries: treeprocessors all run before conversion starts.
|
|
117
|
+
// Safe to call twice (collectAllEntries also calls it directly, for the
|
|
118
|
+
// block-macro's own-page fallback path): once a mark is propagated,
|
|
119
|
+
// tagged, and stripped, a second pass finds nothing left to move.
|
|
120
|
+
function propagateCellContentMarks(doc, prefix) {
|
|
121
|
+
const changeBarsEnabled = doc.hasAttribute('rhrev-change-bars')
|
|
122
|
+
const revnumber = doc.getAttribute('revnumber')
|
|
123
|
+
const changeAttr = (changeBarsEnabled && revnumber) ? prefix + String(revnumber).replace(/\./g, '-') : null
|
|
124
|
+
|
|
125
|
+
doc.findBy((n) => n.getContext() === 'table').forEach((table) => {
|
|
126
|
+
const rows = table.getRows()
|
|
127
|
+
const cells = [].concat(rows.head.flat(), rows.foot.flat(), rows.body.flat())
|
|
128
|
+
cells.forEach((cell) => {
|
|
129
|
+
if (cell.getStyle() !== 'asciidoc') return
|
|
130
|
+
const inner = cell.getInnerDocument ? cell.getInnerDocument() : null
|
|
131
|
+
if (!inner) return
|
|
132
|
+
inner.findBy((n) => ROLLUP_CONTEXTS.includes(n.getContext())).forEach((node) => {
|
|
133
|
+
const attrs = node.getAttributes()
|
|
134
|
+
if (!attrs) return
|
|
135
|
+
Object.keys(attrs).forEach((key) => {
|
|
136
|
+
if (!key.startsWith(prefix)) return
|
|
137
|
+
cell.setAttribute(key, attrs[key])
|
|
138
|
+
if (key === changeAttr) {
|
|
139
|
+
const roles = attrs.role
|
|
140
|
+
node.setAttribute('role', roles ? `${roles} rhrev-changed` : 'rhrev-changed')
|
|
141
|
+
}
|
|
142
|
+
node.removeAttribute(key)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// A marked table cell is not a rollup context itself: it rolls up to its
|
|
150
|
+
// own enclosing table, not the nearest section, and cells are not reachable
|
|
151
|
+
// through the same context-based findBy a normal block-level node is, so
|
|
152
|
+
// this walks table.getRows() directly. Same three-tier fallback as
|
|
153
|
+
// rollupChildRevisionEntries: the table's own entry if it has an id, else
|
|
154
|
+
// its nearest enclosing section's entry, else the document-level -all
|
|
155
|
+
// entry only when neither exists. Same shape as
|
|
156
|
+
// Converter#rollup_table_cell_revision_entries / Html5Converter#rollup_table_cell_revision_entries
|
|
157
|
+
// on the Ruby side.
|
|
158
|
+
function rollupTableCellRevisionEntries(catalog, doc, prefix) {
|
|
159
|
+
doc.findBy((n) => n.getContext() === 'table').forEach((table) => {
|
|
160
|
+
const rows = table.getRows()
|
|
161
|
+
const cells = [].concat(rows.head.flat(), rows.foot.flat(), rows.body.flat())
|
|
162
|
+
cells.forEach((cell) => {
|
|
163
|
+
const attrs = cell.getAttributes()
|
|
164
|
+
if (!attrs) return
|
|
165
|
+
Object.keys(attrs).forEach((key) => {
|
|
166
|
+
if (!key.startsWith(prefix)) return
|
|
167
|
+
if (key.includes('-all') || key.endsWith('-cover')) return
|
|
168
|
+
const revision = key.substring(prefix.length)
|
|
169
|
+
const value = String(attrs[key])
|
|
170
|
+
if (table.getId()) {
|
|
171
|
+
appendChildChangeToEntry(catalog, revision, table, value)
|
|
172
|
+
} else {
|
|
173
|
+
const section = enclosingSection(table)
|
|
174
|
+
if (section && section.getId()) {
|
|
175
|
+
appendChildChangeToEntry(catalog, revision, section, value)
|
|
176
|
+
} else {
|
|
177
|
+
appendChildChangeToAll(catalog, revision, value)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The cell's own <td>/<th> never carries the rhrev-changed role: that
|
|
186
|
+
// needs a converter-level override, the same way the Ruby gem's HTML5
|
|
187
|
+
// converter is overridden. Built and verified working standalone (a real
|
|
188
|
+
// Opal subclass of Html5Converter, registered via ConverterFactory,
|
|
189
|
+
// delegating to super for everything but a marked table), but it broke
|
|
190
|
+
// real Antora builds: this exact demo site has three separate,
|
|
191
|
+
// non-deduped @asciidoctor/core installs (rhrev, the site itself, and
|
|
192
|
+
// baiyibai-extensions, whose cite-it extension wraps Antora's own document
|
|
193
|
+
// loading), one of them (2.2.8 vs 2.2.9 elsewhere) a different version.
|
|
194
|
+
// Antora's own converter/html5.js reads whatever is currently registered
|
|
195
|
+
// as the 'html5' converter and builds a real Opal subclass on top of it;
|
|
196
|
+
// with three non-identical copies of the Opal runtime involved, that
|
|
197
|
+
// crashed (Object.setPrototypeOf called on null or undefined), confirmed
|
|
198
|
+
// against a real local build, not assumed. That is a dependency-hygiene
|
|
199
|
+
// problem in a sibling package, out of scope here.
|
|
200
|
+
//
|
|
201
|
+
// Instead, propagateCellContentMarks above tags the cell's marked nested
|
|
202
|
+
// block directly (a normal rollup-context node, so it needs no converter
|
|
203
|
+
// override at all, the same unmodified converter already supports a role
|
|
204
|
+
// there), rather than the cell itself. The visible result is a border
|
|
205
|
+
// around the changed paragraph inside the cell rather than around the
|
|
206
|
+
// whole cell box; confirmed against the real, unmodified converter output,
|
|
207
|
+
// not assumed to work the same as the PDF gem's margin bar.
|
|
208
|
+
|
|
15
209
|
// Catalog for storing revision entries
|
|
16
210
|
class RevisionCatalog {
|
|
17
211
|
constructor() {
|
|
@@ -24,7 +218,7 @@ class RevisionCatalog {
|
|
|
24
218
|
if (!this.entries[revision]) {
|
|
25
219
|
this.entries[revision] = []
|
|
26
220
|
}
|
|
27
|
-
|
|
221
|
+
|
|
28
222
|
this.entries[revision].push({
|
|
29
223
|
anchor,
|
|
30
224
|
change,
|
|
@@ -66,13 +260,13 @@ function isInitialRelease(revnumber) {
|
|
|
66
260
|
function formatPrevRev(prevTitle, prevDate, doc) {
|
|
67
261
|
const includeDateAttr = doc.getAttribute('rhrev-customization-prevrev-include-date')
|
|
68
262
|
const includeDate = includeDateAttr !== undefined
|
|
69
|
-
|
|
263
|
+
|
|
70
264
|
if (includeDate && prevDate) {
|
|
71
265
|
const dateFormat = doc.getAttribute('rhrev-customization-date-format') || '%Y-%m-%d'
|
|
72
266
|
// Simple date formatting (Node.js doesn't have strftime, keep it simple)
|
|
73
267
|
return `${prevTitle} ${prevDate}`
|
|
74
268
|
}
|
|
75
|
-
|
|
269
|
+
|
|
76
270
|
return prevTitle
|
|
77
271
|
}
|
|
78
272
|
|
|
@@ -83,20 +277,20 @@ function preprocessAttributeContent(text) {
|
|
|
83
277
|
|
|
84
278
|
function formatChangeAsHtmlList(text) {
|
|
85
279
|
if (!text) return ''
|
|
86
|
-
|
|
280
|
+
|
|
87
281
|
const processed = preprocessAttributeContent(text)
|
|
88
|
-
|
|
282
|
+
|
|
89
283
|
// Split by asterisks to create list items
|
|
90
284
|
const tokens = processed.split(/\s(?=\*+\s)/)
|
|
91
285
|
const items = []
|
|
92
|
-
|
|
286
|
+
|
|
93
287
|
if (tokens.length === 1 && !processed.includes('*')) {
|
|
94
288
|
// Single item without asterisk - wrap in list anyway
|
|
95
289
|
items.push({ level: 1, content: processed })
|
|
96
290
|
} else {
|
|
97
291
|
tokens.forEach(token => {
|
|
98
292
|
if (!token.trim()) return
|
|
99
|
-
|
|
293
|
+
|
|
100
294
|
const match = token.trim().match(/^(\*+)\s+(.+)/)
|
|
101
295
|
if (match) {
|
|
102
296
|
items.push({ level: match[1].length, content: match[2] })
|
|
@@ -105,60 +299,58 @@ function formatChangeAsHtmlList(text) {
|
|
|
105
299
|
}
|
|
106
300
|
})
|
|
107
301
|
}
|
|
108
|
-
|
|
302
|
+
|
|
109
303
|
// Always generate HTML list
|
|
110
304
|
const html = ['<ul>']
|
|
111
305
|
items.forEach(item => {
|
|
112
306
|
html.push(`<li>${item.content}</li>`)
|
|
113
307
|
})
|
|
114
308
|
html.push('</ul>')
|
|
115
|
-
|
|
309
|
+
|
|
116
310
|
return html.join('\n')
|
|
117
311
|
}
|
|
118
312
|
|
|
119
313
|
function collectAllEntries(doc) {
|
|
120
314
|
const catalog = new RevisionCatalog()
|
|
121
315
|
const prefix = doc.getAttribute('revhistoryprefix') || 'rhrev'
|
|
122
|
-
|
|
316
|
+
|
|
123
317
|
// Collect document-level entries (-all)
|
|
124
318
|
for (let major = 0; major <= 9; major++) {
|
|
125
319
|
for (let minor = 0; minor <= 9; minor++) {
|
|
126
320
|
const revision = `${major}-${minor}`
|
|
127
321
|
const prevrevAttr = `${revision}-prevrev`
|
|
128
|
-
|
|
322
|
+
|
|
129
323
|
if (doc.getAttribute(prevrevAttr)) {
|
|
130
324
|
const allAttrName = `${prefix}${revision}-all`
|
|
131
325
|
const allValue = doc.getAttribute(allAttrName)
|
|
132
|
-
|
|
326
|
+
|
|
133
327
|
if (allValue) {
|
|
134
328
|
catalog.addAllEntry(revision, allValue)
|
|
135
329
|
}
|
|
136
330
|
}
|
|
137
331
|
}
|
|
138
332
|
}
|
|
139
|
-
|
|
333
|
+
|
|
140
334
|
// Collect block-level entries by scanning document
|
|
141
|
-
const contexts = ['section', 'floating_title', 'example', 'listing', 'table', 'image']
|
|
142
|
-
|
|
143
335
|
function scanBlocks(blocks) {
|
|
144
336
|
if (!blocks) return
|
|
145
|
-
|
|
337
|
+
|
|
146
338
|
blocks.forEach(block => {
|
|
147
339
|
const blockId = block.getId()
|
|
148
340
|
const blockContext = block.getContext()
|
|
149
|
-
|
|
150
|
-
if (
|
|
341
|
+
|
|
342
|
+
if (ROW_LEVEL_CONTEXTS.includes(blockContext) && blockId) {
|
|
151
343
|
// Try both getAttributes() and direct attribute access
|
|
152
344
|
const attrs = block.getAttributes()
|
|
153
|
-
|
|
345
|
+
|
|
154
346
|
// Also check if attributes are exposed differently
|
|
155
347
|
// Some Asciidoctor.js versions expose them via getAttribute(name) only
|
|
156
348
|
Object.keys(attrs).forEach(key => {
|
|
157
349
|
const value = attrs[key]
|
|
158
|
-
|
|
350
|
+
|
|
159
351
|
if (key.startsWith(prefix) && !key.includes('-all') && !key.endsWith('-cover')) {
|
|
160
352
|
const revision = key.substring(prefix.length)
|
|
161
|
-
|
|
353
|
+
|
|
162
354
|
catalog.addEntry(revision, blockId, String(value), {
|
|
163
355
|
title: block.getTitle(),
|
|
164
356
|
context: blockContext
|
|
@@ -166,30 +358,38 @@ function collectAllEntries(doc) {
|
|
|
166
358
|
}
|
|
167
359
|
})
|
|
168
360
|
}
|
|
169
|
-
|
|
361
|
+
|
|
170
362
|
// Recurse into nested blocks
|
|
171
363
|
if (block.getBlocks) {
|
|
172
364
|
scanBlocks(block.getBlocks())
|
|
173
365
|
}
|
|
174
366
|
})
|
|
175
367
|
}
|
|
176
|
-
|
|
368
|
+
|
|
177
369
|
scanBlocks(doc.getBlocks())
|
|
178
|
-
|
|
370
|
+
|
|
371
|
+
// Paragraphs, lists, and the other rollup contexts, plus table cells:
|
|
372
|
+
// see propagateCellContentMarks/rollupChildRevisionEntries/rollupTableCellRevisionEntries
|
|
373
|
+
// above. Must run after the row-level scan above, so a section's own
|
|
374
|
+
// entry, if independently marked, already exists for a child to append onto.
|
|
375
|
+
propagateCellContentMarks(doc, prefix)
|
|
376
|
+
rollupChildRevisionEntries(catalog, doc, prefix)
|
|
377
|
+
rollupTableCellRevisionEntries(catalog, doc, prefix)
|
|
378
|
+
|
|
179
379
|
return catalog
|
|
180
380
|
}
|
|
181
381
|
|
|
182
382
|
function generateInitialReleaseHtml(doc) {
|
|
183
383
|
const discrete = doc.hasAttribute('rhrev-discrete')
|
|
184
384
|
const html = []
|
|
185
|
-
|
|
385
|
+
|
|
186
386
|
// Wrapper
|
|
187
387
|
if (discrete) {
|
|
188
388
|
html.push('<div class="rhrev-section">')
|
|
189
389
|
} else {
|
|
190
390
|
html.push('<section id="revision-history">')
|
|
191
391
|
}
|
|
192
|
-
|
|
392
|
+
|
|
193
393
|
// Title
|
|
194
394
|
const customTitle = doc.getAttribute('rhrev-customization-title')
|
|
195
395
|
if (customTitle !== undefined && customTitle === '') {
|
|
@@ -202,12 +402,12 @@ function generateInitialReleaseHtml(doc) {
|
|
|
202
402
|
const versionLabel = String(doc.getAttribute('version-label') || 'Revision')
|
|
203
403
|
html.push(`<h2>${versionLabel} History</h2>`)
|
|
204
404
|
}
|
|
205
|
-
|
|
405
|
+
|
|
206
406
|
// Simple table
|
|
207
407
|
const initialText = doc.getAttribute('rhrev-customization-initial-release-text') ||
|
|
208
408
|
doc.getAttribute('rhrev-localization-initial-release') ||
|
|
209
409
|
'Initial release'
|
|
210
|
-
|
|
410
|
+
|
|
211
411
|
const columnWidths = String(doc.getAttribute('rhrev-table-column-width') || '30,70').split(',').map(w => w.trim())
|
|
212
412
|
const sectionLabel = doc.getAttribute('rhrev-localization-location') || 'Section'
|
|
213
413
|
|
|
@@ -215,7 +415,7 @@ function generateInitialReleaseHtml(doc) {
|
|
|
215
415
|
const frame = doc.getAttribute('rhrev-table-frame') || 'all'
|
|
216
416
|
const grid = doc.getAttribute('rhrev-table-grid') || 'all'
|
|
217
417
|
html.push(`<table class="tableblock frame-${frame} grid-${grid} stretch">`)
|
|
218
|
-
|
|
418
|
+
|
|
219
419
|
// Caption as title div
|
|
220
420
|
if (!doc.hasAttribute('rhrev-disable-caption')) {
|
|
221
421
|
const caption = doc.getAttribute('rhrev-table-caption')
|
|
@@ -223,7 +423,7 @@ function generateInitialReleaseHtml(doc) {
|
|
|
223
423
|
html.push(`<div class="title">${caption}</div>`)
|
|
224
424
|
}
|
|
225
425
|
}
|
|
226
|
-
|
|
426
|
+
|
|
227
427
|
html.push('<colgroup>')
|
|
228
428
|
columnWidths.forEach(w => {
|
|
229
429
|
html.push(`<col style="width: ${w}%;">`)
|
|
@@ -253,24 +453,24 @@ function generateInitialReleaseHtml(doc) {
|
|
|
253
453
|
html.push('</tr>')
|
|
254
454
|
html.push('</tbody>')
|
|
255
455
|
html.push('</table>')
|
|
256
|
-
|
|
456
|
+
|
|
257
457
|
// Close wrapper
|
|
258
458
|
html.push(discrete ? '</div>' : '</section>')
|
|
259
|
-
|
|
459
|
+
|
|
260
460
|
return html.join('\n')
|
|
261
461
|
}
|
|
262
462
|
|
|
263
463
|
function generateHtmlOutput(doc, catalog, currentPage = '') {
|
|
264
464
|
const discrete = doc.hasAttribute('rhrev-discrete')
|
|
265
465
|
const html = []
|
|
266
|
-
|
|
466
|
+
|
|
267
467
|
// Wrapper
|
|
268
468
|
if (discrete) {
|
|
269
469
|
html.push('<div class="rhrev-section">')
|
|
270
470
|
} else {
|
|
271
471
|
html.push('<section id="revision-history">')
|
|
272
472
|
}
|
|
273
|
-
|
|
473
|
+
|
|
274
474
|
// Title
|
|
275
475
|
const customTitle = doc.getAttribute('rhrev-customization-title')
|
|
276
476
|
if (customTitle !== undefined && customTitle === '') {
|
|
@@ -283,25 +483,25 @@ function generateHtmlOutput(doc, catalog, currentPage = '') {
|
|
|
283
483
|
const versionLabel = String(doc.getAttribute('version-label') || 'Revision')
|
|
284
484
|
html.push(`<h2>${versionLabel} History</h2>`)
|
|
285
485
|
}
|
|
286
|
-
|
|
486
|
+
|
|
287
487
|
// Table
|
|
288
488
|
html.push(generateHtmlTable(doc, catalog, currentPage))
|
|
289
|
-
|
|
489
|
+
|
|
290
490
|
// Close wrapper
|
|
291
491
|
html.push(discrete ? '</div>' : '</section>')
|
|
292
|
-
|
|
492
|
+
|
|
293
493
|
return html.join('\n')
|
|
294
494
|
}
|
|
295
495
|
|
|
296
496
|
function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
297
497
|
const columnWidths = String(doc.getAttribute('rhrev-table-column-width') || '30,70').split(',').map(w => w.trim())
|
|
298
498
|
const html = []
|
|
299
|
-
|
|
499
|
+
|
|
300
500
|
// Build table with frame/grid classes
|
|
301
501
|
const frame = doc.getAttribute('rhrev-table-frame') || 'all'
|
|
302
502
|
const grid = doc.getAttribute('rhrev-table-grid') || 'all'
|
|
303
503
|
html.push(`<table class="tableblock frame-${frame} grid-${grid} stretch">`)
|
|
304
|
-
|
|
504
|
+
|
|
305
505
|
// Caption as title div
|
|
306
506
|
if (!doc.hasAttribute('rhrev-disable-caption')) {
|
|
307
507
|
const caption = doc.getAttribute('rhrev-table-caption')
|
|
@@ -309,19 +509,19 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
309
509
|
html.push(`<div class="title">${caption}</div>`)
|
|
310
510
|
}
|
|
311
511
|
}
|
|
312
|
-
|
|
512
|
+
|
|
313
513
|
html.push('<colgroup>')
|
|
314
514
|
columnWidths.forEach(w => {
|
|
315
515
|
html.push(`<col style="width: ${w}%;">`)
|
|
316
516
|
})
|
|
317
517
|
html.push('</colgroup>')
|
|
318
518
|
html.push('<tbody>')
|
|
319
|
-
|
|
519
|
+
|
|
320
520
|
// Custom first row
|
|
321
521
|
const firstRow = doc.getAttribute('rhrev-customization-first-row')
|
|
322
522
|
const firstRowLeft = doc.getAttribute('rhrev-customization-first-row-left')
|
|
323
523
|
const firstRowRight = doc.getAttribute('rhrev-customization-first-row-right')
|
|
324
|
-
|
|
524
|
+
|
|
325
525
|
if (firstRow) {
|
|
326
526
|
html.push('<tr class="rhrev-custom-first-row">')
|
|
327
527
|
html.push(`<td colspan="2">${firstRow}</td>`)
|
|
@@ -332,11 +532,11 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
332
532
|
html.push(`<td>${firstRowRight || ' '}</td>`)
|
|
333
533
|
html.push('</tr>')
|
|
334
534
|
}
|
|
335
|
-
|
|
535
|
+
|
|
336
536
|
const sectionLabel = doc.getAttribute('rhrev-localization-location') || 'Section'
|
|
337
537
|
const majorChangesText = doc.getAttribute('rhrev-localization-major-changes') || 'Major changes since'
|
|
338
538
|
const allText = doc.getAttribute('rhrev-localization-all') || 'All'
|
|
339
|
-
|
|
539
|
+
|
|
340
540
|
catalog.sortedRevisions().forEach(revision => {
|
|
341
541
|
const prevrevAttr = `${revision}-prevrev`
|
|
342
542
|
const prevrevdateAttr = `${revision}-prevrevdate`
|
|
@@ -344,7 +544,7 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
344
544
|
const prevTitle = String(doc.getAttribute(prevrevAttr) || revision.replaceAll('-', '.'))
|
|
345
545
|
const prevDate = String(doc.getAttribute(prevrevdateAttr) || '')
|
|
346
546
|
const prevRevText = formatPrevRev(prevTitle, prevDate, doc)
|
|
347
|
-
|
|
547
|
+
|
|
348
548
|
// Build header text
|
|
349
549
|
let headerText
|
|
350
550
|
if (prevTitle.trim().toLowerCase().startsWith(revisionLabel.toLowerCase())) {
|
|
@@ -352,13 +552,13 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
352
552
|
} else {
|
|
353
553
|
headerText = `${majorChangesText} ${revisionLabel} ${prevRevText}`
|
|
354
554
|
}
|
|
355
|
-
|
|
555
|
+
|
|
356
556
|
// Header row for this revision
|
|
357
557
|
html.push('<tr class="rhrev-header">')
|
|
358
558
|
html.push(`<th class="tableblock halign-left valign-top">${sectionLabel}</th>`)
|
|
359
559
|
html.push(`<th class="tableblock halign-left valign-top"><strong>${headerText}</strong></th>`)
|
|
360
560
|
html.push('</tr>')
|
|
361
|
-
|
|
561
|
+
|
|
362
562
|
// All entry
|
|
363
563
|
const allChange = catalog.allEntries[revision]
|
|
364
564
|
if (allChange) {
|
|
@@ -367,17 +567,17 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
367
567
|
html.push(`<td>${formatChangeAsHtmlList(allChange)}</td>`)
|
|
368
568
|
html.push('</tr>')
|
|
369
569
|
}
|
|
370
|
-
|
|
570
|
+
|
|
371
571
|
// Block entries
|
|
372
572
|
const entries = catalog.entries[revision] || []
|
|
373
573
|
entries.sort((a, b) => (a.sequence || 0) - (b.sequence || 0))
|
|
374
|
-
|
|
574
|
+
|
|
375
575
|
entries.forEach(entry => {
|
|
376
576
|
const anchor = entry.anchor
|
|
377
|
-
const title = entry.title || anchor.replace(/[_-]/g, ' ').split(' ').map(w =>
|
|
577
|
+
const title = entry.title || anchor.replace(/[_-]/g, ' ').split(' ').map(w =>
|
|
378
578
|
w.charAt(0).toUpperCase() + w.slice(1)
|
|
379
579
|
).join(' ')
|
|
380
|
-
|
|
580
|
+
|
|
381
581
|
html.push('<tr>')
|
|
382
582
|
|
|
383
583
|
// Build link: local anchor if the section lives on the current page (directly or via include)
|
|
@@ -421,10 +621,10 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
421
621
|
html.push('</tr>')
|
|
422
622
|
})
|
|
423
623
|
})
|
|
424
|
-
|
|
624
|
+
|
|
425
625
|
html.push('</tbody>')
|
|
426
626
|
html.push('</table>')
|
|
427
|
-
|
|
627
|
+
|
|
428
628
|
return html.join('\n')
|
|
429
629
|
}
|
|
430
630
|
|
|
@@ -433,30 +633,30 @@ module.exports.register = function (registry, context = {}) {
|
|
|
433
633
|
// Register block macro for rhrev::[]
|
|
434
634
|
registry.blockMacro('rhrev', function () {
|
|
435
635
|
const self = this
|
|
436
|
-
|
|
636
|
+
|
|
437
637
|
self.process(function (parent, target, attrs) {
|
|
438
638
|
const doc = parent.getDocument()
|
|
439
|
-
|
|
639
|
+
|
|
440
640
|
// Check if manual mode
|
|
441
641
|
if (doc.getAttribute('rhrev') === 'manual') {
|
|
442
642
|
return self.createBlock(parent, 'paragraph', '')
|
|
443
643
|
}
|
|
444
|
-
|
|
644
|
+
|
|
445
645
|
// Check initial release handling
|
|
446
646
|
const revnumber = doc.getAttribute('revnumber') || '1.0'
|
|
447
647
|
const initialHandling = doc.getAttribute('rhrev-initial-handling') || 'initial-release'
|
|
448
|
-
|
|
648
|
+
|
|
449
649
|
if (isInitialRelease(revnumber)) {
|
|
450
650
|
if (initialHandling === 'skip') {
|
|
451
651
|
return self.createBlock(parent, 'paragraph', '')
|
|
452
652
|
}
|
|
453
|
-
|
|
653
|
+
|
|
454
654
|
if (initialHandling === 'initial-release') {
|
|
455
655
|
const html = generateInitialReleaseHtml(doc)
|
|
456
656
|
return self.createBlock(parent, 'pass', html)
|
|
457
657
|
}
|
|
458
658
|
}
|
|
459
|
-
|
|
659
|
+
|
|
460
660
|
// Try to use catalog from Antora pipeline extension (component-scoped by default).
|
|
461
661
|
// global.antoraRhrevCatalog is the site-wide catalog; .byComponent holds per-component catalogs.
|
|
462
662
|
let catalog
|
|
@@ -489,37 +689,98 @@ module.exports.register = function (registry, context = {}) {
|
|
|
489
689
|
return self.createBlock(parent, 'pass', html)
|
|
490
690
|
})
|
|
491
691
|
})
|
|
492
|
-
|
|
692
|
+
|
|
693
|
+
// Propagates a marked nested block's rhrev attribute onto its enclosing
|
|
694
|
+
// a| table cell, and strips it from the nested block. See
|
|
695
|
+
// propagateCellContentMarks above for the full reasoning (the same
|
|
696
|
+
// isolated-inner-document gap the PDF gem's converter works around).
|
|
697
|
+
// Must be registered, and therefore run, before the change-bar tagging
|
|
698
|
+
// treeProcessor immediately below: treeprocessors all run before
|
|
699
|
+
// conversion starts, so if propagation happened later, that pass would
|
|
700
|
+
// already have tagged the nested paragraph at its original, unpropagated
|
|
701
|
+
// location, since it is a rollup context in its own right.
|
|
702
|
+
registry.treeProcessor(function () {
|
|
703
|
+
const self = this
|
|
704
|
+
|
|
705
|
+
self.process(function (doc) {
|
|
706
|
+
const prefix = doc.getAttribute('revhistoryprefix') || 'rhrev'
|
|
707
|
+
propagateCellContentMarks(doc, prefix)
|
|
708
|
+
return doc
|
|
709
|
+
})
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
// Tag blocks carrying the current revision attribute with the rhrev-changed
|
|
713
|
+
// role so the site stylesheet can draw an HTML change bar (right-side border).
|
|
714
|
+
// Enabled by rhrev-change-bars, mirroring the PDF backend; color/width/offset
|
|
715
|
+
// overrides are emitted as CSS custom properties. Side is always right in HTML.
|
|
716
|
+
// The first six contexts get their own revision-history row when marked;
|
|
717
|
+
// the other nine (mirrors ROLLUP_CONTEXTS) roll up into their enclosing
|
|
718
|
+
// section's entry instead (see rollupChildRevisionEntries), but still get
|
|
719
|
+
// their own bar here: tagging needs no id or anchor, a role is a role
|
|
720
|
+
// regardless of block type.
|
|
721
|
+
registry.treeProcessor(function () {
|
|
722
|
+
const self = this
|
|
723
|
+
|
|
724
|
+
self.process(function (doc) {
|
|
725
|
+
if (!doc.hasAttribute('rhrev-change-bars')) return doc
|
|
726
|
+
const revnumber = doc.getAttribute('revnumber')
|
|
727
|
+
if (!revnumber) return doc
|
|
728
|
+
const prefix = doc.getAttribute('revhistoryprefix') || 'rhrev'
|
|
729
|
+
const changeAttr = prefix + String(revnumber).replace(/\./g, '-')
|
|
730
|
+
const contexts = ROW_LEVEL_CONTEXTS.concat(ROLLUP_CONTEXTS)
|
|
731
|
+
|
|
732
|
+
let tagged = false
|
|
733
|
+
doc.findBy((b) => contexts.includes(b.getContext())).forEach((block) => {
|
|
734
|
+
const attrs = block.getAttributes()
|
|
735
|
+
if (!attrs || attrs[changeAttr] === undefined) return
|
|
736
|
+
const role = attrs.role ? attrs.role + ' rhrev-changed' : 'rhrev-changed'
|
|
737
|
+
block.setAttribute('role', role)
|
|
738
|
+
tagged = true
|
|
739
|
+
})
|
|
740
|
+
if (!tagged) return doc
|
|
741
|
+
|
|
742
|
+
const overrides = []
|
|
743
|
+
const color = doc.getAttribute('rhrev-change-bars-color')
|
|
744
|
+
if (color) overrides.push(`--rhrev-change-bar-color: #${String(color).replace(/^#/, '')};`)
|
|
745
|
+
const width = doc.getAttribute('rhrev-change-bars-width')
|
|
746
|
+
if (width) overrides.push(`--rhrev-change-bar-width: ${width}px;`)
|
|
747
|
+
const offset = doc.getAttribute('rhrev-change-bars-offset')
|
|
748
|
+
if (offset) overrides.push(`--rhrev-change-bar-offset: ${offset}px;`)
|
|
749
|
+
if (overrides.length) {
|
|
750
|
+
const style = self.createBlock(doc, 'pass', `<style>:root { ${overrides.join(' ')} }</style>`)
|
|
751
|
+
doc.getBlocks().unshift(style)
|
|
752
|
+
}
|
|
753
|
+
return doc
|
|
754
|
+
})
|
|
755
|
+
})
|
|
756
|
+
|
|
493
757
|
// Register treeprocessor for auto-injection
|
|
494
758
|
registry.treeProcessor(function () {
|
|
495
759
|
const self = this
|
|
496
|
-
|
|
760
|
+
|
|
497
761
|
self.process(function (doc) {
|
|
498
762
|
if (!doc.hasAttribute('rhrev')) {
|
|
499
763
|
return doc
|
|
500
764
|
}
|
|
501
|
-
|
|
765
|
+
|
|
502
766
|
const rhrevValue = doc.getAttribute('rhrev')
|
|
503
767
|
if (rhrevValue === 'macro' || rhrevValue === 'manual') {
|
|
504
768
|
return doc
|
|
505
769
|
}
|
|
506
|
-
|
|
770
|
+
|
|
507
771
|
// Auto-inject revision history at document start
|
|
508
772
|
const blocks = doc.getBlocks()
|
|
509
|
-
|
|
510
|
-
// Create rhrev block
|
|
511
|
-
const rhrevBlock = self.createBlock(doc, 'pass', '')
|
|
512
|
-
|
|
773
|
+
|
|
513
774
|
// Check initial release handling
|
|
514
775
|
const revnumber = doc.getAttribute('revnumber') || '1.0'
|
|
515
776
|
const initialHandling = doc.getAttribute('rhrev-initial-handling') || 'initial-release'
|
|
516
|
-
|
|
777
|
+
|
|
517
778
|
let html
|
|
518
779
|
if (isInitialRelease(revnumber)) {
|
|
519
780
|
if (initialHandling === 'skip') {
|
|
520
781
|
return doc
|
|
521
782
|
}
|
|
522
|
-
|
|
783
|
+
|
|
523
784
|
if (initialHandling === 'initial-release') {
|
|
524
785
|
html = generateInitialReleaseHtml(doc)
|
|
525
786
|
} else {
|
|
@@ -528,19 +789,23 @@ module.exports.register = function (registry, context = {}) {
|
|
|
528
789
|
}
|
|
529
790
|
} else {
|
|
530
791
|
const catalog = collectAllEntries(doc)
|
|
531
|
-
|
|
792
|
+
|
|
532
793
|
if (catalog.sortedRevisions().length === 0) {
|
|
533
794
|
return doc
|
|
534
795
|
}
|
|
535
|
-
|
|
796
|
+
|
|
536
797
|
html = generateHtmlOutput(doc, catalog)
|
|
537
798
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
799
|
+
|
|
800
|
+
// Create the block with its content directly: self.createBlock(...).setContent(html)
|
|
801
|
+
// used to be two steps here, but Block#setContent is not a function
|
|
802
|
+
// on a modern @asciidoctor/core (confirmed against 2.2.9, a
|
|
803
|
+
// pre-existing bug, unrelated to anything else in this file).
|
|
804
|
+
const rhrevBlock = self.createBlock(doc, 'pass', html)
|
|
805
|
+
|
|
541
806
|
// Insert at beginning
|
|
542
807
|
blocks.unshift(rhrevBlock)
|
|
543
|
-
|
|
808
|
+
|
|
544
809
|
return doc
|
|
545
810
|
})
|
|
546
811
|
})
|
|
@@ -10,6 +10,53 @@
|
|
|
10
10
|
|
|
11
11
|
const RHREV_MACRO_RX = /^rhrev::(\[.*?\])?$/gm
|
|
12
12
|
|
|
13
|
+
// Used only to parse each page's raw AsciiDoc into a real document tree
|
|
14
|
+
// (buildComponentCatalog below), replacing the previous regex/heuristic-
|
|
15
|
+
// anchor-guessing scan. Never used to convert or register a converter, so
|
|
16
|
+
// none of the cross-package Opal/ConverterFactory conflict from
|
|
17
|
+
// antora-rhrev-asciidoc.js applies here.
|
|
18
|
+
const asciidoctor = require('@asciidoctor/core')()
|
|
19
|
+
|
|
20
|
+
// Antora resource-id includes (page$file.adoc) are not real filesystem
|
|
21
|
+
// paths; a bare asciidoctor.load() outside Antora's own content-catalog-
|
|
22
|
+
// aware include resolver would try, and fail, to read them as literal
|
|
23
|
+
// files. A no-op include processor means each page parses on its own,
|
|
24
|
+
// without needing (or attempting) cross-file resolution at all: the
|
|
25
|
+
// existing includedBy/crossIncludeRx scan below already handles cross-
|
|
26
|
+
// component include detection for href purposes, a separate, unrelated,
|
|
27
|
+
// line-based mechanism (tags have no AST representation, so it stays
|
|
28
|
+
// regex-based on purpose), reused unchanged.
|
|
29
|
+
const noIncludeRegistry = asciidoctor.Extensions.create()
|
|
30
|
+
noIncludeRegistry.includeProcessor(function () {
|
|
31
|
+
this.handles(() => true)
|
|
32
|
+
this.process(() => {})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Mirrors ROLLUP_CONTEXTS in antora-rhrev-asciidoc.js and
|
|
36
|
+
// Converter::ROLLUP_CONTEXTS on the Ruby gem's PDF side: block types with
|
|
37
|
+
// no revision-history row of their own. A marked one rolls up as a bullet
|
|
38
|
+
// into its nearest enclosing section's entry instead (or the document-
|
|
39
|
+
// level -all entry, if it has no enclosing section, or its enclosing
|
|
40
|
+
// section has no id). Duplicated here rather than required from the
|
|
41
|
+
// sibling file: the two files are independent Antora extension entry
|
|
42
|
+
// points, loaded and registered separately, matching the existing
|
|
43
|
+
// duplication between this file and antora-rhrev-asciidoc.js (each has
|
|
44
|
+
// its own copy of, for example, compareRevisions and formatChangeAsHtmlList).
|
|
45
|
+
const ROLLUP_CONTEXTS = ['paragraph', 'ulist', 'olist', 'dlist', 'admonition', 'open', 'quote', 'verse', 'sidebar']
|
|
46
|
+
|
|
47
|
+
// Row-level: gets its own revision-history row when marked, requires an id.
|
|
48
|
+
const ROW_LEVEL_CONTEXTS = ['section', 'floating_title', 'example', 'listing', 'table', 'image']
|
|
49
|
+
|
|
50
|
+
// Walks up from node to its nearest enclosing section, or null if none
|
|
51
|
+
// exists (the document preamble).
|
|
52
|
+
function enclosingSection(node) {
|
|
53
|
+
let section = node.getParent ? node.getParent() : null
|
|
54
|
+
while (section && section.getContext && section.getContext() !== 'section') {
|
|
55
|
+
section = section.getParent ? section.getParent() : null
|
|
56
|
+
}
|
|
57
|
+
return section || null
|
|
58
|
+
}
|
|
59
|
+
|
|
13
60
|
module.exports.register = function ({ config: extensionConfig = {} }) {
|
|
14
61
|
const logger = this.getLogger('rhrev-extension')
|
|
15
62
|
const quiet = extensionConfig.quiet !== undefined ? extensionConfig.quiet : true
|
|
@@ -115,6 +162,41 @@ function mergeCatalogs(catalogs) {
|
|
|
115
162
|
return merged
|
|
116
163
|
}
|
|
117
164
|
|
|
165
|
+
// Finds anchorNode's existing entry for this revision on catalog and
|
|
166
|
+
// appends another bullet onto its change text, or creates the entry
|
|
167
|
+
// (carrying the same cross-page metadata a row-level entry gets: page,
|
|
168
|
+
// component, version, module, stem, matchLine, containingTags, all needed
|
|
169
|
+
// downstream for href resolution and the includedBy check) if anchorNode
|
|
170
|
+
// has none yet. meta carries the marked child's own line/tag information
|
|
171
|
+
// for a rollup entry (anchorNode is a section, not the marked node), or
|
|
172
|
+
// the anchor node's own for a row-level entry (anchorNode is the marked
|
|
173
|
+
// node itself, so meta === the anchor's own position).
|
|
174
|
+
function appendChildChangeToEntry(catalog, revision, anchorNode, changeText, pageMeta, meta) {
|
|
175
|
+
const entries = catalog.entries[revision] || (catalog.entries[revision] = [])
|
|
176
|
+
const anchorId = anchorNode.getId()
|
|
177
|
+
const entry = entries.find((e) => e.anchor === anchorId)
|
|
178
|
+
if (entry) {
|
|
179
|
+
entry.change = (entry.change && String(entry.change).length) ? `${entry.change} * ${changeText}` : changeText
|
|
180
|
+
} else {
|
|
181
|
+
entries.push({
|
|
182
|
+
anchor: anchorId,
|
|
183
|
+
change: changeText,
|
|
184
|
+
title: anchorNode.getTitle ? anchorNode.getTitle() : null,
|
|
185
|
+
sequence: catalog.sequence++,
|
|
186
|
+
...pageMeta,
|
|
187
|
+
...meta
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Appends onto catalog's existing -all change text for this revision, or
|
|
193
|
+
// sets it fresh if none exists yet.
|
|
194
|
+
function appendChildChangeToAll(catalog, revision, changeText) {
|
|
195
|
+
const existing = catalog.allEntries[revision]
|
|
196
|
+
const combined = (existing && String(existing).length) ? `${existing} * ${changeText}` : changeText
|
|
197
|
+
catalog.allEntries[revision] = combined
|
|
198
|
+
}
|
|
199
|
+
|
|
118
200
|
function buildComponentCatalog(pageFiles, logger, quiet) {
|
|
119
201
|
const catalog = {
|
|
120
202
|
entries: {}, // { '1-1': [...], '1-2': [...] }
|
|
@@ -160,22 +242,29 @@ function buildComponentCatalog(pageFiles, logger, quiet) {
|
|
|
160
242
|
}
|
|
161
243
|
}
|
|
162
244
|
|
|
163
|
-
// Collect block-level entries from ALL pages
|
|
245
|
+
// Collect block-level entries from ALL pages, by real parsing (each page
|
|
246
|
+
// its own asciidoctor.load(), sourcemap: true for real line numbers,
|
|
247
|
+
// the no-op include processor above so a cross-component include::
|
|
248
|
+
// never touches the filesystem or throws), not the previous regex/
|
|
249
|
+
// heuristic-anchor-guessing scan: that scan could only recognize a
|
|
250
|
+
// marked block immediately followed, within 200 characters of raw text,
|
|
251
|
+
// by [#anchor], .Title, or a heading line, so it had no way to reach
|
|
252
|
+
// paragraphs, lists, admonitions, or any of the other rollup contexts,
|
|
253
|
+
// or table cells, at all. Real parsing has no such limit.
|
|
164
254
|
pageFiles.forEach(file => {
|
|
165
255
|
if (!file.contents) return
|
|
166
|
-
|
|
256
|
+
|
|
167
257
|
const content = file.contents.toString()
|
|
168
|
-
|
|
258
|
+
|
|
169
259
|
if (!quiet && content.includes(prefix)) {
|
|
170
260
|
logger.debug(`rhrev-extension: Scanning ${file.src?.relative} for block attributes`)
|
|
171
261
|
}
|
|
172
|
-
|
|
173
|
-
// Strip delimited block content (source, literal, listing) to avoid matching examples
|
|
174
|
-
const strippedContent = stripVerbatimBlocks(content)
|
|
175
262
|
|
|
176
|
-
//
|
|
263
|
+
// Tag-region boundary detection stays line-based, on purpose: tags are
|
|
264
|
+
// a raw preprocessor concept for include::, with no AST representation
|
|
265
|
+
// to walk instead. Unrelated to the block-type-scanning gap above.
|
|
177
266
|
const fileTagBoundaries = {}
|
|
178
|
-
|
|
267
|
+
content.split('\n').forEach((line, lineNum) => {
|
|
179
268
|
const startM = line.match(/^\/\/ tag::([^\[\]]+)\[\]$/)
|
|
180
269
|
if (startM) fileTagBoundaries[startM[1]] = { startLine: lineNum }
|
|
181
270
|
const endM = line.match(/^\/\/ end::([^\[\]]+)\[\]$/)
|
|
@@ -183,79 +272,152 @@ function buildComponentCatalog(pageFiles, logger, quiet) {
|
|
|
183
272
|
})
|
|
184
273
|
catalog.fileMeta[file.src?.relative || file.path] = fileTagBoundaries
|
|
185
274
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
275
|
+
let doc
|
|
276
|
+
try {
|
|
277
|
+
doc = asciidoctor.load(content, { safe: 'safe', sourcemap: true, extension_registry: noIncludeRegistry, attributes: componentAttrs })
|
|
278
|
+
} catch (e) {
|
|
279
|
+
if (!quiet) logger.debug(`rhrev-extension: failed to parse ${file.src?.relative}, skipped: ${e.message}`)
|
|
280
|
+
return
|
|
281
|
+
}
|
|
189
282
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
283
|
+
const pageMeta = {
|
|
284
|
+
page: file.src?.relative || file.path,
|
|
285
|
+
component: file.src?.component,
|
|
286
|
+
version: file.src?.version,
|
|
287
|
+
module: file.src?.module || 'ROOT',
|
|
288
|
+
stem: file.src?.stem || (file.src?.basename || '').replace(/\.[^.]+$/, '')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// node.getLineNumber() is 1-based; startLine/endLine above are 0-based
|
|
292
|
+
// (from Array#forEach's index), matching the original regex scan's own
|
|
293
|
+
// 0-based matchLine, so this converts once, here, rather than at every
|
|
294
|
+
// comparison site.
|
|
295
|
+
const metaFor = (node) => {
|
|
296
|
+
const lineNumber = node.getLineNumber ? node.getLineNumber() : null
|
|
297
|
+
const matchLine = lineNumber ? lineNumber - 1 : undefined
|
|
298
|
+
const containingTags = (matchLine === undefined) ? [] : Object.entries(fileTagBoundaries)
|
|
195
299
|
.filter(([, b]) => b.startLine !== undefined && b.endLine !== undefined &&
|
|
196
300
|
matchLine > b.startLine && matchLine < b.endLine)
|
|
197
301
|
.map(([name]) => name)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const matchEnd = match.index + match[0].length
|
|
201
|
-
const nextChunk = strippedContent.substring(matchEnd, matchEnd + 200)
|
|
202
|
-
|
|
203
|
-
// Look for [#anchor], .Title, or === Section in that order
|
|
204
|
-
const anchorMatch = nextChunk.match(/^\s*\[#([^\]]+)\]/)
|
|
205
|
-
const titleMatch = nextChunk.match(/^\s*\.([^\n]+)/)
|
|
206
|
-
const sectionMatch = nextChunk.match(/^\s*(==+)\s+([^\n]+)/)
|
|
207
|
-
|
|
208
|
-
let anchor = null
|
|
209
|
-
let title = null
|
|
210
|
-
|
|
211
|
-
if (anchorMatch) {
|
|
212
|
-
anchor = anchorMatch[1]
|
|
213
|
-
// Still need to find title after anchor
|
|
214
|
-
const afterAnchor = nextChunk.substring(anchorMatch[0].length)
|
|
215
|
-
const titleAfterAnchor = afterAnchor.match(/^\s*\.([^\n]+)/)
|
|
216
|
-
const sectionAfterAnchor = afterAnchor.match(/^\s*(==+)\s+([^\n]+)/)
|
|
217
|
-
title = titleAfterAnchor ? titleAfterAnchor[1] : (sectionAfterAnchor ? sectionAfterAnchor[2] : anchor)
|
|
218
|
-
} else if (sectionMatch) {
|
|
219
|
-
title = sectionMatch[2]
|
|
220
|
-
// Generate anchor from section title (Asciidoctor's default behavior)
|
|
221
|
-
anchor = '_' + title.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '')
|
|
222
|
-
} else if (titleMatch) {
|
|
223
|
-
title = titleMatch[1]
|
|
224
|
-
// No explicit anchor, will use title as fallback
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (!quiet) {
|
|
228
|
-
logger.debug(`rhrev-extension: Found ${revision} -> anchor=${anchor}, title=${title} in ${file.src?.relative}`)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (anchor) {
|
|
232
|
-
if (!catalog.entries[revision]) {
|
|
233
|
-
catalog.entries[revision] = []
|
|
234
|
-
}
|
|
302
|
+
return { matchLine, containingTags }
|
|
303
|
+
}
|
|
235
304
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
305
|
+
// Row-level: same six contexts as before, still requires an id.
|
|
306
|
+
doc.findBy((n) => ROW_LEVEL_CONTEXTS.includes(n.getContext())).forEach((block) => {
|
|
307
|
+
const blockId = block.getId()
|
|
308
|
+
if (!blockId) return
|
|
309
|
+
const attrs = block.getAttributes()
|
|
310
|
+
if (!attrs) return
|
|
311
|
+
Object.keys(attrs).forEach((key) => {
|
|
312
|
+
if (!key.startsWith(prefix)) return
|
|
313
|
+
if (key.includes('-all') || key.endsWith('-cover')) return
|
|
314
|
+
const revision = key.substring(prefix.length)
|
|
315
|
+
catalog.entries[revision] = catalog.entries[revision] || []
|
|
316
|
+
catalog.entries[revision].push({
|
|
317
|
+
anchor: blockId,
|
|
318
|
+
change: String(attrs[key]),
|
|
319
|
+
title: block.getTitle(),
|
|
320
|
+
sequence: catalog.sequence++,
|
|
321
|
+
...pageMeta,
|
|
322
|
+
...metaFor(block)
|
|
323
|
+
})
|
|
250
324
|
if (!quiet) {
|
|
251
|
-
logger.debug(`rhrev-extension: Found block entry ${revision} -> ${
|
|
325
|
+
logger.debug(`rhrev-extension: Found block entry ${revision} -> ${blockId} in ${file.src?.relative}`)
|
|
252
326
|
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
// Table cells cannot carry an rhrev attribute of their own through
|
|
331
|
+
// normal AsciiDoc parsing: a plain (|) cell never parses its content
|
|
332
|
+
// as blocks at all, an attribute list there is just literal text. An
|
|
333
|
+
// AsciiDoc-style (a|) cell does parse its content as blocks, so
|
|
334
|
+
// [rhrev1-2="..."] attaches, but to the first nested block, not the
|
|
335
|
+
// cell, and that nested block lives in a structurally separate inner
|
|
336
|
+
// document (getInnerDocument()), unreachable from the outer document's
|
|
337
|
+
// own findBy. Same gotcha as antora-rhrev-asciidoc.js and the Ruby
|
|
338
|
+
// gem's converter.rb. Copies the mark onto the enclosing cell (for the
|
|
339
|
+
// revision-history entry below) and strips it from the nested node.
|
|
340
|
+
doc.findBy((n) => n.getContext() === 'table').forEach((table) => {
|
|
341
|
+
const rows = table.getRows()
|
|
342
|
+
const cells = [].concat(rows.head.flat(), rows.foot.flat(), rows.body.flat())
|
|
343
|
+
cells.forEach((cell) => {
|
|
344
|
+
if (cell.getStyle() !== 'asciidoc') return
|
|
345
|
+
const inner = cell.getInnerDocument ? cell.getInnerDocument() : null
|
|
346
|
+
if (!inner) return
|
|
347
|
+
inner.findBy((n) => ROLLUP_CONTEXTS.includes(n.getContext())).forEach((node) => {
|
|
348
|
+
const attrs = node.getAttributes()
|
|
349
|
+
if (!attrs) return
|
|
350
|
+
Object.keys(attrs).forEach((key) => {
|
|
351
|
+
if (!key.startsWith(prefix)) return
|
|
352
|
+
cell.setAttribute(key, attrs[key])
|
|
353
|
+
node.removeAttribute(key)
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
// Paragraphs, lists, admonitions, and the other rollup contexts: no
|
|
360
|
+
// row of their own, since they have no title/section-number for the
|
|
361
|
+
// location column. A marked one contributes a bullet to its nearest
|
|
362
|
+
// enclosing section's entry instead (creating that entry, with the
|
|
363
|
+
// section's own metadata, if the section has none yet for this
|
|
364
|
+
// revision), or the document-level -all entry if it has no enclosing
|
|
365
|
+
// section or that section has no id. findBy returns document order on
|
|
366
|
+
// its own, no separate sort needed (the same fix the Ruby side needed
|
|
367
|
+
// for its own node.lineno-based sort, which silently no-ops without
|
|
368
|
+
// sourcemap: true; this file already sets that, and findBy doesn't
|
|
369
|
+
// need it for ordering regardless).
|
|
370
|
+
doc.findBy((n) => ROLLUP_CONTEXTS.includes(n.getContext())).forEach((node) => {
|
|
371
|
+
const attrs = node.getAttributes()
|
|
372
|
+
if (!attrs) return
|
|
373
|
+
const section = enclosingSection(node)
|
|
374
|
+
const hasSection = !!(section && section.getId())
|
|
375
|
+
Object.keys(attrs).forEach((key) => {
|
|
376
|
+
if (!key.startsWith(prefix)) return
|
|
377
|
+
if (key.includes('-all') || key.endsWith('-cover')) return
|
|
378
|
+
const revision = key.substring(prefix.length)
|
|
379
|
+
const value = String(attrs[key])
|
|
380
|
+
if (hasSection) {
|
|
381
|
+
appendChildChangeToEntry(catalog, revision, section, value, pageMeta, metaFor(section))
|
|
382
|
+
} else {
|
|
383
|
+
appendChildChangeToAll(catalog, revision, value)
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
// A marked table cell is not a rollup context itself: it rolls up to
|
|
389
|
+
// its own enclosing table, not the nearest section, and cells are not
|
|
390
|
+
// reachable through the same context-based findBy a normal block-level
|
|
391
|
+
// node is, so this walks table.getRows() directly. Same three-tier
|
|
392
|
+
// fallback as the rollup pass above: the table's own entry if it has
|
|
393
|
+
// an id, else its nearest enclosing section's entry, else the
|
|
394
|
+
// document-level -all entry only when neither exists.
|
|
395
|
+
doc.findBy((n) => n.getContext() === 'table').forEach((table) => {
|
|
396
|
+
const rows = table.getRows()
|
|
397
|
+
const cells = [].concat(rows.head.flat(), rows.foot.flat(), rows.body.flat())
|
|
398
|
+
cells.forEach((cell) => {
|
|
399
|
+
const attrs = cell.getAttributes()
|
|
400
|
+
if (!attrs) return
|
|
401
|
+
Object.keys(attrs).forEach((key) => {
|
|
402
|
+
if (!key.startsWith(prefix)) return
|
|
403
|
+
if (key.includes('-all') || key.endsWith('-cover')) return
|
|
404
|
+
const revision = key.substring(prefix.length)
|
|
405
|
+
const value = String(attrs[key])
|
|
406
|
+
if (table.getId()) {
|
|
407
|
+
appendChildChangeToEntry(catalog, revision, table, value, pageMeta, metaFor(table))
|
|
408
|
+
} else {
|
|
409
|
+
const section = enclosingSection(table)
|
|
410
|
+
if (section && section.getId()) {
|
|
411
|
+
appendChildChangeToEntry(catalog, revision, section, value, pageMeta, metaFor(section))
|
|
412
|
+
} else {
|
|
413
|
+
appendChildChangeToAll(catalog, revision, value)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
})
|
|
418
|
+
})
|
|
257
419
|
})
|
|
258
|
-
|
|
420
|
+
|
|
259
421
|
return catalog
|
|
260
422
|
}
|
|
261
423
|
|
package/package.json
CHANGED