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.
@@ -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