distorted-jekyll 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/distorted-jekyll.rb +0 -4
  4. data/lib/distorted-jekyll/{template/13th-style.css → 13th-style.css} +0 -0
  5. data/lib/distorted-jekyll/13th-style.rb +1 -1
  6. data/lib/distorted-jekyll/_config_default.yml +3 -21
  7. data/lib/distorted-jekyll/invoker.rb +194 -219
  8. data/lib/distorted-jekyll/liquid_liquid.rb +255 -0
  9. data/lib/distorted-jekyll/liquid_liquid/anchor.liquid +5 -0
  10. data/lib/distorted-jekyll/liquid_liquid/anchor_inline.liquid +1 -0
  11. data/lib/distorted-jekyll/liquid_liquid/embed.liquid +1 -0
  12. data/lib/distorted-jekyll/liquid_liquid/img.liquid +1 -0
  13. data/lib/distorted-jekyll/liquid_liquid/object.liquid +5 -0
  14. data/lib/distorted-jekyll/liquid_liquid/picture.liquid +15 -0
  15. data/lib/distorted-jekyll/liquid_liquid/picture.rb +48 -0
  16. data/lib/distorted-jekyll/liquid_liquid/picture_source.liquid +1 -0
  17. data/lib/distorted-jekyll/liquid_liquid/root.liquid +5 -0
  18. data/lib/distorted-jekyll/liquid_liquid/video.liquid +5 -0
  19. data/lib/distorted-jekyll/liquid_liquid/video_source.liquid +1 -0
  20. data/lib/distorted-jekyll/md_injection.rb +30 -25
  21. data/lib/distorted-jekyll/media_molecule.rb +20 -0
  22. data/lib/distorted-jekyll/media_molecule/font.rb +21 -0
  23. data/lib/distorted-jekyll/media_molecule/image.rb +15 -0
  24. data/lib/distorted-jekyll/media_molecule/never_let_you_down.rb +28 -0
  25. data/lib/distorted-jekyll/media_molecule/pdf.rb +108 -0
  26. data/lib/distorted-jekyll/media_molecule/svg.rb +20 -0
  27. data/lib/distorted-jekyll/media_molecule/text.rb +23 -0
  28. data/lib/distorted-jekyll/media_molecule/video.rb +45 -0
  29. data/lib/distorted-jekyll/monkey_business/jekyll/cleaner.rb +68 -1
  30. data/lib/distorted-jekyll/static_state.rb +19 -60
  31. data/lib/distorted-jekyll/the_setting_sun.rb +179 -0
  32. metadata +26 -21
  33. data/lib/distorted-jekyll/floor.rb +0 -266
  34. data/lib/distorted-jekyll/molecule/font.rb +0 -62
  35. data/lib/distorted-jekyll/molecule/image.rb +0 -94
  36. data/lib/distorted-jekyll/molecule/lastresort.rb +0 -51
  37. data/lib/distorted-jekyll/molecule/pdf.rb +0 -79
  38. data/lib/distorted-jekyll/molecule/svg.rb +0 -47
  39. data/lib/distorted-jekyll/molecule/text.rb +0 -62
  40. data/lib/distorted-jekyll/molecule/video.rb +0 -85
  41. data/lib/distorted-jekyll/template/error_code.liquid +0 -3
  42. data/lib/distorted-jekyll/template/font.liquid +0 -32
  43. data/lib/distorted-jekyll/template/image.liquid +0 -32
  44. data/lib/distorted-jekyll/template/lastresort.liquid +0 -20
  45. data/lib/distorted-jekyll/template/pdf.liquid +0 -14
  46. data/lib/distorted-jekyll/template/svg.liquid +0 -32
  47. data/lib/distorted-jekyll/template/text.liquid +0 -32
  48. data/lib/distorted-jekyll/template/video.liquid +0 -11
