distorted-jekyll 0.5.2 → 0.5.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +661 -0
- data/README.md +6 -10
- data/lib/distorted-jekyll.rb +79 -0
- data/lib/distorted-jekyll/13th-style.rb +58 -0
- data/lib/distorted-jekyll/_config_default.yml +79 -0
- data/lib/distorted-jekyll/blocks.rb +16 -0
- data/lib/distorted-jekyll/error_code.rb +24 -0
- data/lib/distorted-jekyll/floor.rb +148 -0
- data/lib/distorted-jekyll/injection_of_love.rb +305 -0
- data/lib/distorted-jekyll/invoker.rb +400 -0
- data/lib/distorted-jekyll/molecule/abstract.rb +238 -0
- data/lib/distorted-jekyll/molecule/font.rb +29 -0
- data/lib/distorted-jekyll/molecule/image.rb +105 -0
- data/lib/distorted-jekyll/molecule/last-resort.rb +54 -0
- data/lib/distorted-jekyll/molecule/pdf.rb +88 -0
- data/lib/distorted-jekyll/molecule/svg.rb +59 -0
- data/lib/distorted-jekyll/molecule/text.rb +74 -0
- data/lib/distorted-jekyll/molecule/video.rb +43 -0
- data/lib/distorted-jekyll/monkey_business/jekyll/cleaner.rb +54 -0
- data/lib/distorted-jekyll/static/font.rb +42 -0
- data/lib/distorted-jekyll/static/image.rb +55 -0
- data/lib/distorted-jekyll/static/lastresort.rb +28 -0
- data/lib/distorted-jekyll/static/pdf.rb +53 -0
- data/lib/distorted-jekyll/static/state.rb +141 -0
- data/lib/distorted-jekyll/static/svg.rb +52 -0
- data/lib/distorted-jekyll/static/text.rb +57 -0
- data/lib/distorted-jekyll/static/video.rb +90 -0
- data/lib/distorted-jekyll/template/13th-style.css +78 -0
- data/lib/distorted-jekyll/template/error_code.liquid +3 -0
- data/lib/distorted-jekyll/template/font.liquid +32 -0
- data/lib/distorted-jekyll/template/image.liquid +32 -0
- data/lib/distorted-jekyll/template/lastresort.liquid +20 -0
- data/lib/distorted-jekyll/template/pdf.liquid +14 -0
- data/lib/distorted-jekyll/template/svg.liquid +32 -0
- data/lib/distorted-jekyll/template/text.liquid +32 -0
- data/lib/distorted-jekyll/template/video.liquid +11 -0
- 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
|