Module: Asciidoctor::Rhrev::Html5Converter

Includes:
Helpers
Defined in:
lib/asciidoctor/rhrev/rhrev_html.rb

Constant Summary collapse

ROLLUP_CONTEXTS =

Mirrors Converter::ROLLUP_CONTEXTS on the PDF side: block types with no revision-history row of their own, a marked one rolls up as a bullet into its nearest enclosing section's entry instead (or the document-level -all entry, if it has no enclosing section, or its enclosing section has no id to anchor on).

[:paragraph, :ulist, :olist, :dlist, :admonition, :open, :quote, :verse, :sidebar].freeze

Instance Method Summary collapse

Methods included from Helpers

#antora_build?, #convert_anchor_to_xref, #debug_log, #format_prev_rev, #needs_asciidoc_cell?, #preprocess_attribute_content, #with_attribute_missing_suppressed

Instance Method Details

#append_child_change_to_all(catalog, revision, change_text) ⇒ Object

Appends onto catalog's existing -all change text for this revision, or sets it fresh if none exists yet. Same shape as Converter#append_child_change_to_all on the PDF side.



421
422
423
424
425
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 421

def append_child_change_to_all catalog, revision, change_text
  existing = catalog.all_entries[revision]
  combined = existing.to_s.empty? ? change_text : "#{existing} * #{change_text}"
  catalog.add_all_entry revision, combined
end

#append_child_change_to_entry(catalog, revision, anchor_node, change_text) ⇒ Object

Finds anchor_node's existing entry for this revision on catalog and appends another bullet onto its :change text, or creates the entry (with the same fields collect_all_entries's own direct-mark loop would set) if anchor_node has none yet. Same shape as Converter#append_child_change_to_entry on the PDF side, adapted to take catalog as an argument since collect_all_entries builds a fresh one locally rather than reading an instance-level revision_history.



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 400

def append_child_change_to_entry catalog, revision, anchor_node, change_text
  entries = catalog.entries[revision] ||= []
  entry = entries.find { |e| e[:anchor] == anchor_node.id }
  if entry
    entry[:change] = entry[:change].to_s.empty? ? change_text : "#{entry[:change]} * #{change_text}"
  else
    catalog.add_entry revision, anchor_node.id, change_text,
      reftext: (anchor_node.respond_to?(:reftext) ? anchor_node.reftext : nil),
      title: anchor_node.title,
      sectnum: (anchor_node.respond_to?(:sectnum) ? anchor_node.sectnum : nil),
      context: anchor_node.context,
      is_chapter: false,
      sectname: (anchor_node.respond_to?(:sectname) ? anchor_node.sectname : nil),
      caption_number: nil,
      source_line: anchor_node.lineno
  end
end

#collect_all_entries(doc) ⇒ Object



495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 495

def collect_all_entries(doc)
  catalog = Catalog.new
  prefix = doc.attr 'revhistoryprefix', 'rhrev'

  # Collect document-level entries (-all)
  # Discover revisions from *-prevrev attributes so any number of
  # segments works (1-1, 1-2-0, ...), not just major-minor
  doc.attributes.each_key do |key|
    key_str = key.to_s
    next unless key_str.end_with?('-prevrev')
    revision = key_str.delete_suffix('-prevrev')
    next unless revision.match?(/\A\d+(?:-\d+)*\z/)

    all_attr_name = "#{prefix}#{revision}-all"
    if (all_value = doc.attr(all_attr_name))
      catalog.add_all_entry revision, all_value
    end

    # Skip -cover entries for HTML
  end

  # Collect block-level entries by scanning document
  contexts = [:section, :floating_title, :example, :listing, :table, :image]
  doc.find_by { |b| contexts.include?(b.context) }.each do |block|
    next unless block.id

    if block.respond_to?(:attributes)
      block.attributes.each do |key, value|
        key_str = key.to_s
        next unless key_str.start_with?(prefix)
        next if key_str.include?('-all') || key_str.end_with?('-cover')

        revision = key_str.sub(prefix, '')

        catalog.add_entry revision, block.id, value.to_s,
          reftext: block.reftext,
          title: block.title,
          sectnum: block.respond_to?(:sectnum) ? block.sectnum : nil,
          context: block.context,
          is_chapter: false,
          sectname: block.respond_to?(:sectname) ? block.sectname : nil,
          caption_number: nil,
          source_line: block.lineno
      end
    end
  end

  rollup_child_revision_entries catalog, doc, prefix
  rollup_table_cell_revision_entries catalog, doc, prefix

  catalog
