distorted-jekyll 0.6.0 → 0.7.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.
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