Skip to content

Commit df82278

Browse files
authored
Copy images from unordeed and definition lists (#1093)
Entirely reworks how we copy images by adding a new type of extension to Asciidoctor: delegating converters. Adding a new converter isn't documented in their user's manual but their code makes it clear that they expect folks to plug in customized extensions. These make copying images *much* simpler because it allows us to intercept the call for inline images correctly *and* it lets us get at the attribute values. This mechanism is also useful for customizing how we generate output and this PR uses it to clean up one particularly gnarly output mechanism that was also getting in the way of copying images. It is now much more readable on both the output generation and image copying side.
1 parent 2d24177 commit df82278

File tree

5 files changed

+203
-155
lines changed

5 files changed

+203
-155
lines changed

resources/asciidoctor/lib/change_admonition/extension.rb

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require 'asciidoctor/extensions'
4+
require_relative '../delegating_converter.rb'
45

56
##
67
# Extensions for marking when something was added, when it *will* be added, or
@@ -25,6 +26,21 @@ def activate(registry)
2526
registry.block_macro ChangeAdmonitionBlock.new(revisionflag, tag), name
2627
registry.inline_macro ChangeAdmonitionInline.new(revisionflag), name
2728
end
29+
DelegatingConverter.setup(registry.document) { |doc| Converter.new doc }
30+
end
31+
32+
##
33+
# Properly renders change admonitions.
34+
class Converter < DelegatingConverter
35+
def admonition(node)
36+
return yield unless (flag = node.attr 'revisionflag')
37+
38+
<<~DOCBOOK.strip
39+
<#{tag_name = node.attr 'name'} revisionflag="#{flag}" revision="#{node.attr 'version'}">
40+
<simpara>#{node.content}</simpara>
41+
</#{tag_name}>
42+
DOCBOOK
43+
end
2844
end
2945

3046
##
@@ -41,27 +57,15 @@ def initialize(revisionflag, tag)
4157

4258
def process(parent, _target, attrs)
4359
version = attrs[:version]
44-
# We can *almost* go through the standard :admonition conversion but
45-
# that won't render the revisionflag or the revision. So we have to
46-
# go with this funny compound pass thing.
47-
admon = Asciidoctor::Block.new(parent, :pass, content_model: :compound)
48-
admon << Asciidoctor::Block.new(
49-
admon, :pass,
50-
attributes: { 'revisionflag' => @revisionflag },
51-
source: tag_source(version)
52-
)
53-
admon << Asciidoctor::Block.new(
54-
admon, :paragraph,
55-
source: attrs[:passtext],
56-
subs: Asciidoctor::Substitutors::NORMAL_SUBS
60+
Asciidoctor::Block.new(
61+
parent, :admonition,
62+
attributes: {
63+
'name' => @tag,
64+
'revisionflag' => @revisionflag,
65+
'version' => version,
66+
},
67+
source: attrs[:passtext]
5768
)
58-
admon << Asciidoctor::Block.new(admon, :pass, source: "</#{@tag}>")
59-
end
60-
61-
def tag_source(version)
62-
<<~HTML
63-
<#{@tag} revisionflag="#{@revisionflag}" revision="#{version}">
64-
HTML
6569
end
6670
end
6771

Lines changed: 63 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
# frozen_string_literal: true
22

3-
require_relative '../scaffold.rb'
3+
require_relative '../delegating_converter.rb'
44
require_relative 'copier.rb'
55

6+
##
7+
# Copies images that are referenced into the same directory as the
8+
# output files.
9+
#
10+
# It finds the images by looking in a comma separated list of directories
11+
# defined by the `resources` attribute.
12+
#
13+
# It can also be configured to copy the images that number callout lists by
14+
# setting `copy-callout-images` to the file extension of the images to copy.
15+
#
16+
# It can also be configured to copy the that decoration admonitions by
17+
# setting `copy-admonition-images` to the file extension of the images
18+
# to copy.
619
module CopyImages
20+
def self.activate(registry)
21+
DelegatingConverter.setup(registry.document) { |doc| Converter.new doc }
22+
end
23+
724
##
8-
# Copies images that are referenced into the same directory as the
9-
# output files.
10-
#
11-
# It finds the images by looking in a comma separated list of directories
12-
# defined by the `resources` attribute.
13-
#
14-
# It can also be configured to copy the images that number callout lists by
15-
# setting `copy-callout-images` to the file extension of the images to copy.
16-
#
17-
# It can also be configured to copy the that decoration admonitions by
18-
# setting `copy-admonition-images` to the file extension of the images
19-
# to copy.
20-
#
21-
class CopyImages < TreeProcessorScaffold
25+
# A Converter implementation that copies images as it sees them.
26+
class Converter < DelegatingConverter
2227
include Asciidoctor::Logging
2328

2429
ADMONITION_IMAGE_FOR_REVISION_FLAG = {
@@ -27,130 +32,79 @@ class CopyImages < TreeProcessorScaffold
2732
'deleted' => 'warning',
2833
}.freeze
2934
CALLOUT_RX = /CO\d+-(\d+)/
30-
INLINE_IMAGE_RX = /(\\)?image:([^:\s\[](?:[^\n\[]*[^\s\[])?)\[/m
31-
DOCBOOK_IMAGE_RX = %r{<imagedata fileref="([^"]+)"/>}m
3235

33-
def initialize(name)
34-
super
36+
def initialize(delegate)
37+
super(delegate)
3538
@copier = Copier.new
3639
end
3740

38-
def process_block(block)
39-
process_inline_image_from_source block
40-
process_inline_image_from_converted block
41-
process_block_image block
42-
process_callout block
43-
process_admonition block
44-
end
41+
#### "Conversion" methods
4542

46-
def process_block_image(block)
47-
return unless block.context == :image
48-
49-
uri = block.image_uri(block.attr('target'))
50-
process_image block, uri
43+
def admonition(node)
44+
if (extension = node.attr 'copy-admonition-images')
45+
if (image = admonition_image node)
46+
path = "images/icons/#{image}.#{extension}"
47+
@copier.copy_image node, path
48+
end
49+
end
50+
yield
5151
end
5252

53-
##
54-
# Scan the inline image from the asciidoc source. One day Asciidoc will
55-
# parse inline things into the AST and we can get at them nicely. Today, we
56-
# have to scrape them from the source of the node.
57-
def process_inline_image_from_source(block)
58-
return unless block.content_model == :simple
59-
60-
block.source.scan(INLINE_IMAGE_RX) do |(escape, target)|
61-
next if escape
62-
63-
# We have to resolve attributes inside the target. But there is a
64-
# "funny" ritual for that because attribute substitution is always
65-
# against the document. We have to play the block's attributes against
66-
# the document, then clear them on the way out.
67-
block.document.playback_attributes block.attributes
68-
target = block.sub_attributes target
69-
block.document.clear_playback_attributes block.attributes
70-
uri = block.image_uri target
71-
process_image block, uri
53+
def colist(node)
54+
if (extension = node.attr 'copy-callout-images')
55+
node.items.each do |item|
56+
copy_image_for_callout_items extension, item
57+
end
7258
end
59+
yield
7360
end
7461

75-
##
76-
# Scan the inline image from the generated docbook. It is not nice that
77-
# this is required but there isn't much we can do about it. We *could*
78-
# rewrite all of the image copying to be against the generated docbook
79-
# using this code but I feel like that'd be slower. For now, we'll stick
80-
# with this.
81-
def process_inline_image_from_converted(block)
82-
return unless block.context == :list_item &&
83-
block.parent.context == :olist
84-
85-
block.text.scan(DOCBOOK_IMAGE_RX) do |(target)|
86-
# We have to resolve attributes inside the target. But there is a
87-
# "funny" ritual for that because attribute substitution is always
88-
# against the document. We have to play the block's attributes against
89-
# the document, then clear them on the way out.
90-
uri = block.image_uri target
91-
process_image block, uri
92-
end
62+
def image(node)
63+
copy_image node, node.attr('target')
64+
yield
65+
end
66+
67+
def inline_image(node)
68+
# Inline images aren't "real" and don't have a source_location so we have
69+
# to get the location from the parent.
70+
copy_image node.parent, node.target
71+
yield
9372
end
9473

95-
def process_image(block, uri)
74+
#### Helper methods
75+
def copy_image(node, uri)
9676
return unless uri
9777
return if Asciidoctor::Helpers.uriish? uri # Skip external images
9878

99-
@copier.copy_image block, uri
79+
@copier.copy_image node, uri
10080
end
10181

102-
def process_callout(block)
103-
callout_extension = block.document.attr 'copy-callout-images'
104-
return unless callout_extension
105-
return unless block.parent && block.parent.context == :colist
106-
107-
coids = block.attr('coids')
82+
def copy_image_for_callout_items(callout_extension, node)
83+
coids = node.attr('coids')
10884
return unless coids
10985

11086
coids.scan(CALLOUT_RX) do |(index)|
111-
@copier.copy_image(
112-
block, "images/icons/callouts/#{index}.#{callout_extension}"
113-
)
87+
path = "images/icons/callouts/#{index}.#{callout_extension}"
88+
@copier.copy_image node, path
11489
end
11590
end
11691

117-
def process_admonition(block)
118-
admonition_extension = block.document.attr 'copy-admonition-images'
119-
return unless admonition_extension
92+
def admonition_image(node)
93+
if (revisionflag = node.attr 'revisionflag')
94+
image = ADMONITION_IMAGE_FOR_REVISION_FLAG[revisionflag]
95+
return image if image
12096

121-
process_standard_admonition admonition_extension, block
122-
process_change_admonition admonition_extension, block
123-
end
124-
125-
def process_standard_admonition(admonition_extension, block)
126-
return unless block.context == :admonition
127-
128-
# The image for a standard admonition comes from the style
129-
style = block.attr 'style'
130-
return unless style
131-
132-
@copier.copy_image(
133-
block, "images/icons/#{style.downcase}.#{admonition_extension}"
134-
)
135-
end
136-
137-
def process_change_admonition(admonition_extension, block)
138-
revisionflag = block.attr 'revisionflag'
139-
return unless revisionflag
140-
141-
admonition_image = ADMONITION_IMAGE_FOR_REVISION_FLAG[revisionflag]
142-
if admonition_image
143-
@copier.copy_image(
144-
block, "images/icons/#{admonition_image}.#{admonition_extension}"
145-
)
146-
else
14797
logger.warn(
14898
message_with_context(
14999
"unknow revisionflag #{revisionflag}",
150-
source_location: block.source_location
100+
source_location: node.source_location
151101
)
152102
)
103+
return
153104
end
105+
# The image for a standard admonition comes from the style
106+
style = node.attr 'style'
107+
style&.downcase
154108
end
155109
end
156110
end
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
##
4+
# Abstract base for implementing a converter that implements some conversions
5+
# and delegates the rest to the "next" converter.
6+
class DelegatingConverter
7+
##
8+
# Setup a converter on a document.
9+
def self.setup(document)
10+
converter = yield document.converter
11+
document.instance_variable_set :@converter, converter
12+
end
13+
14+
def initialize(delegate)
15+
@delegate = delegate
16+
end
17+
18+
def convert(node, transform = nil, opts = {})
19+
# The behavior of this method mirrors Asciidoctor::Base.convert
20+
t = transform || node.node_name
21+
if respond_to? t
22+
send t, node do
23+
# Passes a block that subclasses can call to run the converter chain.
24+
@delegate.convert node, transform, opts
25+
end
26+
else
27+
@delegate.convert node, transform, opts
28+
end
29+
end
30+
end

resources/asciidoctor/lib/extensions.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414

1515
Asciidoctor::Extensions.register CareAdmonition
1616
Asciidoctor::Extensions.register ChangeAdmonition
17+
Asciidoctor::Extensions.register CopyImages
1718
Asciidoctor::Extensions.register do
1819
# Enable storing the source locations so we can look at them. This is required
1920
# for EditMe to get a nice location.
2021
document.sourcemap = true
2122
block_macro LangOverride
2223
preprocessor CrampedInclude
2324
preprocessor ElasticCompatPreprocessor
24-
treeprocessor CopyImages::CopyImages
2525
treeprocessor EditMe
2626
treeprocessor ElasticCompatTreeProcessor
2727
# The tree processors after this must come after ElasticComptTreeProcessor

0 commit comments

Comments
 (0)