asciidoctor-rhrev 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.adoc
CHANGED
|
@@ -4,7 +4,7 @@ rhrev is a comprehensive revision history tracking system for the AsciiDoc and A
|
|
|
4
4
|
|
|
5
5
|
https://baiyibai-antora.gitlab.io
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
== Supported Backends
|
|
8
8
|
|
|
9
9
|
* asciidoctor-pdf
|
|
10
10
|
+
|
|
@@ -21,18 +21,11 @@ image::resources/nodejs-html5-output.png[width=50%]
|
|
|
21
21
|
+
|
|
22
22
|
* Antora-Assembler to asciidoctor-pdf
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
include::doc/modules/ROOT/pages/introduction.adoc[tag=why]
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* *Export to AsciiDoc* for inclusion in other documents
|
|
30
|
-
* *Flexible placement* (auto or manual via macro)
|
|
31
|
-
* *Extensive customization* options for styling and formatting
|
|
32
|
-
* [PDF] *Clickable page references* that link to actual document locations
|
|
33
|
-
* [PDF] *Flexible xrefstyle support* (full, short, basic) for all block types
|
|
34
|
-
* [PDF] *Custom captions* for Examples, Tables, Listings, and Figures
|
|
35
|
-
* *FAST*
|
|
26
|
+
include::doc/modules/ROOT/pages/introduction.adoc[tag=features]
|
|
27
|
+
|
|
28
|
+
include::doc/modules/ROOT/pages/introduction.adoc[tag=quick-start]
|
|
36
29
|
|
|
37
30
|
== Inspiration
|
|
38
31
|
|
|
@@ -44,62 +37,11 @@ This extension draws inspiration from:
|
|
|
44
37
|
|
|
45
38
|
The goal was to bring similar automated revision tracking capabilities to the Asciidoctor ecosystem, with the added benefit of clickable page references and flexible customization options tailored for PDF technical documentation.
|
|
46
39
|
|
|
47
|
-
== Quick Start
|
|
48
|
-
|
|
49
|
-
=== Installation
|
|
50
|
-
|
|
51
|
-
The asciidoctor-pdf-rhrev extension is available as a Ruby Gem.
|
|
52
|
-
|
|
53
|
-
https://rubygems.org/gems/asciidoctor-pdf-rhrev[^]
|
|
54
|
-
|
|
55
|
-
Complete these steps to use:
|
|
56
|
-
|
|
57
|
-
. Install the required Gems:
|
|
58
|
-
+
|
|
59
|
-
[source,bash]
|
|
60
|
-
----
|
|
61
|
-
gem install asciidoctor
|
|
62
|
-
gem install asciidoctor-pdf
|
|
63
|
-
gem install asciidoctor-pdf-rhrev
|
|
64
|
-
----
|
|
65
|
-
+
|
|
66
|
-
. Render your document:
|
|
67
|
-
+
|
|
68
|
-
[source,bash]
|
|
69
|
-
----
|
|
70
|
-
asciidoctor-pdf -r asciidoctor-pdf-rhrev document.adoc
|
|
71
|
-
----
|
|
72
|
-
|
|
73
|
-
=== Basic Usage
|
|
74
|
-
|
|
75
|
-
[source,asciidoc]
|
|
76
|
-
----
|
|
77
|
-
= My Document
|
|
78
|
-
:doctype: book
|
|
79
|
-
:revnumber: 1.1
|
|
80
|
-
:rhrev:
|
|
81
|
-
:version-label: Version
|
|
82
|
-
// 1.0 Metadata
|
|
83
|
-
:1-1-prevrev: 1.0
|
|
84
|
-
:1-1-prevrevdate: 2024-02-01
|
|
85
|
-
:rhrev1-1-all: Feature update
|
|
86
|
-
|
|
87
|
-
[rhrev1-1="Updated introduction"]
|
|
88
|
-
== Introduction
|
|
89
|
-
|
|
90
|
-
Content here...
|
|
91
|
-
----
|
|
92
|
-
|
|
93
40
|
== Documentation
|
|
94
41
|
|
|
95
|
-
|
|
42
|
+
Full documentation is published at https://baiyibai-antora.gitlab.io.
|
|
96
43
|
|
|
97
|
-
|
|
98
|
-
* Advanced features
|
|
99
|
-
* Localization
|
|
100
|
-
* Export functionality
|
|
101
|
-
* xrefstyle support
|
|
102
|
-
* Examples and use cases
|
|
44
|
+
For configuration details, see link:doc/modules/ROOT/pages/usage.adoc[].
|
|
103
45
|
|
|
104
46
|
== Testing
|
|
105
47
|
|
|
@@ -116,7 +58,7 @@ This will generate PDFs for all test cases in the `tests/` directory.
|
|
|
116
58
|
|
|
117
59
|
[source,bash]
|
|
118
60
|
----
|
|
119
|
-
gem build asciidoctor-pdf-rhrev.gemspec
|
|
61
|
+
gem build asciidoctor-pdf-rhrev.gemspec
|
|
120
62
|
gem install ./asciidoctor-pdf-rhrev-1.0.0.gem
|
|
121
63
|
----
|
|
122
64
|
|
|
@@ -130,7 +72,7 @@ Contributions welcome! Please ensure all tests pass before submitting changes.
|
|
|
130
72
|
|
|
131
73
|
== Copyright
|
|
132
74
|
|
|
133
|
-
Copyright (C) 2025-present 白一百 baiyibai
|
|
75
|
+
Copyright (C) 2025-present 白一百 baiyibai
|
|
134
76
|
|
|
135
77
|
* The image _The «Public Domain Mark» icon from Creative Commons with transparent background._ is in the public domain and taken from: +
|
|
136
78
|
https://commons.wikimedia.org/wiki/File:Cc-public_domain_mark.svg[^]
|
|
@@ -39,23 +39,26 @@ class RevisionCatalog {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
sortedRevisions() {
|
|
42
|
-
// Collect all unique revisions from entries and allEntries
|
|
43
42
|
const revisions = new Set([
|
|
44
43
|
...Object.keys(this.entries),
|
|
45
44
|
...Object.keys(this.allEntries)
|
|
46
45
|
])
|
|
47
|
-
|
|
48
|
-
return Array.from(revisions).sort((a, b) => {
|
|
49
|
-
const [aMajor, aMinor] = a.split('-').map(Number)
|
|
50
|
-
const [bMajor, bMinor] = b.split('-').map(Number)
|
|
51
|
-
|
|
52
|
-
if (aMajor !== bMajor) return bMajor - aMajor
|
|
53
|
-
return bMinor - aMinor
|
|
54
|
-
})
|
|
46
|
+
return Array.from(revisions).sort(compareRevisions)
|
|
55
47
|
}
|
|
56
48
|
}
|
|
57
49
|
|
|
58
|
-
|
|
50
|
+
function compareRevisions(a, b) {
|
|
51
|
+
const aParts = a.split('-').map(Number)
|
|
52
|
+
const bParts = b.split('-').map(Number)
|
|
53
|
+
const len = Math.max(aParts.length, bParts.length)
|
|
54
|
+
for (let i = 0; i < len; i++) {
|
|
55
|
+
const aVal = i < aParts.length ? aParts[i] : 0
|
|
56
|
+
const bVal = i < bParts.length ? bParts[i] : 0
|
|
57
|
+
if (aVal !== bVal) return bVal - aVal
|
|
58
|
+
}
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
|
|
59
62
|
function isInitialRelease(revnumber) {
|
|
60
63
|
return revnumber === '1.0' || revnumber === '1'
|
|
61
64
|
}
|
|
@@ -328,7 +331,7 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
328
331
|
const prevrevAttr = `${revision}-prevrev`
|
|
329
332
|
const prevrevdateAttr = `${revision}-prevrevdate`
|
|
330
333
|
const revisionLabel = String(doc.getAttribute('version-label') || 'Revision')
|
|
331
|
-
const prevTitle = String(doc.getAttribute(prevrevAttr) || revision.
|
|
334
|
+
const prevTitle = String(doc.getAttribute(prevrevAttr) || revision.replaceAll('-', '.'))
|
|
332
335
|
const prevDate = String(doc.getAttribute(prevrevdateAttr) || '')
|
|
333
336
|
const prevRevText = formatPrevRev(prevTitle, prevDate, doc)
|
|
334
337
|
|
|
@@ -366,15 +369,43 @@ function generateHtmlTable(doc, catalog, currentPage = '') {
|
|
|
366
369
|
).join(' ')
|
|
367
370
|
|
|
368
371
|
html.push('<tr>')
|
|
369
|
-
|
|
370
|
-
// Build link
|
|
372
|
+
|
|
373
|
+
// Build link: local anchor if the section lives on the current page (directly or via include)
|
|
371
374
|
let linkHref = `#${anchor}`
|
|
372
375
|
if (entry.page && entry.page !== currentPage) {
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
+
const entryComponent = entry.component
|
|
377
|
+
const entryVersion = entry.version
|
|
378
|
+
const entryModule = entry.module || 'ROOT'
|
|
379
|
+
const entryPageStem = entry.stem ||
|
|
380
|
+
entry.page.replace(/^modules\/[^/]+\/pages\//, '').replace(/\.[^.]+$/, '')
|
|
381
|
+
|
|
382
|
+
const currentComponent = doc.getAttribute('page-component-name')
|
|
383
|
+
const currentVersion = doc.getAttribute('page-version')
|
|
384
|
+
const currentModule = doc.getAttribute('page-module') || 'ROOT'
|
|
385
|
+
const currentDocname = doc.getAttribute('docname') || ''
|
|
386
|
+
|
|
387
|
+
// Check if the source page is included into the current page.
|
|
388
|
+
// Key format matches includedBy builder: "component/module/filename"
|
|
389
|
+
const includedBy = global.antoraRhrevCatalog?.includedBy
|
|
390
|
+
const entryPageKey = `${entryComponent}/${entryModule}/${entry.page}`
|
|
391
|
+
const currentPageFile = currentDocname ? `${currentDocname}.adoc` : null
|
|
392
|
+
const isIncludedLocally = currentPageFile && (includedBy?.[entryPageKey] || []).some(inc => {
|
|
393
|
+
if (inc.component !== currentComponent || inc.module !== currentModule) return false
|
|
394
|
+
if (inc.page !== currentPageFile) return false
|
|
395
|
+
if (inc.tag === null) return true
|
|
396
|
+
return entry.containingTags?.includes(inc.tag)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
if (isIncludedLocally) {
|
|
400
|
+
linkHref = `#${anchor}`
|
|
401
|
+
} else if (entryComponent === currentComponent && entryVersion === currentVersion && entryModule === 'ROOT') {
|
|
402
|
+
linkHref = `${entryPageStem}.html#${anchor}`
|
|
403
|
+
} else {
|
|
404
|
+
const modulePart = entryModule !== 'ROOT' ? `${entryModule}/` : ''
|
|
405
|
+
linkHref = `../../${entryComponent}/${entryVersion}/${modulePart}${entryPageStem}.html#${anchor}`
|
|
406
|
+
}
|
|
376
407
|
}
|
|
377
|
-
|
|
408
|
+
|
|
378
409
|
html.push(`<td><a href="${linkHref}">${title}</a></td>`)
|
|
379
410
|
html.push(`<td>${formatChangeAsHtmlList(entry.change)}</td>`)
|
|
380
411
|
html.push('</tr>')
|
|
@@ -416,23 +447,34 @@ module.exports.register = function (registry, context = {}) {
|
|
|
416
447
|
}
|
|
417
448
|
}
|
|
418
449
|
|
|
419
|
-
// Try to use
|
|
420
|
-
//
|
|
421
|
-
let catalog
|
|
422
|
-
|
|
450
|
+
// Try to use catalog from Antora pipeline extension (component-scoped by default).
|
|
451
|
+
// global.antoraRhrevCatalog is the site-wide catalog; .byComponent holds per-component catalogs.
|
|
452
|
+
let catalog
|
|
453
|
+
const rhrevCatalog = global.antoraRhrevCatalog
|
|
454
|
+
if (rhrevCatalog && rhrevCatalog.byComponent) {
|
|
455
|
+
const rhrevScope = doc.getAttribute('rhrev-scope') || 'component'
|
|
456
|
+
if (rhrevScope === 'site') {
|
|
457
|
+
catalog = rhrevCatalog
|
|
458
|
+
} else {
|
|
459
|
+
const componentName = doc.getAttribute('page-component-name') || '__unknown__'
|
|
460
|
+
catalog = rhrevCatalog.byComponent[componentName] || rhrevCatalog
|
|
461
|
+
}
|
|
462
|
+
} else if (rhrevCatalog && rhrevCatalog.entries) {
|
|
463
|
+
catalog = rhrevCatalog
|
|
464
|
+
}
|
|
465
|
+
|
|
423
466
|
if (!catalog || !catalog.entries) {
|
|
424
|
-
// Fallback: collect from current document only (standalone mode)
|
|
425
467
|
catalog = collectAllEntries(doc)
|
|
426
468
|
}
|
|
427
|
-
|
|
469
|
+
|
|
428
470
|
if (catalog.sortedRevisions().length === 0) {
|
|
429
471
|
return self.createBlock(parent, 'paragraph', '')
|
|
430
472
|
}
|
|
431
|
-
|
|
473
|
+
|
|
432
474
|
// Get current page path for cross-page link resolution
|
|
433
|
-
const currentPage = doc.getAttribute('page-component-src-relative') ||
|
|
475
|
+
const currentPage = doc.getAttribute('page-component-src-relative') ||
|
|
434
476
|
doc.getAttribute('docfile') || ''
|
|
435
|
-
|
|
477
|
+
|
|
436
478
|
const html = generateHtmlOutput(doc, catalog, currentPage)
|
|
437
479
|
return self.createBlock(parent, 'pass', html)
|
|
438
480
|
})
|
|
@@ -21,40 +21,105 @@ module.exports.register = function ({ config: extensionConfig = {} }) {
|
|
|
21
21
|
// Hook into contentClassified - we can still modify raw content
|
|
22
22
|
this.on('contentClassified', ({ contentCatalog, siteAsciiDocConfig }) => {
|
|
23
23
|
if (!contentCatalog) return
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
if (!quiet) {
|
|
26
26
|
logger.debug('rhrev-extension: contentClassified event fired')
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
// Find all pages in the catalog
|
|
28
|
+
|
|
30
29
|
const pageFiles = contentCatalog.findBy({ family: 'page' }) || []
|
|
31
|
-
|
|
30
|
+
|
|
32
31
|
if (!quiet) {
|
|
33
32
|
logger.debug(`rhrev-extension: Found ${pageFiles.length} page files`)
|
|
34
33
|
}
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
|
|
35
|
+
// Group pages by Antora component name
|
|
36
|
+
const pagesByComponent = {}
|
|
37
|
+
for (const file of pageFiles) {
|
|
38
|
+
const componentName = file.src?.component || '__unknown__'
|
|
39
|
+
if (!pagesByComponent[componentName]) pagesByComponent[componentName] = []
|
|
40
|
+
pagesByComponent[componentName].push(file)
|
|
42
41
|
}
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
|
|
43
|
+
// Build a catalog for each component
|
|
44
|
+
const byComponent = {}
|
|
45
|
+
for (const [componentName, files] of Object.entries(pagesByComponent)) {
|
|
46
|
+
byComponent[componentName] = buildComponentCatalog(files, logger, quiet)
|
|
47
|
+
if (!quiet) {
|
|
48
|
+
logger.debug(`rhrev-extension: Component '${componentName}' catalog has ${Object.keys(byComponent[componentName].entries).length} revisions`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Build a merged site-wide catalog (= backward-compatible top-level catalog)
|
|
53
|
+
const site = mergeCatalogs(Object.values(byComponent))
|
|
54
|
+
|
|
55
|
+
// Build includedBy map: scan each page for cross-component include directives.
|
|
56
|
+
// Maps "srcComponent/modules/mod/pages/file.adoc" → [{component, version, page, tag}]
|
|
57
|
+
// Used by the Asciidoctor blockMacro to emit local anchors for included sections.
|
|
58
|
+
const includedBy = {}
|
|
59
|
+
const crossIncludeRx = /^include::([^:]+):([^:]+):page\$([^\[]+\.adoc)\[([^\]]*)\]/gm
|
|
60
|
+
for (const targetFiles of Object.values(pagesByComponent)) {
|
|
61
|
+
for (const targetFile of targetFiles) {
|
|
62
|
+
if (!targetFile.contents) continue
|
|
63
|
+
const content = targetFile.contents.toString()
|
|
64
|
+
let m
|
|
65
|
+
crossIncludeRx.lastIndex = 0
|
|
66
|
+
while ((m = crossIncludeRx.exec(content)) !== null) {
|
|
67
|
+
const srcComp = m[1], srcMod = m[2], srcPageFile = m[3], attrs = m[4]
|
|
68
|
+
const tagM = attrs.match(/\btag=([^,\]]+)/)
|
|
69
|
+
const tag = tagM ? tagM[1].trim() : null
|
|
70
|
+
// Key format: "component/module/filename" — matches entry.component + '/' + entry.module + '/' + entry.page
|
|
71
|
+
// (file.src.relative in Antora is just the filename, e.g. "introduction.adoc", not the full path)
|
|
72
|
+
const key = `${srcComp}/${srcMod}/${srcPageFile}`
|
|
73
|
+
if (!includedBy[key]) includedBy[key] = []
|
|
74
|
+
includedBy[key].push({
|
|
75
|
+
component: targetFile.src?.component,
|
|
76
|
+
module: targetFile.src?.module || 'ROOT',
|
|
77
|
+
page: targetFile.src?.relative, // e.g. "rhrev.adoc"
|
|
78
|
+
tag
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Store: top-level is site-wide (backward compat with installed 1.0.1 npm package),
|
|
85
|
+
// byComponent is added for component-scoped lookup in 1.0.2+ asciidoc extension.
|
|
86
|
+
global.antoraRhrevCatalog = site
|
|
87
|
+
global.antoraRhrevCatalog.byComponent = byComponent
|
|
88
|
+
global.antoraRhrevCatalog.includedBy = includedBy
|
|
89
|
+
global.antoraRhrevCatalog.componentFileMeta = Object.fromEntries(
|
|
90
|
+
Object.entries(byComponent).map(([name, cat]) => [name, cat.fileMeta || {}])
|
|
91
|
+
)
|
|
92
|
+
|
|
48
93
|
if (!quiet) {
|
|
49
|
-
logger.debug(
|
|
94
|
+
logger.debug(`rhrev-extension: Catalogs stored — ${Object.keys(byComponent).length} component(s) + site-wide`)
|
|
50
95
|
}
|
|
51
96
|
})
|
|
52
97
|
}
|
|
53
98
|
|
|
54
|
-
function
|
|
99
|
+
function mergeCatalogs(catalogs) {
|
|
100
|
+
const merged = {
|
|
101
|
+
entries: {},
|
|
102
|
+
allEntries: {},
|
|
103
|
+
sortedRevisions() {
|
|
104
|
+
const all = [...Object.keys(this.entries), ...Object.keys(this.allEntries)]
|
|
105
|
+
return [...new Set(all)].sort(compareRevisions)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const catalog of catalogs) {
|
|
109
|
+
for (const [revision, entries] of Object.entries(catalog.entries)) {
|
|
110
|
+
if (!merged.entries[revision]) merged.entries[revision] = []
|
|
111
|
+
merged.entries[revision].push(...entries)
|
|
112
|
+
}
|
|
113
|
+
Object.assign(merged.allEntries, catalog.allEntries)
|
|
114
|
+
}
|
|
115
|
+
return merged
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildComponentCatalog(pageFiles, logger, quiet) {
|
|
55
119
|
const catalog = {
|
|
56
120
|
entries: {}, // { '1-1': [...], '1-2': [...] }
|
|
57
121
|
allEntries: {}, // { '1-1': 'text', '1-2': 'text' }
|
|
122
|
+
fileMeta: {}, // { 'modules/ROOT/pages/file.adoc': { tagName: {startLine, endLine} } }
|
|
58
123
|
sequence: 0,
|
|
59
124
|
|
|
60
125
|
// Add sortedRevisions method for compatibility with Asciidoctor extension
|
|
@@ -64,46 +129,33 @@ function buildGlobalCatalog(pageFiles, logger, quiet) {
|
|
|
64
129
|
...Object.keys(this.allEntries)
|
|
65
130
|
]
|
|
66
131
|
const uniqueRevisions = [...new Set(allRevisions)]
|
|
67
|
-
|
|
68
|
-
return uniqueRevisions.sort((a, b) => {
|
|
69
|
-
const [aMajor, aMinor] = a.split('-').map(Number)
|
|
70
|
-
const [bMajor, bMinor] = b.split('-').map(Number)
|
|
71
|
-
|
|
72
|
-
if (aMajor !== bMajor) return bMajor - aMajor
|
|
73
|
-
return bMinor - aMinor
|
|
74
|
-
})
|
|
132
|
+
return uniqueRevisions.sort(compareRevisions)
|
|
75
133
|
}
|
|
76
134
|
}
|
|
77
|
-
|
|
135
|
+
|
|
78
136
|
// Get component attributes from first file's src.origin (antora.yml)
|
|
79
137
|
const firstFile = pageFiles[0]
|
|
80
138
|
if (!firstFile) return catalog
|
|
81
|
-
|
|
139
|
+
|
|
82
140
|
// Component attributes are in src.origin.descriptor.asciidoc.attributes
|
|
83
141
|
const componentAttrs = firstFile.src?.origin?.descriptor?.asciidoc?.attributes || {}
|
|
84
142
|
const prefix = componentAttrs.revhistoryprefix || 'rhrev'
|
|
85
|
-
|
|
143
|
+
|
|
86
144
|
if (!quiet) {
|
|
87
145
|
logger.debug(`rhrev-extension: Using prefix '${prefix}'`)
|
|
88
146
|
}
|
|
89
|
-
|
|
90
|
-
// Collect document-level entries (-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
catalog.allEntries[revision] = allValue
|
|
102
|
-
|
|
103
|
-
if (!quiet) {
|
|
104
|
-
logger.debug(`rhrev-extension: Found -all entry for ${revision}`)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
147
|
+
|
|
148
|
+
// Collect document-level entries by scanning for *-prevrev attributes (supports N-part versions)
|
|
149
|
+
const prevrevRx = /^([0-9]+(?:-[0-9]+)+)-prevrev$/
|
|
150
|
+
for (const attrName of Object.keys(componentAttrs)) {
|
|
151
|
+
const m = attrName.match(prevrevRx)
|
|
152
|
+
if (!m) continue
|
|
153
|
+
const revision = m[1]
|
|
154
|
+
const allValue = componentAttrs[`${prefix}${revision}-all`]
|
|
155
|
+
if (allValue) {
|
|
156
|
+
catalog.allEntries[revision] = allValue
|
|
157
|
+
if (!quiet) {
|
|
158
|
+
logger.debug(`rhrev-extension: Found -all entry for ${revision}`)
|
|
107
159
|
}
|
|
108
160
|
}
|
|
109
161
|
}
|
|
@@ -118,17 +170,35 @@ function buildGlobalCatalog(pageFiles, logger, quiet) {
|
|
|
118
170
|
logger.debug(`rhrev-extension: Scanning ${file.src?.relative} for block attributes`)
|
|
119
171
|
}
|
|
120
172
|
|
|
121
|
-
//
|
|
122
|
-
const
|
|
173
|
+
// Strip delimited block content (source, literal, listing) to avoid matching examples
|
|
174
|
+
const strippedContent = stripVerbatimBlocks(content)
|
|
175
|
+
|
|
176
|
+
// Compute tag regions in the stripped content (for cross-component include detection)
|
|
177
|
+
const fileTagBoundaries = {}
|
|
178
|
+
strippedContent.split('\n').forEach((line, lineNum) => {
|
|
179
|
+
const startM = line.match(/^\/\/ tag::([^\[\]]+)\[\]$/)
|
|
180
|
+
if (startM) fileTagBoundaries[startM[1]] = { startLine: lineNum }
|
|
181
|
+
const endM = line.match(/^\/\/ end::([^\[\]]+)\[\]$/)
|
|
182
|
+
if (endM && fileTagBoundaries[endM[1]]) fileTagBoundaries[endM[1]].endLine = lineNum
|
|
183
|
+
})
|
|
184
|
+
catalog.fileMeta[file.src?.relative || file.path] = fileTagBoundaries
|
|
185
|
+
|
|
186
|
+
// Match block attribute lines: [rhrev1-1="text"] or [rhrev1-0-1="text"] (N-part versions)
|
|
187
|
+
const blockAttrRx = new RegExp(`\\[${prefix}([0-9]+(?:-[0-9]+)+)="([^"]+)"\\]`, 'g')
|
|
123
188
|
let match
|
|
124
|
-
|
|
125
|
-
while ((match = blockAttrRx.exec(
|
|
189
|
+
|
|
190
|
+
while ((match = blockAttrRx.exec(strippedContent)) !== null) {
|
|
126
191
|
const revision = match[1]
|
|
127
192
|
const changeText = match[2]
|
|
193
|
+
const matchLine = strippedContent.substring(0, match.index).split('\n').length - 1
|
|
194
|
+
const containingTags = Object.entries(fileTagBoundaries)
|
|
195
|
+
.filter(([, b]) => b.startLine !== undefined && b.endLine !== undefined &&
|
|
196
|
+
matchLine > b.startLine && matchLine < b.endLine)
|
|
197
|
+
.map(([name]) => name)
|
|
128
198
|
|
|
129
199
|
// Look for anchor/title IMMEDIATELY after the attribute (within next 200 chars)
|
|
130
200
|
const matchEnd = match.index + match[0].length
|
|
131
|
-
const nextChunk =
|
|
201
|
+
const nextChunk = strippedContent.substring(matchEnd, matchEnd + 200)
|
|
132
202
|
|
|
133
203
|
// Look for [#anchor], .Title, or === Section in that order
|
|
134
204
|
const anchorMatch = nextChunk.match(/^\s*\[#([^\]]+)\]/)
|
|
@@ -162,13 +232,19 @@ function buildGlobalCatalog(pageFiles, logger, quiet) {
|
|
|
162
232
|
if (!catalog.entries[revision]) {
|
|
163
233
|
catalog.entries[revision] = []
|
|
164
234
|
}
|
|
165
|
-
|
|
235
|
+
|
|
166
236
|
catalog.entries[revision].push({
|
|
167
237
|
anchor,
|
|
168
238
|
change: changeText,
|
|
169
239
|
title,
|
|
170
240
|
sequence: catalog.sequence++,
|
|
171
|
-
page: file.src?.relative || file.path
|
|
241
|
+
page: file.src?.relative || file.path,
|
|
242
|
+
component: file.src?.component,
|
|
243
|
+
version: file.src?.version,
|
|
244
|
+
module: file.src?.module || 'ROOT',
|
|
245
|
+
stem: file.src?.stem || (file.src?.basename || '').replace(/\.[^.]+$/, ''),
|
|
246
|
+
matchLine,
|
|
247
|
+
containingTags
|
|
172
248
|
})
|
|
173
249
|
|
|
174
250
|
if (!quiet) {
|
|
@@ -257,18 +333,12 @@ function generateRevisionHistoryHtml(file, catalog, logger, quiet) {
|
|
|
257
333
|
]
|
|
258
334
|
const uniqueRevisions = [...new Set(allRevisions)]
|
|
259
335
|
|
|
260
|
-
const sortedRevisions = uniqueRevisions.sort(
|
|
261
|
-
|
|
262
|
-
const [bMajor, bMinor] = b.split('-').map(Number)
|
|
263
|
-
|
|
264
|
-
if (aMajor !== bMajor) return bMajor - aMajor
|
|
265
|
-
return bMinor - aMinor
|
|
266
|
-
})
|
|
267
|
-
|
|
336
|
+
const sortedRevisions = uniqueRevisions.sort(compareRevisions)
|
|
337
|
+
|
|
268
338
|
// Generate revision rows
|
|
269
339
|
sortedRevisions.forEach(revision => {
|
|
270
340
|
const prevrevAttr = `${revision}-prevrev`
|
|
271
|
-
const prevTitle = String(attrs[prevrevAttr] || revision.
|
|
341
|
+
const prevTitle = String(attrs[prevrevAttr] || revision.replaceAll('-', '.'))
|
|
272
342
|
const prevrevdateAttr = `${revision}-prevrevdate`
|
|
273
343
|
const prevDate = attrs[prevrevdateAttr] || ''
|
|
274
344
|
|
|
@@ -329,6 +399,33 @@ function generateRevisionHistoryHtml(file, catalog, logger, quiet) {
|
|
|
329
399
|
return html.join('\n')
|
|
330
400
|
}
|
|
331
401
|
|
|
402
|
+
function stripVerbatimBlocks(content) {
|
|
403
|
+
const lines = content.split('\n')
|
|
404
|
+
const out = []
|
|
405
|
+
let depth = 0
|
|
406
|
+
for (const line of lines) {
|
|
407
|
+
if (/^-{4,}$|^\.{4,}$|^\+{4,}$/.test(line)) {
|
|
408
|
+
depth = depth > 0 ? depth - 1 : depth + 1
|
|
409
|
+
out.push(line)
|
|
410
|
+
} else if (depth === 0) {
|
|
411
|
+
out.push(line)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return out.join('\n')
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function compareRevisions(a, b) {
|
|
418
|
+
const aParts = a.split('-').map(Number)
|
|
419
|
+
const bParts = b.split('-').map(Number)
|
|
420
|
+
const len = Math.max(aParts.length, bParts.length)
|
|
421
|
+
for (let i = 0; i < len; i++) {
|
|
422
|
+
const aVal = i < aParts.length ? aParts[i] : 0
|
|
423
|
+
const bVal = i < bParts.length ? bParts[i] : 0
|
|
424
|
+
if (aVal !== bVal) return bVal - aVal
|
|
425
|
+
}
|
|
426
|
+
return 0
|
|
427
|
+
}
|
|
428
|
+
|
|
332
429
|
function formatChangeAsHtmlList(text) {
|
|
333
430
|
if (!text) return ''
|
|
334
431
|
|
package/package.json
CHANGED