distorted-jekyll 0.5.6 → 0.5.7

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 (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