asciidoctor-rhrev 1.9.2 → 1.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1da65b61e67581cc903950852a590dbd89c965414c2abaa40059bb8f445e4c6a
4
- data.tar.gz: 2d6fa89231f2724df9152df5346318a05dfdf1465eb64dfd9d29df114ab6e9fa
3
+ metadata.gz: 88e8d8ff30a9b7b8170298f2c5a8099f6a4ded89dcbceecd3557b4a18bf37344
4
+ data.tar.gz: dfb17de4a7e0fb23e98aa6ffd648172a0e74135fbb5b1d4c6d7f4b52569353bc
5
5
  SHA512:
6
- metadata.gz: 3eb4539c96fbdfb34d70335963d9312121ef2a5c93af4f049731822d0ee236b9c8d2feaf296c225e5e50b12486a599da539ff98e6c79948ff0e573991535cf41
7
- data.tar.gz: 9fe21ece1dd48a8d1c4acc605ad1b4fb9872296ff56a2438f9ccf9689d218c392899ea4c243a879957754b9bc537fd807a8b5e279b967f2d25b77c6aca6372ab
6
+ metadata.gz: d2b30034dba30dddef36b20ee6660a689ed61a2577b2c3b4f1e503d43bd88a018f8b22c2885962b2e90e11e6ac2801b2d790f4afb11f32c694ba1c26411514bb
7
+ data.tar.gz: 93b412dc36911718747efd0161dd1e97685a4b1161795c315c20c09168b5dbcc8c74066e89b254d834d225542ab0098a0326f8a1f1954ee18fb1dbdb74b2d4a8
data/CHANGELOG.adoc CHANGED
@@ -1,5 +1,14 @@
1
1
  = Changelog
2
2
 
3
+ == 1.11.0 (2026-10-28)
4
+
5
+ * Change bar links in HTML output, with `rhrev-change-bars-link` (gem and npm). The HTML backend and the Antora extension give each revision history row the id the PDF backend uses for its named destination, and put a transparent link inside every element tagged `rhrev-changed`, to the row of its change, with the same row choice as the PDF backend. A stylesheet carried by the first link of a page places each link over the bar and the gap beside it. A table's link sits in its caption, and a changed cell's link in its own `<td>` or `<th>`. In an Antora site, a page without a revision history table of its own links to the table page of its component, or else to a table page with `rhrev-scope: site`, which `antora-rhrev-html.js` now records. New `antora-extension/change-bar-links.js`; new regression files `tests/regression/html-change-bar-links.rb` and `tests/regression/js-change-bar-links.js`
6
+
7
+ == 1.10.0 (2026-10-28)
8
+
9
+ * Change bar links, enabled with `rhrev-change-bars-link` alongside `rhrev-change-bars` (PDF backend). Every change bar links to the revision history row of its change, and each filled column of a change bar table links to the row of its own revision. A block that rolls up into its section's row or into the `all` entry links to that row, a table cell links to its table's row, and a block without a row of its own links to its revision's header row. The link of a bar also covers the gap between the text and the bar. Links of nested blocks sit above their section's link, so a click follows the innermost change. The revision history table carries one named destination per row, header, and `all` entry. New regression file `tests/regression/change-bar-links.rb` (gem)
10
+ * Note: gem-only; the Antora extension is unaffected, npm still bumps concurrently per the standing 1.5.1 policy
11
+
3
12
  == 1.9.2 (2026-10-27)
4
13
 
5
14
  * Change bar table labels stand upright, one character per line, read from top to bottom, with the labels of all columns sharing one bottom line; labels no longer turn 90 degrees. `ink_change_bar_table_label` draws each label (gem)
@@ -22,6 +22,12 @@ module Asciidoctor
22
22
  # filled, and every page with a box labels each column at the top of
23
23
  # its body, so an isolated change still shows which revision it is.
24
24
  # Without lanes, as in collapse mode, table draws the bar too.
25
+ #
26
+ # With rhrev-change-bars-link set, every bar and every filled table
27
+ # column links to the revision history row of the change it marks:
28
+ # each bar records the marked node it inks for, and once the
29
+ # revision history has rendered with a named destination on every
30
+ # row, ink_change_bar_links annotates the recorded areas.
25
31
  module ChangeBars
26
32
  CHANGE_BAR_STYLES = %w(bar table).freeze
27
33
 
@@ -48,6 +54,12 @@ module Asciidoctor
48
54
  @change_bar_section_stack = []
49
55
  @change_bar_grid_labeled_pages = {}