end

#convert(node, transform = node.node_name, opts = {}) ⇒ Object



135
136
137
138
139
140
141
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 135

def convert node, transform = node.node_name, opts = {}
  if node.context == :rhrev
    convert_rhrev_block node
  else
    super
  end
end

#convert_rhrev_block(node) ⇒ Object



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 243

def convert_rhrev_block node
  doc = node.document
  return '' unless doc.attr? 'rhrev'
  return '' if doc.attr('rhrev') == 'manual'
  
  # Check for initial release handling
  revnumber = doc.attr('revnumber', '1.0')
  initial_handling = doc.attr('rhrev-initial-handling', 'initial-release')
  
  if is_initial_release?(revnumber)
    return '' if initial_handling == 'skip'
    return generate_initial_release_html(doc) if initial_handling == 'initial-release'
  end
  
  # Generate revision history HTML table
  catalog = collect_all_entries(doc)
  # entries.empty? alone missed a document with only a document-level
  # -all entry (set directly via rhrevN-M-all, or reached through the
  # no-section rollup fallback) and no per-block marks at all: nothing
  # to show became nothing rendered even though the -all row itself
  # had real content. Pre-existing gap, not introduced by the rollup
  # work above, but the rollup's own -all fallback tier depends on it.
  return '' if catalog.nil? || (catalog.entries.empty? && catalog.all_entries.empty? && catalog.cover_entries.empty?)

  generate_html_output(doc, catalog)
end

#convert_table(node) ⇒ Object

Marks an individual changed table cell with the rhrev-changed role directly on its /, the requested mechanism: no bundled styling, a stylesheet decides how a changed cell looks, same as every other rhrev-changed element in HTML output.

This cannot be done by tagging the cell's role and letting the base converter pick it up: base Html5Converter#convert_table (checked directly against the installed asciidoctor gem, matching this repo's ~> 2.0 dependency range) builds each cell's class attribute from only halign/valign, it never reads a cell's role or any other attribute. There is no smaller hook to prepend either; the whole per-cell HTML string is built inline in one method, not delegated to any overridable per-cell method. So this vendors that method's body (unchanged except for the one added conditional building cell_class_attribute) rather than reimplementing table rendering from scratch. A future asciidoctor upgrade that changes convert_table needs this diffed against the new version; the guard clause below keeps that cost at zero for every document that does not use HTML change bars, which return super immediately, running the real, unmodified base method.



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 163

