asciidoctor-rhrev 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- It supports several converters/backends:
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
- == Features
24
+ include::doc/modules/ROOT/pages/introduction.adoc[tag=why]
25
25
 
26
- * *Automatic revision tracking* with version-based grouping
27
- * *Dynamic version labels* using `:version-label:` attribute
28
- * *Localization support* for multiple languages
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
- See link:USAGE.adoc[] for complete documentation including:
42
+ Full documentation is published at https://baiyibai-antora.gitlab.io.
96
43
 
97
- * Configuration options
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
- // Helper functions
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.replace('-', '.'))
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 - if entry is from a different page, include page path
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
- // Cross-page link: convert "modules/ROOT/pages/chapter-1.adoc" to "chapter-1.html#anchor"
374
- const pagePath = entry.page.replace('modules/ROOT/pages/', '').replace('.adoc', '.html')
375
- linkHref = `${pagePath}#${anchor}`
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 global catalog from Antora pipeline extension
420
- // Otherwise fall back to collecting from current document only
421
- let catalog = global.antoraRhrevCatalog
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
- // Build global catalog by scanning all pages
37
- const globalCatalog = buildGlobalCatalog(pageFiles, logger, quiet)
38
-
39
- if (!quiet) {
40
- logger.debug(`rhrev-extension: Global catalog has ${Object.keys(globalCatalog.entries).length} revisions`)
41
- logger.debug(`rhrev-extension: Total block entries: ${Object.values(globalCatalog.entries).reduce((sum, arr) => sum + arr.length, 0)}`)
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
- // Store the catalog globally so the Asciidoctor extension can access it
45
- // This allows proper backend-specific rendering (HTML vs PDF)
46
- global.antoraRhrevCatalog = globalCatalog
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('rhrev-extension: Global catalog stored for Asciidoctor extension')
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 buildGlobalCatalog(pageFiles, logger, quiet) {
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 (-all)
91
- for (let major = 0; major <= 9; major++) {
92
- for (let minor = 0; minor <= 9; minor++) {
93
- const revision = `${major}-${minor}`
94
- const prevrevAttr = `${revision}-prevrev`
95
-
96
- if (componentAttrs[prevrevAttr]) {
97
- const allAttrName = `${prefix}${revision}-all`
98
- const allValue = componentAttrs[allAttrName]
99
-
100
- if (allValue) {
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
- // Match block attribute lines: [rhrev1-1="text"]
122
- const blockAttrRx = new RegExp(`\\[${prefix}([0-9]+-[0-9]+)="([^"]+)"\\]`, 'g')
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(content)) !== null) {
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 = content.substring(matchEnd, matchEnd + 200)
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((a, b) => {
261
- const [aMajor, aMinor] = a.split('-').map(Number)
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.replace('-', '.'))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "asciidoctor-rhrev",
3
- "version": "1.0.1",
3
+ "version": "1.2.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": {