50
56
  @change_bar_margin_warned = false
57
+ if doc.attr? 'rhrev-change-bars-link'
58
+ @change_bar_links = []
59
+ @change_bar_link_dests = ::Set.new
60
+ else
61
+ @change_bar_links = @change_bar_link_dests = nil
62
+ end
51
63
  end
52
64
 
53
65
  # Resolved lazily on first ink: the theme (and @media/@folio_placement)
@@ -129,7 +141,7 @@ module Asciidoctor
129
141
  end
130
142
 
131
143
  def record_change_bar_start node
132
- @change_bar_starts[node] = { page: page_number, cursor: cursor, lanes: (node_change_bar_lanes node) }
144
+ @change_bar_starts[node] = { page: page_number, cursor: cursor, lanes: (node_change_bar_lanes node), node: node }
133
145
  end
134
146
 
135
147
  def take_change_bar_start node
@@ -254,7 +266,7 @@ module Asciidoctor
254
266
  # Inks one page's share of a table box between top and bottom: the
255
267
  # columns from change_bar_table_columns, the columns of lanes
256
268
  # filled, the first column nearest the text.
257
- def ink_change_bar_table pgnum, top, bottom, lanes, settings
269
+ def ink_change_bar_table pgnum, top, bottom, lanes, settings, from = nil
258
270
  width = settings[:table_column_width]
259
271
  on_right = change_bar_on_right? pgnum, settings
260
272
  column_of = change_bar_table_columns @change_bar_lane_index.size, settings[:table_max_columns]
@@ -262,8 +274,9 @@ module Asciidoctor
262
274
  left = on_right ? bounds.right + settings[:offset] : bounds.left - settings[:offset] - count * width
263
275
  left = change_bar_inside_page left, count * width, settings[:table_border_width] / 2.0, on_right, settings
264
276
  column_x = ->(column) { on_right ? left + column * width : left + (count - 1 - column) * width }
265
- lanes.map {|lane| column_of[lane] }.uniq.each do |column|
277
+ lanes.group_by {|lane| column_of[lane] }.each do |column, column_lanes|
266
278
  ink_change_bar_fill (column_x.call column), top, bottom, width, settings[:color]
279
+ record_change_bar_link from, column_lanes, (column_x.call column), width, top, bottom if from
267
280
  end
268
281
  save_graphics_state do
269
282
  if settings[:table_grid_width] > 0
@@ -316,6 +329,7 @@ module Asciidoctor
316
329
 
317
330
  # Paints the bar page by page; from/to are { page:, cursor: }, and
318
331
  # from[:lanes] lists the table columns to fill, the first when absent.
332
+ # from[:node] names the marked node a link resolves against.
319
333
  # Called immediately after the block is rendered, so all spanned pages exist.
320
334
  def ink_change_bar from, to
321
335
  return if scratch? || from.nil? || to.nil?
@@ -329,15 +343,21 @@ module Asciidoctor
329
343
  top = pgnum == from[:page] ? from[:cursor] : bounds.top
330
344
  bottom = pgnum == to[:page] ? to[:cursor] : 0
331
345
  next if (height = top - bottom) <= 0
332
- next ink_change_bar_table pgnum, top, bottom, lanes, settings if change_bar_table? settings
333
- ink_change_bar_fill (change_bar_x pgnum, settings), top, bottom, settings[:width], settings[:color]
346
+ next ink_change_bar_table pgnum, top, bottom, lanes, settings, from if change_bar_table? settings
347
+ x = change_bar_x pgnum, settings
348
+ ink_change_bar_fill x, top, bottom, settings[:width], settings[:color]
349
+ # A bar a few points wide makes a poor click target, so its
350
+ # link also covers the gap between the text and the bar
351
+ edge = (change_bar_on_right? pgnum, settings) ? bounds.right : bounds.left
352
+ left = [x, edge].min
353
+ record_change_bar_link from, lanes, left, ([x + settings[:width], edge].max - left), top, bottom
334
354
  end
335
355
  end
336
356
  end
337
357
 
338
- def ink_change_bar_for_extent extent, lanes = nil
358
+ def ink_change_bar_for_extent extent, lanes = nil, node = nil
339
359
  return unless extent
