distorted-jekyll 0.5.6 → 0.5.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +661 -0
  3. data/README.md +6 -10
  4. data/lib/distorted-jekyll.rb +79 -0
  5. data/lib/distorted-jekyll/13th-style.rb +58 -0
  6. data/lib/distorted-jekyll/_config_default.yml +79 -0
  7. data/lib/distorted-jekyll/blocks.rb +16 -0
  8. data/lib/distorted-jekyll/error_code.rb +24 -0
  9. data/lib/distorted-jekyll/floor.rb +148 -0
  10. data/lib/distorted-jekyll/injection_of_love.rb +305 -0
  11. data/lib/distorted-jekyll/invoker.rb +400 -0
  12. data/lib/distorted-jekyll/molecule/abstract.rb +238 -0
  13. data/lib/distorted-jekyll/molecule/font.rb +29 -0
  14. data/lib/distorted-jekyll/molecule/image.rb +105 -0
  15. data/lib/distorted-jekyll/molecule/last-resort.rb +54 -0
  16. data/lib/distorted-jekyll/molecule/pdf.rb +88 -0
  17. data/lib/distorted-jekyll/molecule/svg.rb +59 -0
  18. data/lib/distorted-jekyll/molecule/text.rb +74 -0
  19. data/lib/distorted-jekyll/molecule/video.rb +43 -0
  20. data/lib/distorted-jekyll/monkey_business/jekyll/cleaner.rb +54 -0
  21. data/lib/distorted-jekyll/static/font.rb +42 -0
  22. data/lib/distorted-jekyll/static/image.rb +55 -0
  23. data/lib/distorted-jekyll/static/lastresort.rb +28 -0
  24. data/lib/distorted-jekyll/static/pdf.rb +53 -0
  25. data/lib/distorted-jekyll/static/state.rb +141 -0
  26. data/lib/distorted-jekyll/static/svg.rb +52 -0
  27. data/lib/distorted-jekyll/static/text.rb +57 -0
  28. data/lib/distorted-jekyll/static/video.rb +90 -0
  29. data/lib/distorted-jekyll/template/13th-style.css +78 -0
  30. data/lib/distorted-jekyll/template/error_code.liquid +3 -0
  31. data/lib/distorted-jekyll/template/font.liquid +32 -0
  32. data/lib/distorted-jekyll/template/image.liquid +32 -0
  33. data/lib/distorted-jekyll/template/lastresort.liquid +20 -0
  34. data/lib/distorted-jekyll/template/pdf.liquid +14 -0
  35. data/lib/distorted-jekyll/template/svg.liquid +32 -0
  36. data/lib/distorted-jekyll/template/text.liquid +32 -0
  37. data/lib/distorted-jekyll/template/video.liquid +11 -0
  38. metadata +41 -6
