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,334 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'asciidoctor'
4
+ require 'asciidoctor/converter/html5'
5
+
6
+ module Asciidoctor
7
+ module Rhrev
8
+ # Treeprocessor to auto-inject revision history at document start
9
+ class HtmlTreeprocessor < Asciidoctor::Extensions::Treeprocessor
10
+ def process document
11
+ return document unless document.attr? 'rhrev'
12
+
13
+ rhrev_value = document.attr('rhrev')
14
+ return document if rhrev_value == 'macro' || rhrev_value == 'manual'
15
+
16
+ # Insert rhrev block at the beginning of the document
17
+ rhrev_block = create_block document, :rhrev, nil, {}
18
+ document.blocks.unshift rhrev_block
19
+
20
+ document
21
+ end
22
+ end
23
+
24
+ module Html5Converter
25
+ include Asciidoctor::Rhrev::Helpers
26
+
27
+ def convert node, transform = node.node_name, opts = {}
28
+ if node.context == :rhrev
29
+ convert_rhrev_block node
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ def convert_rhrev_block node
36
+ doc = node.document
37
+ return '' unless doc.attr? 'rhrev'
38
+ return '' if doc.attr('rhrev') == 'manual'
39
+
40
+ # Check for initial release handling
41
+ revnumber = doc.attr('revnumber', '1.0')
42
+ initial_handling = doc.attr('rhrev-initial-handling', 'initial-release')
43
+
44
+ if is_initial_release?(revnumber)
45
+ return '' if initial_handling == 'skip'
46
+ return generate_initial_release_html(doc) if initial_handling == 'initial-release'
47
+ end
48
+
49
+ # Generate revision history HTML table
50
+ catalog = collect_all_entries(doc)
51
+ return '' if catalog.nil? || catalog.entries.empty?
52
+
53
+ generate_html_output(doc, catalog)
54
+ end
55
+
56
+ def is_initial_release?(revnumber)
57
+ revnumber.to_s == '1.0' || revnumber.to_s == '1'
58
+ end
59
+
60
+ def generate_html_output(doc, catalog)
61
+ html = []
62
+ discrete = doc.attr? 'rhrev-discrete'
63
+
64
+ # Wrapper
65
+ html << '<div class="rhrev-section">' if discrete
66
+ html << '<section id="revision-history">' unless discrete
67
+
68
+ # Title
69
+ if (title = doc.attr('rhrev-customization-title'))
70
+ unless title.empty?
71
+ version_label = doc.attr('version-label', 'Revision')
72
+ resolved_title = title.gsub('{version-label}', version_label)
73
+ html << %(<h2>#{resolved_title}</h2>)
74
+ end
75
+ else
76
+ # Default title
77
+ version_label = doc.attr('version-label', 'Revision')
78
+ html << %(<h2>#{version_label} History</h2>)
79
+ end
80
+
81
+ # Table
82
+ html << generate_html_table(doc, catalog)
83
+
84
+ # Close wrapper
85
+ html << (discrete ? '</div>' : '</section>')
86
+
87
+ html.join("\n")
88
+ end
89
+
90
+ def generate_initial_release_html(doc)
91
+ html = []
92
+ discrete = doc.attr? 'rhrev-discrete'
93
+
94
+ # Wrapper
95
+ html << '<div class="rhrev-section">' if discrete
96
+ html << '<section id="revision-history">' unless discrete
97
+
98
+ # Title
99
+ if (title = doc.attr('rhrev-customization-title'))
100
+ unless title.empty?
101
+ version_label = doc.attr('version-label', 'Revision')
102
+ resolved_title = title.gsub('{version-label}', version_label)
103
+ html << %(<h2>#{resolved_title}</h2>)
104
+ end
105
+ else
106
+ version_label = doc.attr('version-label', 'Revision')
107
+ html << %(<h2>#{version_label} History</h2>)
108
+ end
109
+
110
+ # Simple table
111
+ initial_text = doc.attr('rhrev-customization-initial-release-text') ||
112
+ doc.attr('rhrev-localization-initial-release') ||
113
+ 'Initial release'
114
+
115
+ column_widths = doc.attr('rhrev-table-column-width', '30,70').split(',').map(&:strip)
116
+ section_label = doc.attr('rhrev-localization-location', 'Section')
117
+ major_changes_text = doc.attr('rhrev-localization-major-changes', 'Major changes since')
118
+ version_label = doc.attr('version-label', 'Revision')
119
+ revnumber = doc.attr('revnumber', '1.0')
120
+
121
+ # Build table CSS classes with frame and grid
122
+ frame = doc.attr('rhrev-table-frame', 'all')
123
+ grid = doc.attr('rhrev-table-grid', 'all')
124
+ html << %(<table class="tableblock frame-#{frame} grid-#{grid} stretch">)
125
+
126
+ # Caption as title div
127
+ unless doc.attr? 'rhrev-disable-caption'
128
+ if (caption = doc.attr('rhrev-table-caption'))
129
+ html << %(<div class="title">#{caption}</div>)
130
+ end
131
+ end
132
+
133
+ html << '<colgroup>'
134
+ column_widths.each { |w| html << %(<col style="width: #{w}%;">)}
135
+ html << '</colgroup>'
136
+ html << '<tbody>'
137
+ html << '<tr class="rhrev-header">'
138
+ html << %(<th class="tableblock halign-left valign-top">#{section_label}</th>)
139
+ html << %(<th class="tableblock halign-left valign-top"><strong>#{major_changes_text} #{version_label} #{revnumber}</strong></th>)
140
+ html << '</tr>'
141
+ html << '<tr>'
142
+ html << '<td>&nbsp;</td>'
143
+ html << %(<td>#{initial_text}</td>)
144
+ html << '</tr>'
145
+ html << '</tbody>'
146
+ html << '</table>'
147
+
148
+ html << (discrete ? '</div>' : '</section>')
149
+
150
+ html.join("\n")
151
+ end
152
+
153
+ def collect_all_entries(doc)
154
+ catalog = Catalog.new
155
+ prefix = doc.attr 'revhistoryprefix', 'rhrev'
156
+
157
+ # Collect document-level entries (-all)
158
+ (0..9).each do |major|
159
+ (0..9).each do |minor|
160
+ revision = "#{major}-#{minor}"
161
+ prevrev_attr = "#{revision}-prevrev"
162
+
163
+ if doc.attr(prevrev_attr)
164
+ all_attr_name = "#{prefix}#{revision}-all"
165
+ if (all_value = doc.attr(all_attr_name))
166
+ catalog.add_all_entry revision, all_value
167
+ end
168
+
169
+ # Skip -cover entries for HTML
170
+ end
171
+ end
172
+ end
173
+
174
+ # Collect block-level entries by scanning document
175
+ contexts = [:section, :floating_title, :example, :listing, :table, :image]
176
+ doc.find_by { |b| contexts.include?(b.context) }.each do |block|
177
+ next unless block.id
178
+
179
+ if block.respond_to?(:attributes)
180
+ block.attributes.each do |key, value|
181
+ key_str = key.to_s
182
+ next unless key_str.start_with?(prefix)
183
+ next if key_str.include?('-all') || key_str.end_with?('-cover')
184
+
185
+ revision = key_str.sub(prefix, '')
186
+
187
+ catalog.add_entry revision, block.id, value.to_s,
188
+ reftext: block.reftext,
189
+ title: block.title,
190
+ sectnum: block.respond_to?(:sectnum) ? block.sectnum : nil,
191
+ context: block.context,
192
+ is_chapter: false,
193
+ sectname: block.respond_to?(:sectname) ? block.sectname : nil,
194
+ caption_number: nil,
195
+ source_line: block.lineno
196
+ end
197
+ end
198
+ end
199
+
200
+ catalog
201
+ end
202
+
203
+ def generate_html_table(doc, catalog)
204
+ column_widths = doc.attr 'rhrev-table-column-width', '30,70'
205
+ widths = column_widths.split(',').map(&:strip)
206
+
207
+ html = []
208
+
209
+ # Build table CSS classes with frame and grid
210
+ frame = doc.attr('rhrev-table-frame', 'all')
211
+ grid = doc.attr('rhrev-table-grid', 'all')
212
+ html << %(<table class="tableblock frame-#{frame} grid-#{grid} stretch">)
213
+
214
+ # Caption as title div
215
+ unless doc.attr? 'rhrev-disable-caption'
216
+ if (caption = doc.attr('rhrev-table-caption'))
217
+ html << %(<div class="title">#{caption}</div>)
218
+ end
219
+ end
220
+
221
+ html << '<colgroup>'
222
+ widths.each { |w| html << %(<col style="width: #{w}%;">) }
223
+ html << '</colgroup>'
224
+ html << '<tbody>'
225
+
226
+ # Custom first row
227
+ if (first_row = doc.attr('rhrev-customization-first-row'))
228
+ html << '<tr class="rhrev-custom-first-row">'
229
+ html << %(<td colspan="2">#{first_row}</td>)
230
+ html << '</tr>'
231
+ elsif (first_row_left = doc.attr('rhrev-customization-first-row-left')) ||
232
+ (first_row_right = doc.attr('rhrev-customization-first-row-right'))
233
+ html << '<tr class="rhrev-custom-first-row">'
234
+ html << %(<td>#{first_row_left || '&nbsp;'}</td>)
235
+ html << %(<td>#{first_row_right || '&nbsp;'}</td>)
236
+ html << '</tr>'
237
+ end
238
+
239
+ section_label = doc.attr 'rhrev-localization-location', 'Section'
240
+ major_changes_text = doc.attr 'rhrev-localization-major-changes', 'Major changes since'
241
+ all_text = doc.attr 'rhrev-localization-all', 'All'
242
+
243
+ catalog.sorted_revisions.each do |revision|
244
+ prevrev_attr = "#{revision}-prevrev"
245
+ prevrevdate_attr = "#{revision}-prevrevdate"
246
+ revision_label = doc.attr('version-label', 'Revision')
247
+ prev_title = doc.attr(prevrev_attr) || revision.tr('-', '.')
248
+ prev_date = doc.attr(prevrevdate_attr) || ""
249
+ prev_rev_text = format_prev_rev(prev_title, prev_date, doc)
250
+
251
+ # Build header text
252
+ if prev_title.strip.downcase.start_with?(revision_label.downcase)
253
+ header_text = "#{major_changes_text} #{prev_rev_text}"
254
+ else
255
+ header_text = "#{major_changes_text} #{revision_label} #{prev_rev_text}"
256
+ end
257
+
258
+ # Header row for this revision
259
+ html << '<tr class="rhrev-header">'
260
+ html << %(<th class="tableblock halign-left valign-top">#{section_label}</th>)
261
+ html << %(<th class="tableblock halign-left valign-top"><strong>#{header_text}</strong></th>)
262
+ html << '</tr>'
263
+
264
+ # All entry
265
+ if (all_change = catalog.instance_variable_get(:@all_entries)[revision])
266
+ html << '<tr>'
267
+ html << %(<td>#{all_text}</td>)
268
+ html << %(<td>#{format_change_as_html_list(all_change)}</td>)
269
+ html << '</tr>'
270
+ end
271
+
272
+ # Block entries
273
+ entries = catalog.entries[revision] || []
274
+ entries.sort_by! { |e| e[:sequence] || 0 }
275
+
276
+ entries.each do |entry|
277
+ anchor = entry[:anchor]
278
+ title = entry[:title] || anchor.tr('_-', ' ').split.map(&:capitalize).join(' ')
279
+
280
+ html << '<tr>'
281
+ html << %(<td><a href="##{anchor}">#{title}</a></td>)
282
+ html << %(<td>#{format_change_as_html_list(entry[:change])}</td>)
283
+ html << '</tr>'
284
+ end
285
+ end
286
+
287
+ html << '</tbody>'
288
+ html << '</table>'
289
+
290
+ html.join("\n")
291
+ end
292
+
293
+ def format_change_as_html_list(text)
294
+ return '' if text.to_s.empty?
295
+
296
+ # Process attribute content (handle line breaks)
297
+ processed = preprocess_attribute_content(text)
298
+
299
+ # Split by asterisks to create list items
300
+ tokens = processed.split(/\s(?=\*+\s)/)
301
+
302
+ items = []
303
+ if tokens.empty? || tokens.length == 1 && !processed.include?('*')
304
+ # Single item without asterisk - wrap in list anyway
305
+ items << { level: 1, content: processed }
306
+ else
307
+ tokens.each do |token|
308
+ next if token.strip.empty?
309
+
310
+ # Count leading asterisks for nesting level
311
+ if token.strip =~ /^(\*+)\s+(.+)/
312
+ level = $1.length
313
+ content = $2
314
+ items << { level: level, content: content }
315
+ else
316
+ items << { level: 1, content: token.strip }
317
+ end
318
+ end
319
+ end
320
+
321
+ # Always generate HTML list
322
+ html = ['<ul>']
323
+ items.each do |item|
324
+ html << '<li>' + item[:content] + '</li>'
325
+ end
326
+ html << '</ul>'
327
+ html.join("\n")
328
+ end
329
+ end
330
+ end
331
+ end
332
+
333
+ # Extend Html5Converter with our module
334
+ Asciidoctor::Converter::Html5Converter.prepend Asciidoctor::Rhrev::Html5Converter
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # PDF-specific converter - only loaded if asciidoctor-pdf is available
4
+ require 'asciidoctor/pdf' unless defined? Asciidoctor::PDF
5
+ require_relative 'converter'
6
+ require_relative 'renderer'
7
+ require_relative 'exporter'
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Load Asciidoctor first
4
+ require 'asciidoctor' unless defined? Asciidoctor
5
+
6
+ # Load shared components (processors, helpers, catalog)
7
+ require_relative 'rhrev/processors'
8
+ require_relative 'rhrev/helpers'
9
+ require_relative 'rhrev/catalog'
10
+
11
+ # Try to load the PDF converter support if asciidoctor-pdf is available
12
+ begin
13
+ require 'asciidoctor-pdf'
14
+ require_relative 'rhrev/rhrev_pdf'
15
+ rescue LoadError
16
+ # PDF not available, that's ok
17
+ end
18
+
19
+ # Try to load the HTML backend support
20
+ html_available = false
21
+ begin
22
+ require_relative 'rhrev/rhrev_html'
23
+ html_available = true
24
+ rescue LoadError
25
+ # HTML-specific code couldn't be loaded
26
+ end
27
+
28
+ # Register extensions (processors are backend-agnostic)
29
+ Asciidoctor::Extensions.register do
30
+ preprocessor Asciidoctor::Rhrev::XrefSanitizerPreprocessor
31
+ block_macro Asciidoctor::Rhrev::RhrevBlockMacroProcessor
32
+ inline_macro Asciidoctor::Rhrev::PagerhrefInlineMacroProcessor
33
+ treeprocessor Asciidoctor::Rhrev::HtmlTreeprocessor if html_available
34
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'asciidoctor/rhrev'
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: asciidoctor-rhrev
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - 白一百 baiyibai
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: asciidoctor
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ description: A comprehensive revision history tracking system for asciidoctor-pdf
27
+ that automatically creates a grouped, formatted table of document changes with clickable
28
+ page numbers, xrefstyle support, and extensive customization options.
29
+ email: rubygems@baiyibai.cyou
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE
35
+ - README.adoc
36
+ - USAGE.adoc
37
+ - lib/asciidoctor-rhrev.rb
38
+ - lib/asciidoctor/rhrev.rb
39
+ - lib/asciidoctor/rhrev/catalog.rb
40
+ - lib/asciidoctor/rhrev/converter.rb
41
+ - lib/asciidoctor/rhrev/exporter.rb
42
+ - lib/asciidoctor/rhrev/helpers.rb
43
+ - lib/asciidoctor/rhrev/processors.rb
44
+ - lib/asciidoctor/rhrev/renderer.rb
45
+ - lib/asciidoctor/rhrev/rhrev_html.rb
46
+ - lib/asciidoctor/rhrev/rhrev_pdf.rb
47
+ homepage: https://gitlab.com/baiyibai/rhrev-asciidoctor-automatic-revision-history
48
+ licenses:
49
+ - CC0-1.0
50
+ metadata:
51
+ source_code_uri: https://gitlab.com/baiyibai/rhrev-asciidoctor-automatic-revision-history.git
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 3.0.0
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 3.6.7
67
+ specification_version: 4
68
+ summary: Comprehensive revision history tracking extension for asciidoctor and asciidoctor-pdf
69
+ test_files: []