340
- ink_change_bar({ page: extent.from.page, cursor: extent.from.cursor, lanes: lanes },
360
+ ink_change_bar({ page: extent.from.page, cursor: extent.from.cursor, lanes: lanes, node: node },
341
361
  { page: extent.to.page, cursor: extent.to.cursor })
342
362
  end
343
363
 
@@ -365,7 +385,7 @@ module Asciidoctor
365
385
  instance_exec(&block)
366
386
  else
367
387
  block.call extent
368
- ink_change_bar_for_extent extent, lanes
388
+ ink_change_bar_for_extent extent, lanes, node
369
389
  end
370
390
  end
371
391
  else
@@ -382,13 +402,97 @@ module Asciidoctor
382
402
  # casing the others don't.
383
403
  def bracket_change_bar node
384
404
  if @change_bar_attr && !scratch? && (lanes = node_change_bar_lanes node)
385
- bar_from = { page: page_number, cursor: cursor, lanes: lanes }
405
+ bar_from = { page: page_number, cursor: cursor, lanes: lanes, node: node }
386
406
  end
387
407
  result = yield
388
408
  ink_change_bar bar_from, { page: page_number, cursor: cursor } if bar_from
389
409
  result
390
410
  end
391
411
 
412
+ # Records the area of a bar or table column on the current page, at
413
+ # x and width relative to the current bounds, for the link
414
+ # ink_change_bar_links adds once the revision history has rendered.
415
+ def record_change_bar_link from, lanes, x, width, top, bottom
416
+ return unless @change_bar_links && (node = from[:node])
417
+ left = bounds.absolute_left + x
418
+ @change_bar_links << {
419
+ page: page_number,
420
+ rect: [left, bounds.absolute_bottom + bottom, left + width, bounds.absolute_bottom + top],
421
+ node: node,
422
+ lanes: lanes,
423
+ }
424
+ end
425
+
426
+ # Contexts that carry a revision history row of their own when
427
+ # they have an id (Converter#prescan_document).
428
+ OWN_ENTRY_CONTEXTS = [:section, :floating_title, :example, :listing, :table, :image].freeze
429
+
430
+ # The anchor of the revision history row a marked node's change
431
+ # text lands in, :all for the revision's All row, or nil when the
432
+ # node has no row: the node's own id for the contexts with rows of
433
+ # their own, the enclosing table's id for a table cell, and
434
+ # otherwise the enclosing section's id, as rollup_child_revision_
435
+ # entries and rollup_table_cell_revision_entries file them.
436
+ def change_bar_entry_anchor node
437
+ if node.context == :table_cell
438
+ table = node.parent
439
+ table = table.parent until table.nil? || table.context == :table
440
+ return table.id if table&.id
441
+ node = table
442
+ elsif OWN_ENTRY_CONTEXTS.include? node.context
443
+ return node.id
444
+ elsif !(Converter::ROLLUP_CONTEXTS.include? node.context)
445
+ return nil
446
+ end
447
+ (section = enclosing_section node) && section.id ? section.id : :all
448
+ end
449
+
450
+ # Destination names for a revision history row: the row of anchor,
451
+ # the All row, and the revision's header row, for a revision group
452
+ # key such as 1-3.
453
+ def change_bar_entry_dest group, anchor
454
+ %(_rhrev_entry_#{group}_#{anchor})
455
+ end
456
+
457
+ def change_bar_all_dest group
458
+ %(_rhrev_all_#{group})
459
+ end
460
+
461
+ def change_bar_revision_dest group
462
+ %(_rhrev_revision_#{group})
463
+ end
464
+
465
+ # The destination a recorded link points at: the row of the newest
466
+ # revision among the link's lanes that the node marks, or that
467
+ # revision's header row when the revision history has no row for
468
+ # the node. Nil when neither rendered.
469
+ def change_bar_link_dest link
470
+ lane_of = ->(z) { @change_bar_lane_index&.[](z) || 0 }
471
+ marks = @change_bar_matcher.marks link[:node].attributes
472
+ mark, = marks.select {|_, z| link[:lanes].include? lane_of.call z }.max_by {|_, z| lane_of.call z }
473
+ return unless mark
474
+ group = revision_history.revision_key mark.to_s.delete_prefix @revision_prefix
475
+ anchor = change_bar_entry_anchor link[:node]
476
+ row = anchor == :all ? (change_bar_all_dest group) : (change_bar_entry_dest group, anchor if anchor)
477
+ [row, (change_bar_revision_dest group)].find {|name| name && (@change_bar_link_dests.include? name) }
478
+ end
479
+
480
+ # Adds a link annotation over every recorded bar and table column
481
+ # whose revision history row rendered. Viewers hit the annotation
482
+ # added last, and a section's bar, recorded once the section ends,
483
+ # covers the bars of the blocks inside it, so the tallest areas go
484
+ # first and every nested block keeps a link of its own on top.
485
+ def ink_change_bar_links
486
+ return if scratch? || @change_bar_links.nil? || @change_bar_links.empty?
487
+ current = page_number
488
+ @change_bar_links.sort_by.with_index {|link, i| [-(link[:rect][3] - link[:rect][1]), i] }.each do |link|
489
+ next unless (dest = change_bar_link_dest link)
490
+ go_to_page link[:page] unless page_number == link[:page]
491
+ link_annotation link[:rect], Border: [0, 0, 0], Dest: dest
492
+ end
493
+ go_to_page current unless page_number == current
494
+ end
495
+
392
496
  # The heading ink methods run after arrange_heading / start_new_chapter
393
497
  # have settled the page, so this is the first accurate start position.
394
498
  def ink_general_heading node, title, opts = {}
@@ -193,6 +193,9 @@ module Asciidoctor
193
193
  if @revision_history_extent && @catalog
194
194
  ink_revision_history node, @revision_history_extent
195
195
  end
196
+
197
+ # After the revision history, whose rows carry the destinations
198
+ ink_change_bar_links
196
199
 
197
200
  # Re-render tables with pagerhrefs (only in manual mode)
198
201
  ink_pagerhref_tables if @manual_mode && @pagerhref_tables && !@pagerhref_tables.empty?
@@ -314,7 +317,7 @@ module Asciidoctor
314
317
  if @change_bar_attr && !scratch? && (lanes = node_change_bar_lanes node) &&
315
318
  !(!at_page_top? && ((node.option? 'unbreakable') ||
316
319
  ((node.option? 'breakable') && (node.id || node.title?))))
317
- bar_from = { page: page_number, cursor: cursor, lanes: lanes }
320
+ bar_from = { page: page_number, cursor: cursor, lanes: lanes, node: node }
318
321
  end
319
322
  result = super
320
323
  catalog_block_anchor node
@@ -332,14 +335,14 @@ module Asciidoctor
332
335
  # PDF targets import whole pages; there is no cursor flow to mark
333
336
  if @change_bar_attr && !scratch? && (lanes = node_change_bar_lanes node) &&
334
337
  !((node.attr 'target').to_s.downcase.end_with? '.pdf')
335
- bar_from = { page: page_number, cursor: cursor, lanes: lanes }
338
+ bar_from = { page: page_number, cursor: cursor, lanes: lanes, node: node }
336
339
  end
337
340
  result = super
338
341
  catalog_block_anchor node
339
342
  if bar_from
340
343
  bar_to = { page: page_number, cursor: cursor }
341
344
  # Images never split; a page change means the image moved wholesale
342
- bar_from = { page: bar_to[:page], cursor: bounds.top, lanes: bar_from[:lanes] } if bar_to[:page] > bar_from[:page]
345
+ bar_from = bar_from.merge page: bar_to[:page], cursor: bounds.top if bar_to[:page] > bar_from[:page]
343
346
  ink_change_bar bar_from, bar_to
344
347
  end
345
348
  result
@@ -469,9 +472,10 @@ module Asciidoctor
469
472
  # trying to call super itself.
470
473
  def convert_targeted_list node, targets = (list_item_change_bar_targets node)
471
474
  @list_change_bar_target_stack ||= []
472
- @list_change_bar_target_stack.push targets
475
+ # The list rides along so an item's bar links to the list's row
476
+ @list_change_bar_target_stack.push(targets && (targets.merge node: node))
473
477
  if targets && !targets[:whole].empty? && !scratch?
474
- bar_from = { page: page_number, cursor: cursor, lanes: targets[:whole] }
478
+ bar_from = { page: page_number, cursor: cursor, lanes: targets[:whole], node: node }
475
479
  result = yield
476
480
  ink_change_bar bar_from, { page: page_number, cursor: cursor }
477
481
  result
@@ -559,14 +563,14 @@ module Asciidoctor
559
563
  def convert_list_item node, list, opts = {}
560
564
  targets = @list_change_bar_target_stack&.last
561
565
  if targets && (idx = list.items.index node) && (lanes = targets[:items][idx + 1])
562
- ink_targeted_list_item_change_bar(lanes) { super }
566
+ ink_targeted_list_item_change_bar(lanes, targets[:node]) { super }
563
567
  else
564
568
  super
565
569
  end
566
570
  end
567
571
 
568
- def ink_targeted_list_item_change_bar lanes
569
- bar_from = { page: page_number, cursor: cursor, lanes: lanes } if @change_bar_attr && !scratch?
572
+ def ink_targeted_list_item_change_bar lanes, list
573
+ bar_from = { page: page_number, cursor: cursor, lanes: lanes, node: list } if @change_bar_attr && !scratch?
570
574
  result = yield
571
575
  ink_change_bar bar_from, { page: page_number, cursor: cursor } if bar_from
572
576
  result
@@ -449,12 +449,12 @@ module Asciidoctor
449
449
  header_text = "*#{major_changes_text} #{revision_label} #{prev_rev_text}*"
450
450
  end
451
451
 
452
- page_cell = build_text_cell table, 0, "*#{page_label}*"
452
+ page_cell = build_text_cell table, 0, "#{change_bar_link_anchor change_bar_revision_dest revision}*#{page_label}*"
453
453
  header_cell = build_text_cell table, 1, header_text
454
454
  table.rows.body << [page_cell, header_cell]
455
455
 
456
456
  if (all_change = revision_history.instance_variable_get(:@all_entries)[revision])
457
- row = build_list_row table, all_text, all_change, nil, nil, template_location_cell, template_asciidoc_cell, doc, is_all_or_cover: true
457
+ row = build_list_row table, "#{change_bar_link_anchor change_bar_all_dest revision}#{all_text}", all_change, nil, nil, template_location_cell, template_asciidoc_cell, doc, is_all_or_cover: true
458
458
  table.rows.body << row
459
459
  end
460
460
 
@@ -475,7 +475,8 @@ module Asciidoctor
475
475
 
476
476
  entries.each do |entry|
477
477
  location = build_location_text entry, doc
478
- row = build_list_row table, location, entry[:change], entry[:anchor], entry, template_location_cell, template_asciidoc_cell, doc
478
+ dest = change_bar_entry_dest revision, entry[:anchor] if @change_bar_link_dests && entry[:anchor]
479
+ row = build_list_row table, location, entry[:change], entry[:anchor], entry, template_location_cell, template_asciidoc_cell, doc, dest: dest
479
480
  table.rows.body << row
480
481
  end
481
482
  end
@@ -518,15 +519,21 @@ module Asciidoctor
518
519
  lines.join("\n")
519
520
  end
520
521
 
521
- def build_list_row table, location_text, change_text, anchor, entry, template_location_cell, template_asciidoc_cell, doc, is_all_or_cover: false
522
+ def build_list_row table, location_text, change_text, anchor, entry, template_location_cell, template_asciidoc_cell, doc, is_all_or_cover: false, dest: nil
523
+ if dest
524
+ @change_bar_link_dests << dest
525
+ # Raw formatter markup, as asciidoctor-pdf converts an inline anchor
526
+ dest_marker = %(<a id="#{dest}">#{::Asciidoctor::PDF::Converter::DummyText}</a>)
527
+ end
522
528
  if anchor && location_text != '???'
523
529
  location_cell = ::Asciidoctor::Table::Cell.new(template_location_cell.column, "", template_asciidoc_cell.attributes.dup)
524
530
  location_cell.inner_document.blocks.clear
525
531
  para_block = ::Asciidoctor::Block.new(location_cell.inner_document, :paragraph)
526
532
  formatted_location = format_with_role(location_text, 'rhrevpage')
527
- para_block.lines = ["<a anchor=\"#{anchor}\">#{formatted_location}</a>"]
533
+ para_block.lines = ["#{dest_marker}<a anchor=\"#{anchor}\">#{formatted_location}</a>"]
528
534
  location_cell.inner_document.blocks << para_block
529
535
  else
536
+ location_text = %([[#{dest}]]#{location_text}) if dest
530
537
  location_cell = ::Asciidoctor::Table::Cell.new(template_location_cell.column, location_text, template_location_cell.attributes.dup)
531
538
  end
532
539
 
@@ -585,6 +592,14 @@ module Asciidoctor
585
592
  [location_cell, change_cell]
586
593
  end
587
594
 
595
+ # An inline anchor naming dest, which also records it as rendered,
596
+ # or an empty string without rhrev-change-bars-link.
597
+ def change_bar_link_anchor dest
598
+ return '' unless @change_bar_link_dests
599
+ @change_bar_link_dests << dest
600
+ %([[#{dest}]])
601
+ end
602
+
588
603
  def build_cell table, col_idx, text
589
604
  column = table.columns[col_idx]
590
605
  ::Asciidoctor::Table::Cell.new column, text
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'asciidoctor'
4
4
  require 'asciidoctor/converter/html5'
5
+ require 'set'
5
6
 
6
7
  module Asciidoctor
7
8
  module Rhrev
@@ -69,6 +70,74 @@ module Asciidoctor
69
70
  end
70
71
  end
71
72
 
73
+ # Change bar links in HTML output, enabled by rhrev-change-bars-link:
74
+ # each changed element carries a transparent link over its bar and the
75
+ # gap beside it, to the revision history row of its change. The row
76
+ # choice matches the PDF backend's ChangeBars#change_bar_link_dest, and
77
+ # the row ids match its named destinations.
78
+ module HtmlChangeLinks
79
+ OWN_ENTRY_CONTEXTS = [:section, :floating_title, :example, :listing, :table, :image].freeze
80
+
81
+ # Positions each link over the bar of its element. The bar is the
82
+ # element's right border, set by the site stylesheet, so the link
83
+ # spans the border and the padding beside it. It rides on the first
84
+ # link of a document, so a document without links gets none.
85
+ STYLE = '<style>.rhrev-changed { position: relative; } ' \
86
+ '.rhrev-change-link { position: absolute; top: 0; bottom: 0; ' \
87
+ 'right: calc(-1 * var(--rhrev-change-bar-width, 2px)); ' \
88
+ 'width: calc(var(--rhrev-change-bar-offset, 16px) + var(--rhrev-change-bar-width, 2px)); } ' \
89
+ 'caption.rhrev-change-link-host { padding: 0; margin: 0; height: 0; }</style>'
90
+
91
+ module_function
92
+
93
+ def entry_id group, anchor
94
+ %(_rhrev_entry_#{group}_#{anchor})
95
+ end
96
+
97
+ def all_id group
98
+ %(_rhrev_all_#{group})
99
+ end
100
+
101
+ def revision_id group
102
+ %(_rhrev_revision_#{group})
103
+ end
104
+
105
+ # The row ids a node's link tries, best first: the row of its change,
106
+ # then the header of its revision. The change is the node's newest
107
+ # mark that draws a bar.
108
+ def candidates node, matcher, prefix
109
+ marks = matcher.marks node.attributes
110
+ return [] if marks.empty?
111
+ key, = ThirdLevel.sort_marks(marks, prefix, matcher.kind).last
112
+ group = ThirdLevel.group_key (ThirdLevel.revision_of key, prefix), matcher.mode
113
+ anchor = entry_anchor node
114
+ row = anchor == :all ? (all_id group) : (entry_id group, anchor if anchor)
115
+ [row, (revision_id group)].compact
116
+ end
117
+
118
+ # The anchor of the row a node's change text lands in, :all for the
119
+ # All row, or nil when the node has no row.
120
+ def entry_anchor node
121
+ if node.context == :table_cell
122
+ table = node.parent
123
+ table = table.parent until table.nil? || table.context == :table
124
+ return table.id if table&.id
125
+ node = table
126
+ elsif OWN_ENTRY_CONTEXTS.include? node.context
127
+ return node.id
128
+ elsif !(Html5Converter::ROLLUP_CONTEXTS.include? node.context)
129
+ return nil
130
+ end
131
+ section = node.parent
132
+ section = section.parent until section.nil? || section.context == :section
133
+ section&.id ? section.id : :all
134
+ end
135
+
136
+ def link_html href, doc
137
+ %(<a class="rhrev-change-link" href="#{href}" aria-label="#{doc.attr 'version-label', 'Revision'} History"></a>)
138
+ end
139
+ end
140
+
72
141
  # Tags blocks marked with the current revision (revnumber 1.2 => attribute
73
142
  # rhrev1-2) with the rhrev-changed role so a stylesheet can draw an HTML
74
143
  # change bar (border on the right side). Enabled by the rhrev-change-bars
@@ -95,11 +164,13 @@ module Asciidoctor
95
164
  prefix = document.attr 'revhistoryprefix', 'rhrev'
96
165
  matcher = ThirdLevel.matcher document, prefix, revnumber
97
166
 
167
+ link = document.attr? 'rhrev-change-bars-link'
98
168
  tagged = false
99
169
  document.find_by { |b| TRACKED_CONTEXTS.include? b.context }.each do |block|
100
170
  next unless block.attributes && (matcher.match? block.attributes)
101
171
  roles = (block.attr 'role')
102
172
  block.set_attr 'role', [roles, 'rhrev-changed'].compact.join(' ')
173
+ block.instance_variable_set :@rhrev_change_link, (HtmlChangeLinks.candidates block, matcher, prefix) if link
103
174
  tagged = true
104
175
  end
105
176
  return document unless tagged
@@ -144,11 +215,41 @@ module Asciidoctor
144
215
  def convert node, transform = node.node_name, opts = {}
145
216
  if node.context == :rhrev
146
217
  convert_rhrev_block node
218
+ elsif node.context != :table && transform == node.node_name && (candidates = node.instance_variable_get :@rhrev_change_link)
219
+ (link = change_link node.document, candidates) ? (super.sub(/\A\s*<[^>]*>/) { %(#{$&}#{link}) }) : super
147
220
  else
148
221
  super
149
222
  end
150
223
  end
151
224
 
225
+ # The link to the first candidate row the revision history table
226
+ # renders, or nil when it renders none of them. The first link of the
227
+ # document carries the stylesheet.
228
+ def change_link doc, candidates
229
+ return unless (id = candidates.find { |candidate| rhrev_row_ids(doc).include? candidate })
230
+ style = ((@rhrev_styled ||= ::Set.new).add? doc.object_id) ? HtmlChangeLinks::STYLE : ''
231
+ %(#{style}#{HtmlChangeLinks.link_html %(##{id}), doc})
232
+ end
233
+
234
+ # The ids of the revision history rows the document renders, the same
235
+ # set generate_html_table assigns. Empty without a revision history
236
+ # table in the document.
237
+ def rhrev_row_ids doc
238
+ (@rhrev_row_ids ||= {})[doc.object_id] ||= begin
239
+ ids = ::Set.new
240
+ revnumber = doc.attr 'revnumber', '1.0'
241
+ if (doc.find_by(context: :rhrev).any?) && (doc.attr 'rhrev') != 'manual' && !(is_initial_release? revnumber) &&
242
+ (catalog = collect_all_entries doc)
243
+ catalog.sorted_revisions.each do |revision|
244
+ ids << (HtmlChangeLinks.revision_id revision)
245
+ ids << (HtmlChangeLinks.all_id revision) if catalog.all_entries[revision]
246
+ (catalog.entries[revision] || []).each { |entry| ids << (HtmlChangeLinks.entry_id revision, entry[:anchor]) }
247
+ end
248
+ end
249
+ ids
250
+ end
251
+ end
252
+
152
253
  # Marks an individual changed table cell with the rhrev-changed role
153
254
  # directly on its <td>/<th>, the requested mechanism: no bundled
154
255
  # styling, a stylesheet decides how a changed cell looks, same as
@@ -199,7 +300,13 @@ module Asciidoctor
199
300
  class_attribute = %( class="#{classes.join ' '}")
200
301
 
201
302
  result << %(<table#{id_attribute}#{class_attribute}#{width_attribute}>)
202
- result << %(<caption class="title">#{node.captioned_title}</caption>) if node.title?
303
+ table_link = (candidates = node.instance_variable_get :@rhrev_change_link) && (change_link doc, candidates)
304
+ if node.title?
305
+ result << %(<caption class="title">#{table_link}#{node.captioned_title}</caption>)
306
+ elsif table_link
307
+ result << %(<caption class="rhrev-change-link-host">#{table_link}</caption>)
308
+ end
309
+ link = doc.attr? 'rhrev-change-bars-link'
203
310
  if (node.attr 'rowcount') > 0
204
311
  slash = @void_element_slash
205
312
  result << '<colgroup>'
@@ -233,12 +340,16 @@ module Asciidoctor
233
340
 
234
341
  cell_tag_name = (tsec == :head || cell.style == :header ? 'th' : 'td')
235
342
  cell_classes = ['tableblock', %(halign-#{cell.attr 'halign'}), %(valign-#{cell.attr 'valign'})]
236
- cell_classes << 'rhrev-changed' if cell.attributes && (matcher.match? cell.attributes)
343
+ cell_link = nil
344
+ if cell.attributes && (matcher.match? cell.attributes)
345
+ cell_classes << 'rhrev-changed'
346
+ cell_link = change_link doc, (HtmlChangeLinks.candidates cell, matcher, prefix) if link
347
+ end
237
348
  cell_class_attribute = %( class="#{cell_classes.join ' '}")
238
349
  cell_colspan_attribute = cell.colspan ? %( colspan="#{cell.colspan}") : ''
239
350
  cell_rowspan_attribute = cell.rowspan ? %( rowspan="#{cell.rowspan}") : ''
240
351
  cell_style_attribute = (node.document.attr? 'cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'};") : ''
241
- result << %(<#{cell_tag_name}#{cell_class_attribute}#{cell_colspan_attribute}#{cell_rowspan_attribute}#{cell_style_attribute}>#{cell_content}</#{cell_tag_name}>)
352
+ result << %(<#{cell_tag_name}#{cell_class_attribute}#{cell_colspan_attribute}#{cell_rowspan_attribute}#{cell_style_attribute}>#{cell_link}#{cell_content}</#{cell_tag_name}>)
242
353
  end
243
354
  result << '</tr>'
244
355
  end
@@ -607,6 +718,9 @@ module Asciidoctor
607
718
  major_changes_text = doc.attr 'rhrev-localization-major-changes', 'Major changes since'
608
719
  all_text = doc.attr 'rhrev-localization-all', 'All'
609
720
 
721
+ link = doc.attr? 'rhrev-change-bars-link'
722
+ row_id = ->(id) { link ? %( id="#{id}") : '' }
723
+ entry_ids = ::Set.new
610
724
  catalog.sorted_revisions.each do |revision|
611
725
  third_level = ThirdLevel.mode doc
612
726
  revision_label = doc.attr('version-label', 'Revision')
@@ -622,14 +736,14 @@ module Asciidoctor
622
736
  end
623
737
 
624
738
  # Header row for this revision
625
- html << '<tr class="rhrev-header">'
739
+ html << %(<tr class="rhrev-header"#{row_id.call HtmlChangeLinks.revision_id revision}>)
626
740
  html << %(<th class="tableblock halign-left valign-top">#{section_label}</th>)
627
741
  html << %(<th class="tableblock halign-left valign-top"><strong>#{header_text}</strong></th>)
628
742
  html << '</tr>'
629
743
 
630
744
  # All entry
631
745
  if (all_change = catalog.instance_variable_get(:@all_entries)[revision])
632
- html << '<tr>'
746
+ html << %(<tr#{row_id.call HtmlChangeLinks.all_id revision}>)
633
747
  html << %(<td>#{all_text}</td>)
634
748
  html << %(<td>#{format_change_as_html_list(all_change)}</td>)
635
749
  html << '</tr>'
@@ -642,8 +756,9 @@ module Asciidoctor
642
756
  entries.each do |entry|
643
757
  anchor = entry[:anchor]
644
758
  title = entry[:title] || anchor.tr('_-', ' ').split.map(&:capitalize).join(' ')
759
+ id = HtmlChangeLinks.entry_id revision, anchor
645
760
 
646
- html << '<tr>'
761
+ html << %(<tr#{(entry_ids.add? id) ? (row_id.call id) : ''}>)
647
762
  html << %(<td><a href="##{anchor}">#{title}</a></td>)
648
763
  html << %(<td>#{format_change_as_html_list(entry[:change])}</td>)
649
764
  html << '</tr>'
@@ -49,14 +49,14 @@ if defined? ::Prawn::Table
49
49
  module Rhrev
50
50
  module TableCellChangeBarPatch
51
51
  # The change-bar lanes the cell draws (ChangeBars#change_bar_lanes),
52
- # or nil for an unmarked cell.
53
- attr_accessor :rhrev_marked
52
+ # or nil for an unmarked cell, and the marked Asciidoctor cell.
53
+ attr_accessor :rhrev_marked, :rhrev_marked_node
54
54
 
55
55
  def draw_background pt
56
56
  if @rhrev_marked && @pdf.respond_to?(:ink_change_bar)
57
57
  x, y = pt
58
58
  page = @pdf.page_number
59
- @pdf.ink_change_bar({ page: page, cursor: y, lanes: @rhrev_marked }, { page: page, cursor: y - height })
59
+ @pdf.ink_change_bar({ page: page, cursor: y, lanes: @rhrev_marked, node: @rhrev_marked_node }, { page: page, cursor: y - height })
60
60
  end
61
61
  super
62
62
  end
@@ -82,8 +82,10 @@ if defined? ::Prawn::Table
82
82
  next unless (cell_data = data[r] && data[r][c]).is_a?(::Hash)
83
83
  if (content = cell_data[:content]).is_a? ::Prawn::Table::Cell
84
84
  content.rhrev_marked = lanes
85
+ content.rhrev_marked_node = cell
85
86
  else
86
87
  cell_data[:rhrev_marked] = lanes
88
+ cell_data[:rhrev_marked_node] = cell
87
89
  end
88
90
  end
89
91
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asciidoctor-rhrev
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.2
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - 白一百 baiyibai