@@ -0,0 +1,255 @@
1
+ require 'liquid/drop'
2
+ require 'liquid/template'
3
+
4
+
5
+ module Cooltrainer
6
+
7
+ # DistorteD Liquid::Template-caching Hash.
8
+ # Jekyll has its own Liquid cache enabled by default as of 4.0,
9
+ # but the Jekyll::LiquidRenderer::File has a different interface
10
+ # than Liquid::Template (e.g. no :assigns accessor).
11
+ @@watering rescue begin
12
+ @@watering = Hash[]
13
+ end
14
+
15
+
16
+ # Entry-point for MediaMolecules to render HTML (and maybe eventually other formats!).
17
+ #
18
+ # Liquid is arguably a poor choice for this use case since it is designed
19
+ # to handle arbitrary user-supplied templates in a safe way,
20
+ # versus e.g. ERB which allows in-template execution of arbitrary Ruby code,
21
+ # but our templates are bundled here in the Gem (ostensibly) should be trustworthy.
22
+ #
23
+ # I considered using Nokogiri's DocumentFragment Builder instead:
24
+ # fragment = Nokogiri::HTML::DocumentFragment.parse(''.freeze)
25
+ # Nokogiri::HTML::Builder.with(fragment) do |doc|
26
+ # doc.picture {
27
+ # changes.each { |change| doc.source(:srcset=change.name) }
28
+ # }
29
+ # end
30
+ # fragment.to_html
31
+ #
32
+ # But the way DistorteD works (with MIME::Type#distorted_template_method)
33
+ # means we would need a way to collate child elements anyway, since each call
34
+ # to a :distorted_template_method should generate only one variation,
35
+ # meaning we'd end up with a bunch of <source> tag Strings but would
36
+ # still need to collect them under a parent <picture> with a sibling <img>.
37
+ #
38
+ # Liquid is already heavily used by Jekyll, and of course DistorteD-Jekyll
39
+ # itself is a Liquid::Tag, so we may as well use it.
40
+ # Nokogiri, on the other hand, is not an explicit dependency of Jekyll.
41
+ # It will most likely be available, and DD itself pulls it in via SVGO
42
+ # and others, but Liquid will also allow us the flexibility to render
43
+ # formats other than just HTML/XML, e.g. BBCode or even Markdown.
44
+ #
45
+ # I might revisit this design decision once I experience working with more
46
+ # media formats and in more page contexts :)
47
+ ElementalCreation = Struct.new(:element, :name, :parents, :children, :template, :assigns, :change, keyword_init: true) do
48
+
49
+ def initialize(element, change = nil, parents: nil, children: nil, template: nil, assigns: Hash[])
50
+ super(
51
+ # Symbol Template name, corresponding to a Liquid template file name,
52
+ # not necessarily corresponding to the exact name of the element we return.
53
+ element: element,
54
+ change: change,
55
+ name: change&.name || element,
56
+ # Symbol name or Enumerable[Symbol] names of parent Element(s) we should be under,
57
+ # in order from outermost to innermost nesting.
58
+ # This is used to collate Change templates under a required parent Element,
59
+ # e.g. <source> tags must be under a <picture> or a <video> tag.
60
+ parents: parents.nil? ? nil : (parents.is_a?(Enumerable) ? parents.to_a : Array[parents]),
61
+ # Set up a Hash to store any children of this element in an Array-like way
62
+ # using auto-incrementing Integer keys. Its `&default_proc` responds to Symbol Element names,
63
+ # uses :detect to search for and return the first instance of that Symbol iff one exists,
64
+ # and if not it instantiates an Element Struct for that symbol and stores it.
65
+ children: Hash.new { |children_hash, element| children_hash.values.detect(
66
+ # Enumerable#detect will call this `ifnone` Proc if :detect's block returns nil.
67
+ # This Proc will instantiate a new Element with a copy of our :change, then store and return it.
68
+ # Can remove the destructured empty Hash once Ruby 2.7 is gone.
69
+ ->{ children_hash.store(children_hash.length, self.class.new(element, change, **{}))}
70
+ ) { |child| child.element == element } if element.is_a?(Symbol) }.tap { |children_hash|
71
+ # Merge the children-given-to-initialize() into our Hash.
72
+ case children
73
+ when Array then children.map.with_index.with_object(children_hash) { |(v, i), h| h.store(i, v) }
74
+ when Hash then ch.merge(children)
75
+ end
76
+ },
77
+ # Our associated Liquid::Template, based on our element name.
78
+ template: template || self.WATERING(element),
79
+ # Container of variables we want to render in Liquid besides what's covered by the basics.
80
+ assigns: assigns,
81
+ )
82
+
83
+ # Go ahead and define accessors for any assigns we already know about.
84
+ # Others are supported via the :method_missing below.
85
+ assigns.each_key { |assign|
86
+ define_method(assign) do; self[:assigns].fetch(assign, nil); end
87
+ define_method("#{assign.to_s}=") do |value|; self[:assigns].store(assign, value); end
88
+ }
89
+ end
90
+
91
+ # Hash[String] => Integer containing weights for MIME::Type#sub_type sorting weights.
92
+ # Weights are assigned in auto-incrementing Array order and will be called from :<=>.
93
+ # Sub-types near the beginning will sort before sub-types near the bottom.
94
+ # This is important for things like <picture> tags where the first supported <source> child
95
+ # encountered is the one that will be used, so we want vector types (SVG) to come first,
96
+ # then modern annoying formats like AVIF/WebP, then old standby types like PNG.
97
+ # TODO: Sort things other than Images
98
+ SORT_WEIGHTS = [
99
+ 'svg+xml'.freeze,
100
+ 'avif'.freeze,
101
+ 'webp'.freeze,
102
+ 'png'.freeze,
103
+ 'jpeg'.freeze,
104
+ 'gif'.freeze,
105
+ ].map.with_index.to_h
106
+ # Return a static 0 weight for unknown sub_types.
107
+ SORT_WEIGHTS.default_proc = Proc.new { 0 }
108
+
109
+ # Elements should sort themselves under their parent when rendering.
110
+ # Use predefined weights, e.g.:
111
+ # irb> SORT_WEIGHTS['avif']
112
+ # => 1
113
+ # irb> SORT_WEIGHTS['png']
114
+ # => 3
115
+ # irb> SORT_WEIGHTS['avif'] <=> SORT_WEIGHTS['png']
116
+ # => -1
117
+ def <=>(otra)
118
+ SORT_WEIGHTS[self.change&.type&.sub_type] <=> SORT_WEIGHTS[otra&.change&.type&.sub_type]
119
+ end
120
+
121
+ # Take a child Element and store it.
122
+ # If it requests no parents, store it with us.
123
+ # If it requests a parent, forward it there to the same method.
124
+ def mad_child(moon_child)
125
+ parent = moon_child.parents&.shift
126
+ if parent.nil? # When shifting/popping an empty :parents Array
127
+ # Store the child with an incrementing Integer key as if
128
+ # self[Lchildren] were an Array
129
+ self[:children].store(self[:children].length, moon_child)
130
+ else
131
+ # Forward the child to the next level of ElementalCreation.
132
+ # The Struct will be instantiated by the :children Hash's &default_proc
133
+ self[:children][parent].mad_child(moon_child)
134
+ end
135
+ end
136
+
137
+ # Generic Liquid template loader
138
+ # Jekyll's site-wide Liquid renderer caches in 4.0+ and is usable via
139
+ # `site.liquid_renderer.file(cache_key).parse(liquid_string)`,
140
+ # but the Jekyll::LiquidRenderer::File you get back doesn't let us
141
+ # play with the `assigns` directly, so I stopped using the site renderer
142
+ # in favor of our own cache.
143
+ def WATERING(template_filename)
144
+ begin
145
+ # Memoize parsed Templates to this Struct subclass.
146
+ @@watering[template_filename] ||= begin
147
+ template = File.join(__dir__, 'liquid_liquid'.freeze, "#{template_filename}.liquid".freeze)
148
+ Jekyll.logger.debug('DistorteD::WATERING', template)
149
+ Liquid::Template.parse(File.read(template))
150
+ end
151
+ rescue Liquid::SyntaxError => l
152
+ # This shouldn't ever happen unless a new version of Liquid
153
+ # breaks syntax compatibility with our templates somehow.
154
+ l.message
155
+ end
156
+ end #WATERING
157
+
158
+ # Returns the rendered String contents of this and all child Elements.
159
+ def render
160
+ self[:template].render(
161
+ self[:assigns].merge(Hash[
162
+ # Most Elements will be associated with a Change Struct
163
+ # encapsulating a single wanted variation on the source file.
164
+ :change => self[:change].nil? ? nil : Cooltrainer::ChangeDrop.new(self[:change]),
165
+ # Create Elements for every wanted child if they were only given
166
+ # to us as Symbols, then render them all to Strings to include
167
+ # in our own output.
168
+ :children => self[:children]&.values.map { |child|
169
+ child.is_a?(Symbol) ? self.class.new(child, self[:change], parents: self[:name], assigns: self[:assigns]) : child
170
+ }.sort.map(&:render), # :sort will use ElementalCreation's :<=> method.
171
+ ]).transform_keys(&:to_s).transform_values { |value|
172
+ # Liquid wants String keys and values, not Symbols.
173
+ value.is_a?(Symbol) ? value.to_s : value
174
+ }
175
+ )
176
+ end
177
+
178
+ # Calling :flatten on an Array[self] will fail unless we override :to_ary to stop
179
+ # implicitly looking like an Array. Or we could implement :flatten, but this probably
180
+ # fixes other situations too.
181
+ # https://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html
182
+ def to_ary; nil; end
183
+
184
+ # Expose Liquid's 'assigns' accessors for any keys we weren't given upfront.
185
+ def method_missing(meth, *a)
186
+ # Grab the key from the method name, minus the trailing '=' for writers.
187
+ meth.to_s.end_with?('='.freeze) ? self[:assigns].store(meth.to_s.chomp('='.freeze).to_sym, a.first) : self[:assigns].fetch(meth, nil)
188
+ end
189
+
190
+ # Destination filename, or the element name.
191
+ def to_s; (self[:name] || self[:element]).to_s; end
192
+
193
+ # e.g. <ElementalCreation::source name=IIDX-turntable-full.svg, parents=[:anchor, :picture]>
194
+ def inspect; "<#{self.class.name.split('::'.freeze).last}::#{self[:element]} #{
195
+ [:name, :parents, :children].map { |k| (self[k].nil? || self[k]&.empty?) ? nil : " #{k.to_s}=#{self[k]}" }.compact.join(','.freeze)
196
+ }>"; end
197
+
198
+ end # Struct ElementalCreation
199
+
200
+
201
+ # Wrap a Change in a Drop that emits Element attribute keys as well as the value.
202
+ # An underlying ChangeDrop will instantiate us as an instance variable
203
+ # and forward its Change to us, then return that instance from its own :attr method.
204
+ #
205
+ # Our templates can use this to avoid emitting empty attributes
206
+ # for corresponding empty values by calling {{ change.attr.whatever }}
207
+ # instead of invoking the regular ChangeDrop via {{ change.whatever }}.
208
+ #
209
+ # For example, if a <source>'s Change defines a media-query,
210
+ # {{ change.media }} will emit the plain value (e.g. `min-width: 800px`)
211
+ # and would typically be used in a template inside an explicit attribute key,
212
+ # e.g. `<source … media="{{ change.media }}"\>`.
213
+ #
214
+ # A template could instead call this drop via e.g. <source … {{ attr_media }}\>`
215
+ # to emit the same thing if a media-query is set but emit nothing if one isn't!
216
+ class ChangeAttrDrop < Liquid::Drop
217
+ def initialize(change)
218
+ @change = change
219
+ end
220
+ def liquid_method_missing(method)
221
+ # The underlying ChangeDrop is what responds to :attr, so we only
222
+ # need to respond to the Change keys in the same way ChangeDrop does.
223
+ value = @change&.send(method.to_sym)
224
+ # Return an empty String if there is no value, otherwise return `attr="value"`.
225
+ # Intentional leading-space in output so Liquid tags can abut in templates.
226
+ value.nil? ? ''.freeze : " #{method.to_str}=\"#{value.to_s}\""
227
+ end
228
+ end # Struct ChangeAttrDrop
229
+
230
+
231
+ # Wrap a Change in a Drop that our Element Liquid::Templates can use
232
+ # to emit either values-alone or keys-and-values for any attribute
233
+ # of any one variation ot a media file.
234
+ class ChangeDrop < Liquid::Drop
235
+ def initialize(change)
236
+ @change = change
237
+ end
238
+ def initialism
239
+ @initialism ||= @change.type&.sub_type&.to_s.split(MIME::Type::SUB_TYPE_SEPARATORS)[0].upcase
240
+ end
241
+ def attr
242
+ # Use a ChangeAttrDrop to avoid emitting keys for empty values.
243
+ # It will respond to its own Change-key method just like we do.
244
+ @attr ||= Cooltrainer::ChangeAttrDrop.new(@change)
245
+ end
246
+ def liquid_method_missing(method)
247
+ # Liquid will send us String keys only, so translate them
248
+ # to Symbols when looking up in the Change Struct.
249
+ # Don't explicitly call :to_s before returning,
250
+ # because we might be returning an Array.
251
+ @change&.send(method.to_sym) || super
252
+ end
253
+ end # Struct ChangeDrop
254
+
255
+ end
@@ -0,0 +1,5 @@
1
+ <a>
2
+ {%- for child in children %}
3
+ {{ child }}
4
+ {%- endfor -%}
5
+ </a>
@@ -0,0 +1 @@
1
+ <a href="{{ change.dir }}{{ change.name }}{{ fragment }}">{{ change.name }} [{{ change.initialism }}]</a>
@@ -0,0 +1 @@
1
+ <embed src="{{ change.dir }}{{ change.name }}{{ fragment }}"{{ change.attr.type }}{{ change.attr.width }}{{ change.attr.height }}/>
@@ -0,0 +1 @@
1
+ <img src="{{ change.dir }}{{ change.name }}"{{ change.attr.alt }}{{ change.attr.title }}{{ change.attr.loading }}/>
@@ -0,0 +1,5 @@
1
+ <object data="{{ change.dir }}{{ change.name }}{{ fragment }}"{{ change.attr.type }}{{ change.attr.width }}{{ change.attr.height }}>
2
+ {%- for child in children %}
3
+ {{ child }}
4
+ {%- endfor -%}
5
+ </object>
@@ -0,0 +1,15 @@
1
+ {% assign img_child = nil -%}
2
+ <picture>
3
+ {% for child in children %}
4
+ {%- assign test_img = child | slice: 1, 3 -%}
5
+ {%- if test_img == 'img' -%}
6
+ {%- assign img_child = child -%}
7
+ {%- else -%}
8
+ {{ child }}
9
+ {%- endif -%}
10
+ {%- endfor -%}
11
+ {%- unless img_child -%}
12
+ {%- assign img_child = '<img/>' -%}
13
+ {%- endunless -%}
14
+ {{ img_child }}
15
+ </picture>
@@ -0,0 +1,48 @@
1
+ require 'distorted-jekyll/liquid_liquid'
2
+ require 'distorted/modular_technology/vips/save'
3
+ require 'distorted/media_molecule'
4
+
5
+ module Jekyll; end
6
+ module Jekyll::DistorteD; end
7
+ module Jekyll::DistorteD::LiquidLiquid; end
8
+ module Jekyll::DistorteD::LiquidLiquid::Picture
9
+
10
+
11
+ # Returns a CSS media query String for a full-size Image outer_limit allowing btowsers,
12
+ # to properly consider its <source> alongside any generated resized versions of the same Image.
13
+ # https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries
14
+ def self.full_size_media_query(change, vips_image)
15
+ # This is kinda hacky, but use the Setting loader to look for `:outer_limits`
16
+ # of this Type that are larger than this `vips_image` since we won't have visibility of
17
+ # other `changes` from this Class instance method.
18
+ larger_than_us = Jekyll::DistorteD::the_setting_sun(:outer_limits, *(change.type.settings_paths))
19
+ .map { |l| l.fetch(:width, nil)} # Look for a :width key in every outer limit.
20
+ .compact # Discard any limits that don't define :width.
21
+ .keep_if { |w| w > vips_image.width } # Discard any limits whose :width is smaller than us.
22
+ # Add an additional `max` constraint to the media query if this `vips_image`
23
+ # is not the largest one in the <picture>.
24
+ # This effectively makes this <source> useless since so few user-agents will ever
25
+ # have a viewport the exact pixel width of this image, but for our use case
26
+ # it's better to have a useless media query than no media query since it will
27
+ # make browsers pick a better variation than this one.
28
+ return "(min-width: #{vips_image.width}px)#{" and (max-width: #{vips_image.width}px)" unless larger_than_us.empty?}"
29
+ end
30
+
31
+ # Returns an anonymous method that generates a <source> tag for Image output Types.
32
+ def self.render_picture_source
33
+ @@render_picture_source = lambda { |change|
34
+ # Fill in missing CSS media queries for any original-size (tag == null) Change that lacks one.
35
+ if change.width.nil? and not change.type.sub_type.include?('svg'.freeze)
36
+ change.width = to_vips_image.width
37
+ end
38
+ Cooltrainer::ElementalCreation.new(:picture_source, change, parents: Array[:anchor, :picture])
39
+ }
40
+ end
41
+
42
+ # Lots of MediaMolecules will want to render image representations of various media_types,
43
+ # so define a render method once for every Vips-supported Type that can be included/shared.
44
+ Cooltrainer::DistorteD::IMPLANTATION(:OUTER_LIMITS, Cooltrainer::DistorteD::Technology::Vips::Save).each_key { |type|
45
+ define_method(type.distorted_template_method, Jekyll::DistorteD::LiquidLiquid::Picture::render_picture_source)
46
+ }
47
+
48
+ end
@@ -0,0 +1 @@
1
+ <source srcset="{{ change.dir }}{{ change.basename }}{{ change.extname }}{% if change.width %} {{ change.width }}w{% endif %}{% for break_width in change.breaks %}, {{ change.dir }}{{ change.basename }}-{{ break_width }}{{ change.extname }} {{ break_width }}w{% endfor %}"{{ change.attr.type }}{{ change.attr.media }}/>
@@ -0,0 +1,5 @@
1
+ <div class="{{ dan }}">
2
+ {%- for child in children %}
3
+ {{ child }}
4
+ {%endfor%}
5
+ </div>
@@ -0,0 +1,5 @@
1
+ <video controls>
2
+ {% for child in children %}
3
+ {{ child }}
4
+ {%- endfor -%}
5
+ </video>
@@ -0,0 +1 @@
1
+ <source src="{{ change.dir }}{{ change.name }}"{{ change.attr.type }}{{ change.attr.media }}/>
@@ -215,13 +215,13 @@ module Kramdown
215
215
  matched
216
216
  end
217
217
 
218
- def attrs(el, type = :img)
219
- matched = []
218
+ def flatten_attributes(el, type = :img)
219
+ matched = {}
220
220
 
221
221
  if el.is_a? Enumerable
222
222
  # Support an Array of elements...
223
223
  el.each {
224
- |child| matched.push(*attrs(child, type))
224
+ |child| matched.merge!(flatten_attributes(child, type))
225
225
  }
226
226
  else
227
227
  # ...or a tree of elements.
@@ -231,32 +231,36 @@ module Kramdown
231
231
  # will be duplicated in `class` or some other `:attr` anyway.
232
232
  # Those things should be added here if this is ever used in a
233
233
  # more generic context than just parsing the image tags.
234
- matched << el.attr unless el.attr.empty?
234
+ matched.merge!(el.attr) unless el.attr.empty?
235
235
  end
236
236
  unless el.children.empty?
237
237
  # Keep looking even if this element was one we are looking for.
238
238
  el.children.each {
239
- |child| matched.push(*attrs(child, type))
239
+ |child| matched.merge!(flatten_attributes(child, type))
240
240
  }
241
241
  end
242
242
  end
243
243
  matched
244
244
  end
245
245
 
246
- # Convert Markdown element attributes to a string key=value,
247
- # except for `src` (DD-specific)
248
- def to_attrs(k, v)
249
- # DistorteD prefers the media filename as a positional argument,
250
- # not a named kwarg.
251
- if k == 'src'
252
- v.to_s
253
- else
254
- k.to_s + '="' + v.to_s + '"'
255
- end
256
- end
257
-
258
- def distorted(attrs)
259
- "{% distorted #{attrs.map{|k,v| to_attrs(k, v)}.join(' ')} %}"
246
+ # Geenerates a DistorteD Liquid tag String given a Hash of element attributes.
247
+ # Examples:
248
+ # {% distorted rpg-ra11.nfo alt="HellMarch INTENSIFIES" title="C&C RA2:YR NoCD NFO" encoding="IBM437" crop="none" %}
249
+ # {% distorted DistorteD.png alt="DistorteD logo" title="This is so cool" crop="none" loading="lazy" %}
250
+ # The :additional_defaults Hash contains attribute values to set
251
+ # iff those attributes have no user-given value.
252
+ def distorted(attributes, additional_defaults: {})
253
+ "{% distorted #{additional_defaults.transform_keys{ |k|
254
+ # We will end up with a Hash of String-keys and String-values,
255
+ # so make sure override-defaults are String-keyed too.
256
+ k.to_s
257
+ }.merge(attributes).select{ |k, v|
258
+ # Filter out empty values, e.g. from an unfilled title/alt field
259
+ # in a Markdown image.
260
+ not v.empty?
261
+ }.map{ |k, v|
262
+ k.to_s + '="'.freeze + v.to_s + '"'.freeze
263
+ }.join(' '.freeze)} %}"
260
264
  end
