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,697 @@
1
+ require 'fileutils'
2
+
3
+ module Asciidoctor
4
+ module PDF
5
+ module Rhrev
6
+ module Renderer
7
+ include Helpers
8
+
9
+ def allocate_revision_history_extent doc
10
+ # Use accessor method, not instance variable
11
+ return unless revision_history
12
+
13
+ start_page_number = page_number
14
+
15
+ extent = dry_run onto: self do
16
+ ink_revision_history_content doc
17
+ end
18
+
19
+ extent.each_page { |first_page| start_new_page unless first_page }
20
+ move_cursor_to extent.to.cursor
21
+
22
+ end_page_number = page_number
23
+ @rhrev_deferred_pages = (extent.from.page..end_page_number)
24
+
25
+ unless doc.attr? 'rhrev-suppress-new-page-after'
26
+ start_new_page
27
+ end
28
+
29
+ extent
30
+ end
31
+
32
+ def ink_revision_history doc, extent
33
+ return if scratch?
34
+
35
+ go_to_page extent.from.page
36
+ move_cursor_to extent.from.cursor
37
+
38
+ ink_revision_history_content doc
39
+
40
+ end_page = page_number
41
+ start_page = extent.from.page
42
+
43
+ has_foreground = @theme.page_foreground_image || doc.attr('page-foreground-image')
44
+
45
+ if has_foreground
46
+ fg_image = resolve_background_image doc, @theme, 'page-foreground-image'
47
+ if fg_image && fg_image[0]
48
+ (start_page..end_page).each do |pg|
49
+ go_to_page pg
50
+ next if page.imported_page?
51
+ stamp_name = %(foreground-image-#{page.layout})
52
+ stamp stamp_name if (stamp_dictionary_registry.key? stamp_name rescue false)
53
+ end
54
+ end
55
+ end
56
+
57
+ @rhrev_deferred_pages = nil
58
+ end
59
+
60
+ def stamp_foreground_image doc, has_front_cover
61
+ pages = state.pages
62
+ if (first_page = (has_front_cover ? (pages.drop 1) : pages).find {|it| !it.imported_page? }) &&
63
+ (first_page_num = (pages.index first_page) + 1) &&
64
+ (fg_image = resolve_background_image doc, @theme, 'page-foreground-image') && fg_image[0]
65
+ stamps = ::Set.new
66
+ rhrev_range = @rhrev_deferred_pages || (0..0)
67
+ (first_page_num..page_count).each do |num|
68
+ next if rhrev_range.include?(num)
69
+ go_to_page num
70
+ next if page.imported_page?
71
+ unless stamps.include? (stamp_name = %(foreground-image-#{page.layout}))
72
+ create_stamp stamp_name do
73
+ canvas { image fg_image[0], ({ position: :center, vposition: :center }.merge fg_image[1]) }
74
+ end
75
+ stamps << stamp_name
76
+ end
77
+ stamp stamp_name
78
+ end
79
+ end
80
+ end
81
+
82
+ def table_contains_pagerhref? node
83
+ return false unless node.context == :table
84
+ node.rows[:body].each do |row|
85
+ row.each do |cell|
86
+ return true if cell.text.to_s.include?('pagerhref:')
87
+ end
88
+ end
89
+ false
90
+ end
91
+
92
+ def allocate_pagerhref_table_extent node
93
+ extent = dry_run onto: self do
94
+ @rendering_deferred_table = true
95
+ super_convert_table node
96
+ @rendering_deferred_table = false
97
+ end
98
+
99
+ @pagerhref_tables ||= []
100
+ @pagerhref_tables << {
101
+ node: node,
102
+ extent: extent,
103
+ page: page_number
104
+ }
105
+
106
+ move_cursor_to extent.to.cursor
107
+ end
108
+
109
+ def ink_pagerhref_tables
110
+ @anchor_catalog ||= {}
111
+
112
+ @pagerhref_tables.each do |table_info|
113
+ extent = table_info[:extent]
114
+ node = table_info[:node]
115
+
116
+ # Resolve pagerhrefs before rendering
117
+ resolve_pagerhrefs_in_table node
118
+
119
+ go_to_page extent.from.page
120
+ move_cursor_to extent.from.cursor
121
+
122
+ @rendering_deferred_table = true
123
+ super_convert_table node
124
+ @rendering_deferred_table = false
125
+ end
126
+ end
127
+
128
+ def resolve_pagerhrefs_in_table table
129
+ # Recursively search for pagerhref markers in the table and replace them
130
+ debug_log "Resolving pagerhrefs. Anchor catalog has #{@anchor_catalog.keys.size} entries: #{@anchor_catalog.keys.take(10)}", @document
131
+
132
+ table.rows[:body].each do |row|
133
+ row.each do |cell|
134
+ next unless cell.text && cell.text.to_s.include?('pagerhref:')
135
+
136
+ # Replace [pagerhref:anchor] with page number
137
+ # We use a regex that matches the marker created by the inline macro
138
+ cell.text = cell.text.to_s.gsub(/\[pagerhref:(.*?)\]/) do |match|
139
+ anchor = $1
140
+ if (entry = @anchor_catalog[anchor]) && entry[:dest]
141
+ # TODO: Convert physical page to logical page if needed
142
+ entry[:dest][:page].to_s
143
+ else
144
+ debug_log "Pagerhref resolution failed for anchor '#{anchor}'. Catalog has entry? #{@anchor_catalog.key?(anchor)}", @document
145
+ "??"
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ def super_convert_table node
153
+ self.class.superclass.instance_method(:convert_table).bind(self).call(node)
154
+ end
155
+
156
+ def render_adoc_include doc, filepath
157
+ return unless filepath && File.exist?(filepath)
158
+ begin
159
+ content = File.read(filepath)
160
+ subdoc = ::Asciidoctor.load content, safe: :safe, backend: 'pdf'
161
+ subdoc.blocks.each { |block| traverse block }
162
+ rescue => e
163
+ warn "asciidoctor: WARNING: Failed to render include file #{filepath}: #{e.message}"
164
+ end
165
+ end
166
+
167
+ def ink_revision_history_content doc
168
+ collect_document_level_entries doc
169
+
170
+ if doc.attr? 'rhrev-export-to-file'
171
+ export_to_adoc_file doc
172
+ return
173
+ end
174
+
175
+ return if doc.attr('rhrev') == 'manual'
176
+
177
+ if is_initial_release?(doc)
178
+ initial_handling = doc.attr 'rhrev-initial-handling', 'initial-release'
179
+ case initial_handling
180
+ when 'skip'; return
181
+ when 'normal'; # Continue
182
+ else
183
+ render_initial_release_table doc
184
+ return
185
+ end
186
+ end
187
+
188
+ block_type = doc.attr 'rhrev-block-type', 'section'
189
+
190
+ if block_type == 'table' && !(doc.attr? 'rhrev-disable-caption')
191
+ unless doc.attr? 'table-caption'
192
+ doc.set_attr 'table-caption', (doc.attr 'table-caption', 'Table')
193
+ end
194
+ end
195
+
196
+ if block_type == 'section'
197
+ version_label = doc.attr('version-label')
198
+ default_title = "#{version_label} History"
199
+ if (title = doc.attr 'rhrev-customization-title', default_title)
200
+ unless title.empty?
201
+ heading_opts = { align: :left, margin_bottom: (@theme.heading_margin_bottom || 12) }
202
+ theme_font :heading, level: 2 do
203
+ ink_prose title, heading_opts
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ create_revision_table_properly doc if revision_history
210
+ end
211
+
212
+ def is_initial_release? doc
213
+ revnumber = doc.attr('revnumber')
214
+ return false unless revnumber
215
+ revnumber.to_s.strip == '1.0' || revnumber.to_s.strip == '1'
216
+ end
217
+
218
+ def render_initial_release_table doc
219
+ block_type = doc.attr 'rhrev-block-type', 'section'
220
+
221
+ if block_type == 'section'
222
+ version_label = doc.attr('version-label')
223
+ default_title = "#{version_label} History"
224
+ if (title = doc.attr 'rhrev-customization-title', default_title)
225
+ unless title.empty?
226
+ heading_opts = { align: :left, margin_bottom: (@theme.heading_margin_bottom || 12) }
227
+ theme_font :heading, level: 2 do
228
+ ink_prose title, heading_opts
229
+ end
230
+ end
231
+ end
232
+ end
233
+
234
+ column_widths = get_column_widths doc
235
+ table_data = []
236
+
237
+ page_label = doc.attr 'rhrev-localization-page', 'Page'
238
+ major_changes_text = doc.attr 'rhrev-localization-major-changes', 'Major changes since'
239
+
240
+ revision_label = doc.attr('version-label', 'Revision')
241
+ revnumber = doc.attr('revnumber')
242
+
243
+ header_text = "*#{major_changes_text} #{revision_label} #{revnumber}*"
244
+ table_data << ["*#{page_label}*", header_text]
245
+
246
+ initial_text = doc.attr 'rhrev-customization-initial-release-text', 'Initial Release'
247
+ table_data << ["", initial_text]
248
+
249
+ formatted_widths = column_widths.map { |w| w.to_i.to_s }.join(',')
250
+ markup_lines = []
251
+ markup_lines << "[cols='#{formatted_widths}']"
252
+ markup_lines << "|==="
253
+ table_data.each do |row|
254
+ markup_lines << "|#{row[0]} |#{row[1]}"
255
+ end
256
+ markup_lines << "|==="
257
+
258
+ parsed_doc = ::Asciidoctor.load(markup_lines.join("\n"), safe: :safe, backend: 'pdf')
259
+ table = parsed_doc.blocks[0]
260
+
261
+ table.set_attr 'frame', (doc.attr 'rhrev-table-frame', 'all')
262
+ table.set_attr 'grid', (doc.attr 'rhrev-table-grid', 'all')
263
+ if (stripes = doc.attr 'rhrev-table-stripes')
264
+ table.set_attr 'stripes', stripes
265
+ end
266
+
267
+ convert_table table
268
+ end
269
+
270
+ def ink_prose text, opts = {}
271
+ layout_prose text, opts
272
+ end
273
+
274
+ def format_location_for_display location_text, entry, doc
275
+ %(<span class="rhrevpage">#{location_text}</span>)
276
+ end
277
+
278
+ def format_with_role text, role
279
+ %(<span class="#{role}">#{text}</span>)
280
+ end
281
+
282
+ def build_consolidated_list doc, items
283
+ return "" if items.empty?
284
+ items.map { |item| "* #{item}" }.join("\n")
285
+ end
286
+
287
+ def build_description_xrefs anchor, doc, entry
288
+ # Build description xref text directly using <a anchor="..."> markup
289
+ # This is necessary because xref: syntax won't resolve in a parsed subdocument
290
+ xrefstyle_config = doc.attr 'rhrev-description-xrefstyle', ''
291
+ styles = xrefstyle_config.split(/\s+/).reject(&:empty?)
292
+
293
+ has_reftext = entry && entry[:reftext]
294
+
295
+ if styles.empty?
296
+ # Default: use basic style
297
+ display_text = build_xref_text(anchor, 'basic', entry, doc)
298
+ formatted_text = format_with_role(display_text, 'rhrevdescription')
299
+ "<a anchor=\"#{anchor}\">#{formatted_text}</a>"
300
+ elsif styles.length == 1
301
+ display_text = build_xref_text(anchor, styles[0], entry, doc)
302
+ formatted_text = format_with_role(display_text, 'rhrevdescription')
303
+ "<a anchor=\"#{anchor}\">#{formatted_text}</a>"
304
+ elsif styles.join(' ') == 'short basic'
305
+ # Special case: add both short and basic xrefs
306
+ # For sections: check if numbered (respects :!sectnum:)
307
+ is_numbered = entry ? check_if_numbered(entry, anchor, doc) : false
308
+
309
+ if has_reftext || !is_numbered
310
+ # If reftext is defined OR section is unnumbered, just use basic to avoid duplication
311
+ display_text = build_xref_text(anchor, 'basic', entry, doc)
312
+ formatted_text = format_with_role(display_text, 'rhrevdescription')
313
+ "<a anchor=\"#{anchor}\">#{formatted_text}</a>"
314
+ else
315
+ # No reftext and numbered section, safe to add both
316
+ display_short = build_xref_text(anchor, 'short', entry, doc)
317
+ display_basic = build_xref_text(anchor, 'basic', entry, doc)
318
+ formatted_short = format_with_role(display_short, 'rhrevdescription')
319
+ formatted_basic = format_with_role(display_basic, 'rhrevdescription')
320
+ "<a anchor=\"#{anchor}\">#{formatted_short}</a> <a anchor=\"#{anchor}\">#{formatted_basic}</a>"
321
+ end
322
+ else
323
+ # Multiple styles - output each
324
+ xrefs = styles.map do |style|
325
+ display_text = build_xref_text(anchor, style, entry, doc)
326
+ formatted_text = format_with_role(display_text, 'rhrevdescription')
327
+ "<a anchor=\"#{anchor}\">#{formatted_text}</a>"
328
+ end
329
+ xrefs.join(" ")
330
+ end
331
+ end
332
+
333
+ def create_revision_table_properly doc
334
+ asciidoc_table = build_table_via_parsing doc
335
+
336
+ disable_caption = doc.attr? 'rhrev-disable-caption'
337
+ if disable_caption
338
+ saved_caption = doc.attr 'table-caption'
339
+ doc.set_attr 'table-caption', ''
340
+ end
341
+
342
+ convert_table asciidoc_table
343
+
344
+ if disable_caption
345
+ if saved_caption
346
+ doc.set_attr 'table-caption', saved_caption
347
+ else
348
+ doc.delete_attr 'table-caption'
349
+ end
350
+ end
351
+ end
352
+
353
+ def add_custom_first_row_to_markup markup_lines, doc
354
+ if (first_row_merged = doc.attr 'rhrev-customization-first-row')
355
+ processed = preprocess_attribute_content(first_row_merged)
356
+ cell_prefix = needs_asciidoc_cell?(processed) ? "2+a|" : "2+|"
357
+ markup_lines << ""
358
+ markup_lines << "#{cell_prefix}#{processed}"
359
+ else
360
+ first_row_left = doc.attr 'rhrev-customization-first-row-left'
361
+ first_row_right = doc.attr 'rhrev-customization-first-row-right'
362
+
363
+ if first_row_left || first_row_right
364
+ left_processed = first_row_left ? preprocess_attribute_content(first_row_left) : ''
365
+ right_processed = first_row_right ? preprocess_attribute_content(first_row_right) : ''
366
+ left_prefix = needs_asciidoc_cell?(left_processed) ? "a|" : "|"
367
+ right_prefix = needs_asciidoc_cell?(right_processed) ? "a|" : "|"
368
+ markup_lines << ""
369
+ markup_lines << "#{left_prefix}#{left_processed}"
370
+ markup_lines << "#{right_prefix}#{right_processed}"
371
+ end
372
+ end
373
+ end
374
+
375
+ def build_table_via_parsing doc
376
+ column_widths = get_column_widths doc
377
+ markup_lines = []
378
+ formatted_widths = column_widths.map { |w| w.to_i.to_s }.join(',')
379
+ markup_lines << "[cols='#{formatted_widths}']"
380
+ markup_lines << "|==="
381
+
382
+ add_custom_first_row_to_markup markup_lines, doc
383
+
384
+ markup_lines << "|Placeholder"
385
+ markup_lines << "a|* Placeholder list"
386
+ markup_lines << "|==="
387
+
388
+ markup_str = markup_lines.join("\n")
389
+ parsed_doc = ::Asciidoctor.load(markup_str, safe: :safe, backend: 'pdf')
390
+ table = parsed_doc.blocks[0]
391
+
392
+ table.set_attr 'frame', (doc.attr 'rhrev-table-frame', 'all')
393
+ table.set_attr 'grid', (doc.attr 'rhrev-table-grid', 'all')
394
+ if (stripes = doc.attr 'rhrev-table-stripes')
395
+ table.set_attr 'stripes', stripes
396
+ end
397
+ if (caption = doc.attr 'rhrev-table-caption')
398
+ table.title = caption
399
+ end
400
+
401
+ has_custom_first_row = (doc.attr? 'rhrev-customization-first-row') ||
402
+ (doc.attr? 'rhrev-customization-first-row-left') ||
403
+ (doc.attr? 'rhrev-customization-first-row-right')
404
+
405
+ custom_first_row = has_custom_first_row ? table.rows.body[0] : nil
406
+
407
+ placeholder_row = has_custom_first_row ? table.rows.body[1] : table.rows.body[0]
408
+ template_location_cell = placeholder_row[0]
409
+ template_asciidoc_cell = placeholder_row[1]
410
+
411
+ table.rows.body.clear
412
+ table.rows.body << custom_first_row if custom_first_row
413
+
414
+ page_label = doc.attr 'rhrev-localization-page', 'Page'
415
+ major_changes_text = doc.attr 'rhrev-localization-major-changes', 'Major changes since'
416
+ all_text = doc.attr 'rhrev-localization-all', 'All'
417
+ cover_text = doc.attr 'rhrev-localization-cover', 'Cover'
418
+
419
+ revision_history.sorted_revisions.each do |revision|
420
+ prevrev_attr = "#{revision}-prevrev"
421
+ prevrevdate_attr = "#{revision}-prevrevdate"
422
+ revision_label = doc.attr('version-label', 'Revision')
423
+ prev_title = doc.attr(prevrev_attr) || "#{revision.tr('-', '.')}"
424
+ prev_date = doc.attr(prevrevdate_attr) || ""
425
+ prev_rev_text = format_prev_rev(prev_title, prev_date, doc)
426
+
427
+ if prev_title.strip.downcase.start_with?(revision_label.downcase)
428
+ header_text = "*#{major_changes_text} #{prev_rev_text}*"
429
+ else
430
+ header_text = "*#{major_changes_text} #{revision_label} #{prev_rev_text}*"
431
+ end
432
+
433
+ page_cell = build_text_cell table, 0, "*#{page_label}*"
434
+ header_cell = build_text_cell table, 1, header_text
435
+ table.rows.body << [page_cell, header_cell]
436
+
437
+ if (all_change = revision_history.instance_variable_get(:@all_entries)[revision])
438
+ row = build_list_row table, all_text, all_change, nil, nil, template_location_cell, template_asciidoc_cell, doc, is_all_or_cover: true
439
+ table.rows.body << row
440
+ end
441
+
442
+ if (cover_change = revision_history.instance_variable_get(:@cover_entries)[revision])
443
+ unless doc.backend.to_s == 'html5'
444
+ row = build_list_row table, cover_text, cover_change, nil, nil, template_location_cell, template_asciidoc_cell, doc, is_all_or_cover: true
445
+ table.rows.body << row
446
+ end
447
+ end
448
+
449
+ entries = revision_history.entries[revision] || []
450
+ entries = entries.sort_by do |e|
451
+ page_num = e[:dest] ? e[:dest][:page_sortable] : Float::INFINITY
452
+ y_pos = e[:dest] && e[:dest][:y] ? e[:dest][:y] : 0
453
+ # Sort by page (ASC) then y_pos (DESC - top to bottom) then sequence (ASC)
454
+ [page_num, -y_pos, e[:sequence] || 0]
455
+ end
456
+
457
+ entries.each do |entry|
458
+ location = build_location_text entry, doc
459
+ row = build_list_row table, location, entry[:change], entry[:anchor], entry, template_location_cell, template_asciidoc_cell, doc
460
+ table.rows.body << row
461
+ end
462
+ end
463
+
464
+ if doc.attr? 'rhrev-table-extra-blank-row'
465
+ blank_row = build_text_row table, ["\u00A0", "\u00A0"]
466
+ table.rows.body << blank_row
467
+ end
468
+
469
+ table
470
+ end
471
+
472
+ def build_text_row table, cells_data
473
+ cells_data.map.with_index do |text, col_idx|
474
+ build_cell table, col_idx, text
475
+ end
476
+ end
477
+
478
+ def format_as_list text
479
+ text_str = text.to_s
480
+ # Don't skip if it doesn't have * - we might want to force list
481
+
482
+ tokens = text_str.split(/\s(?=\*+\s)/)
483
+ return text_str if tokens.empty?
484
+
485
+ lines = []
486
+ first = tokens.shift
487
+
488
+ # Check if first item already starts with *
489
+ if first.strip.start_with?('*')
490
+ lines << first.strip
491
+ else
492
+ lines << "* #{first.strip}"
493
+ end
494
+
495
+ tokens.each do |token|
496
+ lines << token.strip
497
+ end
498
+
499
+ lines.join("\n")
500
+ end
501
+
502
+ def build_list_row table, location_text, change_text, anchor, entry, template_location_cell, template_asciidoc_cell, doc, is_all_or_cover: false
503
+ if anchor && location_text != '???'
504
+ location_cell = ::Asciidoctor::Table::Cell.new(template_location_cell.column, "", template_asciidoc_cell.attributes.dup)
505
+ location_cell.inner_document.blocks.clear
506
+ para_block = ::Asciidoctor::Block.new(location_cell.inner_document, :paragraph)
507
+ formatted_location = format_with_role(location_text, 'rhrevpage')
508
+ para_block.lines = ["<a anchor=\"#{anchor}\">#{formatted_location}</a>"]
509
+ location_cell.inner_document.blocks << para_block
510
+ else
511
+ location_cell = ::Asciidoctor::Table::Cell.new(template_location_cell.column, location_text, template_location_cell.attributes.dup)
512
+ end
513
+
514
+ change_cell = ::Asciidoctor::Table::Cell.new(template_asciidoc_cell.column, "", template_asciidoc_cell.attributes.dup)
515
+ change_cell.inner_document.blocks.clear
516
+
517
+ cell_doc = change_cell.inner_document
518
+
519
+ if is_all_or_cover
520
+ # For all/cover entries, just add the change text
521
+ processed = preprocess_attribute_content(change_text)
522
+ if processed.include?('* ')
523
+ processed = format_as_list(processed)
524
+ end
525
+ if needs_asciidoc_cell?(processed)
526
+ parsed = ::Asciidoctor.load(processed, safe: :safe, backend: 'pdf')
527
+ parsed.blocks.each do |block|
528
+ block.parent = cell_doc
529
+ cell_doc.blocks << block
530
+ end
531
+ else
532
+ para = ::Asciidoctor::Block.new(cell_doc, :paragraph, source: processed)
533
+ cell_doc.blocks << para
534
+ end
535
+ else
536
+ description_config = doc.attr 'rhrev-customization-description-format', 'xref,desc'
537
+ show_desc = description_config.include?('desc')
538
+ show_xref = description_config.include?('xref')
539
+
540
+ # Add xref as a direct paragraph block (not parsed) - <a anchor="..."> is PDF converter syntax
541
+ if show_xref && anchor
542
+ xref_markup = build_description_xrefs(anchor, doc, entry)
543
+ xref_block = ::Asciidoctor::Block.new(cell_doc, :paragraph)
544
+ xref_block.lines = [xref_markup]
545
+ cell_doc.blocks << xref_block
546
+ end
547
+
548
+ # Add change description text
549
+ if show_desc && !change_text.to_s.empty?
550
+ processed = preprocess_attribute_content(change_text)
551
+ processed = format_as_list(processed)
552
+
553
+ if needs_asciidoc_cell?(processed)
554
+ parsed = ::Asciidoctor.load(processed, safe: :safe, backend: 'pdf')
555
+ parsed.blocks.each do |block|
556
+ block.parent = cell_doc
557
+ cell_doc.blocks << block
558
+ end
559
+ else
560
+ para = ::Asciidoctor::Block.new(cell_doc, :paragraph, source: processed)
561
+ cell_doc.blocks << para
562
+ end
563
+ end
564
+ end
565
+
566
+ [location_cell, change_cell]
567
+ end
568
+
569
+ def build_cell table, col_idx, text
570
+ column = table.columns[col_idx]
571
+ ::Asciidoctor::Table::Cell.new column, text
572
+ end
573
+
574
+ def build_text_cell table, col_idx, text
575
+ build_cell table, col_idx, text
576
+ end
577
+
578
+ def build_location_text entry, doc
579
+ if entry[:dest]
580
+ entry[:dest][:page]
581
+ else
582
+ "???"
583
+ end
584
+ end
585
+
586
+ def check_if_numbered(entry, anchor, doc)
587
+ context = entry[:context]
588
+ if context == :section
589
+ return true unless entry[:sectnum]
590
+ if doc.respond_to?(:catalog) && doc.catalog && doc.catalog[:refs]
591
+ section = doc.catalog[:refs][anchor]
592
+ if section && section.respond_to?(:numbered)
593
+ return section.numbered
594
+ elsif section && section.respond_to?(:numbered?)
595
+ return section.numbered?
596
+ end
597
+ end
598
+ return true
599
+ end
600
+ if entry[:caption_number]
601
+ return true
602
+ end
603
+ false
604
+ end
605
+
606
+ def build_xref_text(anchor, style, entry, doc)
607
+ # If reftext is defined, it overrides everything
608
+ return entry[:reftext] if entry && entry[:reftext]
609
+
610
+ title = entry && entry[:title] ? entry[:title] : nil
611
+ sectnum = entry && entry[:sectnum] ? entry[:sectnum] : nil
612
+ context = entry && entry[:context] ? entry[:context] : nil
613
+ is_chapter = entry && entry[:is_chapter]
614
+ caption_number = entry && entry[:caption_number]
615
+
616
+ # Fallback if no title - convert anchor to title case
617
+ unless title
618
+ # Handle Antora ::: separator - extract fragment for display
619
+ display_anchor = anchor.include?(':::') ? anchor.split(':::').last : anchor
620
+ clean_anchor = display_anchor.sub(/^_/, '')
621
+ return clean_anchor.tr('-', ' ').split.map(&:capitalize).join(' ')
622
+ end
623
+
624
+ # For sections with numbers, construct appropriate xreftext
625
+ if sectnum && context == :section
626
+ numbered = check_if_numbered(entry, anchor, doc)
627
+
628
+ if numbered
629
+ if doc.attr? 'rhrev-customization-xrefstyleshort-remove-trailling-period'
630
+ clean_sectnum = sectnum.to_s.sub(/\.$/, '')
631
+ else
632
+ clean_sectnum = sectnum.to_s
633
+ clean_sectnum += '.' unless clean_sectnum.end_with?('.')
634
+ end
635
+ signifier = is_chapter ? 'Chapter' : 'Section'
636
+
637
+ case style
638
+ when 'full'
639
+ "#{signifier} #{clean_sectnum}, \"#{title}\""
640
+ when 'short'
641
+ "#{signifier} #{clean_sectnum}"
642
+ when 'basic'
643
+ title
644
+ else
645
+ title
646
+ end
647
+ else
648
+ # Section numbering is disabled, just return the title
649
+ title
650
+ end
651
+ elsif caption_number && [:example, :table, :listing, :image].include?(context)
652
+ # For numbered blocks (examples, tables, listings, images), the period must be added to match AsciiDoctor-PDF's default built-in theming/presentation behavior.
653
+ if doc.attr? 'rhrev-customization-xrefstyleshort-remove-trailling-period'
654
+ clean_caption_number = caption_number.to_s.sub(/\.$/, '')
655
+ else
656
+ clean_caption_number = caption_number.to_s
657
+ clean_caption_number += '.' unless clean_caption_number.end_with?('.')
658
+ end
659
+
660
+ signifier = case context
661
+ when :example
662
+ doc.attr('example-caption', 'Example').sub(/\.?\s*$/, '')
663
+ when :table
664
+ doc.attr('table-caption', 'Table').sub(/\.?\s*$/, '')
665
+ when :listing
666
+ doc.attr('listing-caption', 'Listing').sub(/\.?\s*$/, '')
667
+ when :image
668
+ doc.attr('figure-caption', 'Figure').sub(/\.?\s*$/, '')
669
+ else
670
+ context.to_s.capitalize
671
+ end
672
+
673
+ case style
674
+ when 'full'
675
+ "#{signifier} #{clean_caption_number}, \"#{title}\""
676
+ when 'short'
677
+ "#{signifier} #{clean_caption_number}"
678
+ when 'basic'
679
+ title
680
+ else
681
+ title
682
+ end
683
+ else
684
+ # No section number or caption number, just return title for all styles
685
+ title
686
+ end
687
+ end
688
+
689
+ def get_column_widths doc
690
+ widths_str = doc.attr 'rhrev-table-column-width', Asciidoctor::PDF::Rhrev::Catalog.default_columns
691
+ widths_str.split(',').map(&:strip)
692
+ end
693
+
694
+ end
695
+ end
696
+ end
697
+ end