def convert_table node
  doc = node.document
  unless doc.basebackend?('html') && (doc.attr? 'rhrev-change-bars') && (revnumber = doc.attr 'revnumber')
    return super
  end
  prefix = doc.attr 'revhistoryprefix', 'rhrev'
  change_attr = %(#{prefix}#{revnumber.to_s.tr '.', '-'})

  result = []
  id_attribute = node.id ? %( id="#{node.id}") : ''
  frame = 'ends' if (frame = node.attr 'frame', 'all', 'table-frame') == 'topbot'
  classes = ['tableblock', %(frame-#{frame}), %(grid-#{node.attr 'grid', 'all', 'table-grid'})]
  if (stripes = node.attr 'stripes', nil, 'table-stripes')
    classes << %(stripes-#{stripes})
  end
  width_attribute = ''
  if (autowidth = node.option? 'autowidth') && !(node.attr? 'width')
    classes << 'fit-content'
  elsif (tablewidth = node.attr 'tablepcwidth') == 100
    classes << 'stretch'
  else
    width_attribute = %( width="#{tablewidth}%")
  end
  classes << (node.attr 'float') if node.attr? 'float'
  if (role = node.role)
    classes << role
  end
  class_attribute = %( class="#{classes.join ' '}")

  result << %(<table#{id_attribute}#{class_attribute}#{width_attribute}>)
  result << %(<caption class="title">#{node.captioned_title}</caption>) if node.title?
  if (node.attr 'rowcount') > 0
    slash = @void_element_slash
    result << '<colgroup>'
    if autowidth
      result += (Array.new node.columns.size, %(<col#{slash}>))
    else
      node.columns.each do |col|
        result << ((col.option? 'autowidth') ? %(<col#{slash}>) : %(<col width="#{col.attr 'colpcwidth'}%"#{slash}>))
      end
    end
    result << '</colgroup>'
    node.rows.to_h.each do |tsec, rows|
      next if rows.empty?
      result << %(<t#{tsec}>)
      rows.each do |row|
        result << '<tr>'
        row.each do |cell|
          if tsec == :head
            cell_content = cell.text
          else
            case cell.style
            when :asciidoc
              cell_content = %(<div class="content">#{cell.content}</div>)
            when :literal
              cell_content = %(<div class="literal"><pre>#{cell.text}</pre></div>)
            else
              cell_content = (cell_content = cell.content).empty? ? '' : %(<p class="tableblock">#{cell_content.join '</p>
<p class="tableblock">'}</p>)
            end
          end

          cell_tag_name = (tsec == :head || cell.style == :header ? 'th' : 'td')
          cell_classes = ['tableblock', %(halign-#{cell.attr 'halign'}), %(valign-#{cell.attr 'valign'})]
          cell_classes << 'rhrev-changed' if cell.attributes && (cell.attributes.key? change_attr)
          cell_class_attribute = %( class="#{cell_classes.join ' '}")
          cell_colspan_attribute = cell.colspan ? %( colspan="#{cell.colspan}") : ''
          cell_rowspan_attribute = cell.rowspan ? %( rowspan="#{cell.rowspan}") : ''
          cell_style_attribute = (node.document.attr? 'cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'};") : ''
          result << %(<#{cell_tag_name}#{cell_class_attribute}#{cell_colspan_attribute}#{cell_rowspan_attribute}#{cell_style_attribute}>#{cell_content}</#{cell_tag_name}>)
        end
        result << '</tr>'
      end
      result << %(</t#{tsec}>)
    end
  end
  result << '</table>'
  result.join "\n"
end

#enclosing_section(node) ⇒ Object

Walks up from node to its nearest enclosing :section, or nil if none exists (the document preamble). Same shape as Converter#enclosing_section on the PDF side.



387
388
389
390
391
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 387

def enclosing_section node
  section = node.parent
  section = section.parent until section.nil? || section.context == :section
  section
end

#format_change_as_html_list(text) ⇒ Object



641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 641

def format_change_as_html_list(text)
  return '' if text.to_s.empty?
  
  # Process attribute content (handle line breaks)
  processed = preprocess_attribute_content(text)
  
  # Split by asterisks to create list items
  tokens = processed.split(/\s(?=\*+\s)/)
  
  items = []
  if tokens.empty? || tokens.length == 1 && !processed.include?('*')
    # Single item without asterisk - wrap in list anyway
    items << { level: 1, content: processed }
  else
    tokens.each do |token|
      next if token.strip.empty?
      
      # Count leading asterisks for nesting level
      if token.strip =~ /^(\*+)\s+(.+)/
        level = $1.length
        content = $2
        items << { level: level, content: content }
      else
        items << { level: 1, content: token.strip }
      end
    end
  end
  
  # Always generate HTML list
  html = ['<ul>']
  items.each do |item|
    html << '<li>' + item[:content] + '</li>'
  end
  html << '</ul>'
  html.join("\n")
end

#generate_html_output(doc, catalog) ⇒ Object



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 274

def generate_html_output(doc, catalog)
  html = []
  discrete = doc.attr? 'rhrev-discrete'
  
  # Wrapper
  html << '<div class="rhrev-section">' if discrete
  html << '<section id="revision-history">' unless discrete
  
  # Title
  if (title = doc.attr('rhrev-customization-title'))
    unless title.empty?
      version_label = doc.attr('version-label', 'Revision')
      resolved_title = title.gsub('{version-label}', version_label)
      html << %(<h2>#{resolved_title}</h2>)
    end
  else
    # Default title
    version_label = doc.attr('version-label', 'Revision')
    html << %(<h2>#{version_label} History</h2>)
  end
  
  # Table
  html << generate_html_table(doc, catalog)
  
  # Close wrapper
  html << (discrete ? '</div>' : '</section>')
  
  html.join("\n")
end

#generate_html_table(doc, catalog) ⇒ Object



548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 548

def generate_html_table(doc, catalog)
  column_widths = doc.attr 'rhrev-table-column-width', '30,70'
  widths = column_widths.split(',').map(&:strip)
  
  html = []
  
  # Build table CSS classes with frame and grid
  frame = doc.attr('rhrev-table-frame', 'all')
  grid = doc.attr('rhrev-table-grid', 'all')
  html << %(<table class="tableblock frame-#{frame} grid-#{grid} stretch">)
  
  # Caption as title div
  unless doc.attr? 'rhrev-disable-caption'
    if (caption = doc.attr('rhrev-table-caption'))
      html << %(<div class="title">#{caption}</div>)
    end
  end
  
  html << '<colgroup>'
  widths.each { |w| html << %(<col style="width: #{w}%;">) }
  html << '</colgroup>'
  html << '<tbody>'
  
  # Custom first row
  if (first_row = doc.attr('rhrev-customization-first-row'))
    html << '<tr class="rhrev-custom-first-row">'
    html << %(<td colspan="2">#{first_row}</td>)
    html << '</tr>'
  else
    first_row_left = doc.attr('rhrev-customization-first-row-left')
    first_row_right = doc.attr('rhrev-customization-first-row-right')
    if first_row_left || first_row_right
      html << '<tr class="rhrev-custom-first-row">'
      html << %(<td>#{first_row_left || '&nbsp;'}</td>)
      html << %(<td>#{first_row_right || '&nbsp;'}</td>)
      html << '</tr>'
    end
  end
  
   = doc.attr 'rhrev-localization-location', 'Section'
  major_changes_text = doc.attr 'rhrev-localization-major-changes', 'Major changes since'
  all_text = doc.attr 'rhrev-localization-all', 'All'
  
  catalog.sorted_revisions.each do |revision|
    prevrev_attr = "#{revision}-prevrev"
    prevrevdate_attr = "#{revision}-prevrevdate"
    revision_label = doc.attr('version-label', 'Revision')
    prev_title = doc.attr(prevrev_attr) || revision.tr('-', '.')
    prev_date = doc.attr(prevrevdate_attr) || ""
    prev_rev_text = format_prev_rev(prev_title, prev_date, doc)
    
    # Build header text
    if prev_title.strip.downcase.start_with?(revision_label.downcase)
      header_text = "#{major_changes_text} #{prev_rev_text}"
    else
      header_text = "#{major_changes_text} #{revision_label} #{prev_rev_text}"
    end
    
    # Header row for this revision
    html << '<tr class="rhrev-header">'
    html << %(<th class="tableblock halign-left valign-top">#{}</th>)
    html << %(<th class="tableblock halign-left valign-top"><strong>#{header_text}</strong></th>)
    html << '</tr>'
    
    # All entry
    if (all_change = catalog.instance_variable_get(:@all_entries)[revision])
      html << '<tr>'
      html << %(<td>#{all_text}</td>)
      html << %(<td>#{format_change_as_html_list(all_change)}</td>)
      html << '</tr>'
    end
    
    # Block entries
    entries = catalog.entries[revision] || []
    entries.sort_by! { |e| e[:sequence] || 0 }
    
    entries.each do |entry|
      anchor = entry[:anchor]
      title = entry[:title] || anchor.tr('_-', ' ').split.map(&:capitalize).join(' ')
      
      html << '<tr>'
      html << %(<td><a href="##{anchor}">#{title}</a></td>)
      html << %(<td>#{format_change_as_html_list(entry[:change])}</td>)
      html << '</tr>'
    end
  end
  
  html << '</tbody>'
  html << '</table>'
  
  html.join("\n")
end

#generate_initial_release_html(doc) ⇒ Object



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 304

def generate_initial_release_html(doc)
  html = []
  discrete = doc.attr? 'rhrev-discrete'
  
  # Wrapper
  html << '<div class="rhrev-section">' if discrete
  html << '<section id="revision-history">' unless discrete
  
  # Title
  if (title = doc.attr('rhrev-customization-title'))
    unless title.empty?
      version_label = doc.attr('version-label', 'Revision')
      resolved_title = title.gsub('{version-label}', version_label)
      html << %(<h2>#{resolved_title}</h2>)
    end
  else
    version_label = doc.attr('version-label', 'Revision')
    html << %(<h2>#{version_label} History</h2>)
  end
  
  # Simple table
  initial_text = doc.attr('rhrev-customization-initial-release-text') ||
                 doc.attr('rhrev-localization-initial-release') ||
                 'Initial release'
  
  column_widths = doc.attr('rhrev-table-column-width', '30,70').split(',').map(&:strip)
   = doc.attr('rhrev-localization-location', 'Section')

  # Build table CSS classes with frame and grid
  frame = doc.attr('rhrev-table-frame', 'all')
  grid = doc.attr('rhrev-table-grid', 'all')
  html << %(<table class="tableblock frame-#{frame} grid-#{grid} stretch">)
  
  # Caption as title div
  unless doc.attr? 'rhrev-disable-caption'
    if (caption = doc.attr('rhrev-table-caption'))
      html << %(<div class="title">#{caption}</div>)
    end
  end
  
  html << '<colgroup>'
  column_widths.each { |w| html << %(<col style="width: #{w}%;">)}
  html << '</colgroup>'
  html << '<tbody>'

  # Custom first row
  if (first_row = doc.attr('rhrev-customization-first-row'))
    html << '<tr class="rhrev-custom-first-row">'
    html << %(<td colspan="2">#{first_row}</td>)
    html << '</tr>'
  else
    first_row_left = doc.attr('rhrev-customization-first-row-left')
    first_row_right = doc.attr('rhrev-customization-first-row-right')
    if first_row_left || first_row_right
      html << '<tr class="rhrev-custom-first-row">'
      html << %(<td>#{first_row_left || '&nbsp;'}</td>)
      html << %(<td>#{first_row_right || '&nbsp;'}</td>)
      html << '</tr>'
    end
  end

  html << '<tr>'
  html << %(<th class="tableblock halign-left valign-top">#{}</th>)
  html << %(<td class="tableblock halign-left valign-top">#{initial_text}</td>)
  html << '</tr>'
  html << '</tbody>'
  html << '</table>'
  
  html << (discrete ? '</div>' : '</section>')
  
  html.join("\n")
end

#is_initial_release?(revnumber) ⇒ Boolean

Returns:

  • (Boolean)


270
271
272
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 270

def is_initial_release?(revnumber)
  revnumber.to_s == '1.0' || revnumber.to_s == '1'
end

#rollup_child_revision_entries(catalog, doc, prefix) ⇒ Object

Gathers every marked rollup-context node in one unified, document-order traversal (same reasoning as Converter#rollup_child_revision_entries: a per-context-type find_by(context:) loop would find every paragraph in the whole document before any list, scrambling bullet order within a section). Must run after the direct-mark loop above, so a section's own entry, if it is also independently marked, already exists for a child's bullet to append onto instead of racing to create a duplicate.



434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 434

def rollup_child_revision_entries catalog, doc, prefix
  nodes = doc.find_by { |node| ROLLUP_CONTEXTS.include? node.context }
  nodes.each do |node|
    attr_entries = (node.instance_variable_get(:@attributes) rescue nil)
    next if attr_entries.nil? || attr_entries.empty?

    section = enclosing_section node
    has_section = section && section.id

    attr_entries.each do |key, value|
      key_str = key.to_s
      next unless key_str.start_with?(prefix)
      next if key_str.include?('-all') || key_str.end_with?('-cover')

      revision = key_str.sub(prefix, '')
      if has_section
        append_child_change_to_entry catalog, revision, section, value.to_s
      else
        append_child_change_to_all catalog, revision, value.to_s
      end
    end
  end
end

#rollup_table_cell_revision_entries(catalog, doc, prefix) ⇒ Object

A marked table cell is not a rollup context itself (ROLLUP_CONTEXTS): it rolls up to its own enclosing table, not to the nearest section, and cells are not reachable through the same context-based find_by a normal block-level node is, so this walks table.rows/[:body] directly, same shape as Converter#rollup_table_cell_revision_entries on the PDF side. Same three-tier fallback as rollup_child_revision_entries: the table's own entry if it has an id, else its nearest enclosing section's entry, else the document-level -all entry only when neither exists. By the time this runs, cell.attributes already carries any mark that started on a nested block inside an a| cell: TableCellMarkPropagationHtmlTreeprocessor propagates it during the treeprocessor phase, before this (a converter method) ever runs.



470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
# File 'lib/asciidoctor/rhrev/rhrev_html.rb', line 470

def rollup_table_cell_revision_entries catalog, doc, prefix
  doc.find_by(context: :table).each do |table|
    cells = (table.rows[:head] + table.rows[:body]).flatten
    cells.each do |cell|
      attr_entries = (cell.instance_variable_get(:@attributes) rescue nil)
      next if attr_entries.nil? || attr_entries.empty?

      attr_entries.each do |key, value|
        key_str = key.to_s
        next unless key_str.start_with?(prefix)
        next if key_str.include?('-all') || key_str.end_with?('-cover')

        revision = key_str.sub(prefix, '')
        if table.id
          append_child_change_to_entry catalog, revision, table, value.to_s
        elsif (section = enclosing_section(table)) && section.id
          append_child_change_to_entry catalog, revision, section, value.to_s
        else
          append_child_change_to_all catalog, revision, value.to_s
        end
      end
    end
  end
end