asciidoctor-rhrev 1.0.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.
- checksums.yaml +7 -0
- data/LICENSE +25 -0
- data/README.adoc +136 -0
- data/USAGE.adoc +653 -0
- data/lib/asciidoctor/rhrev/catalog.rb +92 -0
- data/lib/asciidoctor/rhrev/converter.rb +437 -0
- data/lib/asciidoctor/rhrev/exporter.rb +242 -0
- data/lib/asciidoctor/rhrev/helpers.rb +117 -0
- data/lib/asciidoctor/rhrev/processors.rb +48 -0
- data/lib/asciidoctor/rhrev/renderer.rb +697 -0
- data/lib/asciidoctor/rhrev/rhrev_html.rb +334 -0
- data/lib/asciidoctor/rhrev/rhrev_pdf.rb +7 -0
- data/lib/asciidoctor/rhrev.rb +34 -0
- data/lib/asciidoctor-rhrev.rb +3 -0
- metadata +69 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
module Asciidoctor
|
|
2
|
+
module PDF
|
|
3
|
+
module Rhrev
|
|
4
|
+
module Exporter
|
|
5
|
+
def should_export_to_file? doc
|
|
6
|
+
doc.attr?('rhrev-export-to-file') || doc.attr?('rhrev-adoc-output')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def export_to_adoc_file doc
|
|
10
|
+
# Prevent double export
|
|
11
|
+
return if @export_completed
|
|
12
|
+
@export_completed = true
|
|
13
|
+
|
|
14
|
+
export_attr = doc.attr('rhrev-export-to-file')
|
|
15
|
+
adoc_output = doc.attr('rhrev-adoc-output')
|
|
16
|
+
|
|
17
|
+
# Determine output filename
|
|
18
|
+
# rhrev-export-to-file can be: true (use rhrev-adoc-output or default), or a filename
|
|
19
|
+
if export_attr == true || export_attr == 'true' || export_attr == ''
|
|
20
|
+
output_file = adoc_output || 'revhistory.adoc'
|
|
21
|
+
else
|
|
22
|
+
output_file = export_attr || adoc_output || 'revhistory.adoc'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
return unless output_file
|
|
26
|
+
|
|
27
|
+
# Make path relative to document directory if not absolute
|
|
28
|
+
unless output_file.start_with?('/')
|
|
29
|
+
doc_dir = doc.attr('docdir') || Dir.pwd
|
|
30
|
+
output_file = File.join(doc_dir, output_file)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Create directory if it doesn't exist
|
|
34
|
+
output_dir = File.dirname(output_file)
|
|
35
|
+
FileUtils.mkdir_p(output_dir) unless output_dir == '.' || File.directory?(output_dir)
|
|
36
|
+
|
|
37
|
+
debug_log "Exporting revision history to: #{output_file}", doc
|
|
38
|
+
|
|
39
|
+
output = []
|
|
40
|
+
|
|
41
|
+
# Get column configuration
|
|
42
|
+
column_widths = doc.attr 'rhrev-table-column-width', '30,70'
|
|
43
|
+
|
|
44
|
+
output << "[cols='#{column_widths}']"
|
|
45
|
+
output << "|==="
|
|
46
|
+
|
|
47
|
+
# Get localization strings
|
|
48
|
+
page_label = doc.attr 'rhrev-localization-page', 'Page'
|
|
49
|
+
major_changes_text = doc.attr 'rhrev-localization-major-changes', 'Major changes since'
|
|
50
|
+
all_text = doc.attr 'rhrev-localization-all', 'All'
|
|
51
|
+
cover_text = doc.attr 'rhrev-localization-cover', 'Cover'
|
|
52
|
+
|
|
53
|
+
# Get export format (pagerhref or xref)
|
|
54
|
+
export_format = doc.attr 'rhrev-export-format', 'pagerhref'
|
|
55
|
+
|
|
56
|
+
# Process each revision
|
|
57
|
+
revision_history.sorted_revisions.each do |revision|
|
|
58
|
+
prevrev_attr = "#{revision}-prevrev"
|
|
59
|
+
prevrevdate_attr = "#{revision}-prevrevdate"
|
|
60
|
+
revision_label = doc.attr('version-label', 'Revision')
|
|
61
|
+
prev_title = doc.attr(prevrev_attr) || revision.tr('-', '.')
|
|
62
|
+
prev_date = doc.attr(prevrevdate_attr) || ""
|
|
63
|
+
prev_rev_text = format_prev_rev(prev_title, prev_date, doc)
|
|
64
|
+
|
|
65
|
+
# Build header text, avoiding "Revision Revision" repetition
|
|
66
|
+
if prev_title.strip.downcase.start_with?(revision_label.downcase)
|
|
67
|
+
header_text = "*#{major_changes_text} #{prev_rev_text}*"
|
|
68
|
+
else
|
|
69
|
+
header_text = "*#{major_changes_text} #{revision_label} #{prev_rev_text}*"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
output << ""
|
|
73
|
+
output << "a|*#{page_label}*"
|
|
74
|
+
output << "a|#{header_text}"
|
|
75
|
+
|
|
76
|
+
# All entry
|
|
77
|
+
if (all_change = revision_history.instance_variable_get(:@all_entries)[revision])
|
|
78
|
+
output << ""
|
|
79
|
+
output << "a|#{all_text}"
|
|
80
|
+
output << "a|"
|
|
81
|
+
export_change_text output, all_change, is_all_or_cover: true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Cover entry
|
|
85
|
+
if (cover_change = revision_history.instance_variable_get(:@cover_entries)[revision])
|
|
86
|
+
next if doc.backend.to_s == 'html5'
|
|
87
|
+
output << ""
|
|
88
|
+
output << "a|#{cover_text}"
|
|
89
|
+
output << "a|"
|
|
90
|
+
export_change_text output, cover_change, is_all_or_cover: true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Block entries - sort by sequence (source order in document)
|
|
94
|
+
entries = revision_history.entries[revision] || []
|
|
95
|
+
entries = entries.sort_by { |e| e[:sequence] || 0 }
|
|
96
|
+
|
|
97
|
+
entries.each do |entry|
|
|
98
|
+
page_num = entry[:dest] ? entry[:dest][:page] : '???'
|
|
99
|
+
|
|
100
|
+
# Convert anchor to portable xref format for Antora export
|
|
101
|
+
xref_anchor = convert_anchor_to_xref_for_export(entry[:anchor], doc)
|
|
102
|
+
|
|
103
|
+
output << ""
|
|
104
|
+
if export_format == 'pagerhref'
|
|
105
|
+
# Use pagerhref macro for manual mode imports
|
|
106
|
+
output << "a|pagerhref:#{xref_anchor}[]"
|
|
107
|
+
else
|
|
108
|
+
# Use xref syntax with page number
|
|
109
|
+
output << "a|xref:#{xref_anchor}[#{page_num}]"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Build description cell with xrefs based on rhrev-description-xrefstyle
|
|
113
|
+
output << build_export_description_xrefs(xref_anchor, doc, entry)
|
|
114
|
+
output << "" # Blank line before bullet list for AsciiDoc parsing
|
|
115
|
+
export_change_with_bullets output, entry[:change]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
output << "|==="
|
|
120
|
+
|
|
121
|
+
File.write(output_file, output.join("\n"))
|
|
122
|
+
debug_log("Exported revision history to #{output_file}", doc)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Convert Antora-style anchor to xref format for export
|
|
126
|
+
# e.g., "chapter-1:::section-1-1" -> "chapter-1.adoc#section-1-1"
|
|
127
|
+
def convert_anchor_to_xref_for_export anchor, doc = nil
|
|
128
|
+
return anchor unless anchor
|
|
129
|
+
|
|
130
|
+
# Skip transformation for standalone asciidoctor-pdf builds
|
|
131
|
+
return anchor if doc && !antora_build?(doc)
|
|
132
|
+
|
|
133
|
+
# Auto-detect separator (---- for xml_ids, ::: for default)
|
|
134
|
+
separator = if anchor.include?('----')
|
|
135
|
+
'----'
|
|
136
|
+
elsif anchor.include?(':::')
|
|
137
|
+
':::'
|
|
138
|
+
else
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
if separator
|
|
143
|
+
parts = anchor.split(separator, 2)
|
|
144
|
+
page_id = parts[0]
|
|
145
|
+
fragment = parts[1]
|
|
146
|
+
"#{page_id}.adoc##{fragment}"
|
|
147
|
+
else
|
|
148
|
+
# No separator - just a page reference
|
|
149
|
+
"#{anchor}.adoc"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Build description cell xrefs based on rhrev-description-xrefstyle
|
|
154
|
+
def build_export_description_xrefs xref_target, doc, entry = nil
|
|
155
|
+
xrefstyle_config = doc.attr 'rhrev-description-xrefstyle', ''
|
|
156
|
+
styles = xrefstyle_config.split(/\s+/).reject(&:empty?)
|
|
157
|
+
|
|
158
|
+
# Check if entry has reftext defined
|
|
159
|
+
has_reftext = entry && entry[:reftext] && !entry[:reftext].to_s.empty?
|
|
160
|
+
|
|
161
|
+
# Check if entry is numbered (sections with sectnum, or blocks with caption_number)
|
|
162
|
+
is_numbered = entry && (entry[:sectnum] || entry[:caption_number])
|
|
163
|
+
|
|
164
|
+
if styles.empty?
|
|
165
|
+
# No xrefstyle specified - use default (empty brackets)
|
|
166
|
+
"a|xref:#{xref_target}[]"
|
|
167
|
+
elsif styles.length == 1
|
|
168
|
+
# Single xrefstyle
|
|
169
|
+
"a|xref:#{xref_target}[xrefstyle=#{styles[0]}]"
|
|
170
|
+
elsif styles.join(' ') == 'short basic'
|
|
171
|
+
# Special case: "short basic"
|
|
172
|
+
# - Numbered items (sections with sectnum, blocks with caption) → ALWAYS output both
|
|
173
|
+
# e.g., "Section 1.1" + "Features", "Table 5" + "Pin Descriptions"
|
|
174
|
+
# - Unnumbered items with reftext (floating titles) → output only basic
|
|
175
|
+
# to avoid duplication like "Register X Register X"
|
|
176
|
+
if is_numbered
|
|
177
|
+
# Numbered item - always use both styles
|
|
178
|
+
xrefs = styles.map { |style| "xref:#{xref_target}[xrefstyle=#{style}]" }
|
|
179
|
+
"a|#{xrefs.join(" \n")}"
|
|
180
|
+
elsif has_reftext
|
|
181
|
+
# Unnumbered with reftext - use basic only to avoid duplication
|
|
182
|
+
"a|xref:#{xref_target}[xrefstyle=basic]"
|
|
183
|
+
else
|
|
184
|
+
# Unnumbered without reftext - use both styles
|
|
185
|
+
xrefs = styles.map { |style| "xref:#{xref_target}[xrefstyle=#{style}]" }
|
|
186
|
+
"a|#{xrefs.join(" \n")}"
|
|
187
|
+
end
|
|
188
|
+
else
|
|
189
|
+
# Multiple xrefstyles - output each
|
|
190
|
+
xrefs = styles.map { |style| "xref:#{xref_target}[xrefstyle=#{style}]" }
|
|
191
|
+
"a|#{xrefs.join(" \n")}"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Export change text, matching embedded behavior for all/cover entries
|
|
196
|
+
def export_change_text output, change_text, is_all_or_cover: false
|
|
197
|
+
lines = change_text.to_s.split(/\r?\n/).map(&:strip).reject(&:empty?)
|
|
198
|
+
|
|
199
|
+
# Single line without asterisks = plain text
|
|
200
|
+
if is_all_or_cover && lines.length == 1 && !lines[0].include?('*')
|
|
201
|
+
output << lines[0]
|
|
202
|
+
else
|
|
203
|
+
export_change_with_bullets output, change_text
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Export change text with proper bullet formatting
|
|
208
|
+
def export_change_with_bullets output, change_text
|
|
209
|
+
return if change_text.to_s.empty?
|
|
210
|
+
|
|
211
|
+
lines = change_text.to_s.split(/\r?\n/).map(&:strip).reject(&:empty?)
|
|
212
|
+
|
|
213
|
+
lines.each do |line|
|
|
214
|
+
# Split on inline asterisks (e.g., "Text. * Bullet one ** Sub-bullet")
|
|
215
|
+
tokens = line.split(/\s(?=\*+\s)/)
|
|
216
|
+
first = tokens.shift
|
|
217
|
+
|
|
218
|
+
# First token becomes a bullet
|
|
219
|
+
if first && !first.empty?
|
|
220
|
+
first = first.strip
|
|
221
|
+
if first.start_with?('*')
|
|
222
|
+
output << first
|
|
223
|
+
else
|
|
224
|
+
output << "* #{first}"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Remaining tokens are already asterisk-prefixed
|
|
229
|
+
tokens.each do |token|
|
|
230
|
+
token = token.strip
|
|
231
|
+
if token =~ /^(\*+)\s+(.*)/
|
|
232
|
+
stars = $1
|
|
233
|
+
content = $2.strip
|
|
234
|
+
output << "#{stars} #{content}"
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module Asciidoctor
|
|
2
|
+
module Rhrev
|
|
3
|
+
module Helpers
|
|
4
|
+
# Check if we're running under Antora (vs standalone asciidoctor-pdf)
|
|
5
|
+
def antora_build? doc
|
|
6
|
+
# Antora sets these attributes automatically
|
|
7
|
+
doc.attr?('page-component-name') || doc.attr?('page-module') || doc.attr?('page-relative-src-path')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Convert Antora-style anchor ID to standard AsciiDoc xref format for export
|
|
11
|
+
def convert_anchor_to_xref anchor, doc = nil
|
|
12
|
+
return anchor unless anchor
|
|
13
|
+
|
|
14
|
+
# For PDF generation (even in Antora), we want internal links if possible.
|
|
15
|
+
# Returning the anchor ID directly allows Asciidoctor PDF to resolve it internally.
|
|
16
|
+
# The previous logic converted to file.adoc#id which creates broken external links in PDF.
|
|
17
|
+
return anchor
|
|
18
|
+
|
|
19
|
+
# Skip transformation for standalone asciidoctor-pdf builds
|
|
20
|
+
return anchor if doc && !antora_build?(doc)
|
|
21
|
+
|
|
22
|
+
# Auto-detect separator from the anchor itself
|
|
23
|
+
# Check for xml_ids separator first (---- four dashes), then default (:::)
|
|
24
|
+
separator = if anchor.include?('----')
|
|
25
|
+
'----'
|
|
26
|
+
elsif anchor.include?(':::')
|
|
27
|
+
':::'
|
|
28
|
+
else
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if separator
|
|
33
|
+
# Split into page and fragment
|
|
34
|
+
parts = anchor.split(separator, 2)
|
|
35
|
+
page_id = parts[0]
|
|
36
|
+
fragment = parts[1]
|
|
37
|
+
"#{page_id}.adoc##{fragment}"
|
|
38
|
+
else
|
|
39
|
+
# No separator - just a page reference, add .adoc suffix
|
|
40
|
+
"#{anchor}.adoc"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def preprocess_attribute_content content_string
|
|
45
|
+
# Preprocess attribute content to handle AsciiDoc line continuations
|
|
46
|
+
# Compatible with both .yml and .adoc import statements
|
|
47
|
+
return content_string unless content_string
|
|
48
|
+
|
|
49
|
+
# Replace " +" with hard line break, but preserve escaped " \+"
|
|
50
|
+
# Also handle literal \n sequences if they come from YAML
|
|
51
|
+
content_string.gsub('\\n', "\n")
|
|
52
|
+
.gsub(/(?<!\\) \+$/, " +")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def format_prev_rev prev_title, prev_date, doc
|
|
56
|
+
return "" if prev_title.to_s.empty?
|
|
57
|
+
|
|
58
|
+
# Only include date if rhrev-customization-prevrev-include-date attribute is set
|
|
59
|
+
include_date = doc.attr? 'rhrev-customization-prevrev-include-date'
|
|
60
|
+
|
|
61
|
+
if include_date && !prev_date.to_s.empty?
|
|
62
|
+
date_format = doc.attr 'rhrev-customization-date-format', '%Y-%m-%d'
|
|
63
|
+
begin
|
|
64
|
+
# Try to parse and format date if possible
|
|
65
|
+
parsed_date = Date.parse(prev_date)
|
|
66
|
+
formatted_date = parsed_date.strftime(date_format)
|
|
67
|
+
"#{prev_title} - #{formatted_date}"
|
|
68
|
+
rescue
|
|
69
|
+
# Fallback if date parsing fails
|
|
70
|
+
"#{prev_title} - #{prev_date}"
|
|
71
|
+
end
|
|
72
|
+
else
|
|
73
|
+
"#{prev_title}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def needs_asciidoc_cell? content
|
|
78
|
+
return false unless content
|
|
79
|
+
# Check for AsciiDoc formatting markers
|
|
80
|
+
content.include?('*') ||
|
|
81
|
+
content.include?('_') ||
|
|
82
|
+
content.include?('`') ||
|
|
83
|
+
content.include?('http') ||
|
|
84
|
+
content.include?('xref:') ||
|
|
85
|
+
content.include?('<<') ||
|
|
86
|
+
content.include?('link:') ||
|
|
87
|
+
content.include?('+') # Line breaks
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def debug_log(message, doc = nil)
|
|
91
|
+
return unless doc&.attr?('rhrev-debug')
|
|
92
|
+
if doc.respond_to?(:logger)
|
|
93
|
+
doc.logger.debug "[RHREV] #{message}"
|
|
94
|
+
else
|
|
95
|
+
warn "[RHREV] #{message}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def with_attribute_missing_suppressed doc
|
|
100
|
+
# Suppress attribute-missing warnings
|
|
101
|
+
# Save current attribute-missing setting
|
|
102
|
+
saved_attribute_missing = doc.attr 'attribute-missing'
|
|
103
|
+
# Set to skip to suppress warnings
|
|
104
|
+
doc.set_attr 'attribute-missing', 'skip'
|
|
105
|
+
|
|
106
|
+
yield
|
|
107
|
+
ensure
|
|
108
|
+
# Restore attribute-missing setting
|
|
109
|
+
if saved_attribute_missing
|
|
110
|
+
doc.set_attr 'attribute-missing', saved_attribute_missing
|
|
111
|
+
else
|
|
112
|
+
doc.delete_attr 'attribute-missing'
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Asciidoctor
|
|
2
|
+
module Rhrev
|
|
3
|
+
# Preprocessor to sanitize xref syntax before parsing
|
|
4
|
+
# This fixes Antora Assembler's xref:page:::anchor[] syntax that triggers warnings
|
|
5
|
+
class XrefSanitizerPreprocessor < Asciidoctor::Extensions::Preprocessor
|
|
6
|
+
def process document, reader
|
|
7
|
+
# Convert xref:page:::anchor[] to <<page:::anchor>>
|
|
8
|
+
# This prevents "unknown name for block macro" warnings
|
|
9
|
+
lines = reader.read_lines
|
|
10
|
+
sanitized = lines.map do |line|
|
|
11
|
+
if line.match?(/xref:[\w-]+:::[^\[\]]+\[\]/)
|
|
12
|
+
line.gsub(/xref:([\w-]+:::[^\[\]]+)\[\]/, '<<\1>>')
|
|
13
|
+
elsif line.match?(/xref:[\w-]+----[^\[\]]+\[\]/)
|
|
14
|
+
line.gsub(/xref:([\w-]+----[^\[\]]+)\[\]/, '<<\1>>')
|
|
15
|
+
else
|
|
16
|
+
line
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
reader.restore_lines sanitized
|
|
21
|
+
reader
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Block macro processor for rhrev::[] macro placement
|
|
26
|
+
class RhrevBlockMacroProcessor < Asciidoctor::Extensions::BlockMacroProcessor
|
|
27
|
+
use_dsl
|
|
28
|
+
named :rhrev
|
|
29
|
+
|
|
30
|
+
def process parent, target, attrs
|
|
31
|
+
# Create a block with context :rhrev that the converter will recognize
|
|
32
|
+
create_block parent, :rhrev, nil, attrs
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Inline macro processor for pagerhref:anchor[] page number references
|
|
37
|
+
class PagerhrefInlineMacroProcessor < Asciidoctor::Extensions::InlineMacroProcessor
|
|
38
|
+
use_dsl
|
|
39
|
+
named :pagerhref
|
|
40
|
+
|
|
41
|
+
def process parent, target, attrs
|
|
42
|
+
# Create a marker that the converter will recognize and replace with page number
|
|
43
|
+
# Format: [pagerhref:anchor]
|
|
44
|
+
create_inline parent, :quoted, "[pagerhref:#{target}]", type: :unquoted
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|