asciidoctor-rhrev 1.8.2 → 1.9.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 +4 -4
- data/CHANGELOG.adoc +11 -0
- data/lib/asciidoctor/rhrev/catalog.rb +44 -9
- data/lib/asciidoctor/rhrev/change_bars.rb +187 -20
- data/lib/asciidoctor/rhrev/converter.rb +109 -55
- data/lib/asciidoctor/rhrev/exporter.rb +2 -4
- data/lib/asciidoctor/rhrev/mark_targets.rb +57 -0
- data/lib/asciidoctor/rhrev/renderer.rb +2 -4
- data/lib/asciidoctor/rhrev/rhrev_html.rb +30 -20
- data/lib/asciidoctor/rhrev/table_cell_change_bar_patch.rb +6 -4
- data/lib/asciidoctor/rhrev/third_level.rb +212 -0
- data/lib/asciidoctor/rhrev.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e018143599f73eace12e5f22c99a54119721a6358e61abb67d5040b81118712b
|
|
4
|
+
data.tar.gz: 194a1c032bd7602dfffe37c6bf87e069ff64d19e172244e3f05fab5effdf7947
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1f9463fa7dc56dbb4f0d521ff547806724927ffb4b4f87289a30262bf2f7aba2812b6c99eb5eb58927d30e346b80740591f16fe0458559e1e1c2341765b2c64
|
|
7
|
+
data.tar.gz: 69433c330dca726113054335cff0ac2856882f6f18d07eaf31866ecec4f9543f6276a871f9c8409f942e1e2ee784a69b0ae06943c68b222c054c8c57d73677f9
|
data/CHANGELOG.adoc
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
= Changelog
|
|
2
2
|
|
|
3
|
+
== 1.9.0 (2026-10-27)
|
|
4
|
+
|
|
5
|
+
* Third-level revisions such as `1.3.2`. The `rhrev-third-level` attribute selects `collapse` or `expand`, and `rhrev-third-level-kind` selects `draft`, the default, or `amendment`. With either mode, every mark of the current `x.y` release up to the current third level draws a change bar. `collapse` groups the revision history table by `x.y`, merging change texts and `all` and `cover` entries, with the group header reading `x-y-prevrev` and falling back to the lowest third-level `prevrev`. `expand` keeps one group per revision. `draft` orders `x.y` after its drafts, and a bare `x.y` mark beside its own drafts logs a warning and is ignored everywhere, `all` and `cover` entries included; `amendment` orders `x.y` before its amendments. New `third_level.rb` and `antora-extension/third-level.js`; the Antora extensions parse every page first so a release ignored on one page is ignored on all. Unset, output is unchanged (gem and npm)
|
|
6
|
+
* Change bar style `table`, set with `rhrev-change-bars-style` or the theme key `rhrev_change_bars_style` (PDF backend). In expand mode every changed block gets a box in the margin with one column per revision, its own revisions' columns filled, and each page labels the columns at the top of its body. The `table` theme keys set `column-width`, `max-columns`, `border-color`, `border-width`, `grid-color`, `grid-width`, and the label font family, style, size, and color. Beyond `max-columns`, the first column accumulates the oldest revisions and carries the newest one's label; labels wider than a column turn 90 degrees. The default style `bar` and collapse mode draw the single bar (gem)
|
|
7
|
+
* New regression files `tests/regression/third-level.rb` and `tests/regression/js-third-level.js`
|
|
8
|
+
|
|
9
|
+
== 1.8.3 (2026-10-22)
|
|
10
|
+
|
|
11
|
+
* Fix per-item list change bars landing on another revision's items when marks for several revisions stack one per attribute line (PDF backend). Asciidoctor numbers positional entries per attribute line and merges the lines into one hash, so a later line's targets overwrote an earlier line's: `[rhrev1-2="...",2]` followed by `[rhrev1-3="..."]` barred item 2 for revision 1.3 instead of the whole list, and a current mark on the first line took the targets of the line below it. The new `mark_targets.rb` hooks `AttributeList#parse_into`, reads each block attribute line's own memoized parse result, and records which positional entries follow which mark on that line; `Converter#list_item_change_bar_targets` reads the current mark's own record and falls back to every positional entry only for a list built without one. Reported against a real document. Stacked-line checks in `tests/regression/list-item-target-change-bars.rb` cover a current mark with no targets, a current mark on the first line, a later line with fewer targets, a custom `revhistoryprefix`, and a style before the mark, plus a render spy that fails (bars on the wrong items) without the fix
|
|
12
|
+
* Note: gem-only; the Antora extension has no per-item list targets, npm still bumps concurrently per the standing 1.5.1 policy
|
|
13
|
+
|
|
3
14
|
== 1.8.2 (2026-10-06)
|
|
4
15
|
|
|
5
16
|
* Change the initial-release table's default text from `Initial Release` to `Initial release` and render it bold (PDF backend), matching the bold `Page` cell beside it; `rhrev-customization-initial-release-text` still overrides the text and is rendered bold too
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative 'third_level'
|
|
2
|
+
|
|
1
3
|
module Asciidoctor
|
|
2
4
|
module Rhrev
|
|
3
5
|
class Catalog
|
|
@@ -5,11 +7,21 @@ module Asciidoctor
|
|
|
5
7
|
attr_accessor :all_entries
|
|
6
8
|
attr_accessor :cover_entries
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
# third_level is the rhrev-third-level mode; in collapse mode every
|
|
11
|
+
# revision is stored under its x-y group (ThirdLevel.group_key).
|
|
12
|
+
# kind is the rhrev-third-level-kind, which orders an x-y revision
|
|
13
|
+
# relative to its x-y-z revisions (ThirdLevel.sort_key).
|
|
14
|
+
def initialize third_level: nil, kind: nil
|
|
9
15
|
@entries = {} # Hash of revision => [entries]
|
|
10
16
|
@all_entries = {} # Hash of revision => text for 'all' entries
|
|
11
17
|
@cover_entries = {} # Hash of revision => text for 'cover' entries
|
|
12
18
|
@sequence_counter = 0 # Counter to track document order
|
|
19
|
+
@third_level = third_level
|
|
20
|
+
@kind = kind
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def revision_key revision
|
|
24
|
+
::Asciidoctor::Rhrev::ThirdLevel.group_key revision, @third_level
|
|
13
25
|
end
|
|
14
26
|
|
|
15
27
|
def self.default_labels
|
|
@@ -24,15 +36,26 @@ module Asciidoctor
|
|
|
24
36
|
end
|
|
25
37
|
|
|
26
38
|
def add_entry revision, anchor, change_text, reftext: nil, title: nil, sectnum: nil, context: nil, is_chapter: false, sectname: nil, caption_number: nil, source_line: nil
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
key = revision_key revision
|
|
40
|
+
@entries[key] ||= []
|
|
41
|
+
|
|
42
|
+
# Prevent duplicates (e.g. if collected multiple times). In collapse
|
|
43
|
+
# mode, one anchor marked in several revisions of the same group
|
|
44
|
+
# merges into one entry, its change texts joined as bullets.
|
|
45
|
+
if (existing = @entries[key].find { |e| e[:anchor] == anchor })
|
|
46
|
+
return if existing[:revisions].include? revision
|
|
47
|
+
existing[:revisions] << revision
|
|
48
|
+
unless change_text.to_s.empty?
|
|
49
|
+
existing[:change] = existing[:change].to_s.empty? ? change_text : "#{existing[:change]} * #{change_text}"
|
|
50
|
+
end
|
|
51
|
+
return
|
|
52
|
+
end
|
|
31
53
|
|
|
32
54
|
@sequence_counter += 1
|
|
33
55
|
# Use source_line if available, otherwise fall back to sequence counter
|
|
34
56
|
sequence = source_line || @sequence_counter
|
|
35
|
-
@entries[
|
|
57
|
+
@entries[key] << {
|
|
58
|
+
revisions: [revision],
|
|
36
59
|
anchor: anchor,
|
|
37
60
|
change: change_text,
|
|
38
61
|
dest: nil,
|
|
@@ -48,11 +71,23 @@ module Asciidoctor
|
|
|
48
71
|
end
|
|
49
72
|
|
|
50
73
|
def add_all_entry revision, change_text
|
|
51
|
-
@all_entries[revision] = change_text
|
|
74
|
+
@all_entries[revision_key revision] = change_text
|
|
52
75
|
end
|
|
53
76
|
|
|
54
77
|
def add_cover_entry revision, change_text
|
|
55
|
-
@cover_entries[revision] = change_text
|
|
78
|
+
@cover_entries[revision_key revision] = change_text
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Adds change_text to the revision's 'all' or 'cover' text, joined as
|
|
82
|
+
# another bullet when the group already has one.
|
|
83
|
+
def append_all_entry revision, change_text
|
|
84
|
+
existing = @all_entries[revision_key revision]
|
|
85
|
+
add_all_entry revision, (existing.to_s.empty? ? change_text : "#{existing} * #{change_text}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def append_cover_entry revision, change_text
|
|
89
|
+
existing = @cover_entries[revision_key revision]
|
|
90
|
+
add_cover_entry revision, (existing.to_s.empty? ? change_text : "#{existing} * #{change_text}")
|
|
56
91
|
end
|
|
57
92
|
|
|
58
93
|
def link_dest_to_page anchor, physical_page_number, start_page_number, y: nil
|
|
@@ -121,7 +156,7 @@ module Asciidoctor
|
|
|
121
156
|
|
|
122
157
|
def parse_revision rev_string
|
|
123
158
|
# Parse "1-1", "1-2", "1-10-2" into comparable array [1, 1], [1, 2], [1, 10, 2]
|
|
124
|
-
rev_string
|
|
159
|
+
::Asciidoctor::Rhrev::ThirdLevel.sort_key rev_string, @kind
|
|
125
160
|
end
|
|
126
161
|
end
|
|
127
162
|
end
|
|
@@ -8,16 +8,33 @@ module Asciidoctor
|
|
|
8
8
|
# Enabled by the rhrev-change-bars document attribute.
|
|
9
9
|
#
|
|
10
10
|
# Styling comes from the PDF theme by default (rhrev_change_bars category:
|
|
11
|
-
# color, width, offset, side
|
|
12
|
-
# rhrev-change-bars-
|
|
11
|
+
# style, color, width, offset, side, and the table_* keys), the first
|
|
12
|
+
# five overridable per document via the rhrev-change-bars-style/
|
|
13
|
+
# -color/-width/-offset/-side attributes.
|
|
14
|
+
#
|
|
15
|
+
# With rhrev-third-level set, every mark of the current x.y release
|
|
16
|
+
# draws the bar (ThirdLevel::Matcher). In expand mode a mark naming a
|
|
17
|
+
# revision belongs to a lane of its own, the lowest revision nearest
|
|
18
|
+
# the text (assign_change_bar_lanes), so every lane stands for exactly
|
|
19
|
+
# one revision. Style bar draws one bar whatever the lanes. Style
|
|
20
|
+
# table draws the lanes as a table: every changed block draws a thin
|
|
21
|
+
# box with one column per lane, the columns of its own revisions
|
|
22
|
+
# filled, and every page with a box labels each column at the top of
|
|
23
|
+
# its body, so an isolated change still shows which revision it is.
|
|
24
|
+
# Without lanes, as in collapse mode, table draws the bar too.
|
|
13
25
|
module ChangeBars
|
|
26
|
+
CHANGE_BAR_STYLES = %w(bar table).freeze
|
|
27
|
+
|
|
14
28
|
def init_change_bars doc
|
|
15
29
|
@change_bar_attr = nil
|
|
30
|
+
@change_bar_matcher = nil
|
|
31
|
+
@change_bar_lane_index = nil
|
|
16
32
|
@change_bar_settings = nil
|
|
17
33
|
return unless doc.attr? 'rhrev-change-bars'
|
|
18
34
|
# Without a revnumber there is no "current" revision to match
|
|
19
35
|
return unless (revnumber = doc.attr 'revnumber')
|
|
20
|
-
@
|
|
36
|
+
@change_bar_matcher = ::Asciidoctor::Rhrev::ThirdLevel.matcher doc, @revision_prefix, revnumber
|
|
37
|
+
@change_bar_attr = @change_bar_matcher.exact
|
|
21
38
|
# Capture attribute values only; holding the Document in an ivar here
|
|
22
39
|
# would leak it into the scratch prototype, which must stay marshalable
|
|
23
40
|
@change_bar_overrides = {
|
|
@@ -25,9 +42,11 @@ module Asciidoctor
|
|
|
25
42
|
width: (doc.attr 'rhrev-change-bars-width'),
|
|
26
43
|
offset: (doc.attr 'rhrev-change-bars-offset'),
|
|
27
44
|
side: (doc.attr 'rhrev-change-bars-side'),
|
|
45
|
+
style: (doc.attr 'rhrev-change-bars-style'),
|
|
28
46
|
}
|
|
29
47
|
@change_bar_starts = {}
|
|
30
48
|
@change_bar_section_stack = []
|
|
49
|
+
@change_bar_grid_labeled_pages = {}
|
|
31
50
|
end
|
|
32
51
|
|
|
33
52
|
# Resolved lazily on first ink: the theme (and @media/@folio_placement)
|
|
@@ -40,22 +59,76 @@ module Asciidoctor
|
|
|
40
59
|
theme = @theme
|
|
41
60
|
side = overrides[:side] || theme&.rhrev_change_bars_side ||
|
|
42
61
|
(@media == 'prepress' ? 'outer' : 'right')
|
|
62
|
+
color = change_bar_color overrides[:color] || theme&.rhrev_change_bars_color || 'FF0000'
|
|
63
|
+
border_color = change_bar_color theme&.rhrev_change_bars_table_border_color || '999999'
|
|
64
|
+
border_width = (theme&.rhrev_change_bars_table_border_width || 0.25).to_f
|
|
43
65
|
{
|
|
44
|
-
|
|
66
|
+
style: (change_bar_style overrides[:style] || theme&.rhrev_change_bars_style),
|
|
67
|
+
color: color,
|
|
45
68
|
width: (overrides[:width] || theme&.rhrev_change_bars_width || 2).to_f,
|
|
46
69
|
offset: (overrides[:offset] || theme&.rhrev_change_bars_offset || 16).to_f,
|
|
47
70
|
side: side.to_s,
|
|
71
|
+
table_column_width: (theme&.rhrev_change_bars_table_column_width || 4).to_f,
|
|
72
|
+
table_max_columns: [(theme&.rhrev_change_bars_table_max_columns || 8).to_i, 1].max,
|
|
73
|
+
table_border_color: border_color,
|
|
74
|
+
table_border_width: border_width,
|
|
75
|
+
table_grid_color: (change_bar_color theme&.rhrev_change_bars_table_grid_color || border_color),
|
|
76
|
+
table_grid_width: (theme&.rhrev_change_bars_table_grid_width || border_width).to_f,
|
|
77
|
+
table_label_font_family: theme&.rhrev_change_bars_table_label_font_family,
|
|
78
|
+
table_label_font_style: (theme&.rhrev_change_bars_table_label_font_style || 'normal').to_sym,
|
|
79
|
+
table_label_font_size: (theme&.rhrev_change_bars_table_label_font_size || 5).to_f,
|
|
80
|
+
table_label_font_color: (change_bar_color theme&.rhrev_change_bars_table_label_font_color || '666666'),
|
|
48
81
|
}
|
|
49
82
|
end
|
|
50
83
|
end
|
|
51
84
|
|
|
85
|
+
def change_bar_color value
|
|
86
|
+
value.to_s.delete_prefix('#').upcase
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# bar or table, bar when unset. An unrecognized style warns once
|
|
90
|
+
# and draws the bar.
|
|
91
|
+
def change_bar_style value
|
|
92
|
+
return 'bar' if value.nil? || value.to_s.empty?
|
|
93
|
+
return value.to_s if CHANGE_BAR_STYLES.include? value.to_s
|
|
94
|
+
logger.warn %(rhrev-change-bars-style must be bar or table, got '#{value}'; using bar)
|
|
95
|
+
'bar'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Numbers the lanes from the revisions of the current x.y release
|
|
99
|
+
# the document marks, so a revision keeps the same lane on every
|
|
100
|
+
# page: the x.y release itself first when the document marks it,
|
|
101
|
+
# nil standing for it, then each third level in ascending order.
|
|
102
|
+
def assign_change_bar_lanes third_levels
|
|
103
|
+
return unless @change_bar_matcher&.mode == 'expand'
|
|
104
|
+
levels = third_levels.uniq
|
|
105
|
+
ordered = ((levels.include? nil) ? [nil] : []) + levels.compact.sort
|
|
106
|
+
@change_bar_lane_index = ordered.each_with_index.to_h
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The lanes a node's own attributes fill, one per matching mark,
|
|
110
|
+
# sorted, or nil when none of its marks belongs to the current
|
|
111
|
+
# revision. Without lanes, as in collapse mode, every mark fills
|
|
112
|
+
# lane 0.
|
|
113
|
+
def change_bar_lanes attributes
|
|
114
|
+
return nil unless @change_bar_matcher
|
|
115
|
+
marks = @change_bar_matcher.marks attributes
|
|
116
|
+
return nil if marks.empty?
|
|
117
|
+
marks.values.map {|z| @change_bar_lane_index&.[](z) || 0 }.uniq.sort
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The lanes a node's own marks draw, or nil.
|
|
121
|
+
def node_change_bar_lanes node
|
|
122
|
+
return nil unless @change_bar_attr && node.respond_to?(:attributes) && node.attributes
|
|
123
|
+
change_bar_lanes node.attributes
|
|
124
|
+
end
|
|
125
|
+
|
|
52
126
|
def change_bar? node
|
|
53
|
-
|
|
54
|
-
node.attributes && (node.attributes.key? @change_bar_attr)
|
|
127
|
+
!(node_change_bar_lanes node).nil?
|
|
55
128
|
end
|
|
56
129
|
|
|
57
130
|
def record_change_bar_start node
|
|
58
|
-
@change_bar_starts[node] = { page: page_number, cursor: cursor }
|
|
131
|
+
@change_bar_starts[node] = { page: page_number, cursor: cursor, lanes: (node_change_bar_lanes node) }
|
|
59
132
|
end
|
|
60
133
|
|
|
61
134
|
def take_change_bar_start node
|
|
@@ -112,39 +185,133 @@ module Asciidoctor
|
|
|
112
185
|
# number (honoring pdf-folio-placement inversion), matching prepress
|
|
113
186
|
# physical folio placement.
|
|
114
187
|
def change_bar_x pgnum, settings
|
|
188
|
+
(change_bar_on_right? pgnum, settings) ? bounds.right + settings[:offset] : bounds.left - settings[:offset] - settings[:width]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def change_bar_on_right? pgnum, settings
|
|
115
192
|
case settings[:side]
|
|
116
193
|
when 'right'
|
|
117
|
-
|
|
194
|
+
true
|
|
118
195
|
when 'outer', 'inner'
|
|
119
196
|
recto = (page_side pgnum, @folio_placement && @folio_placement[:inverted]) == :recto
|
|
120
|
-
|
|
197
|
+
settings[:side] == 'outer' ? recto : !recto
|
|
198
|
+
else
|
|
199
|
+
false
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Fills a bar or a table column between top and bottom on the
|
|
204
|
+
# current page.
|
|
205
|
+
def ink_change_bar_fill x, top, bottom, width, color
|
|
206
|
+
bounding_box [x, top], width: width, height: top - bottom do
|
|
207
|
+
fill_bounds color
|
|
121
208
|
end
|
|
122
|
-
on_right ? bounds.right + settings[:offset] : bounds.left - settings[:offset] - settings[:width]
|
|
123
209
|
end
|
|
124
210
|
|
|
125
|
-
#
|
|
211
|
+
# Whether bars draw as a table: with style table, once the
|
|
212
|
+
# document's revisions have lanes, as in expand mode.
|
|
213
|
+
def change_bar_table? settings
|
|
214
|
+
settings[:style] == 'table' && !(@change_bar_lane_index.nil? || @change_bar_lane_index.empty?)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# The table's columns, at most max of them: an array mapping each
|
|
218
|
+
# lane to its column. Up to max lanes, each lane has a column of its
|
|
219
|
+
# own. Beyond max, the first column accumulates the oldest lanes and
|
|
220
|
+
# every newer lane keeps a column of its own, so each further
|
|
221
|
+
# revision shifts the others one column toward the text.
|
|
222
|
+
def change_bar_table_columns total, max
|
|
223
|
+
return (0...total).to_a if total <= max
|
|
224
|
+
merged = total - max + 1
|
|
225
|
+
(0...total).map {|lane| lane < merged ? 0 : lane - merged + 1 }
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Inks one page's share of a table box between top and bottom: the
|
|
229
|
+
# columns from change_bar_table_columns, the columns of lanes
|
|
230
|
+
# filled, the first column nearest the text.
|
|
231
|
+
def ink_change_bar_table pgnum, top, bottom, lanes, settings
|
|
232
|
+
width = settings[:table_column_width]
|
|
233
|
+
on_right = change_bar_on_right? pgnum, settings
|
|
234
|
+
column_of = change_bar_table_columns @change_bar_lane_index.size, settings[:table_max_columns]
|
|
235
|
+
count = column_of.last + 1
|
|
236
|
+
left = on_right ? bounds.right + settings[:offset] : bounds.left - settings[:offset] - count * width
|
|
237
|
+
column_x = ->(column) { on_right ? left + column * width : left + (count - 1 - column) * width }
|
|
238
|
+
lanes.map {|lane| column_of[lane] }.uniq.each do |column|
|
|
239
|
+
ink_change_bar_fill (column_x.call column), top, bottom, width, settings[:color]
|
|
240
|
+
end
|
|
241
|
+
save_graphics_state do
|
|
242
|
+
if settings[:table_grid_width] > 0
|
|
243
|
+
stroke_color settings[:table_grid_color]
|
|
244
|
+
line_width settings[:table_grid_width]
|
|
245
|
+
(1...count).each {|i| stroke_vertical_line top, bottom, at: left + i * width }
|
|
246
|
+
end
|
|
247
|
+
if settings[:table_border_width] > 0
|
|
248
|
+
stroke_color settings[:table_border_color]
|
|
249
|
+
line_width settings[:table_border_width]
|
|
250
|
+
stroke_rectangle [left, top], count * width, top - bottom
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
ink_change_bar_table_labels pgnum, column_of, column_x, settings unless @change_bar_grid_labeled_pages[pgnum]
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Labels each column at the top of the page body with the
|
|
257
|
+
# third-level number of the newest lane it holds, 0 for the x.y
|
|
258
|
+
# release itself. The labels sit above every box on the page,
|
|
259
|
+
# whichever box inks first. When any label is wider than a column,
|
|
260
|
+
# every label reads upward, turned 90 degrees and centered on its
|
|
261
|
+
# column. A label font size of 0 omits the labels.
|
|
262
|
+
def ink_change_bar_table_labels pgnum, column_of, column_x, settings
|
|
263
|
+
@change_bar_grid_labeled_pages[pgnum] = true
|
|
264
|
+
return unless (size = settings[:table_label_font_size]) > 0
|
|
265
|
+
revisions = @change_bar_lane_index.invert
|
|
266
|
+
newest = {}
|
|
267
|
+
column_of.each_with_index {|column, lane| newest[column] = lane }
|
|
268
|
+
save_graphics_state do
|
|
269
|
+
fill_color settings[:table_label_font_color]
|
|
270
|
+
font (settings[:table_label_font_family] || font.family), style: settings[:table_label_font_style] do
|
|
271
|
+
font_size size do
|
|
272
|
+
width = settings[:table_column_width]
|
|
273
|
+
labels = newest.map {|column, lane| [column, (revisions[lane] || 0).to_s] }
|
|
274
|
+
if labels.any? {|_, label| (width_of label) > width }
|
|
275
|
+
# Turned text extends from its baseline toward lower x, so
|
|
276
|
+
# the baseline sits half a digit height past the center.
|
|
277
|
+
shift = (font.ascender - font.descender) * 0.35
|
|
278
|
+
labels.each do |column, label|
|
|
279
|
+
draw_text label, at: [(column_x.call column) + width / 2.0 + shift, bounds.top + 2], rotate: 90
|
|
280
|
+
end
|
|
281
|
+
else
|
|
282
|
+
labels.each do |column, label|
|
|
283
|
+
draw_text label, at: [(column_x.call column) + (width - (width_of label)) / 2.0, bounds.top + 2], rotate: 0
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Paints the bar page by page; from/to are { page:, cursor: }, and
|
|
292
|
+
# from[:lanes] lists the table columns to fill, the first when absent.
|
|
126
293
|
# Called immediately after the block is rendered, so all spanned pages exist.
|
|
127
294
|
def ink_change_bar from, to
|
|
128
295
|
return if scratch? || from.nil? || to.nil?
|
|
129
296
|
return if to[:page] < from[:page]
|
|
130
297
|
return if to[:page] == from[:page] && to[:cursor] >= from[:cursor]
|
|
131
298
|
settings = change_bar_settings
|
|
299
|
+
lanes = from[:lanes] || [0]
|
|
132
300
|
float do
|
|
133
301
|
(from[:page]..to[:page]).each do |pgnum|
|
|
134
302
|
go_to_page pgnum unless page_number == pgnum
|
|
135
303
|
top = pgnum == from[:page] ? from[:cursor] : bounds.top
|
|
136
304
|
bottom = pgnum == to[:page] ? to[:cursor] : 0
|
|
137
305
|
next if (height = top - bottom) <= 0
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
end
|
|
306
|
+
next ink_change_bar_table pgnum, top, bottom, lanes, settings if change_bar_table? settings
|
|
307
|
+
ink_change_bar_fill (change_bar_x pgnum, settings), top, bottom, settings[:width], settings[:color]
|
|
141
308
|
end
|
|
142
309
|
end
|
|
143
310
|
end
|
|
144
311
|
|
|
145
|
-
def ink_change_bar_for_extent extent
|
|
312
|
+
def ink_change_bar_for_extent extent, lanes = nil
|
|
146
313
|
return unless extent
|
|
147
|
-
ink_change_bar({ page: extent.from.page, cursor: extent.from.cursor },
|
|
314
|
+
ink_change_bar({ page: extent.from.page, cursor: extent.from.cursor, lanes: lanes },
|
|
148
315
|
{ page: extent.to.page, cursor: extent.to.cursor })
|
|
149
316
|
end
|
|
150
317
|
|
|
@@ -164,7 +331,7 @@ module Asciidoctor
|
|
|
164
331
|
# The extent yielded here is exact across page breaks.
|
|
165
332
|
def arrange_block node, &block
|
|
166
333
|
if @change_bar_attr && !scratch? &&
|
|
167
|
-
(ARRANGED_BLOCK_CONTEXTS.include? node.context) && (
|
|
334
|
+
(ARRANGED_BLOCK_CONTEXTS.include? node.context) && (lanes = node_change_bar_lanes node)
|
|
168
335
|
super node do |extent|
|
|
169
336
|
if scratch?
|
|
170
337
|
# measurement pass: upstream instance_execs this wrapper on the scratch
|
|
@@ -172,7 +339,7 @@ module Asciidoctor
|
|
|
172
339
|
instance_exec(&block)
|
|
173
340
|
else
|
|
174
341
|
block.call extent
|
|
175
|
-
ink_change_bar_for_extent extent
|
|
342
|
+
ink_change_bar_for_extent extent, lanes
|
|
176
343
|
end
|
|
177
344
|
end
|
|
178
345
|
else
|
|
@@ -188,8 +355,8 @@ module Asciidoctor
|
|
|
188
355
|
# out as a shared helper since none of the four need any special-
|
|
189
356
|
# casing the others don't.
|
|
190
357
|
def bracket_change_bar node
|
|
191
|
-
if @change_bar_attr && !scratch? && (
|
|
192
|
-
bar_from = { page: page_number, cursor: cursor }
|
|
358
|
+
if @change_bar_attr && !scratch? && (lanes = node_change_bar_lanes node)
|
|
359
|
+
bar_from = { page: page_number, cursor: cursor, lanes: lanes }
|
|
193
360
|
end
|
|
194
361
|
result = yield
|
|
195
362
|
ink_change_bar bar_from, { page: page_number, cursor: cursor } if bar_from
|
|
@@ -37,7 +37,7 @@ module Asciidoctor
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def revision_history
|
|
40
|
-
@catalog ||= Catalog.new
|
|
40
|
+
@catalog ||= Catalog.new third_level: @third_level_mode, kind: @third_level_kind
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# The physical PDF page where the document's own body content starts
|
|
@@ -159,6 +159,12 @@ module Asciidoctor
|
|
|
159
159
|
|
|
160
160
|
def convert_document node
|
|
161
161
|
@revision_prefix = node.attr 'revhistoryprefix', 'rhrev'
|
|
162
|
+
@third_level_mode = ::Asciidoctor::Rhrev::ThirdLevel.mode node
|
|
163
|
+
@third_level_kind = ::Asciidoctor::Rhrev::ThirdLevel.kind node
|
|
164
|
+
# Lifting cell text marks first lets the mixed-release check see
|
|
165
|
+
# them; prescan_document lifts again, which finds nothing left.
|
|
166
|
+
::Asciidoctor::Rhrev::CellTextMarks.lift node, @revision_prefix
|
|
167
|
+
::Asciidoctor::Rhrev::ThirdLevel.drop_mixed_release_marks node, @revision_prefix
|
|
162
168
|
init_change_bars node
|
|
163
169
|
|
|
164
170
|
# Check if we're in manual mode (pagerhref support needed)
|
|
@@ -304,10 +310,10 @@ module Asciidoctor
|
|
|
304
310
|
# Skip recording when the upstream table-container rewrap will trigger
|
|
305
311
|
# (condition replicated from asciidoctor-pdf convert_table); the
|
|
306
312
|
# re-entrant conversion of the attribute-preserving dup records instead
|
|
307
|
-
if @change_bar_attr && !scratch? && (
|
|
313
|
+
if @change_bar_attr && !scratch? && (lanes = node_change_bar_lanes node) &&
|
|
308
314
|
!(!at_page_top? && ((node.option? 'unbreakable') ||
|
|
309
315
|
((node.option? 'breakable') && (node.id || node.title?))))
|
|
310
|
-
bar_from = { page: page_number, cursor: cursor }
|
|
316
|
+
bar_from = { page: page_number, cursor: cursor, lanes: lanes }
|
|
311
317
|
end
|
|
312
318
|
result = super
|
|
313
319
|
catalog_block_anchor node
|
|
@@ -323,16 +329,16 @@ module Asciidoctor
|
|
|
323
329
|
collect_revision_entries node unless @prescan_complete
|
|
324
330
|
update_revision_entry_metadata node
|
|
325
331
|
# PDF targets import whole pages; there is no cursor flow to mark
|
|
326
|
-
if @change_bar_attr && !scratch? && (
|
|
332
|
+
if @change_bar_attr && !scratch? && (lanes = node_change_bar_lanes node) &&
|
|
327
333
|
!((node.attr 'target').to_s.downcase.end_with? '.pdf')
|
|
328
|
-
bar_from = { page: page_number, cursor: cursor }
|
|
334
|
+
bar_from = { page: page_number, cursor: cursor, lanes: lanes }
|
|
329
335
|
end
|
|
330
336
|
result = super
|
|
331
337
|
catalog_block_anchor node
|
|
332
338
|
if bar_from
|
|
333
339
|
bar_to = { page: page_number, cursor: cursor }
|
|
334
340
|
# Images never split; a page change means the image moved wholesale
|
|
335
|
-
bar_from = { page: bar_to[:page], cursor: bounds.top } if bar_to[:page] > bar_from[:page]
|
|
341
|
+
bar_from = { page: bar_to[:page], cursor: bounds.top, lanes: bar_from[:lanes] } if bar_to[:page] > bar_from[:page]
|
|
336
342
|
ink_change_bar bar_from, bar_to
|
|
337
343
|
end
|
|
338
344
|
result
|
|
@@ -435,20 +441,19 @@ module Asciidoctor
|
|
|
435
441
|
|
|
436
442
|
def convert_dlist node
|
|
437
443
|
targets = list_item_change_bar_targets node
|
|
438
|
-
if
|
|
444
|
+
if targets && !targets[:items].empty? && !(DLIST_STYLES_ROUTING_THROUGH_LIST_ITEM.include? node.style)
|
|
439
445
|
log :warn, %(rhrev per-item list marking is not supported for a dlist using the '#{node.style || 'default'}' style; marking the whole block instead)
|
|
440
|
-
targets = :
|
|
446
|
+
targets = { whole: (targets[:items].values.flatten + targets[:whole]).uniq.sort, items: {} }
|
|
441
447
|
end
|
|
442
448
|
convert_targeted_list(node, targets) { super }
|
|
443
449
|
end
|
|
444
450
|
|
|
445
451
|
# Shared by convert_ulist/convert_olist/convert_dlist: computes the
|
|
446
|
-
#
|
|
447
|
-
#
|
|
448
|
-
# enclosing list's own targets,
|
|
449
|
-
#
|
|
450
|
-
#
|
|
451
|
-
# all). The stack, not a single ivar, matters for a list nested
|
|
452
|
+
# targets (unless the caller, convert_dlist, already resolved
|
|
453
|
+
# them), pushes them so convert_list_item can read the innermost
|
|
454
|
+
# enclosing list's own targets, brackets the whole list with the
|
|
455
|
+
# lanes of every mark that targets the whole list, and leaves each
|
|
456
|
+
# targeted item's lanes to convert_list_item. The stack, not a single ivar, matters for a list nested
|
|
452
457
|
# inside a marked item's own content: the same shape
|
|
453
458
|
# @change_bar_section_stack already uses for nested sections.
|
|
454
459
|
#
|
|
@@ -464,8 +469,11 @@ module Asciidoctor
|
|
|
464
469
|
def convert_targeted_list node, targets = (list_item_change_bar_targets node)
|
|
465
470
|
@list_change_bar_target_stack ||= []
|
|
466
471
|
@list_change_bar_target_stack.push targets
|
|
467
|
-
if targets
|
|
468
|
-
|
|
472
|
+
if targets && !targets[:whole].empty? && !scratch?
|
|
473
|
+
bar_from = { page: page_number, cursor: cursor, lanes: targets[:whole] }
|
|
474
|
+
result = yield
|
|
475
|
+
ink_change_bar bar_from, { page: page_number, cursor: cursor }
|
|
476
|
+
result
|
|
469
477
|
else
|
|
470
478
|
yield
|
|
471
479
|
end
|
|
@@ -473,29 +481,51 @@ module Asciidoctor
|
|
|
473
481
|
@list_change_bar_target_stack.pop
|
|
474
482
|
end
|
|
475
483
|
|
|
476
|
-
# The
|
|
477
|
-
#
|
|
478
|
-
#
|
|
479
|
-
#
|
|
480
|
-
#
|
|
481
|
-
#
|
|
482
|
-
#
|
|
483
|
-
#
|
|
484
|
+
# The targets for the list currently being converted, or nil when
|
|
485
|
+
# no mark on it belongs to the current revision. Otherwise a Hash:
|
|
486
|
+
# :whole holds the lanes drawn over the whole list, :items maps a
|
|
487
|
+
# 1-based item position to the lanes drawn over that item alone.
|
|
488
|
+
# Each matching mark contributes its own lanes (change_bar_lanes) to
|
|
489
|
+
# the whole list when it names no item, or to each item its
|
|
490
|
+
# trailing positional attributes name, an N-M token naming a range.
|
|
491
|
+
#
|
|
492
|
+
# Only a mark's own positional attributes count. With stacked
|
|
493
|
+
# attribute lines the merged hash mixes every line's positional
|
|
494
|
+
# entries, so the per-line record MarkTargets keeps at parse time
|
|
495
|
+
# decides; a list built without one (through the extension API, for
|
|
496
|
+
# instance) falls back to every positional entry.
|
|
484
497
|
def list_item_change_bar_targets node
|
|
485
498
|
return nil unless change_bar? node
|
|
486
499
|
raw = node.instance_variable_get(:@attributes)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
500
|
+
by_mark = raw[::Asciidoctor::Rhrev::MarkTargets::KEY]
|
|
501
|
+
whole = []
|
|
502
|
+
items = {}
|
|
503
|
+
@change_bar_matcher.marks(raw).each_key do |mark|
|
|
504
|
+
lanes = change_bar_lanes({ mark => true })
|
|
505
|
+
tokens = if by_mark && (by_mark.key? mark)
|
|
506
|
+
by_mark[mark]
|
|
507
|
+
else
|
|
508
|
+
raw.keys.select {|k| ::Integer === k }.sort.map {|k| raw[k] }
|
|
509
|
+
end
|
|
510
|
+
positions = ::Set.new
|
|
511
|
+
tokens.each do |token|
|
|
512
|
+
token = token.to_s
|
|
513
|
+
if (m = /\A(\d+)-(\d+)\z/.match token)
|
|
514
|
+
(m[1].to_i..m[2].to_i).each {|i| positions << i }
|
|
515
|
+
elsif /\A\d+\z/.match? token
|
|
516
|
+
positions << token.to_i
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
if positions.empty?
|
|
520
|
+
whole |= lanes
|
|
521
|
+
else
|
|
522
|
+
positions.each {|i| items[i] = (items[i] || []) | lanes }
|
|
496
523
|
end
|
|
497
524
|
end
|
|
498
|
-
|
|
525
|
+
whole.sort!
|
|
526
|
+
items.transform_values! {|lanes| (lanes - whole).sort }
|
|
527
|
+
items.reject! {|_, lanes| lanes.empty? }
|
|
528
|
+
{ whole: whole, items: items }
|
|
499
529
|
end
|
|
500
530
|
|
|
501
531
|
# A single item within a ulist/olist (and a dlist using the
|
|
@@ -527,15 +557,15 @@ module Asciidoctor
|
|
|
527
557
|
# the "should this bar" question on its own.
|
|
528
558
|
def convert_list_item node, list, opts = {}
|
|
529
559
|
targets = @list_change_bar_target_stack&.last
|
|
530
|
-
if
|
|
531
|
-
ink_targeted_list_item_change_bar { super }
|
|
560
|
+
if targets && (idx = list.items.index node) && (lanes = targets[:items][idx + 1])
|
|
561
|
+
ink_targeted_list_item_change_bar(lanes) { super }
|
|
532
562
|
else
|
|
533
563
|
super
|
|
534
564
|
end
|
|
535
565
|
end
|
|
536
566
|
|
|
537
|
-
def ink_targeted_list_item_change_bar
|
|
538
|
-
bar_from = { page: page_number, cursor: cursor } if @change_bar_attr && !scratch?
|
|
567
|
+
def ink_targeted_list_item_change_bar lanes
|
|
568
|
+
bar_from = { page: page_number, cursor: cursor, lanes: lanes } if @change_bar_attr && !scratch?
|
|
539
569
|
result = yield
|
|
540
570
|
ink_change_bar bar_from, { page: page_number, cursor: cursor } if bar_from
|
|
541
571
|
result
|
|
@@ -620,7 +650,7 @@ module Asciidoctor
|
|
|
620
650
|
|
|
621
651
|
debug_log "Found entry candidate on #{node.context} id=#{node.id}", @document
|
|
622
652
|
|
|
623
|
-
attr_entries.each do |key, value|
|
|
653
|
+
::Asciidoctor::Rhrev::ThirdLevel.sort_marks(attr_entries, @revision_prefix, @third_level_kind).each do |key, value|
|
|
624
654
|
key_str = key.to_s
|
|
625
655
|
next unless key_str.start_with?(@revision_prefix)
|
|
626
656
|
next if key_str.include?('-all') || key_str.end_with?('-cover')
|
|
@@ -720,7 +750,7 @@ module Asciidoctor
|
|
|
720
750
|
section = enclosing_section node
|
|
721
751
|
has_section = section && section.id
|
|
722
752
|
|
|
723
|
-
attr_entries.each do |key, value|
|
|
753
|
+
::Asciidoctor::Rhrev::ThirdLevel.sort_marks(attr_entries, @revision_prefix, @third_level_kind).each do |key, value|
|
|
724
754
|
key_str = key.to_s
|
|
725
755
|
next unless key_str.start_with?(@revision_prefix)
|
|
726
756
|
next if key_str.include?('-all') || key_str.end_with?('-cover')
|
|
@@ -753,7 +783,7 @@ module Asciidoctor
|
|
|
753
783
|
# directly, so a synthesized table entry looks the same as one from
|
|
754
784
|
# the normal collect_revision_entries path.
|
|
755
785
|
def append_child_change_to_entry revision, anchor_node, change_text
|
|
756
|
-
entries = revision_history.entries[revision] ||= []
|
|
786
|
+
entries = revision_history.entries[revision_history.revision_key revision] ||= []
|
|
757
787
|
entry = entries.find { |e| e[:anchor] == anchor_node.id }
|
|
758
788
|
if entry
|
|
759
789
|
entry[:change] = entry[:change].to_s.empty? ? change_text : "#{entry[:change]} * #{change_text}"
|
|
@@ -777,14 +807,9 @@ module Asciidoctor
|
|
|
777
807
|
end
|
|
778
808
|
|
|
779
809
|
# Appends onto the document's existing -all change text for this
|
|
780
|
-
# revision, or sets it fresh if none exists yet.
|
|
781
|
-
# overwrites unconditionally, so the existing value has to be read
|
|
782
|
-
# and folded in here, unlike add_entry it has no anchor to guard
|
|
783
|
-
# against being called more than once.
|
|
810
|
+
# revision, or sets it fresh if none exists yet.
|
|
784
811
|
def append_child_change_to_all revision, change_text
|
|
785
|
-
|
|
786
|
-
combined = existing.to_s.empty? ? change_text : "#{existing} * #{change_text}"
|
|
787
|
-
revision_history.add_all_entry revision, combined
|
|
812
|
+
revision_history.append_all_entry revision, change_text
|
|
788
813
|
end
|
|
789
814
|
|
|
790
815
|
# A table cell marked with an rhrev attribute is not a rollup context
|
|
@@ -906,7 +931,7 @@ module Asciidoctor
|
|
|
906
931
|
|
|
907
932
|
revision = key_str.sub("#{@revision_prefix}", '')
|
|
908
933
|
|
|
909
|
-
entries = revision_history.entries[revision]
|
|
934
|
+
entries = revision_history.entries[revision_history.revision_key revision]
|
|
910
935
|
next unless entries
|
|
911
936
|
|
|
912
937
|
entry = entries.find { |e| e[:anchor] == node.id }
|
|
@@ -1048,25 +1073,53 @@ module Asciidoctor
|
|
|
1048
1073
|
@document_entries_collected = true
|
|
1049
1074
|
|
|
1050
1075
|
# Discover revisions from *-prevrev attributes so any number of
|
|
1051
|
-
# segments works (1-1, 1-2-0, ...), not just major-minor
|
|
1076
|
+
# segments works (1-1, 1-2-0, ...), not just major-minor. With
|
|
1077
|
+
# rhrev-third-level set, a third-level revision often declares no
|
|
1078
|
+
# prevrev of its own, so its -all and -cover attributes count too.
|
|
1079
|
+
# Ascending order keeps merged collapse-mode text in revision order.
|
|
1080
|
+
revisions = []
|
|
1052
1081
|
doc.attributes.each_key do |key|
|
|
1053
1082
|
key_str = key.to_s
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1083
|
+
if key_str.end_with?('-prevrev')
|
|
1084
|
+
revision = key_str.delete_suffix('-prevrev')
|
|
1085
|
+
elsif @third_level_mode && (m = /-(?:all|cover)\z/.match(key_str))
|
|
1086
|
+
revision = ::Asciidoctor::Rhrev::ThirdLevel.revision_of key_str.delete_suffix(m[0]), @revision_prefix
|
|
1087
|
+
else
|
|
1088
|
+
next
|
|
1089
|
+
end
|
|
1090
|
+
revisions << revision if revision&.match?(/\A\d+(?:-\d+)*\z/)
|
|
1091
|
+
end
|
|
1057
1092
|
|
|
1093
|
+
revisions.uniq.sort_by { |revision| revision_history.parse_revision revision }.each do |revision|
|
|
1058
1094
|
all_attr_name = "#{@revision_prefix}#{revision}-all"
|
|
1059
1095
|
if (all_value = doc.attr(all_attr_name))
|
|
1060
|
-
revision_history.
|
|
1096
|
+
revision_history.append_all_entry revision, all_value
|
|
1061
1097
|
end
|
|
1062
1098
|
|
|
1063
1099
|
cover_attr_name = "#{@revision_prefix}#{revision}-cover"
|
|
1064
1100
|
if (cover_value = doc.attr(cover_attr_name))
|
|
1065
|
-
revision_history.
|
|
1101
|
+
revision_history.append_cover_entry revision, cover_value
|
|
1066
1102
|
end
|
|
1067
1103
|
end
|
|
1068
1104
|
end
|
|
1069
1105
|
|
|
1106
|
+
# Every revision of the current x.y release the document marks
|
|
1107
|
+
# anywhere, block, list, or table cell, as its third-level number,
|
|
1108
|
+
# or nil for the x.y release itself, for the expand-mode lanes.
|
|
1109
|
+
def discover_third_level_lanes doc
|
|
1110
|
+
return unless @change_bar_matcher&.mode == 'expand'
|
|
1111
|
+
found = []
|
|
1112
|
+
collect = proc do |attributes|
|
|
1113
|
+
@change_bar_matcher.marks(attributes).each_value { |z| found << z }
|
|
1114
|
+
end
|
|
1115
|
+
doc.find_by(traverse_documents: true) { true }.each do |node|
|
|
1116
|
+
collect.call node.attributes if node.respond_to?(:attributes)
|
|
1117
|
+
next unless node.context == :table
|
|
1118
|
+
(node.rows[:head] + node.rows[:body] + node.rows[:foot]).flatten.each { |cell| collect.call cell.attributes }
|
|
1119
|
+
end
|
|
1120
|
+
assign_change_bar_lanes found
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1070
1123
|
def prescan_document doc
|
|
1071
1124
|
debug_log "Starting prescan_document", doc
|
|
1072
1125
|
|
|
@@ -1094,6 +1147,7 @@ module Asciidoctor
|
|
|
1094
1147
|
propagate_cell_content_marks doc
|
|
1095
1148
|
rollup_child_revision_entries doc
|
|
1096
1149
|
rollup_table_cell_revision_entries doc
|
|
1150
|
+
discover_third_level_lanes doc
|
|
1097
1151
|
|
|
1098
1152
|
@prescan_complete = true
|
|
1099
1153
|
debug_log "Prescan complete. Catalog has #{revision_history.entries.values.flatten.size} entries.", doc
|
|
@@ -55,11 +55,9 @@ module Asciidoctor
|
|
|
55
55
|
|
|
56
56
|
# Process each revision
|
|
57
57
|
revision_history.sorted_revisions.each do |revision|
|
|
58
|
-
prevrev_attr = "#{revision}-prevrev"
|
|
59
|
-
prevrevdate_attr = "#{revision}-prevrevdate"
|
|
60
58
|
revision_label = doc.attr('version-label', 'Revision')
|
|
61
|
-
prev_title =
|
|
62
|
-
prev_date =
|
|
59
|
+
prev_title = ::Asciidoctor::Rhrev::ThirdLevel.group_attr(doc, revision, 'prevrev', @third_level_mode) || revision.tr('-', '.')
|
|
60
|
+
prev_date = ::Asciidoctor::Rhrev::ThirdLevel.group_attr(doc, revision, 'prevrevdate', @third_level_mode) || ""
|
|
63
61
|
prev_rev_text = format_prev_rev(prev_title, prev_date, doc)
|
|
64
62
|
|
|
65
63
|
# Build header text, avoiding "Revision Revision" repetition
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Asciidoctor
|
|
4
|
+
module Rhrev
|
|
5
|
+
# Records which positional list-item targets belong to which rhrev mark,
|
|
6
|
+
# one block attribute line at a time.
|
|
7
|
+
#
|
|
8
|
+
# Stacked attribute lines merge into one attribute hash, and Asciidoctor
|
|
9
|
+
# numbers positional entries per line, so a later line's positional
|
|
10
|
+
# entries overwrite an earlier line's at the same index:
|
|
11
|
+
#
|
|
12
|
+
# [rhrev1-2="old",2]
|
|
13
|
+
# [rhrev1-3="new"]
|
|
14
|
+
#
|
|
15
|
+
# leaves {"rhrev1-2"=>"old", 2=>"2", "rhrev1-3"=>"new"}, and the merged
|
|
16
|
+
# hash no longer says whether target 2 belongs to rhrev1-2 or rhrev1-3.
|
|
17
|
+
# Each line is still parsed on its own, through AttributeList#parse_into,
|
|
18
|
+
# whose per-line result is memoized, so reading it here costs no second
|
|
19
|
+
# parse. Every positional entry on a line belongs to the nearest mark
|
|
20
|
+
# before it on that same line; a mark with no positional entry after it
|
|
21
|
+
# targets the whole list.
|
|
22
|
+
#
|
|
23
|
+
# The record is stored on the block's own attribute hash under KEY, as a
|
|
24
|
+
# Hash of mark name => Array of raw target tokens. KEY never starts with
|
|
25
|
+
# an attribute name character, so it collides with no AsciiDoc attribute
|
|
26
|
+
# and is never mistaken for an rhrev mark by any prefix scan.
|
|
27
|
+
module MarkTargets
|
|
28
|
+
KEY = '@rhrev-mark-targets'
|
|
29
|
+
|
|
30
|
+
def self.record attributes, line, prefix
|
|
31
|
+
by_mark = current = nil
|
|
32
|
+
line.each do |key, value|
|
|
33
|
+
if ::String === key
|
|
34
|
+
next unless key.start_with? prefix
|
|
35
|
+
(by_mark ||= (attributes[KEY] ||= {}))[current = key] = []
|
|
36
|
+
elsif current && ::Integer === key
|
|
37
|
+
by_mark[current] << value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Only a block attribute line passes the document itself as the block
|
|
43
|
+
# (Parser.parse_block_metadata_line, sub_result: true); every other
|
|
44
|
+
# caller, such as a block macro's attribute list, passes none.
|
|
45
|
+
module AttributeListHook
|
|
46
|
+
def parse_into attributes, positional_attrs = []
|
|
47
|
+
if ::Asciidoctor::Document === @block
|
|
48
|
+
MarkTargets.record attributes, (parse positional_attrs), (@block.attr 'revhistoryprefix', 'rhrev')
|
|
49
|
+
end
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
::Asciidoctor::AttributeList.prepend ::Asciidoctor::Rhrev::MarkTargets::AttributeListHook
|
|
@@ -438,11 +438,9 @@ module Asciidoctor
|
|
|
438
438
|
cover_text = doc.attr 'rhrev-localization-cover', 'Cover'
|
|
439
439
|
|
|
440
440
|
revision_history.sorted_revisions.each do |revision|
|
|
441
|
-
prevrev_attr = "#{revision}-prevrev"
|
|
442
|
-
prevrevdate_attr = "#{revision}-prevrevdate"
|
|
443
441
|
revision_label = doc.attr('version-label', 'Revision')
|
|
444
|
-
prev_title =
|
|
445
|
-
prev_date =
|
|
442
|
+
prev_title = ::Asciidoctor::Rhrev::ThirdLevel.group_attr(doc, revision, 'prevrev', @third_level_mode) || "#{revision.tr('-', '.')}"
|
|
443
|
+
prev_date = ::Asciidoctor::Rhrev::ThirdLevel.group_attr(doc, revision, 'prevrevdate', @third_level_mode) || ""
|
|
446
444
|
prev_rev_text = format_prev_rev(prev_title, prev_date, doc)
|
|
447
445
|
|
|
448
446
|
if prev_title.strip.downcase.start_with?(revision_label.downcase)
|
|
@@ -42,6 +42,7 @@ module Asciidoctor
|
|
|
42
42
|
prefix = document.attr 'revhistoryprefix', 'rhrev'
|
|
43
43
|
|
|
44
44
|
::Asciidoctor::Rhrev::CellTextMarks.lift document, prefix
|
|
45
|
+
::Asciidoctor::Rhrev::ThirdLevel.drop_mixed_release_marks document, prefix
|
|
45
46
|
|
|
46
47
|
document.find_by(context: :table).each do |table|
|
|
47
48
|
cells = (table.rows[:head] + table.rows[:body] + table.rows[:foot]).flatten
|
|
@@ -91,11 +92,11 @@ module Asciidoctor
|
|
|
91
92
|
return document unless document.attr? 'rhrev-change-bars'
|
|
92
93
|
return document unless (revnumber = document.attr 'revnumber')
|
|
93
94
|
prefix = document.attr 'revhistoryprefix', 'rhrev'
|
|
94
|
-
|
|
95
|
+
matcher = ThirdLevel.matcher document, prefix, revnumber
|
|
95
96
|
|
|
96
97
|
tagged = false
|
|
97
98
|
document.find_by { |b| TRACKED_CONTEXTS.include? b.context }.each do |block|
|
|
98
|
-
next unless block.attributes && (block.attributes
|
|
99
|
+
next unless block.attributes && (matcher.match? block.attributes)
|
|
99
100
|
roles = (block.attr 'role')
|
|
100
101
|
block.set_attr 'role', [roles, 'rhrev-changed'].compact.join(' ')
|
|
101
102
|
tagged = true
|
|
@@ -173,7 +174,7 @@ module Asciidoctor
|
|
|
173
174
|
return super
|
|
174
175
|
end
|
|
175
176
|
prefix = doc.attr 'revhistoryprefix', 'rhrev'
|
|
176
|
-
|
|
177
|
+
matcher = ThirdLevel.matcher doc, prefix, revnumber
|
|
177
178
|
|
|
178
179
|
result = []
|
|
179
180
|
id_attribute = node.id ? %( id="#{node.id}") : ''
|
|
@@ -231,7 +232,7 @@ module Asciidoctor
|
|
|
231
232
|
|
|
232
233
|
cell_tag_name = (tsec == :head || cell.style == :header ? 'th' : 'td')
|
|
233
234
|
cell_classes = ['tableblock', %(halign-#{cell.attr 'halign'}), %(valign-#{cell.attr 'valign'})]
|
|
234
|
-
cell_classes << 'rhrev-changed' if cell.attributes && (cell.attributes
|
|
235
|
+
cell_classes << 'rhrev-changed' if cell.attributes && (matcher.match? cell.attributes)
|
|
235
236
|
cell_class_attribute = %( class="#{cell_classes.join ' '}")
|
|
236
237
|
cell_colspan_attribute = cell.colspan ? %( colspan="#{cell.colspan}") : ''
|
|
237
238
|
cell_rowspan_attribute = cell.rowspan ? %( rowspan="#{cell.rowspan}") : ''
|
|
@@ -405,7 +406,7 @@ module Asciidoctor
|
|
|
405
406
|
# take catalog as an argument since collect_all_entries builds a fresh
|
|
406
407
|
# one locally rather than reading an instance-level revision_history.
|
|
407
408
|
def append_child_change_to_entry catalog, revision, anchor_node, change_text
|
|
408
|
-
entries = catalog.entries[revision] ||= []
|
|
409
|
+
entries = catalog.entries[catalog.revision_key revision] ||= []
|
|
409
410
|
entry = entries.find { |e| e[:anchor] == anchor_node.id }
|
|
410
411
|
if entry
|
|
411
412
|
entry[:change] = entry[:change].to_s.empty? ? change_text : "#{entry[:change]} * #{change_text}"
|
|
@@ -426,9 +427,7 @@ module Asciidoctor
|
|
|
426
427
|
# or sets it fresh if none exists yet. Same shape as
|
|
427
428
|
# Converter#append_child_change_to_all on the PDF side.
|
|
428
429
|
def append_child_change_to_all catalog, revision, change_text
|
|
429
|
-
|
|
430
|
-
combined = existing.to_s.empty? ? change_text : "#{existing} * #{change_text}"
|
|
431
|
-
catalog.add_all_entry revision, combined
|
|
430
|
+
catalog.append_all_entry revision, change_text
|
|
432
431
|
end
|
|
433
432
|
|
|
434
433
|
# Gathers every marked rollup-context node in one unified, document-order
|
|
@@ -447,7 +446,7 @@ module Asciidoctor
|
|
|
447
446
|
section = enclosing_section node
|
|
448
447
|
has_section = section && section.id
|
|
449
448
|
|
|
450
|
-
attr_entries.each do |key, value|
|
|
449
|
+
ThirdLevel.sort_marks(attr_entries, prefix, (ThirdLevel.kind doc)).each do |key, value|
|
|
451
450
|
key_str = key.to_s
|
|
452
451
|
next unless key_str.start_with?(prefix)
|
|
453
452
|
next if key_str.include?('-all') || key_str.end_with?('-cover')
|
|
@@ -500,21 +499,33 @@ module Asciidoctor
|
|
|
500
499
|
end
|
|
501
500
|
|
|
502
501
|
def collect_all_entries(doc)
|
|
503
|
-
|
|
502
|
+
third_level = ThirdLevel.mode doc
|
|
503
|
+
kind = ThirdLevel.kind doc
|
|
504
|
+
catalog = Catalog.new third_level: third_level, kind: kind
|
|
504
505
|
prefix = doc.attr 'revhistoryprefix', 'rhrev'
|
|
505
506
|
|
|
506
507
|
# Collect document-level entries (-all)
|
|
507
508
|
# Discover revisions from *-prevrev attributes so any number of
|
|
508
|
-
# segments works (1-1, 1-2-0, ...), not just major-minor
|
|
509
|
+
# segments works (1-1, 1-2-0, ...), not just major-minor. With
|
|
510
|
+
# rhrev-third-level set, a revision's -all attribute counts too,
|
|
511
|
+
# same as Converter#collect_document_level_entries on the PDF side.
|
|
512
|
+
revisions = []
|
|
509
513
|
doc.attributes.each_key do |key|
|
|
510
514
|
key_str = key.to_s
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
515
|
+
if key_str.end_with?('-prevrev')
|
|
516
|
+
revision = key_str.delete_suffix('-prevrev')
|
|
517
|
+
elsif third_level && key_str.end_with?('-all')
|
|
518
|
+
revision = ThirdLevel.revision_of key_str.delete_suffix('-all'), prefix
|
|
519
|
+
else
|
|
520
|
+
next
|
|
521
|
+
end
|
|
522
|
+
revisions << revision if revision&.match?(/\A\d+(?:-\d+)*\z/)
|
|
523
|
+
end
|
|
514
524
|
|
|
525
|
+
revisions.uniq.sort_by { |revision| catalog.parse_revision revision }.each do |revision|
|
|
515
526
|
all_attr_name = "#{prefix}#{revision}-all"
|
|
516
527
|
if (all_value = doc.attr(all_attr_name))
|
|
517
|
-
catalog.
|
|
528
|
+
catalog.append_all_entry revision, all_value
|
|
518
529
|
end
|
|
519
530
|
|
|
520
531
|
# Skip -cover entries for HTML
|
|
@@ -526,7 +537,7 @@ module Asciidoctor
|
|
|
526
537
|
next unless block.id
|
|
527
538
|
|
|
528
539
|
if block.respond_to?(:attributes)
|
|
529
|
-
block.attributes.each do |key, value|
|
|
540
|
+
ThirdLevel.sort_marks(block.attributes, prefix, kind).each do |key, value|
|
|
530
541
|
key_str = key.to_s
|
|
531
542
|
next unless key_str.start_with?(prefix)
|
|
532
543
|
next if key_str.include?('-all') || key_str.end_with?('-cover')
|
|
@@ -596,11 +607,10 @@ module Asciidoctor
|
|
|
596
607
|
all_text = doc.attr 'rhrev-localization-all', 'All'
|
|
597
608
|
|
|
598
609
|
catalog.sorted_revisions.each do |revision|
|
|
599
|
-
|
|
600
|
-
prevrevdate_attr = "#{revision}-prevrevdate"
|
|
610
|
+
third_level = ThirdLevel.mode doc
|
|
601
611
|
revision_label = doc.attr('version-label', 'Revision')
|
|
602
|
-
prev_title =
|
|
603
|
-
prev_date =
|
|
612
|
+
prev_title = ThirdLevel.group_attr(doc, revision, 'prevrev', third_level) || revision.tr('-', '.')
|
|
613
|
+
prev_date = ThirdLevel.group_attr(doc, revision, 'prevrevdate', third_level) || ""
|
|
604
614
|
prev_rev_text = format_prev_rev(prev_title, prev_date, doc)
|
|
605
615
|
|
|
606
616
|
# Build header text
|
|
@@ -48,13 +48,15 @@ if defined? ::Prawn::Table
|
|
|
48
48
|
module PDF
|
|
49
49
|
module Rhrev
|
|
50
50
|
module TableCellChangeBarPatch
|
|
51
|
+
# The change-bar lanes the cell draws (ChangeBars#change_bar_lanes),
|
|
52
|
+
# or nil for an unmarked cell.
|
|
51
53
|
attr_accessor :rhrev_marked
|
|
52
54
|
|
|
53
55
|
def draw_background pt
|
|
54
56
|
if @rhrev_marked && @pdf.respond_to?(:ink_change_bar)
|
|
55
57
|
x, y = pt
|
|
56
58
|
page = @pdf.page_number
|
|
57
|
-
@pdf.ink_change_bar({ page: page, cursor: y }, { page: page, cursor: y - height })
|
|
59
|
+
@pdf.ink_change_bar({ page: page, cursor: y, lanes: @rhrev_marked }, { page: page, cursor: y - height })
|
|
58
60
|
end
|
|
59
61
|
super
|
|
60
62
|
end
|
|
@@ -76,12 +78,12 @@ if defined? ::Prawn::Table
|
|
|
76
78
|
rows = node.rows
|
|
77
79
|
(rows[:head] + rows[:body] + rows[:foot]).each_with_index do |row, r|
|
|
78
80
|
row.each_with_index do |cell, c|
|
|
79
|
-
next unless cell.respond_to?(:attributes) && cell.attributes && (cell.attributes
|
|
81
|
+
next unless cell.respond_to?(:attributes) && cell.attributes && (lanes = document.change_bar_lanes cell.attributes)
|
|
80
82
|
next unless (cell_data = data[r] && data[r][c]).is_a?(::Hash)
|
|
81
83
|
if (content = cell_data[:content]).is_a? ::Prawn::Table::Cell
|
|
82
|
-
content.rhrev_marked =
|
|
84
|
+
content.rhrev_marked = lanes
|
|
83
85
|
else
|
|
84
|
-
cell_data[:rhrev_marked] =
|
|
86
|
+
cell_data[:rhrev_marked] = lanes
|
|
85
87
|
end
|
|
86
88
|
end
|
|
87
89
|
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Asciidoctor
|
|
4
|
+
module Rhrev
|
|
5
|
+
# Support for x.y.z revision numbers, selected by the rhrev-third-level
|
|
6
|
+
# document attribute:
|
|
7
|
+
#
|
|
8
|
+
# collapse:: the revision history table groups every revision by its
|
|
9
|
+
# first two levels, and one change bar covers every mark of the
|
|
10
|
+
# current x.y release.
|
|
11
|
+
# expand:: the table lists every revision on its own, as without the
|
|
12
|
+
# attribute; the PDF backend draws one lane for every mark of the
|
|
13
|
+
# current x.y release plus one lane per third-level revision further
|
|
14
|
+
# out in the margin.
|
|
15
|
+
#
|
|
16
|
+
# Without the attribute, only a mark for exactly the current revnumber
|
|
17
|
+
# draws a bar, and every revision gets its own table group.
|
|
18
|
+
#
|
|
19
|
+
# The rhrev-third-level-kind attribute says what an x.y.z revision is:
|
|
20
|
+
#
|
|
21
|
+
# draft:: the default; x.y.1, x.y.2, ... are drafts leading up to the
|
|
22
|
+
# release x.y, so x.y sorts after its drafts, an x.y mark draws no bar
|
|
23
|
+
# while the revnumber names a draft, and an x.y mark beside marks of
|
|
24
|
+
# its own drafts is dropped with a warning (drop_mixed_release_marks).
|
|
25
|
+
# amendment:: x.y.1, x.y.2, ... amend the released x.y, so x.y sorts
|
|
26
|
+
# before them and its marks draw a bar for every amendment.
|
|
27
|
+
module ThirdLevel
|
|
28
|
+
MODES = %w(expand collapse).freeze
|
|
29
|
+
KINDS = %w(draft amendment).freeze
|
|
30
|
+
DEFAULT_KIND = 'draft'
|
|
31
|
+
RevisionRx = /\A\d+(?:-\d+)*\z/
|
|
32
|
+
|
|
33
|
+
# The document's mode, 'expand' or 'collapse', or nil when the
|
|
34
|
+
# attribute is unset. An unrecognized value warns once per document
|
|
35
|
+
# and falls back to nil.
|
|
36
|
+
def self.mode doc
|
|
37
|
+
return nil unless doc.attr? 'rhrev-third-level'
|
|
38
|
+
value = (doc.attr 'rhrev-third-level').to_s
|
|
39
|
+
return value if MODES.include? value
|
|
40
|
+
unless doc.instance_variable_get :@rhrev_third_level_warned
|
|
41
|
+
doc.instance_variable_set :@rhrev_third_level_warned, true
|
|
42
|
+
doc.logger.warn %(rhrev-third-level must be expand or collapse, got '#{value}'; ignoring it)
|
|
43
|
+
end
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The document's kind, 'draft' or 'amendment', or nil when
|
|
48
|
+
# rhrev-third-level is unset. An unrecognized value warns once per
|
|
49
|
+
# document and falls back to the default.
|
|
50
|
+
def self.kind doc
|
|
51
|
+
return nil unless mode doc
|
|
52
|
+
return DEFAULT_KIND unless doc.attr? 'rhrev-third-level-kind'
|
|
53
|
+
value = (doc.attr 'rhrev-third-level-kind').to_s
|
|
54
|
+
return value if KINDS.include? value
|
|
55
|
+
unless doc.instance_variable_get :@rhrev_third_level_kind_warned
|
|
56
|
+
doc.instance_variable_set :@rhrev_third_level_kind_warned, true
|
|
57
|
+
doc.logger.warn %(rhrev-third-level-kind must be draft or amendment, got '#{value}'; using #{DEFAULT_KIND})
|
|
58
|
+
end
|
|
59
|
+
DEFAULT_KIND
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The comparable form of a revision. In draft kind an x-y revision
|
|
63
|
+
# is the release its x-y-z drafts lead up to, so it sorts after them;
|
|
64
|
+
# otherwise x-y sorts before x-y-1.
|
|
65
|
+
def self.sort_key revision, kind
|
|
66
|
+
parts = revision.split('-').map(&:to_i)
|
|
67
|
+
kind == 'draft' && parts.size == 2 ? (parts << ::Float::INFINITY) : parts
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The x-y groups whose release mark appears beside marks of its own
|
|
71
|
+
# drafts, each with the draft revisions found, from a list of the
|
|
72
|
+
# revisions a draft-kind document marks.
|
|
73
|
+
def self.mixed_draft_groups revisions
|
|
74
|
+
revisions = revisions.uniq
|
|
75
|
+
revisions.select {|revision| revision.count('-') == 1 }.filter_map do |release|
|
|
76
|
+
drafts = revisions.select {|revision| revision.start_with?(%(#{release}-)) && revision.count('-') == 2 }
|
|
77
|
+
[release, drafts.sort_by {|revision| sort_key revision, nil }] unless drafts.empty?
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Calls the block with the attributes of every node that carries
|
|
82
|
+
# marks: the document, every block in it and in nested documents,
|
|
83
|
+
# and every table cell. Cell text marks count only once
|
|
84
|
+
# CellTextMarks.lift has run.
|
|
85
|
+
def self.each_marked_attributes doc
|
|
86
|
+
doc.find_by(traverse_documents: true) { true }.each do |node|
|
|
87
|
+
yield node.attributes
|
|
88
|
+
next unless node.context == :table
|
|
89
|
+
(node.rows[:head] + node.rows[:body] + node.rows[:foot]).flatten.each {|cell| yield cell.attributes }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The revision an attribute names as a mark, or as an -all or -cover
|
|
94
|
+
# entry, or nil.
|
|
95
|
+
def self.marked_revision_of key, prefix
|
|
96
|
+
revision_of key.to_s.sub(/-(?:all|cover)\z/, ''), prefix
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# In a draft-kind document, removes every mark, -all, and -cover
|
|
100
|
+
# entry of an x-y release that the document also marks drafts of,
|
|
101
|
+
# warning once per release, so no backend shows a release beside its
|
|
102
|
+
# own drafts. Runs once per document, after CellTextMarks.lift.
|
|
103
|
+
def self.drop_mixed_release_marks doc, prefix
|
|
104
|
+
return unless kind(doc) == 'draft'
|
|
105
|
+
return if doc.instance_variable_get :@rhrev_mixed_releases_dropped
|
|
106
|
+
doc.instance_variable_set :@rhrev_mixed_releases_dropped, true
|
|
107
|
+
revisions = []
|
|
108
|
+
each_marked_attributes(doc) do |attributes|
|
|
109
|
+
attributes.each_key {|key| (revision = marked_revision_of key, prefix) && (revisions << revision) }
|
|
110
|
+
end
|
|
111
|
+
releases = mixed_draft_groups(revisions).map do |release, drafts|
|
|
112
|
+
doc.logger.warn %(#{prefix}#{release} marks the release that its drafts #{drafts.map {|d| prefix + d }.join ', '} lead up to; ignoring the #{prefix}#{release} marks, mark each change with the draft that made it)
|
|
113
|
+
release
|
|
114
|
+
end
|
|
115
|
+
return if releases.empty?
|
|
116
|
+
each_marked_attributes(doc) do |attributes|
|
|
117
|
+
attributes.delete_if {|key, _| releases.include? marked_revision_of(key, prefix) }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The revision a mark names, "1-3-2" for rhrev1-3-2, or nil for any
|
|
122
|
+
# other attribute, including -all, -cover, and configuration keys
|
|
123
|
+
# sharing the prefix.
|
|
124
|
+
def self.revision_of key, prefix
|
|
125
|
+
key = key.to_s
|
|
126
|
+
return nil unless key.start_with? prefix
|
|
127
|
+
revision = key.delete_prefix prefix
|
|
128
|
+
(RevisionRx.match? revision) ? revision : nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The attribute pairs with every mark in ascending revision order and
|
|
132
|
+
# every other attribute after them, so change texts merged into one
|
|
133
|
+
# collapse-mode entry follow revision order, not source order.
|
|
134
|
+
def self.sort_marks attributes, prefix, kind = nil
|
|
135
|
+
attributes.sort_by.with_index do |(key, _), index|
|
|
136
|
+
revision = revision_of key, prefix
|
|
137
|
+
revision ? [0, (sort_key revision, kind), index] : [1, [], index]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The table group a revision belongs to: its first two levels in
|
|
142
|
+
# collapse mode, the revision itself otherwise.
|
|
143
|
+
def self.group_key revision, mode
|
|
144
|
+
return revision unless mode == 'collapse'
|
|
145
|
+
parts = revision.split '-'
|
|
146
|
+
parts.size > 2 ? (parts.take 2).join('-') : revision
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A table group's prevrev or prevrevdate value: the group's own
|
|
150
|
+
# attribute, or in collapse mode, the lowest third-level revision's
|
|
151
|
+
# attribute that the document sets.
|
|
152
|
+
def self.group_attr doc, revision, suffix, mode
|
|
153
|
+
if (value = doc.attr %(#{revision}-#{suffix}))
|
|
154
|
+
return value
|
|
155
|
+
end
|
|
156
|
+
return nil unless mode == 'collapse' && revision.count('-') == 1
|
|
157
|
+
rx = /\A#{::Regexp.escape revision}-(\d+)-#{suffix}\z/
|
|
158
|
+
lowest = doc.attributes.keys.filter_map {|key| (m = rx.match key.to_s) && [m[1].to_i, key] }.min
|
|
159
|
+
lowest && (doc.attr lowest[1])
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The Matcher for a document's revnumber, mode, and kind.
|
|
163
|
+
def self.matcher doc, prefix, revnumber
|
|
164
|
+
Matcher.new prefix, revnumber, (mode doc), (kind doc)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Decides which marks on a node draw a change bar for the current
|
|
168
|
+
# revnumber, and which third-level revision each belongs to.
|
|
169
|
+
class Matcher
|
|
170
|
+
attr_reader :mode, :kind, :exact
|
|
171
|
+
|
|
172
|
+
def initialize prefix, revnumber, mode, kind = DEFAULT_KIND
|
|
173
|
+
@prefix = prefix
|
|
174
|
+
@mode = mode
|
|
175
|
+
@kind = kind
|
|
176
|
+
revnumber = revnumber.to_s
|
|
177
|
+
@exact = %(#{prefix}#{revnumber.tr '.', '-'})
|
|
178
|
+
if mode && (/\A\d+(?:\.\d+)+\z/.match? revnumber)
|
|
179
|
+
parts = revnumber.split('.').map(&:to_i)
|
|
180
|
+
@base = parts.take 2
|
|
181
|
+
@current_z = parts[2]
|
|
182
|
+
else
|
|
183
|
+
@mode = nil
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# A Hash of each matching mark name to its third-level number, or
|
|
188
|
+
# to nil for a mark without one. Empty when nothing matches.
|
|
189
|
+
def marks attributes
|
|
190
|
+
return {} unless attributes
|
|
191
|
+
return ((attributes.key? @exact) ? { @exact => nil } : {}) unless @mode
|
|
192
|
+
result = {}
|
|
193
|
+
attributes.each_key do |key|
|
|
194
|
+
next unless ::String === key && (revision = ThirdLevel.revision_of key, @prefix)
|
|
195
|
+
parts = revision.split('-').map(&:to_i)
|
|
196
|
+
next unless (parts.take 2) == @base
|
|
197
|
+
z = parts[2]
|
|
198
|
+
if @current_z
|
|
199
|
+
next if z ? z > @current_z : @kind == 'draft'
|
|
200
|
+
end
|
|
201
|
+
result[key] = z
|
|
202
|
+
end
|
|
203
|
+
result
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def match? attributes
|
|
207
|
+
!(marks attributes).empty?
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
data/lib/asciidoctor/rhrev.rb
CHANGED
|
@@ -7,6 +7,8 @@ require 'asciidoctor' unless defined? Asciidoctor
|
|
|
7
7
|
require_relative 'rhrev/processors'
|
|
8
8
|
require_relative 'rhrev/helpers'
|
|
9
9
|
require_relative 'rhrev/cell_text_marks'
|
|
10
|
+
require_relative 'rhrev/mark_targets'
|
|
11
|
+
require_relative 'rhrev/third_level'
|
|
10
12
|
require_relative 'rhrev/catalog'
|
|
11
13
|
|
|
12
14
|
# Try to load the PDF converter support if asciidoctor-pdf is available
|
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.
|
|
4
|
+
version: 1.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 白一百 baiyibai
|
|
@@ -43,12 +43,14 @@ files:
|
|
|
43
43
|
- lib/asciidoctor/rhrev/converter.rb
|
|
44
44
|
- lib/asciidoctor/rhrev/exporter.rb
|
|
45
45
|
- lib/asciidoctor/rhrev/helpers.rb
|
|
46
|
+
- lib/asciidoctor/rhrev/mark_targets.rb
|
|
46
47
|
- lib/asciidoctor/rhrev/processors.rb
|
|
47
48
|
- lib/asciidoctor/rhrev/renderer.rb
|
|
48
49
|
- lib/asciidoctor/rhrev/rhrev_html.rb
|
|
49
50
|
- lib/asciidoctor/rhrev/rhrev_pdf.rb
|
|
50
51
|
- lib/asciidoctor/rhrev/table_cell_change_bar_patch.rb
|
|
51
52
|
- lib/asciidoctor/rhrev/table_draw_patch.rb
|
|
53
|
+
- lib/asciidoctor/rhrev/third_level.rb
|
|
52
54
|
homepage: https://gitlab.com/baiyibai/rhrev-asciidoctor-automatic-revision-history
|
|
53
55
|
licenses:
|
|
54
56
|
- CC0-1.0
|