261
265
 
262
266
  # Kramdown entry point
@@ -278,11 +282,10 @@ module Kramdown
278
282
  when 0..1
279
283
  # Render one (1) image/video/whatever. This behavior is the same
280
284
  # regardless if the image is in a single-item list or just by itself.
281
- distorted(attrs(imgs.first)&.first&.merge({
282
- # Images default to `attention` cropping for desktop/mobile versatility.
283
- # Override this for single images unless a cropping value was already set.
284
- 'crop'.freeze => attrs(imgs.first)&.first&.dig('crop'.freeze) || 'none'.freeze,
285
- }))
285
+ distorted(
286
+ flatten_attributes(imgs.first),
287
+ additional_defaults: {:crop => 'none'.freeze},
288
+ )
286
289
  else
287
290
  # Render a conceptual group (DD::BLOCKS)
288
291
 
@@ -296,7 +299,9 @@ module Kramdown
296
299
  raise "MD->img regex returned an unequal number of listed and unlisted tags."
297
300
  end
298
301
 
299
- "{% distort -%}\n#{list_imgs.map{|img| distorted(*attrs(img))}.join("\n")}\n{% enddistort %}"
302
+ "{% distort -%}\n#{list_imgs.map{ |img|
303
+ distorted(flatten_attributes(img))
304
+ }.join("\n")}\n{% enddistort %}"
300
305
  end
301
306
  end
302
307