distorted-jekyll 0.5.4 → 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.
- checksums.yaml +4 -4
- data/LICENSE +661 -0
- data/README.md +7 -11
- data/lib/distorted-jekyll.rb +75 -0
- data/lib/distorted-jekyll/13th-style.css +79 -0
- data/lib/distorted-jekyll/13th-style.rb +58 -0
- data/lib/distorted-jekyll/_config_default.yml +63 -0
- data/lib/distorted-jekyll/blocks.rb +16 -0
- data/lib/distorted-jekyll/invoker.rb +234 -0
- data/lib/distorted-jekyll/liquid_liquid.rb +255 -0
- data/lib/distorted-jekyll/liquid_liquid/anchor.liquid +5 -0
- data/lib/distorted-jekyll/liquid_liquid/anchor_inline.liquid +1 -0
- data/lib/distorted-jekyll/liquid_liquid/embed.liquid +1 -0
- data/lib/distorted-jekyll/liquid_liquid/img.liquid +1 -0
- data/lib/distorted-jekyll/liquid_liquid/object.liquid +5 -0
- data/lib/distorted-jekyll/liquid_liquid/picture.liquid +15 -0
- data/lib/distorted-jekyll/liquid_liquid/picture.rb +48 -0
- data/lib/distorted-jekyll/liquid_liquid/picture_source.liquid +1 -0
- data/lib/distorted-jekyll/liquid_liquid/root.liquid +5 -0
- data/lib/distorted-jekyll/liquid_liquid/video.liquid +5 -0
- data/lib/distorted-jekyll/liquid_liquid/video_source.liquid +1 -0
- data/lib/distorted-jekyll/md_injection.rb +310 -0
- data/lib/distorted-jekyll/media_molecule.rb +20 -0
- data/lib/distorted-jekyll/media_molecule/font.rb +21 -0
- data/lib/distorted-jekyll/media_molecule/image.rb +15 -0
- data/lib/distorted-jekyll/media_molecule/never_let_you_down.rb +28 -0
- data/lib/distorted-jekyll/media_molecule/pdf.rb +108 -0
- data/lib/distorted-jekyll/media_molecule/svg.rb +20 -0
- data/lib/distorted-jekyll/media_molecule/text.rb +23 -0
- data/lib/distorted-jekyll/media_molecule/video.rb +45 -0
- data/lib/distorted-jekyll/monkey_business/jekyll/cleaner.rb +121 -0
- data/lib/distorted-jekyll/static_state.rb +160 -0
- data/lib/distorted-jekyll/the_setting_sun.rb +179 -0
- metadata +37 -34
@@ -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 @@
|
|
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,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 @@
|
|
1
|
+
<source src="{{ change.dir }}{{ change.name }}"{{ change.attr.type }}{{ change.attr.media }}/>
|
@@ -0,0 +1,310 @@
|
|
1
|
+
require 'kramdown'
|
2
|
+
|
3
|
+
# Replace standard Markdown image syntax with instances of DistorteD
|
4
|
+
# via its Liquid tag.
|
5
|
+
#
|
6
|
+
#
|
7
|
+
### SYNTAX
|
8
|
+
#
|
9
|
+
# I'm calling individual media elements "images" here because I'm overloading
|
10
|
+
# the Markdown image syntax to express them. There is no dedicated
|
11
|
+
# syntax for other media types in any flavor of Markdown as of 2019,
|
12
|
+
# so that seemed like the cleanest and sanest way to deal
|
13
|
+
# with non-images in DistorteD.
|
14
|
+
#
|
15
|
+
# DistorteD::Invoker will do the media type inspection and handling
|
16
|
+
# once we get into Liquid Land. This is the approach suggested by
|
17
|
+
# CommonMark: https://talk.commonmark.org/t/embedded-audio-and-video/441/15
|
18
|
+
#
|
19
|
+
# Media elements will display as a visibly related group when
|
20
|
+
# two or more Markdown image tags exist in consecutive Markdown
|
21
|
+
# unordered (e.g. *-+) list item or ordered (e.g. 1.) list item lines.
|
22
|
+
# Their captions should be hidden until opened in a lightbox or as a tooltip
|
23
|
+
# on desktop browsers via ::hover state.
|
24
|
+
#
|
25
|
+
# The inspiration for this display can be seen in the handling
|
26
|
+
# of two, three, or four images in any single post on Tw*tter.
|
27
|
+
# DistorteD expands on the concept by supporting things such as groups
|
28
|
+
# of more than four media elements and elements of heterogeneous media types.
|
29
|
+
#
|
30
|
+
# Standalone image elements (not contained in list item) should be displayed
|
31
|
+
# as single solo elements spanning the entire usable width of the
|
32
|
+
# container element, whatever that happens to be at the point where
|
33
|
+
# our regex excised a block of Markdown for us to work with.
|
34
|
+
#
|
35
|
+
#
|
36
|
+
### TECHNICAL CONSIDERATIONS
|
37
|
+
#
|
38
|
+
# Jekyll processes Liquid templates before processing page/post Markdown,
|
39
|
+
# so we can't rely on customizing the Markdown renderer's `:img` element
|
40
|
+
# output as a means to invoke DistorteD.
|
41
|
+
# https://jekyllrb.com/tutorials/orderofinterpretation/
|
42
|
+
#
|
43
|
+
# Prefer the POSIX-style bracket expressions (e.g. [[:digit:]]) over
|
44
|
+
# basic character classes (e.g. \d) in regex because they match Unicode
|
45
|
+
# instead of just ASCII.
|
46
|
+
# https://ruby-doc.org/core/Regexp.html#class-Regexp-label-Character+Classes
|
47
|
+
#
|
48
|
+
#
|
49
|
+
### INLINE ATTRIBUTE LISTS
|
50
|
+
#
|
51
|
+
# Support additional arguments passed to DistorteD via Kramdown-style
|
52
|
+
# inline attribute lists.
|
53
|
+
# https://kramdown.gettalong.org/syntax.html#images
|
54
|
+
# https://kramdown.gettalong.org/syntax.html#inline-attribute-lists
|
55
|
+
#
|
56
|
+
# IALs can be on the same line or on a line before or after their
|
57
|
+
# associated flow element.
|
58
|
+
# In ambiguous situations (flow elements both before and after an IAL),
|
59
|
+
# the IAL applies to the flow element before it.
|
60
|
+
# All associated elements and IALs must be on contiguous lines.
|
61
|
+
#
|
62
|
+
# This page provides a regex for parsing IALs, Section 5.3:
|
63
|
+
# https://golem.ph.utexas.edu/~distler/maruku/proposal.html
|
64
|
+
#
|
65
|
+
#
|
66
|
+
### SOLUTION
|
67
|
+
#
|
68
|
+
# A `pre_render` hook uses this regex to process Markdown source files
|
69
|
+
# and replace instances of the Markdown image syntax with instances of
|
70
|
+
# DistorteD's Liquid tags.
|
71
|
+
# Single images will be replaced with {% distorted %}.
|
72
|
+
# Multiple list-item images will be replaced with a {% distort %} block.
|
73
|
+
#
|
74
|
+
# By doing with with a regex (sorry!!) I hope to avoid a hard dependency on
|
75
|
+
# any one particular Markdown engine. Though I only support Kramdown for now,
|
76
|
+
# any engine that supports IALs should be fine.
|
77
|
+
#
|
78
|
+
# High-level explanation of what we intend to match:
|
79
|
+
#
|
80
|
+
# {:optional_ial => line_before_image} # Iff preceded by a blank line!
|
81
|
+
# (optional_list_item)? {:optional_ial => same_line}
|
82
|
+
# {:optional_ial => next_consecutive_line}
|
83
|
+
# Repeat both preceding matches (together) any number of times to parse
|
84
|
+
# a {% distort %} block.
|
85
|
+
# See inline comments below for more detail.
|
86
|
+
MD_IMAGE_REGEX = %r&
|
87
|
+
# Matching group of a single image tag.
|
88
|
+
(
|
89
|
+
# Optional preceding-line attribute list.
|
90
|
+
(
|
91
|
+
# One blank line, because:
|
92
|
+
# "If a block IAL is directly after and before a block-level element,
|
93
|
+
# it is applied to preceding element." —Kramdown BAL docs
|
94
|
+
#
|
95
|
+
# /\R/ - A linebreak: \n, \v, \f, \r \u0085 (NEXT LINE),
|
96
|
+
# \u2028 (LINE SEPARATOR), \u2029 (PARAGRAPH SEPARATOR) or \r\n.
|
97
|
+
^$\R
|
98
|
+
# Any amount of blank space on the line before block IAL
|
99
|
+
^[[:blank:]]*
|
100
|
+
# IAL regex from Section 5.3:
|
101
|
+
# https://golem.ph.utexas.edu/~distler/maruku/proposal.html
|
102
|
+
(?<block_ial_before>\{:(\\\}|[^\}])*\})
|
103
|
+
# Any amount of trailing whitespace followed by a newline.
|
104
|
+
[[:blank:]]*$\R
|
105
|
+
)? # Match all of that, or nothing.
|
106
|
+
# Begin matching the line that contains an image.
|
107
|
+
^
|
108
|
+
# Match anything that might be between that start-of-line
|
109
|
+
# and the first character (!) of an image.
|
110
|
+
(
|
111
|
+
# From Gruber's original Markdown page:
|
112
|
+
# "List markers typically start at the left margin, but may be indented
|
113
|
+
# by up to three spaces."
|
114
|
+
# Include both unordered (-+*) and ordered (\d\. like `1.`) lists.
|
115
|
+
(?<li>[ ]{0,3}[-\*\+|\d\.]
|
116
|
+
# Support an optional IAL for the list element as shown in Kramdown docs:
|
117
|
+
# https://kramdown.gettalong.org/syntax.html#lists
|
118
|
+
# Ctrl+F "{:.cls}"
|
119
|
+
(?<li_ial>\{:(\\\}|[^\}])*\})?
|
120
|
+
# "List markers must be followed by one or more spaces or a tab."
|
121
|
+
# https://daringfireball.net/projects/markdown/syntax#list
|
122
|
+
([ ]+|\t))
|
123
|
+
)? # Although any preceding elements are optional!
|
124
|
+
# Match Markdown image syntax:
|
125
|
+
# {:other="options"}
|
126
|
+
# beginning with the alt tag:
|
127
|
+
!\[(?<alt>(\\[[:print:]]|[^\]])*)\]
|
128
|
+
# Continuing with img src as anything after the '(' and before ')' or
|
129
|
+
# before anything that could be a title.
|
130
|
+
# Assume titles will be quoted.
|
131
|
+
\((?<src>(\\[[:print:]]|[^'")]+))
|
132
|
+
# Title is optional.
|
133
|
+
# Ignore double-quotes in single-quoted titles and single-quotes
|
134
|
+
# in double-quoted titles otherwise we can't use contractions.
|
135
|
+
# Don't include the title's opening or closing quotes in the capture.
|
136
|
+
('(?<title>(\\[[:print:]]|[^']*))'|"(?<title>(\\[[:print:]]|[^"]*))")?
|
137
|
+
# The closing ')' will always be present, title or no.
|
138
|
+
\)
|
139
|
+
# Optional IAL on the same line as the :img element **with no space between them**:
|
140
|
+
# "A span IAL (or two or more span IALs) has to be put directly after
|
141
|
+
# the span-level element to which it should be applied, no additional
|
142
|
+
# character is allowed between, otherwise it is ignored and only
|
143
|
+
# removed from the output." —Kramdown IAL docs
|
144
|
+
(?<span_ial>\{:(\\\}|[^\}])*\})*[[:print:]]*$\R
|
145
|
+
# Also support optional BALs on the lines following the image.
|
146
|
+
(^[[:blank:]]*(?<block_ial_after>\{:(\\\}|[^\}])*\})+$\R)*
|
147
|
+
)+ # Capture multiple images together for block display.
|
148
|
+
&x
|
149
|
+
|
150
|
+
|
151
|
+
def md_injection
|
152
|
+
Proc.new { |document, payload|
|
153
|
+
# Compare any given document's file extension to the list of enabled
|
154
|
+
# Markdown file extensions in Jekyll's config.
|
155
|
+
if payload['site']['markdown_ext'].include? document.extname.downcase[1..-1]
|
156
|
+
# Convert Markdown images to {% distorted %} tags.
|
157
|
+
#
|
158
|
+
# Use the same Markdown parser as Jekyll to avoid parsing inconsistencies
|
159
|
+
# between this pre_render hook and the main Markdown render step.
|
160
|
+
# This is still effectively a Markdown-parsing regex (and still
|
161
|
+
# effectively a bad idea lol) but it's the cleanest way I can come up
|
162
|
+
# with right now for separating DistorteD-destined Markdown from
|
163
|
+
# the rest of any given page.
|
164
|
+
# NOTE: Attribute List Definitions elsewhere in a Markdown document
|
165
|
+
# will be lost when converting this way. I might end up just parsing
|
166
|
+
# the entire document once with my own `to_liquid` converter, but I've been
|
167
|
+
# avoiding that as it seems wasteful because Jekyll then renders the entire
|
168
|
+
# Markdown document a second time immediately after our Liquid tag.
|
169
|
+
# It's fast enough that I should stop trying to prematurely optimize this :)
|
170
|
+
# TODO: Implement MD → DD love using only the #{CONFIGURED_MARKDOWN_ENGINE},
|
171
|
+
# searching for :img elements inside :li elements to build BLOCKS.
|
172
|
+
document.content = document.content.gsub(MD_IMAGE_REGEX) { |match|
|
173
|
+
Kramdown::Document.new(match).to_liquid
|
174
|
+
}
|
175
|
+
end
|
176
|
+
}
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
# Kramdown implementation of Markdown AST -> Liquid converter.
|
181
|
+
# Renders Markdown element attributes as key=value, also under the assumption
|
182
|
+
# of using gem 'liquid-tag-parser': https://github.com/dmalan/liquid-tag-parser
|
183
|
+
module Kramdown
|
184
|
+
module Converter
|
185
|
+
class Liquid < Base
|
186
|
+
|
187
|
+
# The incoming parsed Markdown tree will include many spurious elements,
|
188
|
+
# like container paragraph elements and the list item elements when
|
189
|
+
# parsing DD Grid style Markdown. Use this function to map a tree of
|
190
|
+
# arbitrary elements to a flat list of elements of a single type.
|
191
|
+
def children(el, type)
|
192
|
+
matched = []
|
193
|
+
|
194
|
+
if el.is_a? Enumerable
|
195
|
+
# We might want to run this against an Array output from an
|
196
|
+
# earlier invokation of this method.
|
197
|
+
el.each {
|
198
|
+
|item| matched.push(*children(item, type))
|
199
|
+
}
|
200
|
+
elsif el.type.equal? type
|
201
|
+
# If we find the type we're looking for, stop and return it.
|
202
|
+
# Let it bring its children along with it instead of recursing
|
203
|
+
# into them. This will let us match container-only elements
|
204
|
+
# such as <li> by type without considering the type of its children,
|
205
|
+
# for situation where its children are really what we want.
|
206
|
+
matched.push(el)
|
207
|
+
else
|
208
|
+
# Otherwise keep looking down the tree.
|
209
|
+
unless el.children.empty?
|
210
|
+
el.children.each {
|
211
|
+
|child| matched.push(*children(child, type))
|
212
|
+
}
|
213
|
+
end
|
214
|
+
end
|
215
|
+
matched
|
216
|
+
end
|
217
|
+
|
218
|
+
def flatten_attributes(el, type = :img)
|
219
|
+
matched = {}
|
220
|
+
|
221
|
+
if el.is_a? Enumerable
|
222
|
+
# Support an Array of elements...
|
223
|
+
el.each {
|
224
|
+
|child| matched.merge!(flatten_attributes(child, type))
|
225
|
+
}
|
226
|
+
else
|
227
|
+
# ...or a tree of elements.
|
228
|
+
if el.type.equal? type
|
229
|
+
# Images won't have a `:value` — only `:attr`s — and the only
|
230
|
+
# important things in their `:options` (e.g. IAL contents)
|
231
|
+
# will be duplicated in `class` or some other `:attr` anyway.
|
232
|
+
# Those things should be added here if this is ever used in a
|
233
|
+
# more generic context than just parsing the image tags.
|
234
|
+
matched.merge!(el.attr) unless el.attr.empty?
|
235
|
+
end
|
236
|
+
unless el.children.empty?
|
237
|
+
# Keep looking even if this element was one we are looking for.
|
238
|
+
el.children.each {
|
239
|
+
|child| matched.merge!(flatten_attributes(child, type))
|
240
|
+
}
|
241
|
+
end
|
242
|
+
end
|
243
|
+
matched
|
244
|
+
end
|
245
|
+
|
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)} %}"
|
264
|
+
end
|
265
|
+
|
266
|
+
# Kramdown entry point
|
267
|
+
def convert(el)
|
268
|
+
# The parsed "images" may also be audio, video, or some other
|
269
|
+
# media type. There is only one Markdown image tag, however.
|
270
|
+
imgs = children(el, :img)
|
271
|
+
|
272
|
+
# Enable conceptual-grouping (BLOCKS) mode if the count of list item
|
273
|
+
# elements matches the count of image elements in our
|
274
|
+
# chunk of Markdown. Technically I should check to make sure each
|
275
|
+
# image is the child of one of those list items,
|
276
|
+
# but this is way easier until I (hopefully never) find a parsing
|
277
|
+
# corner-case where this doesn't hold up.
|
278
|
+
lists = children(el, :li)
|
279
|
+
list_imgs = lists.map{|li| children(li, :img)}.flatten
|
280
|
+
|
281
|
+
case lists.count
|
282
|
+
when 0..1
|
283
|
+
# Render one (1) image/video/whatever. This behavior is the same
|
284
|
+
# regardless if the image is in a single-item list or just by itself.
|
285
|
+
distorted(
|
286
|
+
flatten_attributes(imgs.first),
|
287
|
+
additional_defaults: {:crop => 'none'.freeze},
|
288
|
+
)
|
289
|
+
else
|
290
|
+
# Render a conceptual group (DD::BLOCKS)
|
291
|
+
|
292
|
+
if imgs.count != list_imgs.count
|
293
|
+
# Sanity check :img count vs :img-in-:li count.
|
294
|
+
# We should support the corner case where the regex matches
|
295
|
+
# multiple consecutive lines, but with mixed list item status,
|
296
|
+
# e.g. a solo image abuts a conceptual group and gets globbed
|
297
|
+
# into a single match.
|
298
|
+
# For now, however:
|
299
|
+
raise "MD->img regex returned an unequal number of listed and unlisted tags."
|
300
|
+
end
|
301
|
+
|
302
|
+
"{% distort -%}\n#{list_imgs.map{ |img|
|
303
|
+
distorted(flatten_attributes(img))
|
304
|
+
}.join("\n")}\n{% enddistort %}"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|