asciidoctor-rhrev 1.3.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -2,16 +2,315 @@
|
|
|
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> DOES carry the rhrev-changed role, via a
|
|
186
|
+
// converter-level override, patched directly onto the SAME live converter
|
|
187
|
+
// class object Antora's own pipeline is already using for this document
|
|
188
|
+
// (doc.getConverter().$$class, reached through Asciidoctor's own public
|
|
189
|
+
// API), never a separately-required or separately-registered class.
|
|
190
|
+
//
|
|
191
|
+
// EARLIER ATTEMPT, SUPERSEDED (kept here since the next engineer will
|
|
192
|
+
// otherwise reach for the same idea first): a real Opal subclass of
|
|
193
|
+
// Html5Converter, registered via ConverterFactory, the same way the Ruby
|
|
194
|
+
// gem's PDF converter is overridden. Built and verified working
|
|
195
|
+
// standalone, but broke real Antora builds: this exact demo site had
|
|
196
|
+
// three separate, non-deduped @asciidoctor/core installs (rhrev, the site
|
|
197
|
+
// itself, and baiyibai-extensions, whose cite-it extension wraps Antora's
|
|
198
|
+
// own document loading), one of them a different version. Antora's own
|
|
199
|
+
// converter/html5.js reads whatever is currently registered as the
|
|
200
|
+
// 'html5' converter and builds a real Opal subclass on top of that; with
|
|
201
|
+
// non-identical copies of the Opal runtime involved, defining a SECOND,
|
|
202
|
+
// independent subclass crashed (Object.setPrototypeOf called on null or
|
|
203
|
+
// undefined), confirmed against a real local build, not assumed. A
|
|
204
|
+
// require('@antora/asciidoc-loader') from inside this file cannot avoid
|
|
205
|
+
// that risk either, even now that baiyibai-extensions' own version drift
|
|
206
|
+
// is fixed (1.15.1): this package ships its own @asciidoctor/core copy
|
|
207
|
+
// for its own test suite, so a plain require() here resolves against
|
|
208
|
+
// THIS package's node_modules first, not necessarily the consuming
|
|
209
|
+
// site's, and remains a live, standing risk of resolving a different
|
|
210
|
+
// copy regardless of what any sibling package currently pins.
|
|
211
|
+
//
|
|
212
|
+
// doc.getConverter() sidesteps the whole class of problem: it returns
|
|
213
|
+
// whichever converter object is ACTUALLY resolved for THIS document, in
|
|
214
|
+
// THIS process, by Antora's own pipeline, guaranteed correct by
|
|
215
|
+
// construction rather than by keeping multiple packages' pins in sync.
|
|
216
|
+
// Confirmed directly: asciidoctor.load(text).getConverter() already
|
|
217
|
+
// returns a real, usable converter instance, with a real $$class, before
|
|
218
|
+
// convert() is ever called, meaning it is available in time from inside a
|
|
219
|
+
// treeprocessor (treeprocessors run during load(), before conversion).
|
|
220
|
+
const PATCHED_CONVERT_TABLE = Symbol.for('rhrev.patchedConvertTable')
|
|
221
|
+
|
|
222
|
+
// Idempotency is keyed on the converter CLASS, not the document: a single
|
|
223
|
+
// Antora build converts many documents against the same resolved
|
|
224
|
+
// converter class object, and redefining $convert_table would otherwise
|
|
225
|
+
// re-wrap the previous wrapper on every single page, nesting deeper each
|
|
226
|
+
// time (and, worse, the second pass would capture the FIRST WRAPPER as
|
|
227
|
+
// its own "original", not the real one, once the reference below is
|
|
228
|
+
// overwritten).
|
|
229
|
+
//
|
|
230
|
+
// Captures the real, original $convert_table function reference directly
|
|
231
|
+
// off the class's own prototype before replacing it, and calls that
|
|
232
|
+
// reference directly, rather than Opal.find_super_dispatcher (Antora's
|
|
233
|
+
// own html5.js pattern, copied from initially, then found wrong for this
|
|
234
|
+
// case, confirmed against a real crash: find_super_dispatcher climbs an
|
|
235
|
+
// inheritance chain, correct when Opal.klass creates a genuine SUBCLASS
|
|
236
|
+
// the way Antora's own converter/html5.js does, but this patches the
|
|
237
|
+
// SAME class object in place, no subclass involved, so there is no
|
|
238
|
+
// superclass boundary to dispatch across; the original implementation
|
|
239
|
+
// lives directly on classDef.$$prototype itself, confirmed directly, and
|
|
240
|
+
// Opal.defn below overwrites that exact slot).
|
|
241
|
+
function patchConvertTableForCellTagging (doc) {
|
|
242
|
+
const Opal = global.Opal
|
|
243
|
+
if (!Opal) return
|
|
244
|
+
const converter = doc.getConverter && doc.getConverter()
|
|
245
|
+
const classDef = converter && converter.$$class
|
|
246
|
+
if (!classDef || classDef[PATCHED_CONVERT_TABLE]) return
|
|
247
|
+
const originalConvertTable = classDef.$$prototype['$convert_table']
|
|
248
|
+
if (typeof originalConvertTable !== 'function') return
|
|
249
|
+
classDef[PATCHED_CONVERT_TABLE] = true
|
|
250
|
+
|
|
251
|
+
Opal.defn(classDef, '$convert_table', function convertTableWithCellTags (node) {
|
|
252
|
+
const html = originalConvertTable.call(this, node)
|
|
253
|
+
return typeof html === 'string' ? liftRhrevChangedToCells(html) : html
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Moves the rhrev-changed marker from a marked cell's own nested content
|
|
258
|
+
// (propagateCellContentMarks above) onto its enclosing <td>/<th>, so the
|
|
259
|
+
// site stylesheet highlights the whole cell box, matching the PDF
|
|
260
|
+
// backend's own margin bar spanning the cell's full row height, rather
|
|
261
|
+
// than only the changed paragraph inside it. Operates on the already-
|
|
262
|
+
// rendered HTML string, not the AST: Asciidoctor's real convert_table has
|
|
263
|
+
// no per-cell hook to override cleanly (table rendering is one self-
|
|
264
|
+
// contained method, the same reason the PDF gem needed a Prawn-level
|
|
265
|
+
// patch rather than an ordinary converter override, confirmed against
|
|
266
|
+
// Asciidoctor's own source), so this calls the real, unmodified super()
|
|
267
|
+
// implementation first and only adjusts its output afterward.
|
|
268
|
+
//
|
|
269
|
+
// Finds the nearest PRECEDING <td/<th opening tag before each marker,
|
|
270
|
+
// confirmed against real rendered output (a plain cell's own <p
|
|
271
|
+
// class="tableblock"> vs an a| cell's <div class="content">, both
|
|
272
|
+
// verified). A table nested inside a cell, itself containing its own
|
|
273
|
+
// <td>/<th> tags between the marker and its true enclosing cell, would
|
|
274
|
+
// defeat that heuristic: a known, accepted limitation, not handled.
|
|
275
|
+
function liftRhrevChangedToCells (html) {
|
|
276
|
+
const cellTagRx = /<(td|th)\b[^>]*>/g
|
|
277
|
+
const cellTags = []
|
|
278
|
+
let m
|
|
279
|
+
while ((m = cellTagRx.exec(html))) {
|
|
280
|
+
cellTags.push({ start: m.index, end: m.index + m[0].length, text: m[0] })
|
|
281
|
+
}
|
|
282
|
+
if (cellTags.length === 0) return html
|
|
283
|
+
|
|
284
|
+
const markerRx = /class="[^"]*\brhrev-changed\b[^"]*"/g
|
|
285
|
+
const toPatch = new Set()
|
|
286
|
+
while ((m = markerRx.exec(html))) {
|
|
287
|
+
let owner
|
|
288
|
+
for (const tag of cellTags) {
|
|
289
|
+
if (tag.start <= m.index) owner = tag
|
|
290
|
+
else break
|
|
291
|
+
}
|
|
292
|
+
if (owner) toPatch.add(owner)
|
|
293
|
+
}
|
|
294
|
+
if (toPatch.size === 0) return html
|
|
295
|
+
|
|
296
|
+
let out = ''
|
|
297
|
+
let cursor = 0
|
|
298
|
+
for (const tag of cellTags) {
|
|
299
|
+
if (!toPatch.has(tag)) continue
|
|
300
|
+
out += html.slice(cursor, tag.start)
|
|
301
|
+
out += addRoleClass(tag.text, 'rhrev-changed')
|
|
302
|
+
cursor = tag.end
|
|
303
|
+
}
|
|
304
|
+
out += html.slice(cursor)
|
|
305
|
+
return out
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function addRoleClass (tagText, role) {
|
|
309
|
+
if (new RegExp(`\\bclass="[^"]*\\b${role}\\b[^"]*"`).test(tagText)) return tagText
|
|
310
|
+
if (/\bclass="/.test(tagText)) return tagText.replace(/\bclass="/, `class="${role} `)
|
|
311
|
+
return tagText.replace(/^<(td|th)\b/, `<$1 class="${role}"`)
|
|
312
|
+
}
|
|
313
|
+
|
|
15
314
|
// Catalog for storing revision entries
|
|
16
315
|
class RevisionCatalog {
|
|
17
316
|
constructor() {
|
|
@@ -24,7 +323,7 @@ class RevisionCatalog {
|
|
|
24
323
|
if (!this.entries[revision]) {
|
|
25
324
|
this.entries[revision] = []
|
|
26
325
|
}
|
|
27
|
-
|
|
326
|
+
|
|
28
327
|
this.entries[revision].push({
|
|
29
328
|
anchor,
|
|
30
329
|
change,
|
|
@@ -66,13 +365,13 @@ function isInitialRelease(revnumber) {
|
|
|
66
365
|
function formatPrevRev(prevTitle, prevDate, doc) {
|
|
67
366
|
const includeDateAttr = doc.getAttribute('rhrev-customization-prevrev-include-date')
|
|
68
367
|
const includeDate = includeDateAttr !== undefined
|
|
69
|
-
|
|
368
|
+
|
|
70
369
|
if (includeDate && prevDate) {
|
|
71
370
|
const dateFormat = doc.getAttribute('rhrev-customization-date-format') || '%Y-%m-%d'
|
|
72
371
|
// Simple date formatting (Node.js doesn't have strftime, keep it simple)
|
|
73
372
|
return `${prevTitle} ${prevDate}`
|
|
74
373
|
}
|
|
75
|
-
|
|
374
|
+
|
|
76
375
|
return prevTitle
|
|
77
376
|
}
|
|
78
377
|
|
|
@@ -83,20 +382,20 @@ function preprocessAttributeContent(text) {
|
|
|
83
382
|
|
|
84
383
|
function formatChangeAsHtmlList(text) {
|
|
85
384
|
if (!text) return ''
|
|
86
|
-
|
|
385
|
+
|
|
87
386
|
const processed = preprocessAttributeContent(text)
|
|
88
|
-
|
|
387
|
+
|
|
89
388
|
// Split by asterisks to create list items
|
|
90
389
|
const tokens = processed.split(/\s(?=\*+\s)/)
|
|
91
390
|
const items = []
|
|
92
|
-
|
|
391
|
+
|
|
93
392
|
if (tokens.length === 1 && !processed.includes('*')) {
|
|
94
393
|
// Single item without asterisk - wrap in list anyway
|
|
95
394
|
items.push({ level: 1, content: processed })
|
|
96
395
|
} else {
|
|
97
396
|
tokens.forEach(token => {
|
|
98
397
|
if (!token.trim()) return
|
|
99
|
-
|
|
398
|
+
|
|
100
399
|
const match = token.trim().match(/^(\*+)\s+(.+)/)
|
|
101
400
|
if (match) {
|
|
102
401
|
items.push({ level: match[1].length, content: match[2] })
|
|
@@ -105,60 +404,58 @@ function formatChangeAsHtmlList(text) {
|
|
|
105
404
|
}
|
|
106
405
|
})
|
|
107
406
|
}
|
|
108
|
-
|
|
407
|
+
|
|
109
408
|
// Always generate HTML list
|
|
110
409
|
const html = ['<ul>']
|
|
111
410
|
items.forEach(item => {
|
|
112
411
|
html.push(`<li>${item.content}</li>`)
|
|
113
412
|
})
|
|
114
413
|
html.push('</ul>')
|
|
115
|
-
|
|
414
|
+
|
|
116
415
|
return html.join('\n')
|
|
117
416
|
}
|
|
118
417
|
|
|
119
418
|
function collectAllEntries(doc) {
|
|
120
419
|
const catalog = new RevisionCatalog()
|
|
121
420
|
const prefix = doc.getAttribute('revhistoryprefix') || 'rhrev'
|
|
122
|
-
|
|
421
|
+
|
|
123
422
|
// Collect document-level entries (-all)
|
|
124
423
|
for (let major = 0; major <= 9; major++) {
|
|
125
424
|
for (let minor = 0; minor <= 9; minor++) {
|
|
126
425
|
const revision = `${major}-${minor}`
|
|
127
426
|
const prevrevAttr = `${revision}-prevrev`
|
|
128
|
-
|
|
427
|
+
|
|
129
428
|
if (doc.getAttribute(prevrevAttr)) {
|
|
130
429
|
const allAttrName = `${prefix}${revision}-all`
|
|
131
430
|
const allValue = doc.getAttribute(allAttrName)
|
|
132
|
-
|
|
431
|
+
|
|
133
432
|
if (allValue) {
|
|
134
433
|
catalog.addAllEntry(revision, allValue)
|
|
135
434
|
}
|
|
136
435
|
}
|
|
137
436
|
}
|
|
138
437
|
}
|
|
139
|
-
|
|
438
|
+
|
|
140
439
|
// Collect block-level entries by scanning document
|
|
141
|
-
const contexts = ['section', 'floating_title', 'example', 'listing', 'table', 'image']
|
|
142
|
-
|
|
143
440
|
function scanBlocks(blocks) {
|
|
144
441
|
if (!blocks) return
|
|
145
|
-
|
|
442
|
+
|
|
146
443
|
blocks.forEach(block => {
|
|
147
444
|
const blockId = block.getId()
|
|
148
445
|
const blockContext = block.getContext()
|
|
149
|
-
|
|
150
|
-
if (
|
|
446
|
+
|
|
447
|
+
if (ROW_LEVEL_CONTEXTS.includes(blockContext) && blockId) {
|
|
151
448
|
// Try both getAttributes() and direct attribute access
|
|
152
449
|
const attrs = block.getAttributes()
|
|
153
|
-
|
|
450
|
+
|
|
154
451
|
// Also check if attributes are exposed differently
|
|
155
452
|
// Some Asciidoctor.js versions expose them via getAttribute(name) only
|
|
156
453
|
Object.keys(attrs).forEach(key => {
|
|
157
454
|
const value = attrs[key]
|
|
158
|
-
|
|
455
|
+
|
|
159
456
|
if (key.startsWith(prefix) && !key.includes('-all') && !key.endsWith('-cover')) {
|
|
160
457
|
const revision = key.substring(prefix.length)
|
|
161
|
-
|
|
458
|
+
|
|
162
459
|
catalog.addEntry(revision, blockId, String(value), {
|
|
163
460
|
title: block.getTitle(),
|
|
164
461
|
context: blockContext
|
|
@@ -166,30 +463,38 @@ function collectAllEntries(doc) {
|
|
|
166
463
|
}
|
|
167
464
|
})
|
|
168
465
|
}
|
|
169
|
-
|
|
466
|
+
|
|
170
467
|
// Recurse into nested blocks
|
|
171
468
|
if (block.getBlocks) {
|
|
172
469
|
scanBlocks(block.getBlocks())
|
|
173
470
|
}
|
|
174
471
|
})
|
|
175
472
|
}
|
|
176
|
-
|
|
473
|
+
|
|
177
474
|
scanBlocks(doc.getBlocks())
|
|
178
|
-
|
|
475
|
+
|
|
476
|
+
// Paragraphs, lists, and the other rollup contexts, plus table cells:
|
|
477
|
+
// see propagateCellContentMarks/rollupChildRevisionEntries/rollupTableCellRevisionEntries
|
|
478
|
+
// above. Must run after the row-level scan above, so a section's own
|
|
479
|
+
// entry, if independently marked, already exists for a child to append onto.
|
|
480
|
+
propagateCellContentMarks(doc, prefix)
|
|
481
|
+
rollupChildRevisionEntries(catalog, doc, prefix)
|
|
482
|
+
rollupTableCellRevisionEntries(catalog, doc, prefix)
|
|
483
|
+
|
|
179
484
|
return catalog
|
|
180
485
|
}
|
|
181
486
|
|
|
182
487
|
function generateInitialReleaseHtml(doc) {
|
|
183
488
|
const discrete = doc.hasAttribute('rhrev-discrete')
|
|
184
489
|
const html = []
|
|
185
|
-
|
|
490
|
+
|
|
186
491
|
// Wrapper
|
|
187
492
|
if (discrete) {
|
|
188
493
|
html.push('<div class="rhrev-section">')
|
|
189
494
|
} else {
|
|
190
495
|
html.push('<section id="revision-history">')
|
|
191
496
|
}
|
|
192
|
-
|
|
497
|
+
|
|
193
498
|
// Title
|
|
194
499
|
const customTitle = doc.getAttribute('rhrev-customization-title')
|
|
195
500
|
if (customTitle !== undefined && customTitle === '') {
|
|
@@ -202,12 +507,12 @@ function generateInitialReleaseHtml(doc) {
|
|
|
202
507
|
const versionLabel = String(doc.getAttribute('version-label') || 'Revision')
|
|
203
508
|
html.push(`<h2>${versionLabel} History</h2>`)
|
|
204
509
|
}
|
|
205
|
-
|
|
510
|
+
|
|
206
511
|
// Simple table
|
|
207
512
|
const initialText = doc.getAttribute('rhrev-customization-initial-release-text') ||
|
|
208
513
|
doc.getAttribute('rhrev-localization-initial-release') ||
|
|
209
514
|
'Initial release'
|
|
210
|
-
|
|
515
|
+
|
|
211
516
|
const columnWidths = String(doc.getAttribute('rhrev-table-column-width') || '30,70').split(',').map(w => w.trim())
|
|
212
517
|
const sectionLabel = doc.getAttribute('rhrev-localization-location') || 'Section'
|
|
213
518
|
|
|
@@ -215,7 +520,7 @@ function generateInitialReleaseHtml(doc) {
|
|
|
215
520
|
const frame = doc.getAttribute('rhrev-table-frame') || 'all'
|
|
216
521
|
const grid = doc.getAttribute('rhrev-table-grid') || 'all'
|
|
217
522
|
html.push(`<table class="tableblock frame-${frame} grid-${grid} stretch">`)
|
|
218
|
-
|
|
523
|
+
|
|
219
524
|
// Caption as title div
|
|
220
525
|
if (!doc.hasAttribute('rhrev-disable-caption')) {
|
|
221
526
|
const caption = doc.getAttribute('rhrev-table-caption')
|
|
@@ -223,7 +528,7 @@ function generateInitialReleaseHtml(doc) {
|
|
|
223
528
|
html.push(`<div class="title">${caption}</div>`)
|
|
224
529
|
}
|
|
225
530
|
}
|
|
226
|
-
|
|
531
|
+
|
|
227
532
|
html.push('<colgroup>')
|
|
228
533
|
columnWidths.forEach(w => {
|
|
229
534
|
html.push(`<col style="width: ${w}%;">`)
|
|
@@ -253,24 +558,24 @@ function generateInitialReleaseHtml(doc) {
|
|
|
253
558
|
html.push('</tr>')
|
|
254
559
|
html.push('</tbody>')
|
|
255
560
|
html.push('</table>')
|
|
256
|
-
|
|
561
|
+
|
|
257
562
|
// Close wrapper
|
|
258
563
|
html.push(discrete ? '</div>' : '</section>')
|
|
259
|
-
|
|
564
|
+
|
|
260
565
|
return html.join('\n')
|
|
261
566
|
}
|
|
262
567
|
|
|
263
568
|
function generateHtmlOutput(doc, catalog, currentPage = '') {
|
|
264
569
|
const discrete = doc.hasAttribute('rhrev-discrete')
|
|
265
570
|
const html = []
|
|
266
|
-
|
|
571
|
+
|
|
267
572
|
// Wrapper
|
|
268
573
|
if (discrete) {
|
|
269
574
|
html.push('<div class="rhrev-section">')
|
|
270
575
|
} else {
|
|
271
576
|
html.push('<section id="revision-history">')
|
|
272
577
|
}
|
|
273
|
-
|
|
578
|
+
|
|
274
579
|
// Title
|
|
275
580
|
const customTitle = doc.getAttribute('rhrev-customization-title')
|
|
276
581
|
if (customTitle !== undefined && customTitle === '') {
|
|
@@ -283,25 +588,25 @@ function generateHtmlOutput(doc, catalog, currentPage = '') {
|
|
|
283
588
|
const versionLabel = String(doc.getAttribute('version-label') || 'Revision')
|
|
284
589
|
html.push(`<h2>${versionLabel} History</h2>`)
|
|
285
590
|
}
|
|
286
|
-
|
|
591
|
+
|
|
287
592
|
// Table
|
|
288
593
|
html.push(generateHtmlTable(doc, catalog, currentPage))
|
|
289
|
-
|
|
594
|
+
|
|
290
595
|
// Close wrapper
|
|
291
596
|
html.push(discrete ? '</div>' : '</section>')
|
|
292
|
-
|
|
597
|
+
|
|
293
598
|
return html.join('\n')
|
|
294
599
|
}
|
|
295
600
|
|
|
296
601
|
function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
297
602
|
const columnWidths = String(doc.getAttribute('rhrev-table-column-width') || '30,70').split(',').map(w => w.trim())
|
|
298
603
|
const html = []
|
|
299
|
-
|
|
604
|
+
|
|
300
605
|
// Build table with frame/grid classes
|
|
301
606
|
const frame = doc.getAttribute('rhrev-table-frame') || 'all'
|
|
302
607
|
const grid = doc.getAttribute('rhrev-table-grid') || 'all'
|
|
303
608
|
html.push(`<table class="tableblock frame-${frame} grid-${grid} stretch">`)
|
|
304
|
-
|
|
609
|
+
|
|
305
610
|
// Caption as title div
|
|
306
611
|
if (!doc.hasAttribute('rhrev-disable-caption')) {
|
|
307
612
|
const caption = doc.getAttribute('rhrev-table-caption')
|
|
@@ -309,19 +614,19 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
309
614
|
html.push(`<div class="title">${caption}</div>`)
|
|
310
615
|
}
|
|
311
616
|
}
|
|
312
|
-
|
|
617
|
+
|
|
313
618
|
html.push('<colgroup>')
|
|
314
619
|
columnWidths.forEach(w => {
|
|
315
620
|
html.push(`<col style="width: ${w}%;">`)
|
|
316
621
|
})
|
|
317
622
|
html.push('</colgroup>')
|
|
318
623
|
html.push('<tbody>')
|
|
319
|
-
|
|
624
|
+
|
|
320
625
|
// Custom first row
|
|
321
626
|
const firstRow = doc.getAttribute('rhrev-customization-first-row')
|
|
322
627
|
const firstRowLeft = doc.getAttribute('rhrev-customization-first-row-left')
|
|
323
628
|
const firstRowRight = doc.getAttribute('rhrev-customization-first-row-right')
|
|
324
|
-
|
|
629
|
+
|
|
325
630
|
if (firstRow) {
|
|
326
631
|
html.push('<tr class="rhrev-custom-first-row">')
|
|
327
632
|
html.push(`<td colspan="2">${firstRow}</td>`)
|
|
@@ -332,11 +637,11 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
332
637
|
html.push(`<td>${firstRowRight || ' '}</td>`)
|
|
333
638
|
html.push('</tr>')
|
|
334
639
|
}
|
|
335
|
-
|
|
640
|
+
|
|
336
641
|
const sectionLabel = doc.getAttribute('rhrev-localization-location') || 'Section'
|
|
337
642
|
const majorChangesText = doc.getAttribute('rhrev-localization-major-changes') || 'Major changes since'
|
|
338
643
|
const allText = doc.getAttribute('rhrev-localization-all') || 'All'
|
|
339
|
-
|
|
644
|
+
|
|
340
645
|
catalog.sortedRevisions().forEach(revision => {
|
|
341
646
|
const prevrevAttr = `${revision}-prevrev`
|
|
342
647
|
const prevrevdateAttr = `${revision}-prevrevdate`
|
|
@@ -344,7 +649,7 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
344
649
|
const prevTitle = String(doc.getAttribute(prevrevAttr) || revision.replaceAll('-', '.'))
|
|
345
650
|
const prevDate = String(doc.getAttribute(prevrevdateAttr) || '')
|
|
346
651
|
const prevRevText = formatPrevRev(prevTitle, prevDate, doc)
|
|
347
|
-
|
|
652
|
+
|
|
348
653
|
// Build header text
|
|
349
654
|
let headerText
|
|
350
655
|
if (prevTitle.trim().toLowerCase().startsWith(revisionLabel.toLowerCase())) {
|
|
@@ -352,13 +657,13 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
352
657
|
} else {
|
|
353
658
|
headerText = `${majorChangesText} ${revisionLabel} ${prevRevText}`
|
|
354
659
|
}
|
|
355
|
-
|
|
660
|
+
|
|
356
661
|
// Header row for this revision
|
|
357
662
|
html.push('<tr class="rhrev-header">')
|
|
358
663
|
html.push(`<th class="tableblock halign-left valign-top">${sectionLabel}</th>`)
|
|
359
664
|
html.push(`<th class="tableblock halign-left valign-top"><strong>${headerText}</strong></th>`)
|
|
360
665
|
html.push('</tr>')
|
|
361
|
-
|
|
666
|
+
|
|
362
667
|
// All entry
|
|
363
668
|
const allChange = catalog.allEntries[revision]
|
|
364
669
|
if (allChange) {
|
|
@@ -367,17 +672,17 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
367
672
|
html.push(`<td>${formatChangeAsHtmlList(allChange)}</td>`)
|
|
368
673
|
html.push('</tr>')
|
|
369
674
|
}
|
|
370
|
-
|
|
675
|
+
|
|
371
676
|
// Block entries
|
|
372
677
|
const entries = catalog.entries[revision] || []
|
|
373
678
|
entries.sort((a, b) => (a.sequence || 0) - (b.sequence || 0))
|
|
374
|
-
|
|
679
|
+
|
|
375
680
|
entries.forEach(entry => {
|
|
376
681
|
const anchor = entry.anchor
|
|
377
|
-
const title = entry.title || anchor.replace(/[_-]/g, ' ').split(' ').map(w =>
|
|
682
|
+
const title = entry.title || anchor.replace(/[_-]/g, ' ').split(' ').map(w =>
|
|
378
683
|
w.charAt(0).toUpperCase() + w.slice(1)
|
|
379
684
|
).join(' ')
|
|
380
|
-
|
|
685
|
+
|
|
381
686
|
html.push('<tr>')
|
|
382
687
|
|
|
383
688
|
// Build link: local anchor if the section lives on the current page (directly or via include)
|
|
@@ -421,10 +726,10 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
421
726
|
html.push('</tr>')
|
|
422
727
|
})
|
|
423
728
|
})
|
|
424
|
-
|
|
729
|
+
|
|
425
730
|
html.push('</tbody>')
|
|
426
731
|
html.push('</table>')
|
|
427
|
-
|
|
732
|
+
|
|
428
733
|
return html.join('\n')
|
|
429
734
|
}
|
|
430
735
|
|
|
@@ -433,30 +738,30 @@ module.exports.register = function (registry, context = {}) {
|
|
|
433
738
|
// Register block macro for rhrev::[]
|
|
434
739
|
registry.blockMacro('rhrev', function () {
|
|
435
740
|
const self = this
|
|
436
|
-
|
|
741
|
+
|
|
437
742
|
self.process(function (parent, target, attrs) {
|
|
438
743
|
const doc = parent.getDocument()
|
|
439
|
-
|
|
744
|
+
|
|
440
745
|
// Check if manual mode
|
|
441
746
|
if (doc.getAttribute('rhrev') === 'manual') {
|
|
442
747
|
return self.createBlock(parent, 'paragraph', '')
|
|
443
748
|
}
|
|
444
|
-
|
|
749
|
+
|
|
445
750
|
// Check initial release handling
|
|
446
751
|
const revnumber = doc.getAttribute('revnumber') || '1.0'
|
|
447
752
|
const initialHandling = doc.getAttribute('rhrev-initial-handling') || 'initial-release'
|
|
448
|
-
|
|
753
|
+
|
|
449
754
|
if (isInitialRelease(revnumber)) {
|
|
450
755
|
if (initialHandling === 'skip') {
|
|
451
756
|
return self.createBlock(parent, 'paragraph', '')
|
|
452
757
|
}
|
|
453
|
-
|
|
758
|
+
|
|
454
759
|
if (initialHandling === 'initial-release') {
|
|
455
760
|
const html = generateInitialReleaseHtml(doc)
|
|
456
761
|
return self.createBlock(parent, 'pass', html)
|
|
457
762
|
}
|
|
458
763
|
}
|
|
459
|
-
|
|
764
|
+
|
|
460
765
|
// Try to use catalog from Antora pipeline extension (component-scoped by default).
|
|
461
766
|
// global.antoraRhrevCatalog is the site-wide catalog; .byComponent holds per-component catalogs.
|
|
462
767
|
let catalog
|
|
@@ -489,11 +794,35 @@ module.exports.register = function (registry, context = {}) {
|
|
|
489
794
|
return self.createBlock(parent, 'pass', html)
|
|
490
795
|
})
|
|
491
796
|
})
|
|
492
|
-
|
|
797
|
+
|
|
798
|
+
// Propagates a marked nested block's rhrev attribute onto its enclosing
|
|
799
|
+
// a| table cell, and strips it from the nested block. See
|
|
800
|
+
// propagateCellContentMarks above for the full reasoning (the same
|
|
801
|
+
// isolated-inner-document gap the PDF gem's converter works around).
|
|
802
|
+
// Must be registered, and therefore run, before the change-bar tagging
|
|
803
|
+
// treeProcessor immediately below: treeprocessors all run before
|
|
804
|
+
// conversion starts, so if propagation happened later, that pass would
|
|
805
|
+
// already have tagged the nested paragraph at its original, unpropagated
|
|
806
|
+
// location, since it is a rollup context in its own right.
|
|
807
|
+
registry.treeProcessor(function () {
|
|
808
|
+
const self = this
|
|
809
|
+
|
|
810
|
+
self.process(function (doc) {
|
|
811
|
+
const prefix = doc.getAttribute('revhistoryprefix') || 'rhrev'
|
|
812
|
+
propagateCellContentMarks(doc, prefix)
|
|
813
|
+
return doc
|
|
814
|
+
})
|
|
815
|
+
})
|
|
816
|
+
|
|
493
817
|
// Tag blocks carrying the current revision attribute with the rhrev-changed
|
|
494
818
|
// role so the site stylesheet can draw an HTML change bar (right-side border).
|
|
495
819
|
// Enabled by rhrev-change-bars, mirroring the PDF backend; color/width/offset
|
|
496
820
|
// overrides are emitted as CSS custom properties. Side is always right in HTML.
|
|
821
|
+
// The first six contexts get their own revision-history row when marked;
|
|
822
|
+
// the other nine (mirrors ROLLUP_CONTEXTS) roll up into their enclosing
|
|
823
|
+
// section's entry instead (see rollupChildRevisionEntries), but still get
|
|
824
|
+
// their own bar here: tagging needs no id or anchor, a role is a role
|
|
825
|
+
// regardless of block type.
|
|
497
826
|
registry.treeProcessor(function () {
|
|
498
827
|
const self = this
|
|
499
828
|
|
|
@@ -503,7 +832,18 @@ module.exports.register = function (registry, context = {}) {
|
|
|
503
832
|
if (!revnumber) return doc
|
|
504
833
|
const prefix = doc.getAttribute('revhistoryprefix') || 'rhrev'
|
|
505
834
|
const changeAttr = prefix + String(revnumber).replace(/\./g, '-')
|
|
506
|
-
const contexts =
|
|
835
|
+
const contexts = ROW_LEVEL_CONTEXTS.concat(ROLLUP_CONTEXTS)
|
|
836
|
+
|
|
837
|
+
// A marked table cell was already tagged on its own nested content by
|
|
838
|
+
// propagateCellContentMarks, in the earlier treeprocessor: that walk
|
|
839
|
+
// is scoped to each cell's own, structurally separate inner document,
|
|
840
|
+
// invisible to this treeprocessor's own outer-document findBy below,
|
|
841
|
+
// so `tagged` (which only reflects THIS loop) is never a reliable
|
|
842
|
+
// signal for whether a cell needs patching. Runs unconditionally once
|
|
843
|
+
// change bars are known enabled, before the tagged/return-early check,
|
|
844
|
+
// so a document whose only marked content is a table cell (no other
|
|
845
|
+
// block matched below) still gets its converter patched.
|
|
846
|
+
patchConvertTableForCellTagging(doc)
|
|
507
847
|
|
|
508
848
|
let tagged = false
|
|
509
849
|
doc.findBy((b) => contexts.includes(b.getContext())).forEach((block) => {
|
|
@@ -533,33 +873,30 @@ module.exports.register = function (registry, context = {}) {
|
|
|
533
873
|
// Register treeprocessor for auto-injection
|
|
534
874
|
registry.treeProcessor(function () {
|
|
535
875
|
const self = this
|
|
536
|
-
|
|
876
|
+
|
|
537
877
|
self.process(function (doc) {
|
|
538
878
|
if (!doc.hasAttribute('rhrev')) {
|
|
539
879
|
return doc
|
|
540
880
|
}
|
|
541
|
-
|
|
881
|
+
|
|
542
882
|
const rhrevValue = doc.getAttribute('rhrev')
|
|
543
883
|
if (rhrevValue === 'macro' || rhrevValue === 'manual') {
|
|
544
884
|
return doc
|
|
545
885
|
}
|
|
546
|
-
|
|
886
|
+
|
|
547
887
|
// Auto-inject revision history at document start
|
|
548
888
|
const blocks = doc.getBlocks()
|
|
549
|
-
|
|
550
|
-
// Create rhrev block
|
|
551
|
-
const rhrevBlock = self.createBlock(doc, 'pass', '')
|
|
552
|
-
|
|
889
|
+
|
|
553
890
|
// Check initial release handling
|
|
554
891
|
const revnumber = doc.getAttribute('revnumber') || '1.0'
|
|
555
892
|
const initialHandling = doc.getAttribute('rhrev-initial-handling') || 'initial-release'
|
|
556
|
-
|
|
893
|
+
|
|
557
894
|
let html
|
|
558
895
|
if (isInitialRelease(revnumber)) {
|
|
559
896
|
if (initialHandling === 'skip') {
|
|
560
897
|
return doc
|
|
561
898
|
}
|
|
562
|
-
|
|
899
|
+
|
|
563
900
|
if (initialHandling === 'initial-release') {
|
|
564
901
|
html = generateInitialReleaseHtml(doc)
|
|
565
902
|
} else {
|
|
@@ -568,19 +905,23 @@ module.exports.register = function (registry, context = {}) {
|
|
|
568
905
|
}
|
|
569
906
|
} else {
|
|
570
907
|
const catalog = collectAllEntries(doc)
|
|
571
|
-
|
|
908
|
+
|
|
572
909
|
if (catalog.sortedRevisions().length === 0) {
|
|
573
910
|
return doc
|
|
574
911
|
}
|
|
575
|
-
|
|
912
|
+
|
|
576
913
|
html = generateHtmlOutput(doc, catalog)
|
|
577
914
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
915
|
+
|
|
916
|
+
// Create the block with its content directly: self.createBlock(...).setContent(html)
|
|
917
|
+
// used to be two steps here, but Block#setContent is not a function
|
|
918
|
+
// on a modern @asciidoctor/core (confirmed against 2.2.9, a
|
|
919
|
+
// pre-existing bug, unrelated to anything else in this file).
|
|
920
|
+
const rhrevBlock = self.createBlock(doc, 'pass', html)
|
|
921
|
+
|
|
581
922
|
// Insert at beginning
|
|
582
923
|
blocks.unshift(rhrevBlock)
|
|
583
|
-
|
|
924
|
+
|
|
584
925
|
return doc
|
|
585
926
|
})
|
|
586
927
|
})
|
|
@@ -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