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.
- 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)? {:optional_ial => same_line}
|
82
|
+
# {:optional_ial => next_consecutive_line}
|
83
|
+
# Repeat both preceding matches (together) any number of times to parse
|
84
|
+
# a {% distort %} block.
|
85
|
+
# See inline comments below for more detail.
|
86
|
+
MD_IMAGE_REGEX = %r&
|
87
|
+
# Matching group of a single image tag.
|
88
|
+
(
|
89
|
+
# Optional preceding-line attribute list.
|
90
|
+
(
|
91
|
+
# One blank line, because:
|
92
|
+
# "If a block IAL is directly after and before a block-level element,
|
93
|
+
# it is applied to preceding element." —Kramdown BAL docs
|
94
|
+
#
|
95
|
+
# /\R/ - A linebreak: \n, \v, \f, \r \u0085 (NEXT LINE),
|
96
|
+
# \u2028 (LINE SEPARATOR), \u2029 (PARAGRAPH SEPARATOR) or \r\n.
|
97
|
+
^$\R
|
98
|
+
# Any amount of blank space on the line before block IAL
|
99
|
+
^[[:blank:]]*
|
100
|
+
# IAL regex from Section 5.3:
|
101
|
+
# https://golem.ph.utexas.edu/~distler/maruku/proposal.html
|
102
|
+
(?<block_ial_before>\{:(\\\}|[^\}])*\})
|
103
|
+
# Any amount of trailing whitespace followed by a newline.
|
104
|
+
[[:blank:]]*$\R
|
105
|
+
)? # Match all of that, or nothing.
|
106
|
+
# Begin matching the line that contains an image.
|
107
|
+
^
|
108
|
+
# Match anything that might be between that start-of-line
|
109
|
+
# and the first character (!) of an image.
|
110
|
+
(
|
111
|
+
# From Gruber's original Markdown page:
|
112
|
+
# "List markers typically start at the left margin, but may be indented
|
113
|
+
# by up to three spaces."
|
114
|
+
# Include both unordered (-+*) and ordered (\d\. like `1.`) lists.
|
115
|
+
(?<li>[ ]{0,3}[-\*\+|\d\.]
|
116
|
+
# Support an optional IAL for the list element as shown in Kramdown docs:
|
117
|
+
# https://kramdown.gettalong.org/syntax.html#lists
|
118
|
+
# Ctrl+F "{:.cls}"
|
119
|
+
(?<li_ial>\{:(\\\}|[^\}])*\})?
|
120
|
+
# "List markers must be followed by one or more spaces or a tab."
|
121
|
+
# https://daringfireball.net/projects/markdown/syntax#list
|
122
|
+
([ ]+|\t))
|
123
|
+
)? # Although any preceding elements are optional!
|
124
|
+
# Match Markdown image syntax:
|
125
|
+
# {:other="options"}
|
126
|
+
# beginning with the alt tag:
|
127
|
+
!\[(?<alt>(\\[[:print:]]|[^\]])*)\]
|
128
|
+
# Continuing with img src as anything after the '(' and before ')' or
|
129
|
+
# before anything that could be a title.
|
130
|
+
# Assume titles will be quoted.
|
131
|
+
\((?<src>(\\[[:print:]]|[^'")]+))
|
132
|
+
# Title is optional.
|
133
|
+
# Ignore double-quotes in single-quoted titles and single-quotes
|
134
|
+
# in double-quoted titles otherwise we can't use contractions.
|
135
|
+
# Don't include the title's opening or closing quotes in the capture.
|
136
|
+
('(?<title>(\\[[:print:]]|[^']*))'|"(?<title>(\\[[:print:]]|[^"]*))")?
|
137
|
+
# The closing ')' will always be present, title or no.
|
138
|
+
\)
|
139
|
+
# Optional IAL on the same line as the :img element **with no space between them**:
|
140
|
+
# "A span IAL (or two or more span IALs) has to be put directly after
|
141
|
+
# the span-level element to which it should be applied, no additional
|
142
|
+
# character is allowed between, otherwise it is ignored and only
|
143
|
+
# removed from the output." —Kramdown IAL docs
|
144
|
+
(?<span_ial>\{:(\\\}|[^\}])*\})*[[:print:]]*$\R
|
145
|
+
# Also support optional BALs on the lines following the image.
|
146
|
+
(^[[:blank:]]*(?<block_ial_after>\{:(\\\}|[^\}])*\})+$\R)*
|
147
|
+
)+ # Capture multiple images together for block display.
|
148
|
+
&x
|
149
|
+
|
150
|
+
|
151
|
+
def md_injection
|
152
|
+
Proc.new { |document, payload|
|
153
|
+
# Compare any given document's file extension to the list of enabled
|
154
|
+
# Markdown file extensions in Jekyll's config.
|
155
|
+
if payload['site']['markdown_ext'].include? document.extname.downcase[1..-1]
|
156
|
+
# Convert Markdown images to {% distorted %} tags.
|
157
|
+
#
|
158
|
+
# Use the same Markdown parser as Jekyll to avoid parsing inconsistencies
|
159
|
+
# between this pre_render hook and the main Markdown render step.
|
160
|
+
# This is still effectively a Markdown-parsing regex (and still
|
161
|
+
# effectively a bad idea lol) but it's the cleanest way I can come up
|
162
|
+
# with right now for separating DistorteD-destined Markdown from
|
163
|
+
# the rest of any given page.
|
164
|
+
# NOTE: Attribute List Definitions elsewhere in a Markdown document
|
165
|
+
# will be lost when converting this way. I might end up just parsing
|
166
|
+
# the entire document once with my own `to_liquid` converter, but I've been
|
167
|
+
# avoiding that as it seems wasteful because Jekyll then renders the entire
|
168
|
+
# Markdown document a second time immediately after our Liquid tag.
|
169
|
+
# It's fast enough that I should stop trying to prematurely optimize this :)
|
170
|
+
# TODO: Implement MD → DD love using only the #{CONFIGURED_MARKDOWN_ENGINE},
|
171
|
+
# searching for :img elements inside :li elements to build BLOCKS.
|
172
|
+
document.content = document.content.gsub(MD_IMAGE_REGEX) { |match|
|
173
|
+
Kramdown::Document.new(match).to_liquid
|
174
|
+
}
|
175
|
+
end
|
176
|
+
}
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
# Kramdown implementation of Markdown AST -> Liquid converter.
|
181
|
+
# Renders Markdown element attributes as key=value, also under the assumption
|
182
|
+
# of using gem 'liquid-tag-parser': https://github.com/dmalan/liquid-tag-parser
|
183
|
+
module Kramdown
|
184
|
+
module Converter
|
185
|
+
class Liquid < Base
|
186
|
+
|
187
|
+
# The incoming parsed Markdown tree will include many spurious elements,
|
188
|
+
# like container paragraph elements and the list item elements when
|
189
|
+
# parsing DD Grid style Markdown. Use this function to map a tree of
|
190
|
+
# arbitrary elements to a flat list of elements of a single type.
|
191
|
+
def children(el, type)
|
192
|
+
matched = []
|
193
|
+
|
194
|
+
if el.is_a? Enumerable
|
195
|
+
# We might want to run this against an Array output from an
|
196
|
+
# earlier invokation of this method.
|
197
|
+
el.each {
|
198
|
+
|item| matched.push(*children(item, type))
|
199
|
+
}
|
200
|
+
elsif el.type.equal? type
|
201
|
+
# If we find the type we're looking for, stop and return it.
|
202
|
+
# Let it bring its children along with it instead of recursing
|
203
|
+
# into them. This will let us match container-only elements
|
204
|
+
# such as <li> by type without considering the type of its children,
|
205
|
+
# for situation where its children are really what we want.
|
206
|
+
matched.push(el)
|
207
|
+
else
|
208
|
+
# Otherwise keep looking down the tree.
|
209
|
+
unless el.children.empty?
|
210
|
+
el.children.each {
|
211
|
+
|child| matched.push(*children(child, type))
|
212
|
+
}
|
213
|
+
end
|
214
|
+
end
|
215
|
+
matched
|
216
|
+
end
|
217
|
+
|
218
|
+
def 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
|
+
# 
|
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
|