asciidoctor-rhrev 1.8.3 → 1.9.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.
|
@@ -17,7 +17,23 @@
|
|
|
17
17
|
// of their own. A marked one rolls up as a bullet into its nearest
|
|
18
18
|
// enclosing section's entry instead (or the document-level -all entry, if
|
|
19
19
|
// it has no enclosing section, or its enclosing section has no id).
|
|
20
|
-
const { liftCellTextMarks } = require('./cell-text-marks.js')
|
|
20
|
+
const { liftCellTextMarks, cellTextMarks } = require('./cell-text-marks.js')
|
|
21
|
+
const {
|
|
22
|
+
thirdLevelMode,
|
|
23
|
+
thirdLevelKind,
|
|
24
|
+
revisionOf,
|
|
25
|
+
groupAttr,
|
|
26
|
+
groupKey,
|
|
27
|
+
sortMarkKeys,
|
|
28
|
+
revisionComparator,
|
|
29
|
+
mixedDraftGroups,
|
|
30
|
+
mixedDraftMessage,
|
|
31
|
+
markedRevisionOf,
|
|
32
|
+
markedRevisions,
|
|
33
|
+
dropReleaseMarks,
|
|
34
|
+
Matcher,
|
|
35
|
+
REVISION_RX
|
|
36
|
+
} = require('./third-level.js')
|
|
21
37
|
|
|
22
38
|
const ROLLUP_CONTEXTS = ['paragraph', 'ulist', 'olist', 'dlist', 'admonition', 'open', 'quote', 'verse', 'sidebar']
|
|
23
39
|
|
|
@@ -40,7 +56,8 @@ function enclosingSection(node) {
|
|
|
40
56
|
// appends another bullet onto its change text, or creates the entry if
|
|
41
57
|
// anchorNode has none yet. Same shape as Converter#append_child_change_to_entry.
|
|
42
58
|
function appendChildChangeToEntry(catalog, revision, anchorNode, changeText) {
|
|
43
|
-
const
|
|
59
|
+
const key = catalog.revisionKey(revision)
|
|
60
|
+
const entries = catalog.entries[key] || (catalog.entries[key] = [])
|
|
44
61
|
const anchorId = anchorNode.getId()
|
|
45
62
|
const entry = entries.find((e) => e.anchor === anchorId)
|
|
46
63
|
if (entry) {
|
|
@@ -56,9 +73,7 @@ function appendChildChangeToEntry(catalog, revision, anchorNode, changeText) {
|
|
|
56
73
|
// Appends onto catalog's existing -all change text for this revision, or
|
|
57
74
|
// sets it fresh if none exists yet. Same shape as Converter#append_child_change_to_all.
|
|
58
75
|
function appendChildChangeToAll(catalog, revision, changeText) {
|
|
59
|
-
|
|
60
|
-
const combined = (existing && String(existing).length) ? `${existing} * ${changeText}` : changeText
|
|
61
|
-
catalog.addAllEntry(revision, combined)
|
|
76
|
+
catalog.appendAllEntry(revision, changeText)
|
|
62
77
|
}
|
|
63
78
|
|
|
64
79
|
// Gathers every marked rollup-context node in one unified, document-order
|
|
@@ -74,7 +89,7 @@ function rollupChildRevisionEntries(catalog, doc, prefix) {
|
|
|
74
89
|
if (!attrs) return
|
|
75
90
|
const section = enclosingSection(node)
|
|
76
91
|
const hasSection = !!(section && section.getId())
|
|
77
|
-
Object.keys(attrs).forEach((key) => {
|
|
92
|
+
sortMarkKeys(Object.keys(attrs), prefix, catalog.kind).forEach((key) => {
|
|
78
93
|
if (!key.startsWith(prefix)) return
|
|
79
94
|
if (key.includes('-all') || key.endsWith('-cover')) return
|
|
80
95
|
const revision = key.substring(prefix.length)
|
|
@@ -122,7 +137,8 @@ function rollupChildRevisionEntries(catalog, doc, prefix) {
|
|
|
122
137
|
function propagateCellContentMarks(doc, prefix) {
|
|
123
138
|
const changeBarsEnabled = doc.hasAttribute('rhrev-change-bars')
|
|
124
139
|
const revnumber = doc.getAttribute('revnumber')
|
|
125
|
-
const
|
|
140
|
+
const matcher = (changeBarsEnabled && revnumber) ? new Matcher(prefix, revnumber, docThirdLevelMode(doc), docThirdLevelKind(doc), docIgnoredReleases(doc, prefix)) : null
|
|
141
|
+
const isChangeMark = matcher ? (key) => matcher.isMark(key) : null
|
|
126
142
|
|
|
127
143
|
// First the raw-text rule (cell-text-marks.js): an rhrev attribute list
|
|
128
144
|
// alone on the first line of any cell's content, any style, any row
|
|
@@ -131,7 +147,7 @@ function propagateCellContentMarks(doc, prefix) {
|
|
|
131
147
|
// content as blocks. The role span it wraps a marked plain cell's text in
|
|
132
148
|
// is what patchConvertTableForCellTagging's HTML lift finds for such a
|
|
133
149
|
// cell, the same way a marked nested node's role serves an a| cell.
|
|
134
|
-
liftCellTextMarks(doc, prefix, {
|
|
150
|
+
liftCellTextMarks(doc, prefix, { isChangeMark, wrapForTag: !!matcher })
|
|
135
151
|
|
|
136
152
|
doc.findBy((n) => n.getContext() === 'table').forEach((table) => {
|
|
137
153
|
const rows = table.getRows()
|
|
@@ -146,7 +162,7 @@ function propagateCellContentMarks(doc, prefix) {
|
|
|
146
162
|
Object.keys(attrs).forEach((key) => {
|
|
147
163
|
if (!key.startsWith(prefix)) return
|
|
148
164
|
cell.setAttribute(key, attrs[key])
|
|
149
|
-
if (
|
|
165
|
+
if (matcher && matcher.isMark(key)) {
|
|
150
166
|
const roles = attrs.role
|
|
151
167
|
node.setAttribute('role', roles ? `${roles} rhrev-changed` : 'rhrev-changed')
|
|
152
168
|
}
|
|
@@ -322,20 +338,99 @@ function addRoleClass (tagText, role) {
|
|
|
322
338
|
return tagText.replace(/^<(td|th)\b/, `<$1 class="${role}"`)
|
|
323
339
|
}
|
|
324
340
|
|
|
325
|
-
//
|
|
341
|
+
// The document's rhrev-third-level mode, warning once per document on an
|
|
342
|
+
// unrecognized value.
|
|
343
|
+
function docThirdLevelMode(doc) {
|
|
344
|
+
return thirdLevelMode(doc.getAttribute('rhrev-third-level'), (message) => {
|
|
345
|
+
if (doc.$$rhrevThirdLevelWarned) return
|
|
346
|
+
doc.$$rhrevThirdLevelWarned = true
|
|
347
|
+
doc.getLogger().warn(message)
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// The document's rhrev-third-level-kind, or null without a mode, warning
|
|
352
|
+
// once per document on an unrecognized value.
|
|
353
|
+
function docThirdLevelKind(doc) {
|
|
354
|
+
return thirdLevelKind(doc.getAttribute('rhrev-third-level-kind'), docThirdLevelMode(doc), (message) => {
|
|
355
|
+
if (doc.$$rhrevThirdLevelKindWarned) return
|
|
356
|
+
doc.$$rhrevThirdLevelKindWarned = true
|
|
357
|
+
doc.getLogger().warn(message)
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// The x-y releases a draft-kind document marks beside marks of their own
|
|
362
|
+
// drafts, warning once per release, same as
|
|
363
|
+
// ThirdLevel.drop_mixed_release_marks on the Ruby side. Reads the cell
|
|
364
|
+
// text marks liftCellTextMarks has not lifted yet, so it gives the same
|
|
365
|
+
// answer before and after propagateCellContentMarks. Memoized on the
|
|
366
|
+
// document.
|
|
367
|
+
function docMixedReleases(doc, prefix) {
|
|
368
|
+
if (doc.$$rhrevMixedReleases) return doc.$$rhrevMixedReleases
|
|
369
|
+
let releases = []
|
|
370
|
+
if (docThirdLevelKind(doc) === 'draft') {
|
|
371
|
+
const revisions = markedRevisions(doc, prefix)
|
|
372
|
+
doc.findBy((n) => n.getContext() === 'table').forEach((table) => {
|
|
373
|
+
const rows = table.getRows()
|
|
374
|
+
;[].concat(rows.head.flat(), rows.foot.flat(), rows.body.flat()).forEach((cell) => {
|
|
375
|
+
Object.keys(cellTextMarks(cell, prefix)).forEach((key) => {
|
|
376
|
+
const revision = markedRevisionOf(key, prefix)
|
|
377
|
+
if (revision) revisions.push(revision)
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
releases = mixedDraftGroups(revisions).map(([release, drafts]) => {
|
|
382
|
+
doc.getLogger().warn(mixedDraftMessage(prefix, release, drafts))
|
|
383
|
+
return release
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
doc.$$rhrevMixedReleases = releases
|
|
387
|
+
return releases
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// The releases whose marks the document ignores: its own mixed releases,
|
|
391
|
+
// plus those of its component catalog, which antora-rhrev-html.js finds
|
|
392
|
+
// across every page of the component.
|
|
393
|
+
function docIgnoredReleases(doc, prefix) {
|
|
394
|
+
const componentName = doc.getAttribute('page-component-name')
|
|
395
|
+
const component = componentName && global.antoraRhrevCatalog && global.antoraRhrevCatalog.byComponent &&
|
|
396
|
+
global.antoraRhrevCatalog.byComponent[componentName]
|
|
397
|
+
return [...new Set(docMixedReleases(doc, prefix).concat((component && component.ignored) || []))]
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Catalog for storing revision entries. In collapse mode (thirdLevel) every
|
|
401
|
+
// revision is stored under its x-y group, and one anchor marked in several
|
|
402
|
+
// revisions of a group merges into one entry. kind orders an x-y revision
|
|
403
|
+
// relative to its x-y-z revisions.
|
|
326
404
|
class RevisionCatalog {
|
|
327
|
-
constructor() {
|
|
405
|
+
constructor(thirdLevel = null, kind = null) {
|
|
328
406
|
this.entries = {} // Block-level entries: { '1-1': [...], '1-2': [...] }
|
|
329
407
|
this.allEntries = {} // Document-level -all entries: { '1-1': 'text', '1-2': 'text' }
|
|
330
408
|
this.sequence = 0
|
|
409
|
+
this.thirdLevel = thirdLevel
|
|
410
|
+
this.kind = kind
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
revisionKey(revision) {
|
|
414
|
+
return groupKey(revision, this.thirdLevel)
|
|
331
415
|
}
|
|
332
416
|
|
|
333
417
|
addEntry(revision, anchor, change, metadata = {}) {
|
|
334
|
-
|
|
335
|
-
|
|
418
|
+
const key = this.revisionKey(revision)
|
|
419
|
+
if (!this.entries[key]) {
|
|
420
|
+
this.entries[key] = []
|
|
336
421
|
}
|
|
337
422
|
|
|
338
|
-
this.
|
|
423
|
+
if (this.thirdLevel === 'collapse') {
|
|
424
|
+
const existing = this.entries[key].find((e) => e.anchor === anchor)
|
|
425
|
+
if (existing) {
|
|
426
|
+
if (change && String(change).length) {
|
|
427
|
+
existing.change = (existing.change && String(existing.change).length) ? `${existing.change} * ${change}` : change
|
|
428
|
+
}
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
this.entries[key].push({
|
|
339
434
|
anchor,
|
|
340
435
|
change,
|
|
341
436
|
title: metadata.title || null,
|
|
@@ -345,7 +440,12 @@ class RevisionCatalog {
|
|
|
345
440
|
}
|
|
346
441
|
|
|
347
442
|
addAllEntry(revision, change) {
|
|
348
|
-
this.allEntries[revision] = change
|
|
443
|
+
this.allEntries[this.revisionKey(revision)] = change
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
appendAllEntry(revision, change) {
|
|
447
|
+
const existing = this.allEntries[this.revisionKey(revision)]
|
|
448
|
+
this.addAllEntry(revision, (existing && String(existing).length) ? `${existing} * ${change}` : change)
|
|
349
449
|
}
|
|
350
450
|
|
|
351
451
|
sortedRevisions() {
|
|
@@ -353,20 +453,8 @@ class RevisionCatalog {
|
|
|
353
453
|
...Object.keys(this.entries),
|
|
354
454
|
...Object.keys(this.allEntries)
|
|
355
455
|
])
|
|
356
|
-
return Array.from(revisions).sort(
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function compareRevisions(a, b) {
|
|
361
|
-
const aParts = a.split('-').map(Number)
|
|
362
|
-
const bParts = b.split('-').map(Number)
|
|
363
|
-
const len = Math.max(aParts.length, bParts.length)
|
|
364
|
-
for (let i = 0; i < len; i++) {
|
|
365
|
-
const aVal = i < aParts.length ? aParts[i] : 0
|
|
366
|
-
const bVal = i < bParts.length ? bParts[i] : 0
|
|
367
|
-
if (aVal !== bVal) return bVal - aVal
|
|
456
|
+
return Array.from(revisions).sort(revisionComparator(this.kind))
|
|
368
457
|
}
|
|
369
|
-
return 0
|
|
370
458
|
}
|
|
371
459
|
|
|
372
460
|
function isInitialRelease(revnumber) {
|
|
@@ -427,25 +515,31 @@ function formatChangeAsHtmlList(text) {
|
|
|
427
515
|
}
|
|
428
516
|
|
|
429
517
|
function collectAllEntries(doc) {
|
|
430
|
-
const
|
|
518
|
+
const thirdLevel = docThirdLevelMode(doc)
|
|
519
|
+
const catalog = new RevisionCatalog(thirdLevel, docThirdLevelKind(doc))
|
|
431
520
|
const prefix = doc.getAttribute('revhistoryprefix') || 'rhrev'
|
|
432
521
|
|
|
433
|
-
// Collect document-level entries (-all)
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
}
|
|
522
|
+
// Collect document-level entries (-all). Discover revisions from
|
|
523
|
+
// *-prevrev attributes so any number of segments works, the same as the
|
|
524
|
+
// Ruby side; with rhrev-third-level set, a revision's -all attribute
|
|
525
|
+
// counts too. Ascending order keeps merged collapse-mode text in
|
|
526
|
+
// revision order.
|
|
527
|
+
const revisions = new Set()
|
|
528
|
+
Object.keys(doc.getAttributes()).forEach((key) => {
|
|
529
|
+
let revision = null
|
|
530
|
+
if (key.endsWith('-prevrev')) {
|
|
531
|
+
revision = key.slice(0, -'-prevrev'.length)
|
|
532
|
+
} else if (thirdLevel && key.endsWith('-all')) {
|
|
533
|
+
revision = revisionOf(key.slice(0, -'-all'.length), prefix)
|
|
447
534
|
}
|
|
448
|
-
|
|
535
|
+
if (revision && REVISION_RX.test(revision)) revisions.add(revision)
|
|
536
|
+
})
|
|
537
|
+
Array.from(revisions).sort(revisionComparator(catalog.kind)).reverse().forEach((revision) => {
|
|
538
|
+
const allValue = doc.getAttribute(`${prefix}${revision}-all`)
|
|
539
|
+
if (allValue) {
|
|
540
|
+
catalog.appendAllEntry(revision, allValue)
|
|
541
|
+
}
|
|
542
|
+
})
|
|
449
543
|
|
|
450
544
|
// Collect block-level entries by scanning document
|
|
451
545
|
function scanBlocks(blocks) {
|
|
@@ -461,7 +555,7 @@ function collectAllEntries(doc) {
|
|
|
461
555
|
|
|
462
556
|
// Also check if attributes are exposed differently
|
|
463
557
|
// Some Asciidoctor.js versions expose them via getAttribute(name) only
|
|
464
|
-
Object.keys(attrs).forEach(key => {
|
|
558
|
+
sortMarkKeys(Object.keys(attrs), prefix, catalog.kind).forEach(key => {
|
|
465
559
|
const value = attrs[key]
|
|
466
560
|
|
|
467
561
|
if (key.startsWith(prefix) && !key.includes('-all') && !key.endsWith('-cover')) {
|
|
@@ -654,11 +748,11 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
654
748
|
const allText = doc.getAttribute('rhrev-localization-all') || 'All'
|
|
655
749
|
|
|
656
750
|
catalog.sortedRevisions().forEach(revision => {
|
|
657
|
-
const
|
|
658
|
-
const
|
|
751
|
+
const thirdLevel = docThirdLevelMode(doc)
|
|
752
|
+
const docAttrs = doc.getAttributes()
|
|
659
753
|
const revisionLabel = String(doc.getAttribute('version-label') || 'Revision')
|
|
660
|
-
const prevTitle = String(
|
|
661
|
-
const prevDate = String(
|
|
754
|
+
const prevTitle = String(groupAttr(docAttrs, revision, 'prevrev', thirdLevel) || revision.replaceAll('-', '.'))
|
|
755
|
+
const prevDate = String(groupAttr(docAttrs, revision, 'prevrevdate', thirdLevel) || '')
|
|
662
756
|
const prevRevText = formatPrevRev(prevTitle, prevDate, doc)
|
|
663
757
|
|
|
664
758
|
// Build header text
|
|
@@ -820,7 +914,13 @@ module.exports.register = function (registry, context = {}) {
|
|
|
820
914
|
|
|
821
915
|
self.process(function (doc) {
|
|
822
916
|
const prefix = doc.getAttribute('revhistoryprefix') || 'rhrev'
|
|
917
|
+
// Dropping the ignored releases' marks before and after propagation
|
|
918
|
+
// removes them from blocks and nested cell content first, then from
|
|
919
|
+
// the cell text marks propagation lifts onto their cells.
|
|
920
|
+
const ignored = docIgnoredReleases(doc, prefix)
|
|
921
|
+
dropReleaseMarks(doc, prefix, ignored)
|
|
823
922
|
propagateCellContentMarks(doc, prefix)
|
|
923
|
+
dropReleaseMarks(doc, prefix, ignored)
|
|
824
924
|
return doc
|
|
825
925
|
})
|
|
826
926
|
})
|
|
@@ -842,7 +942,7 @@ module.exports.register = function (registry, context = {}) {
|
|
|
842
942
|
const revnumber = doc.getAttribute('revnumber')
|
|
843
943
|
if (!revnumber) return doc
|
|
844
944
|
const prefix = doc.getAttribute('revhistoryprefix') || 'rhrev'
|
|
845
|
-
const
|
|
945
|
+
const matcher = new Matcher(prefix, revnumber, docThirdLevelMode(doc), docThirdLevelKind(doc), docIgnoredReleases(doc, prefix))
|
|
846
946
|
const contexts = ROW_LEVEL_CONTEXTS.concat(ROLLUP_CONTEXTS)
|
|
847
947
|
|
|
848
948
|
// A marked table cell was already tagged on its own nested content by
|
|
@@ -859,7 +959,7 @@ module.exports.register = function (registry, context = {}) {
|
|
|
859
959
|
let tagged = false
|
|
860
960
|
doc.findBy((b) => contexts.includes(b.getContext())).forEach((block) => {
|
|
861
961
|
const attrs = block.getAttributes()
|
|
862
|
-
if (!attrs
|
|
962
|
+
if (!matcher.match(attrs)) return
|
|
863
963
|
const role = attrs.role ? attrs.role + ' rhrev-changed' : 'rhrev-changed'
|
|
864
964
|
block.setAttribute('role', role)
|
|
865
965
|
tagged = true
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { liftCellTextMarks } = require('./cell-text-marks.js')
|
|
4
|
+
const {
|
|
5
|
+
thirdLevelMode,
|
|
6
|
+
thirdLevelKind,
|
|
7
|
+
revisionOf,
|
|
8
|
+
groupKey,
|
|
9
|
+
sortMarkKeys,
|
|
10
|
+
revisionComparator,
|
|
11
|
+
mixedDraftGroups,
|
|
12
|
+
mixedDraftMessage,
|
|
13
|
+
markedRevisionOf,
|
|
14
|
+
markedRevisions,
|
|
15
|
+
dropReleaseMarks,
|
|
16
|
+
REVISION_RX
|
|
17
|
+
} = require('./third-level.js')
|
|
4
18
|
|
|
5
19
|
/**
|
|
6
20
|
* Antora pipeline extension for revision history (rhrev).
|
|
@@ -43,7 +57,7 @@ noIncludeRegistry.includeProcessor(function () {
|
|
|
43
57
|
// sibling file: the two files are independent Antora extension entry
|
|
44
58
|
// points, loaded and registered separately, matching the existing
|
|
45
59
|
// duplication between this file and antora-rhrev-asciidoc.js (each has
|
|
46
|
-
// its own copy of, for example,
|
|
60
|
+
// its own copy of, for example, formatChangeAsHtmlList).
|
|
47
61
|
const ROLLUP_CONTEXTS = ['paragraph', 'ulist', 'olist', 'dlist', 'admonition', 'open', 'quote', 'verse', 'sidebar']
|
|
48
62
|
|
|
49
63
|
// Row-level: gets its own revision-history row when marked, requires an id.
|
|
@@ -151,9 +165,10 @@ function mergeCatalogs(catalogs) {
|
|
|
151
165
|
allEntries: {},
|
|
152
166
|
sortedRevisions() {
|
|
153
167
|
const all = [...Object.keys(this.entries), ...Object.keys(this.allEntries)]
|
|
154
|
-
return [...new Set(all)].sort(
|
|
168
|
+
return [...new Set(all)].sort(revisionComparator(this.kind))
|
|
155
169
|
}
|
|
156
170
|
}
|
|
171
|
+
merged.kind = (catalogs.find((catalog) => catalog.kind) || {}).kind || null
|
|
157
172
|
for (const catalog of catalogs) {
|
|
158
173
|
for (const [revision, entries] of Object.entries(catalog.entries)) {
|
|
159
174
|
if (!merged.entries[revision]) merged.entries[revision] = []
|
|
@@ -174,7 +189,8 @@ function mergeCatalogs(catalogs) {
|
|
|
174
189
|
// the anchor node's own for a row-level entry (anchorNode is the marked
|
|
175
190
|
// node itself, so meta === the anchor's own position).
|
|
176
191
|
function appendChildChangeToEntry(catalog, revision, anchorNode, changeText, pageMeta, meta) {
|
|
177
|
-
const
|
|
192
|
+
const key = groupKey(revision, catalog.thirdLevel)
|
|
193
|
+
const entries = catalog.entries[key] || (catalog.entries[key] = [])
|
|
178
194
|
const anchorId = anchorNode.getId()
|
|
179
195
|
const entry = entries.find((e) => e.anchor === anchorId)
|
|
180
196
|
if (entry) {
|
|
@@ -194,9 +210,10 @@ function appendChildChangeToEntry(catalog, revision, anchorNode, changeText, pag
|
|
|
194
210
|
// Appends onto catalog's existing -all change text for this revision, or
|
|
195
211
|
// sets it fresh if none exists yet.
|
|
196
212
|
function appendChildChangeToAll(catalog, revision, changeText) {
|
|
197
|
-
const
|
|
213
|
+
const key = groupKey(revision, catalog.thirdLevel)
|
|
214
|
+
const existing = catalog.allEntries[key]
|
|
198
215
|
const combined = (existing && String(existing).length) ? `${existing} * ${changeText}` : changeText
|
|
199
|
-
catalog.allEntries[
|
|
216
|
+
catalog.allEntries[key] = combined
|
|
200
217
|
}
|
|
201
218
|
|
|
202
219
|
function buildComponentCatalog(pageFiles, logger, quiet) {
|
|
@@ -213,7 +230,7 @@ function buildComponentCatalog(pageFiles, logger, quiet) {
|
|
|
213
230
|
...Object.keys(this.allEntries)
|
|
214
231
|
]
|
|
215
232
|
const uniqueRevisions = [...new Set(allRevisions)]
|
|
216
|
-
return uniqueRevisions.sort(
|
|
233
|
+
return uniqueRevisions.sort(revisionComparator(this.kind))
|
|
217
234
|
}
|
|
218
235
|
}
|
|
219
236
|
|
|
@@ -224,20 +241,63 @@ function buildComponentCatalog(pageFiles, logger, quiet) {
|
|
|
224
241
|
// Component attributes are in src.origin.descriptor.asciidoc.attributes
|
|
225
242
|
const componentAttrs = firstFile.src?.origin?.descriptor?.asciidoc?.attributes || {}
|
|
226
243
|
const prefix = componentAttrs.revhistoryprefix || 'rhrev'
|
|
244
|
+
catalog.thirdLevel = thirdLevelMode(componentAttrs['rhrev-third-level'], (message) => logger.warn(message))
|
|
245
|
+
catalog.kind = thirdLevelKind(componentAttrs['rhrev-third-level-kind'], catalog.thirdLevel, (message) => logger.warn(message))
|
|
227
246
|
|
|
228
247
|
if (!quiet) {
|
|
229
248
|
logger.debug(`rhrev-extension: Using prefix '${prefix}'`)
|
|
230
249
|
}
|
|
231
250
|
|
|
232
|
-
//
|
|
251
|
+
// Parse every page before collecting anything (the parse itself is
|
|
252
|
+
// described where the block-level entries are collected): a draft-kind
|
|
253
|
+
// component ignores a release
|
|
254
|
+
// marked beside its own drafts on any page, which is only known once
|
|
255
|
+
// every page has been read. catalog.ignored lists those releases for
|
|
256
|
+
// antora-rhrev-asciidoc.js, so page tagging ignores them too.
|
|
257
|
+
const parsedPages = []
|
|
258
|
+
pageFiles.forEach((file) => {
|
|
259
|
+
if (!file.contents) return
|
|
260
|
+
const content = file.contents.toString()
|
|
261
|
+
let doc
|
|
262
|
+
try {
|
|
263
|
+
doc = asciidoctor.load(content, { safe: 'safe', sourcemap: true, extension_registry: noIncludeRegistry, attributes: componentAttrs })
|
|
264
|
+
} catch (e) {
|
|
265
|
+
if (!quiet) logger.debug(`rhrev-extension: failed to parse ${file.src?.relative}, skipped: ${e.message}`)
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
liftCellTextMarks(doc, prefix)
|
|
269
|
+
parsedPages.push({ file, content, doc })
|
|
270
|
+
})
|
|
271
|
+
catalog.ignored = []
|
|
272
|
+
if (catalog.kind === 'draft') {
|
|
273
|
+
const marked = Object.keys(componentAttrs).map((key) => markedRevisionOf(key, prefix)).filter(Boolean)
|
|
274
|
+
parsedPages.forEach(({ doc }) => marked.push(...markedRevisions(doc, prefix)))
|
|
275
|
+
catalog.ignored = mixedDraftGroups(marked).map(([release, drafts]) => {
|
|
276
|
+
logger.warn(mixedDraftMessage(prefix, release, drafts))
|
|
277
|
+
return release
|
|
278
|
+
})
|
|
279
|
+
parsedPages.forEach(({ doc }) => dropReleaseMarks(doc, prefix, catalog.ignored))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Collect document-level entries by scanning for *-prevrev attributes
|
|
283
|
+
// (supports N-part versions); with rhrev-third-level set, a revision's
|
|
284
|
+
// -all attribute counts too. Ascending order keeps merged collapse-mode
|
|
285
|
+
// text in revision order.
|
|
233
286
|
const prevrevRx = /^([0-9]+(?:-[0-9]+)+)-prevrev$/
|
|
287
|
+
const revisions = new Set()
|
|
234
288
|
for (const attrName of Object.keys(componentAttrs)) {
|
|
235
289
|
const m = attrName.match(prevrevRx)
|
|
236
|
-
if (
|
|
237
|
-
|
|
290
|
+
if (m) {
|
|
291
|
+
revisions.add(m[1])
|
|
292
|
+
} else if (catalog.thirdLevel && attrName.endsWith('-all')) {
|
|
293
|
+
const revision = revisionOf(attrName.slice(0, -'-all'.length), prefix)
|
|
294
|
+
if (revision && REVISION_RX.test(revision)) revisions.add(revision)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (const revision of Array.from(revisions).sort(revisionComparator(catalog.kind)).reverse()) {
|
|
238
298
|
const allValue = componentAttrs[`${prefix}${revision}-all`]
|
|
239
|
-
if (allValue) {
|
|
240
|
-
catalog
|
|
299
|
+
if (allValue && !catalog.ignored.includes(revision)) {
|
|
300
|
+
appendChildChangeToAll(catalog, revision, allValue)
|
|
241
301
|
if (!quiet) {
|
|
242
302
|
logger.debug(`rhrev-extension: Found -all entry for ${revision}`)
|
|
243
303
|
}
|
|
@@ -253,11 +313,7 @@ function buildComponentCatalog(pageFiles, logger, quiet) {
|
|
|
253
313
|
// by [#anchor], .Title, or a heading line, so it had no way to reach
|
|
254
314
|
// paragraphs, lists, admonitions, or any of the other rollup contexts,
|
|
255
315
|
// or table cells, at all. Real parsing has no such limit.
|
|
256
|
-
|
|
257
|
-
if (!file.contents) return
|
|
258
|
-
|
|
259
|
-
const content = file.contents.toString()
|
|
260
|
-
|
|
316
|
+
parsedPages.forEach(({ file, content, doc }) => {
|
|
261
317
|
if (!quiet && content.includes(prefix)) {
|
|
262
318
|
logger.debug(`rhrev-extension: Scanning ${file.src?.relative} for block attributes`)
|
|
263
319
|
}
|
|
@@ -274,14 +330,6 @@ function buildComponentCatalog(pageFiles, logger, quiet) {
|
|
|
274
330
|
})
|
|
275
331
|
catalog.fileMeta[file.src?.relative || file.path] = fileTagBoundaries
|
|
276
332
|
|
|
277
|
-
let doc
|
|
278
|
-
try {
|
|
279
|
-
doc = asciidoctor.load(content, { safe: 'safe', sourcemap: true, extension_registry: noIncludeRegistry, attributes: componentAttrs })
|
|
280
|
-
} catch (e) {
|
|
281
|
-
if (!quiet) logger.debug(`rhrev-extension: failed to parse ${file.src?.relative}, skipped: ${e.message}`)
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
|
|
285
333
|
const pageMeta = {
|
|
286
334
|
page: file.src?.relative || file.path,
|
|
287
335
|
component: file.src?.component,
|
|
@@ -310,12 +358,20 @@ function buildComponentCatalog(pageFiles, logger, quiet) {
|
|
|
310
358
|
if (!blockId) return
|
|
311
359
|
const attrs = block.getAttributes()
|
|
312
360
|
if (!attrs) return
|
|
313
|
-
Object.keys(attrs).forEach((key) => {
|
|
361
|
+
sortMarkKeys(Object.keys(attrs), prefix, catalog.kind).forEach((key) => {
|
|
314
362
|
if (!key.startsWith(prefix)) return
|
|
315
363
|
if (key.includes('-all') || key.endsWith('-cover')) return
|
|
316
364
|
const revision = key.substring(prefix.length)
|
|
317
|
-
catalog.
|
|
318
|
-
|
|
365
|
+
if (catalog.thirdLevel === 'collapse') {
|
|
366
|
+
const grouped = catalog.entries[groupKey(revision, 'collapse')] || []
|
|
367
|
+
if (grouped.some((e) => e.anchor === blockId && e.page === pageMeta.page)) {
|
|
368
|
+
appendChildChangeToEntry(catalog, revision, block, String(attrs[key]), pageMeta, metaFor(block))
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const rowKey = groupKey(revision, catalog.thirdLevel)
|
|
373
|
+
catalog.entries[rowKey] = catalog.entries[rowKey] || []
|
|
374
|
+
catalog.entries[rowKey].push({
|
|
319
375
|
anchor: blockId,
|
|
320
376
|
change: String(attrs[key]),
|
|
321
377
|
title: block.getTitle(),
|
|
@@ -378,7 +434,7 @@ function buildComponentCatalog(pageFiles, logger, quiet) {
|
|
|
378
434
|
if (!attrs) return
|
|
379
435
|
const section = enclosingSection(node)
|
|
380
436
|
const hasSection = !!(section && section.getId())
|
|
381
|
-
Object.keys(attrs).forEach((key) => {
|
|
437
|
+
sortMarkKeys(Object.keys(attrs), prefix, catalog.kind).forEach((key) => {
|
|
382
438
|
if (!key.startsWith(prefix)) return
|
|
383
439
|
if (key.includes('-all') || key.endsWith('-cover')) return
|
|
384
440
|
const revision = key.substring(prefix.length)
|
|
@@ -501,7 +557,7 @@ function generateRevisionHistoryHtml(file, catalog, logger, quiet) {
|
|
|
501
557
|
]
|
|
502
558
|
const uniqueRevisions = [...new Set(allRevisions)]
|
|
503
559
|
|
|
504
|
-
const sortedRevisions = uniqueRevisions.sort(
|
|
560
|
+
const sortedRevisions = uniqueRevisions.sort(revisionComparator(catalog.kind))
|
|
505
561
|
|
|
506
562
|
// Generate revision rows
|
|
507
563
|
sortedRevisions.forEach(revision => {
|
|
@@ -582,18 +638,6 @@ function stripVerbatimBlocks(content) {
|
|
|
582
638
|
return out.join('\n')
|
|
583
639
|
}
|
|
584
640
|
|
|
585
|
-
function compareRevisions(a, b) {
|
|
586
|
-
const aParts = a.split('-').map(Number)
|
|
587
|
-
const bParts = b.split('-').map(Number)
|
|
588
|
-
const len = Math.max(aParts.length, bParts.length)
|
|
589
|
-
for (let i = 0; i < len; i++) {
|
|
590
|
-
const aVal = i < aParts.length ? aParts[i] : 0
|
|
591
|
-
const bVal = i < bParts.length ? bParts[i] : 0
|
|
592
|
-
if (aVal !== bVal) return bVal - aVal
|
|
593
|
-
}
|
|
594
|
-
return 0
|
|
595
|
-
}
|
|
596
|
-
|
|
597
641
|
function formatChangeAsHtmlList(text) {
|
|
598
642
|
if (!text) return ''
|
|
599
643
|
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
// (global.Opal), so the accepted syntax matches the Ruby side exactly. When
|
|
22
22
|
// no Opal runtime is present the cell is left untouched.
|
|
23
23
|
//
|
|
24
|
-
// wrapForTag: when set, and
|
|
25
|
-
// current revision
|
|
24
|
+
// wrapForTag: when set, and isChangeMark accepts one of the lifted
|
|
25
|
+
// attributes (a mark of the current revision, change bars enabled,
|
|
26
|
+
// third-level.js Matcher#isMark), the remaining
|
|
26
27
|
// cell text is wrapped in an unconstrained role span carrying the
|
|
27
28
|
// rhrev-changed role. Antora's HTML pipeline cannot tag a cell's own
|
|
28
29
|
// <td>/<th> from a treeprocessor; antora-rhrev-asciidoc.js's converter
|
|
@@ -49,8 +50,18 @@ function parseAttributeList (list) {
|
|
|
49
50
|
return attrs
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
// The rhrev marks a cell's raw text carries on its first line, as a plain
|
|
54
|
+
// object, without lifting them. Empty when there are none.
|
|
55
|
+
function cellTextMarks (cell, prefix) {
|
|
56
|
+
const raw = cell.text
|
|
57
|
+
const m = typeof raw === 'string' && LEADING_ATTRIBUTE_LIST_RX.exec(raw)
|
|
58
|
+
const attrs = m && parseAttributeList(m[1])
|
|
59
|
+
if (!attrs) return {}
|
|
60
|
+
return Object.fromEntries(Object.entries(attrs).filter(([key]) => key.startsWith(prefix)))
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
// Lifts a mark from one cell. Returns true when the cell changed.
|
|
53
|
-
function liftCellTextMark (cell, prefix, {
|
|
64
|
+
function liftCellTextMark (cell, prefix, { isChangeMark = null, wrapForTag = false } = {}) {
|
|
54
65
|
const raw = cell.text
|
|
55
66
|
if (typeof raw !== 'string') return false
|
|
56
67
|
const m = LEADING_ATTRIBUTE_LIST_RX.exec(raw)
|
|
@@ -62,7 +73,7 @@ function liftCellTextMark (cell, prefix, { changeAttr = null, wrapForTag = false
|
|
|
62
73
|
markKeys.forEach((key) => cell.setAttribute(key, attrs[key]))
|
|
63
74
|
let rest = raw.slice(m[0].length)
|
|
64
75
|
const style = cell.getStyle()
|
|
65
|
-
if (wrapForTag &&
|
|
76
|
+
if (wrapForTag && isChangeMark && markKeys.some(isChangeMark) && style !== 'asciidoc' && style !== 'literal') {
|
|
66
77
|
rest = `[.rhrev-changed]##${rest}##`
|
|
67
78
|
}
|
|
68
79
|
cell.text = rest
|
|
@@ -81,4 +92,4 @@ function liftCellTextMarks (doc, prefix, options = {}) {
|
|
|
81
92
|
return lifted
|
|
82
93
|
}
|
|
83
94
|
|
|
84
|
-
module.exports = { liftCellTextMarks, liftCellTextMark, LEADING_ATTRIBUTE_LIST_RX }
|
|
95
|
+
module.exports = { liftCellTextMarks, liftCellTextMark, cellTextMarks, LEADING_ATTRIBUTE_LIST_RX }
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Support for x.y.z revision numbers, selected by the rhrev-third-level
|
|
4
|
+
// attribute. Mirrors lib/asciidoctor/rhrev/third_level.rb on the Ruby side:
|
|
5
|
+
//
|
|
6
|
+
// collapse: the revision history table groups every revision by its first
|
|
7
|
+
// two levels, and the change bar covers every mark of the current x.y
|
|
8
|
+
// release.
|
|
9
|
+
// expand: the table lists every revision on its own, and the change bar
|
|
10
|
+
// covers every mark of the current x.y release. The third-level lanes
|
|
11
|
+
// are PDF-only; HTML draws one bar per element.
|
|
12
|
+
//
|
|
13
|
+
// Without the attribute, only a mark for exactly the current revnumber
|
|
14
|
+
// draws a bar, and every revision gets its own table group.
|
|
15
|
+
//
|
|
16
|
+
// The rhrev-third-level-kind attribute says what an x.y.z revision is:
|
|
17
|
+
//
|
|
18
|
+
// draft: the default; x.y.1, x.y.2, ... are drafts leading up to the
|
|
19
|
+
// release x.y, so x.y sorts after its drafts, an x.y mark draws no bar
|
|
20
|
+
// while the revnumber names a draft, and an x.y mark beside marks of its
|
|
21
|
+
// own drafts is dropped with a warning (mixedDraftGroups,
|
|
22
|
+
// dropReleaseMarks).
|
|
23
|
+
// amendment: x.y.1, x.y.2, ... amend the released x.y, so x.y sorts
|
|
24
|
+
// before them and its marks draw a bar for every amendment.
|
|
25
|
+
|
|
26
|
+
const MODES = ['expand', 'collapse']
|
|
27
|
+
const KINDS = ['draft', 'amendment']
|
|
28
|
+
const DEFAULT_KIND = 'draft'
|
|
29
|
+
const REVISION_RX = /^\d+(?:-\d+)*$/
|
|
30
|
+
|
|
31
|
+
// The mode for an attribute value, 'expand' or 'collapse', or null when
|
|
32
|
+
// the attribute is unset. An unrecognized value calls warn and yields null.
|
|
33
|
+
function thirdLevelMode (value, warn) {
|
|
34
|
+
if (value === undefined || value === null || value === false) return null
|
|
35
|
+
value = String(value)
|
|
36
|
+
if (MODES.includes(value)) return value
|
|
37
|
+
if (warn) warn(`rhrev-third-level must be expand or collapse, got '${value}'; ignoring it`)
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The kind, 'draft' or 'amendment', for an attribute value, or null when
|
|
42
|
+
// mode is null. An unrecognized value calls warn and yields the default.
|
|
43
|
+
function thirdLevelKind (value, mode, warn) {
|
|
44
|
+
if (!mode) return null
|
|
45
|
+
if (value === undefined || value === null || value === false) return DEFAULT_KIND
|
|
46
|
+
value = String(value)
|
|
47
|
+
if (KINDS.includes(value)) return value
|
|
48
|
+
if (warn) warn(`rhrev-third-level-kind must be draft or amendment, got '${value}'; using ${DEFAULT_KIND}`)
|
|
49
|
+
return DEFAULT_KIND
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The revision a mark names, "1-3-2" for rhrev1-3-2, or null for any other
|
|
53
|
+
// attribute, including -all, -cover, and configuration keys sharing the
|
|
54
|
+
// prefix.
|
|
55
|
+
function revisionOf (key, prefix) {
|
|
56
|
+
if (!key.startsWith(prefix)) return null
|
|
57
|
+
const revision = key.substring(prefix.length)
|
|
58
|
+
return REVISION_RX.test(revision) ? revision : null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function revisionParts (revision) {
|
|
62
|
+
return revision.split('-').map(Number)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The comparable form of a revision. In draft kind an x-y revision is the
|
|
66
|
+
// release its x-y-z drafts lead up to, so it sorts after them; otherwise
|
|
67
|
+
// x-y sorts as x-y-0, before x-y-1.
|
|
68
|
+
function sortKey (revision, kind) {
|
|
69
|
+
const parts = revisionParts(revision)
|
|
70
|
+
return kind === 'draft' && parts.length === 2 ? parts.concat(Infinity) : parts
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function compareAscending (a, b) {
|
|
74
|
+
const len = Math.max(a.length, b.length)
|
|
75
|
+
for (let i = 0; i < len; i++) {
|
|
76
|
+
const x = i < a.length ? a[i] : 0
|
|
77
|
+
const y = i < b.length ? b[i] : 0
|
|
78
|
+
if (x !== y) return x < y ? -1 : 1
|
|
79
|
+
}
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A comparator ordering revisions newest first for the given kind.
|
|
84
|
+
function revisionComparator (kind) {
|
|
85
|
+
return (a, b) => compareAscending(sortKey(b, kind), sortKey(a, kind))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The x-y groups whose release mark appears beside marks of its own
|
|
89
|
+
// drafts, each as [release, drafts], from the revisions a document marks.
|
|
90
|
+
function mixedDraftGroups (revisions) {
|
|
91
|
+
const unique = [...new Set(revisions)]
|
|
92
|
+
return unique
|
|
93
|
+
.filter((revision) => revision.split('-').length === 2)
|
|
94
|
+
.map((release) => [release, unique
|
|
95
|
+
.filter((revision) => revision.startsWith(`${release}-`) && revision.split('-').length === 3)
|
|
96
|
+
.sort((a, b) => compareAscending(revisionParts(a), revisionParts(b)))])
|
|
97
|
+
.filter(([, drafts]) => drafts.length > 0)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The warning for one mixed group (mixedDraftGroups).
|
|
101
|
+
function mixedDraftMessage (prefix, release, drafts) {
|
|
102
|
+
return `${prefix}${release} marks the release that its drafts ${drafts.map((d) => prefix + d).join(', ')} lead up to; ignoring the ${prefix}${release} marks, mark each change with the draft that made it`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The revision an attribute names as a mark, or as an -all or -cover
|
|
106
|
+
// entry, or null.
|
|
107
|
+
function markedRevisionOf (key, prefix) {
|
|
108
|
+
return revisionOf(key.replace(/-(?:all|cover)$/, ''), prefix)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Calls fn(node, attributes) for every node that carries marks: the
|
|
112
|
+
// document, every block in it and in nested documents, and every table
|
|
113
|
+
// cell.
|
|
114
|
+
function eachMarkedNode (doc, fn) {
|
|
115
|
+
doc.findBy({ traverse_documents: true }, () => true).forEach((node) => {
|
|
116
|
+
fn(node, node.getAttributes() || {})
|
|
117
|
+
if (node.getContext() !== 'table') return
|
|
118
|
+
const rows = node.getRows()
|
|
119
|
+
;[].concat(rows.head.flat(), rows.foot.flat(), rows.body.flat()).forEach((cell) => fn(cell, cell.getAttributes() || {}))
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Every revision doc marks, through marks, -all, and -cover entries.
|
|
124
|
+
function markedRevisions (doc, prefix) {
|
|
125
|
+
const revisions = []
|
|
126
|
+
eachMarkedNode(doc, (node, attrs) => Object.keys(attrs).forEach((key) => {
|
|
127
|
+
const revision = markedRevisionOf(key, prefix)
|
|
128
|
+
if (revision) revisions.push(revision)
|
|
129
|
+
}))
|
|
130
|
+
return revisions
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Removes every mark, -all, and -cover entry of the given releases from
|
|
134
|
+
// doc, same as ThirdLevel.drop_mixed_release_marks on the Ruby side.
|
|
135
|
+
function dropReleaseMarks (doc, prefix, releases) {
|
|
136
|
+
if (!releases.length) return
|
|
137
|
+
eachMarkedNode(doc, (node, attrs) => Object.keys(attrs).forEach((key) => {
|
|
138
|
+
if (releases.includes(markedRevisionOf(key, prefix))) node.removeAttribute(key)
|
|
139
|
+
}))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The table group a revision belongs to: its first two levels in collapse
|
|
143
|
+
// mode, the revision itself otherwise.
|
|
144
|
+
function groupKey (revision, mode) {
|
|
145
|
+
if (mode !== 'collapse') return revision
|
|
146
|
+
const parts = revision.split('-')
|
|
147
|
+
return parts.length > 2 ? parts.slice(0, 2).join('-') : revision
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A table group's prevrev or prevrevdate value from a plain attribute
|
|
151
|
+
// object: the group's own attribute, or in collapse mode, the lowest
|
|
152
|
+
// third-level revision's attribute that is set.
|
|
153
|
+
function groupAttr (attrs, revision, suffix, mode) {
|
|
154
|
+
const own = attrs[`${revision}-${suffix}`]
|
|
155
|
+
if (own !== undefined && own !== null && own !== '') return own
|
|
156
|
+
if (mode !== 'collapse' || revision.split('-').length !== 2) return null
|
|
157
|
+
const rx = new RegExp(`^${revision}-(\\d+)-${suffix}$`)
|
|
158
|
+
let lowest = null
|
|
159
|
+
for (const key of Object.keys(attrs)) {
|
|
160
|
+
const m = rx.exec(key)
|
|
161
|
+
if (m && (lowest === null || Number(m[1]) < lowest[0])) lowest = [Number(m[1]), key]
|
|
162
|
+
}
|
|
163
|
+
return lowest ? attrs[lowest[1]] : null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Attribute keys with every mark first, in ascending revision order, so
|
|
167
|
+
// change texts merged into one collapse-mode entry follow revision order,
|
|
168
|
+
// not source order. Every other key keeps its place after them.
|
|
169
|
+
function sortMarkKeys (keys, prefix, kind = null) {
|
|
170
|
+
return keys
|
|
171
|
+
.map((key, index) => ({ key, index, revision: revisionOf(key, prefix) }))
|
|
172
|
+
.sort((a, b) => {
|
|
173
|
+
if (!a.revision !== !b.revision) return a.revision ? -1 : 1
|
|
174
|
+
const byRevision = a.revision ? compareAscending(sortKey(a.revision, kind), sortKey(b.revision, kind)) : 0
|
|
175
|
+
return byRevision || a.index - b.index
|
|
176
|
+
})
|
|
177
|
+
.map(({ key }) => key)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Decides which marks draw a change bar for the current revnumber.
|
|
181
|
+
class Matcher {
|
|
182
|
+
// ignored lists releases whose marks never match (mixedDraftGroups).
|
|
183
|
+
constructor (prefix, revnumber, mode, kind = DEFAULT_KIND, ignored = []) {
|
|
184
|
+
revnumber = String(revnumber)
|
|
185
|
+
this.prefix = prefix
|
|
186
|
+
this.kind = kind
|
|
187
|
+
this.ignored = ignored
|
|
188
|
+
this.exact = prefix + revnumber.replace(/\./g, '-')
|
|
189
|
+
this.mode = null
|
|
190
|
+
if (mode && /^\d+(?:\.\d+)+$/.test(revnumber)) {
|
|
191
|
+
const parts = revnumber.split('.').map(Number)
|
|
192
|
+
this.mode = mode
|
|
193
|
+
this.base = parts.slice(0, 2)
|
|
194
|
+
this.currentZ = parts[2]
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
isMark (key) {
|
|
199
|
+
if (!this.mode) return key === this.exact
|
|
200
|
+
const revision = revisionOf(key, this.prefix)
|
|
201
|
+
if (!revision || this.ignored.includes(revision)) return false
|
|
202
|
+
const parts = revisionParts(revision)
|
|
203
|
+
if (parts[0] !== this.base[0] || parts[1] !== this.base[1]) return false
|
|
204
|
+
if (this.currentZ === undefined) return true
|
|
205
|
+
return parts.length > 2 ? parts[2] <= this.currentZ : this.kind !== 'draft'
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
match (attrs) {
|
|
209
|
+
return !!attrs && Object.keys(attrs).some((key) => this.isMark(key))
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
thirdLevelMode,
|
|
215
|
+
thirdLevelKind,
|
|
216
|
+
revisionOf,
|
|
217
|
+
groupKey,
|
|
218
|
+
groupAttr,
|
|
219
|
+
sortMarkKeys,
|
|
220
|
+
revisionComparator,
|
|
221
|
+
mixedDraftGroups,
|
|
222
|
+
mixedDraftMessage,
|
|
223
|
+
markedRevisionOf,
|
|
224
|
+
markedRevisions,
|
|
225
|
+
dropReleaseMarks,
|
|
226
|
+
Matcher,
|
|
227
|
+
REVISION_RX
|
|
228
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "asciidoctor-rhrev",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Revision history block macro extension for Asciidoctor.js and Antora (HTML5 backend)",
|
|
5
5
|
"main": "antora-extension/antora-rhrev-asciidoc.js",
|
|
6
6
|
"scripts": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"antora-extension/antora-rhrev-html.js",
|
|
29
29
|
"antora-extension/antora-rhrev-placeholder.js",
|
|
30
30
|
"antora-extension/cell-text-marks.js",
|
|
31
|
+
"antora-extension/third-level.js",
|
|
31
32
|
"README.adoc",
|
|
32
33
|
"LICENSE"
|
|
33
34
|
]
|