@@ -0,0 +1,305 @@
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)? ![alt](image){: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
+ # ![alt text](/some/path/to/image.png 'title text'){: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 attrs(el, type = :img)
219
+ matched = []
220
+
221
+ if el.is_a? Enumerable
222
+ # Support an Array of elements...
223
+ el.each {
224
+ |child| matched.push(*attrs(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 << 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.push(*attrs(child, type))
240
+ }
241
+ end
242
+ end
243
+ matched
244
+ end
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(' ')} %}"
260
+ end
261
+
262
+ # Kramdown entry point
263
+ def convert(el)
264
+ # The parsed "images" may also be audio, video, or some other
265
+ # media type. There is only one Markdown image tag, however.
266
+ imgs = children(el, :img)
267
+
268
+ # Enable conceptual-grouping (BLOCKS) mode if the count of list item
269
+ # elements matches the count of image elements in our
270
+ # chunk of Markdown. Technically I should check to make sure each
271
+ # image is the child of one of those list items,
272
+ # but this is way easier until I (hopefully never) find a parsing
273
+ # corner-case where this doesn't hold up.
274
+ lists = children(el, :li)
275
+ list_imgs = lists.map{|li| children(li, :img)}.flatten
276
+
277
+ case lists.count
278
+ when 0..1
279
+ # Render one (1) image/video/whatever. This behavior is the same
280
+ # 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
+ }))
286
+ else
287
+ # Render a conceptual group (DD::BLOCKS)
288
+
289
+ if imgs.count != list_imgs.count
290
+ # Sanity check :img count vs :img-in-:li count.
291
+ # We should support the corner case where the regex matches
292
+ # multiple consecutive lines, but with mixed list item status,
293
+ # e.g. a solo image abuts a conceptual group and gets globbed
294
+ # into a single match.
295
+ # For now, however:
296
+ raise "MD->img regex returned an unequal number of listed and unlisted tags."
297
+ end
298
+
299
+ "{% distort -%}\n#{list_imgs.map{|img| distorted(*attrs(img))}.join("\n")}\n{% enddistort %}"
300
+ end
301
+ end
302
+
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,400 @@
1
+ # Our custom Exceptions
2
+ require 'distorted-jekyll/error_code'
3
+
4
+ # Configuration-loading code
5
+ require 'distorted-jekyll/floor'
6
+
7
+ # Configuration data manipulations
8
+ require 'distorted-jekyll/molecule/abstract'
9
+
10
+ # Media-type drivers
11
+ require 'distorted-jekyll/molecule/font'
12
+ require 'distorted-jekyll/molecule/image'
13
+ require 'distorted-jekyll/molecule/text'
14
+ require 'distorted-jekyll/molecule/pdf'
15
+ require 'distorted-jekyll/molecule/svg'
16
+ require 'distorted-jekyll/molecule/video'
17
+ require 'distorted-jekyll/molecule/last-resort'
18
+
19
+ # Set.to_hash
20
+ require 'distorted/monkey_business/set'
21
+
22
+ # Slip in and out of phenomenon
23
+ require 'liquid/tag'
24
+ require 'liquid/tag/parser'
25
+
26
+ # Explicitly required for l/t/parser since a1cfa27c27cf4d4c308da2f75fbae88e9d5ae893
27
+ require 'shellwords'
28
+
29
+ # Set is in stdlib but is not in core.
30
+ require 'set'
31
+
32
+ # MIME Magic 🧙‍♀️
33
+ require 'mime/types'
34
+ require 'ruby-filemagic'
35
+
36
+ # I mean, this is why we're here, right?
37
+ require 'jekyll'
38
+
39
+
40
+ module Jekyll
41
+ module DistorteD
42
+ class Invoker < Liquid::Tag
43
+
44
+ GEM_ROOT = File.dirname(__FILE__).freeze
45
+
46
+ # Mix in config-loading methods.
47
+ include Jekyll::DistorteD::Molecule::Abstract
48
+
49
+ # Enabled media_type drivers. These will be attempted back to front.
50
+ # TODO: Make this configurable.
51
+ MEDIA_MOLECULES = [
52
+ Jekyll::DistorteD::Molecule::LastResort,
53
+ Jekyll::DistorteD::Molecule::Font,
54
+ Jekyll::DistorteD::Molecule::Text,
55
+ Jekyll::DistorteD::Molecule::PDF,
56
+ Jekyll::DistorteD::Molecule::SVG,
57
+ Jekyll::DistorteD::Molecule::Video,
58
+ Jekyll::DistorteD::Molecule::Image,
59
+ ]
60
+
61
+ # Any any attr value will get a to_sym if shorter than this
62
+ # totally arbitrary length, or if the attr key is in the plugged
63
+ # Molecule's set of attrs that take only a defined set of values.
64
+ # My chosen boundary length fits all of the outer-limit tag names I use,
65
+ # like 'medium'. It fits the longest value of Vips::Interesting too,
66
+ # though `crop` will be symbolized based on the other condition.
67
+ ARBITRARY_ATTR_SYMBOL_STRING_LENGTH_BOUNDARY = 13
68
+
69
+
70
+ # 𝘏𝘖𝘞 𝘈𝘙𝘌 𝘠𝘖𝘜 𝘎𝘌𝘕𝘛𝘓𝘌𝘔𝘌𝘕 !!
71
+ def initialize(tag_name, arguments, liquid_options)
72
+ super
73
+ # Tag name as given to Liquid::Template.register_tag().
74
+ @tag_name = tag_name.to_sym
75
+
76
+ # Liquid leaves argument parsing totally up to us.
77
+ # Use the envygeeks/liquid-tag-parser library to wrangle them.
78
+ parsed_arguments = Liquid::Tag::Parser.new(arguments)
79
+
80
+ # Filename is the only non-keyword argument our tag should ever get.
81
+ # It's spe-shul and gets its own definition outside the attr loop.
82
+ if parsed_arguments.key?(:src)
83
+ @name = parsed_arguments[:src]
84
+ else
85
+ @name = parsed_arguments[:argv1]
86
+ end
87
+
88
+ # If we didn't get one of the two above options there is nothing we
89
+ # can do but bail.
90
+ unless @name
91
+ raise "Failed to get a usable filename from #{arguments}"
92
+ end
93
+
94
+ # Guess MIME Magic from the filename. For example:
95
+ # `distorted IIDX-Readers-Unboxing.jpg: [#<MIME::Type: image/jpeg>]`
96
+ #
97
+ # Types#type_for can return multiple possibilities for a filename.
98
+ # For example, an XML file: [application/xml, text/xml].
99
+ mime = MIME::Types.type_for(@name).to_set
100
+
101
+ # We can't proceed without a usable media type.
102
+ # Look at the actual file iff the filename wasn't enough to guess.
103
+ unless mime.empty?
104
+ Jekyll.logger.debug(@tag_name, "Detected #{@name} media types: #{mime}")
105
+ else
106
+ # Did we fail to guess any MIME::Types from the given filename?
107
+ # We're going to have to look at the actual file
108
+ # (or at least its first four bytes).
109
+ # `@mime` will be readable/writable in the FileMagic.open block context
110
+ # since it was already defined in the outer scope.
111
+ FileMagic.open(:mime) do |fm|
112
+ # TODO: Support finding files in paths deeper than the Site source.
113
+ # There's no good way to get the path here of the Markdown file
114
+ # that included our Tag, so relative paths won't work if given
115
+ # as just a filename. It should work if supplied like:
116
+ # ![The coolest image ever](/2020/04/20/some-post/hahanofileextension)
117
+ # This limitation is normally not a problem since we can guess
118
+ # the MIME::Types just based on the filename.
119
+ # It would be possible to supply the Markdown document's path
120
+ # as an additional argument to {% distorted %} when converting
121
+ # Markdown in `injection_of_love`, but I am resisting that
122
+ # approach because it would make DD's Liquid and Markdown entrypoints
123
+ # no longer exactly equivalent, and that's not okay with me.
124
+ test_path = File.join(
125
+ Jekyll::DistorteD::Floor::config(:source),
126
+ Jekyll::DistorteD::Floor::config(:collections_dir),
127
+ @name,
128
+ )
129
+ # The second argument makes fm.file return just the simple
130
+ # MIME::Type String, e.g.:
131
+ #
132
+ # irb(main):006:1* fm.file('/home/okeeblow/IIDX-turntable.svg')
133
+ # => "image/svg+xml; charset=us-ascii"
134
+ # irb(main):009:1* fm.file('/home/okeeblow/IIDX-turntable.svg', true)
135
+ # => "image/svg"
136
+ #
137
+ # However MIME::Types won't take short variants like 'image/svg',
138
+ # so explicitly have FM return long types and split it ourself
139
+ # on the semicolon:
140
+ #
141
+ # irb(main):038:0> "image/svg+xml; charset=us-ascii".split(';').first
142
+ # => "image/svg+xml"
143
+ mime = Set[MIME::Types[fm.file(@name, false).split(';'.freeze).first]]
144
+ end
145
+
146
+ # Did we still not get a type from FileMagic?
147
+ unless mime
148
+ if Jekyll::DistorteD::Floor::config(self.class.const_get(:CONFIG_ROOT), :last_resort)
149
+ Jekyll.logger.debug(@tag_name, "Falling back to bare <img> for #{@name}")
150
+ mime = Jekyll::DistorteD::Molecule::LastResort::MIME_TYPES
151
+ else
152
+ raise MediaTypeNotFoundError.new(@name)
153
+ end
154
+ end
155
+ end
156
+
157
+ # Array of drivers to try auto-plugging. Take a shallow copy first because
158
+ # these will get popped off the end for plug attempts.
159
+ media_molecules = MEDIA_MOLECULES.dup
160
+
161
+ ## Media Driver Autoplugging
162
+ #
163
+ # Take the union of this file's detected MIME::Types and
164
+ # the supported MEDIA_TYPES declared in each molecule.
165
+ # Molecules will likely declare their Types with a regex:
166
+ # https://rdoc.info/gems/mime-types/MIME%2FTypes:[]
167
+ #
168
+ #
169
+ # Still-Image Mime::Types Example:
170
+ # MIME::Types.type_for('IIDX-Readers-Unboxing.jpg')
171
+ # => [#<MIME::Type: image/jpeg>]
172
+ #
173
+ # Video MIME::Types Example:
174
+ # MIME::Types.type_for('play.mp4') => [
175
+ # #<MIME::Type: application/mp4>,
176
+ # #<MIME::Type: audio/mp4>,
177
+ # #<MIME::Type: video/mp4>,
178
+ # #<MIME::Type: video/vnd.objectvideo>
179
+ # ]
180
+ #
181
+ #
182
+ # Molecule declared-supported MIME::Types Example:
183
+ # (huge list)
184
+ # MIME_TYPES = MIME::Types[/^#{MEDIA_TYPE}/, :complete => true]
185
+ #
186
+ #
187
+ # Detected & Declared MIME::Types Union Example:
188
+ # MIME::Types.type_for('play.mp4') & MIME::Types[/^video/, :complete => true]
189
+ # => [#<MIME::Type: video/mp4>, #<MIME::Type: video/vnd.objectvideo>]
190
+ #
191
+ # ^ This non-empty example union means we sould try this driver for this file.
192
+ #
193
+ #
194
+ # Loop until we've found a match or tried all available drivers.
195
+ loop do
196
+ # Attempt to plug the last driver in the array of enabled drivers.
197
+ molecule = media_molecules.pop
198
+
199
+ # This will be nil once we've tried them all and run out and are on the last loop.
200
+ if molecule == nil
201
+ if Jekyll::DistorteD::Floor::config(self.class.const_get(:CONFIG_ROOT), :last_resort)
202
+ Jekyll.logger.debug(@tag_name, "Falling back to a bare <img> for #{name}")
203
+ @mime = Jekyll::DistorteD::Molecule::LastResort::MIME_TYPES
204
+ molecule = Jekyll::DistorteD::Molecule::LastResort
205
+ else
206
+ raise MediaTypeNotImplementedError.new(@name)
207
+ end
208
+ end
209
+
210
+ Jekyll.logger.debug(@tag_name, "Trying to plug #{@name} into #{molecule}")
211
+
212
+ # We found a potentially-compatible driver iff the union set is non-empty.
213
+ if not (mime & molecule.const_get(:MIME_TYPES)).empty?
214
+ @mime = mime & molecule.const_get(:MIME_TYPES)
215
+ Jekyll.logger.debug(@tag_name, "Enabling #{molecule} for #{@name}: #{mime}")
216
+
217
+ # Override Invoker's stubs by prepending the driver's methods to our DD instance's singleton class.
218
+ # https://devalot.com/articles/2008/09/ruby-singleton
219
+ # `self.singleton_class.extend(molecule)` doesn't work in this context.
220
+ self.singleton_class.instance_variable_set(:@media_molecule, molecule)
221
+
222
+ # Set instance variables for the combined set of HTML element
223
+ # attributes used for this media_type. The global set is defined in this file
224
+ # (Invoker), and the media_type-specific set is appended to that during auto-plug.
225
+ attrs = (self.singleton_class.const_get(:GLOBAL_ATTRS) + molecule.const_get(:ATTRS)).to_hash
226
+ attrs.each_pair do |attr, val|
227
+ # An attr supplied to the Liquid tag should override any from the config
228
+ liquid_val = parsed_arguments&.dig(attr)
229
+ # nil.to_s is '', so print 'nil' for readability.
230
+ Jekyll.logger.debug("Liquid #{attr}", liquid_val || 'nil')
231
+
232
+ if liquid_val.is_a?(String)
233
+ # Symbolize String values of any attr that has a Molecule-defined list
234
+ # of acceptable values, or — completely arbitrarily — any String value
235
+ # shorter than an arbitrarily-chosen constant.
236
+ # Otherwise freeze them.
237
+ if (liquid_val.length <= ARBITRARY_ATTR_SYMBOL_STRING_LENGTH_BOUNDARY) or
238
+ molecule.const_get(:ATTRS_VALUES).key?(attr)
239
+ liquid_val = liquid_val&.to_sym
240
+ elsif liquid_val.length > ARBITRARY_ATTR_SYMBOL_STRING_LENGTH_BOUNDARY
241
+ # Will be default in Ruby 3.
242
+ liquid_val = liquid_val&.freeze
243
+ end
244
+ end
245
+
246
+ attrs[attr] = liquid_val
247
+ end
248
+
249
+ # Save attrs to our instance as the data source for Molecule::Abstract.attrs.
250
+ @attrs = attrs
251
+
252
+ # Plug the chosen Media Molecule!
253
+ # Using Module#prepend puts the Molecule's ahead in the ancestor chain
254
+ # of any defined here, or any defined in an `include`d module.
255
+ (class <<self; prepend @media_molecule; end)
256
+
257
+ # Break out of the `loop`, a.k.a. stop auto-plugging!
258
+ break
259
+ end
260
+
261
+ end
262
+ end
263
+
264
+ # Called by Jekyll::Renderer
265
+ # https://github.com/jekyll/jekyll/blob/HEAD/lib/jekyll/renderer.rb
266
+ # https://jekyllrb.com/tutorials/orderofinterpretation/
267
+ def render(context)
268
+ render_to_output_buffer(context, '')
269
+ end
270
+
271
+ # A future Liquid version (5.0?) will call this function directly
272
+ # instead of calling render()
273
+ def render_to_output_buffer(context, output)
274
+ # Get Jekyll Site object back from tag rendering context registers so we
275
+ # can get configuration data and path information from it and
276
+ # then pass it along to our StaticFile subclass.
277
+ site = context.registers[:site]
278
+
279
+ # The rendering context's `first` page will be the one that invoked us.
280
+ page_data = context.environments.first['page'.freeze]
281
+
282
+ #
283
+ # Our subclass' additional args:
284
+ # dest - The String path to the generated `url` folder of the page HTML output
285
+ base = site.source
286
+
287
+ # `relative_path` doesn't seem to always exist, but `path` does? idk.
288
+ # I was testing with `relative_path` only with `_posts`, but it broke
289
+ # when I invoked DD on a _page. Both have `path`.
290
+ dir = File.dirname(page_data['path'.freeze])
291
+
292
+ # Every one of Ruby's `File.directory?` / `Pathname.directory?` /
293
+ # `FileTest.directory?` methods actually tests that path on the
294
+ # real filesystem, but we shouldn't look at the FS here because
295
+ # this function gets called when the Site.dest directory does
296
+ # not exist yet!
297
+ # Hackily look at the last character to see if the URL is a
298
+ # directory (like configured on cooltrainer) or a `.html`
299
+ # (or other extension) like the default Jekyll config.
300
+ # Get the dirname if the url is not a dir itself.
301
+ @dd_dest = @url = page_data['url'.freeze]
302
+ unless @dd_dest[-1] == Jekyll::DistorteD::Floor::PATH_SEPARATOR
303
+ @dd_dest = File.dirname(@dd_dest)
304
+ # Append the trailing slash so we don't have to do it
305
+ # in the Liquid templates.
306
+ @dd_dest << Jekyll::DistorteD::Floor::PATH_SEPARATOR
307
+ end
308
+
309
+ # Create an instance of the media-appropriate Jekyll::StaticFile subclass.
310
+ #
311
+ # StaticFile args:
312
+ # site - The Jekyll Site object.
313
+ # base - The String path to the Jekyll::Site.source, e.g. /home/okeeblow/Works/cooltrainer
314
+ # dir - The String path between <base> and the source file, e.g. _posts/2018-10-15-super-cool-post
315
+ # name - The String filename of the original media, e.g. cool.jpg
316
+ # mime - The Set of MIME::Types of the original media.
317
+ # attrs - The Set of attributes given to our Liquid tag, if any.
318
+ # dd_dest - The String path under Site.dest to DD's top-level media output directory.
319
+ # url - The URL of the page this tag is on.
320
+ static_file = self.static_file(
321
+ site,
322
+ base,
323
+ dir,
324
+ @name,
325
+ @mime,
326
+ @attrs,
327
+ @dd_dest,
328
+ @url,
329
+ )
330
+
331
+ # Add our new file to the list that will be handled
332
+ # by Jekyll's built-in StaticFile generator.
333
+ # Our StaticFile children implement a write() that invokes DistorteD,
334
+ # but this lets us avoid writing our own Generator.
335
+ site.static_files << static_file
336
+ end
337
+
338
+ # Called by a Molecule-specific render() method since they will
339
+ # all load their Liquid template files in the same way.
340
+ # Bail out if this is not handled by the module we just mixed in.
341
+ # Any media Molecule must override this to return an instance of
342
+ # their media-type-appropriate StaticFile subclass.
343
+ def static_file(site, base, dir, name, mime, attrs, dd_dest, url)
344
+ raise MediaTypeNotImplementedError.new(name)
345
+ end
346
+
347
+ # Generic Liquid template loader that will be used in every MediaMolecule.
348
+ # Callers will call `render(**{:template => vars})` on the Object returned
349
+ # by this method.
350
+ def parse_template(site: nil, name: nil)
351
+ site = site || Jekyll.sites.first
352
+ begin
353
+ # Use a given filename, or detect one based on media-type.
354
+ if name.nil?
355
+ # Template filename is based on the MEDIA_TYPE and/or SUB_TYPE declared
356
+ # in the plugged MediaMolecule for the given input file.
357
+ if self.singleton_class.const_defined?(:SUB_TYPE)
358
+ name = "#{self.singleton_class.const_get(:SUB_TYPE)}.liquid".freeze
359
+ else
360
+ name = "#{self.singleton_class.const_get(:MEDIA_TYPE)}.liquid".freeze
361
+ end
362
+ elsif not name.include?('.liquid'.freeze)
363
+ # Support filename arguments with and without file extension.
364
+ # The given String might already be frozen, so concatenating
365
+ # the extension might fail. Just set a new version.
366
+ name = "#{name}.liquid"
367
+ end
368
+ template = File.join(
369
+ self.singleton_class.const_get(:GEM_ROOT),
370
+ 'template'.freeze,
371
+ name,
372
+ )
373
+
374
+ # Jekyll's Liquid renderer caches in 4.0+.
375
+ if Jekyll::DistorteD::Floor::config(
376
+ Jekyll::DistorteD::Floor::CONFIG_ROOT,
377
+ :cache_templates,
378
+ )
379
+ # file(path) is the caching function, with path as the cache key.
380
+ # The `template` here will be the full path, so no versions of this
381
+ # gem should ever conflict. For example, right now during dev it's:
382
+ # `/home/okeeblow/Works/DistorteD/lib/image.liquid`
383
+ Jekyll.logger.debug('DistorteD', "Parsing #{template} with caching renderer.")
384
+ site.liquid_renderer.file(template).parse(File.read(template))
385
+ else
386
+ # Re-read the template just for this piece of media.
387
+ Jekyll.logger.debug('DistorteD', "Parsing #{template} with fresh (uncached) renderer.")
388
+ Liquid::Template.parse(File.read(template))
389
+ end
390
+
391
+ rescue Liquid::SyntaxError => l
392
+ # This shouldn't ever happen unless a new version of Liquid
393
+ # breaks syntax compatibility with our templates somehow.
394
+ l.message
395
+ end
396
+ end # parse_template
397
+
398
+ end
399
+ end
400
+ end