distorted-jekyll 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +661 -0
- data/README.md +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 +81 -0
- data/lib/distorted-jekyll/blocks.rb +16 -0
- data/lib/distorted-jekyll/floor.rb +266 -0
- data/lib/distorted-jekyll/invoker.rb +259 -0
- data/lib/distorted-jekyll/md_injection.rb +305 -0
- data/lib/distorted-jekyll/molecule/font.rb +62 -0
- data/lib/distorted-jekyll/molecule/image.rb +94 -0
- data/lib/distorted-jekyll/molecule/lastresort.rb +51 -0
- data/lib/distorted-jekyll/molecule/pdf.rb +79 -0
- data/lib/distorted-jekyll/molecule/svg.rb +47 -0
- data/lib/distorted-jekyll/molecule/text.rb +62 -0
- data/lib/distorted-jekyll/molecule/video.rb +85 -0
- data/lib/distorted-jekyll/monkey_business/jekyll/cleaner.rb +54 -0
- data/lib/distorted-jekyll/static_state.rb +201 -0
- data/lib/distorted-jekyll/template/13th-style.css +79 -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 +32 -34
@@ -0,0 +1,259 @@
|
|
1
|
+
# Our custom Exceptions
|
2
|
+
require 'distorted/error_code'
|
3
|
+
|
4
|
+
# MIME::Typer
|
5
|
+
require 'distorted/checking_you_out'
|
6
|
+
|
7
|
+
# Configuration-loading code
|
8
|
+
require 'distorted-jekyll/floor'
|
9
|
+
require 'distorted-jekyll/static_state'
|
10
|
+
|
11
|
+
# Media-type drivers
|
12
|
+
require 'distorted-jekyll/molecule/font'
|
13
|
+
require 'distorted-jekyll/molecule/image'
|
14
|
+
require 'distorted-jekyll/molecule/text'
|
15
|
+
require 'distorted-jekyll/molecule/pdf'
|
16
|
+
require 'distorted-jekyll/molecule/svg'
|
17
|
+
require 'distorted-jekyll/molecule/video'
|
18
|
+
require 'distorted-jekyll/molecule/lastresort'
|
19
|
+
|
20
|
+
# Set.to_hash
|
21
|
+
require 'distorted/monkey_business/set'
|
22
|
+
|
23
|
+
# Slip in and out of phenomenon
|
24
|
+
require 'liquid/tag'
|
25
|
+
require 'liquid/tag/parser'
|
26
|
+
|
27
|
+
# Explicitly required for l/t/parser since a1cfa27c27cf4d4c308da2f75fbae88e9d5ae893
|
28
|
+
require 'shellwords'
|
29
|
+
|
30
|
+
# Set is in stdlib but is not in core.
|
31
|
+
require 'set'
|
32
|
+
|
33
|
+
# I mean, this is why we're here, right?
|
34
|
+
require 'jekyll'
|
35
|
+
|
36
|
+
|
37
|
+
module Jekyll
|
38
|
+
module DistorteD
|
39
|
+
class Invoker < Liquid::Tag
|
40
|
+
|
41
|
+
GEM_ROOT = File.dirname(__FILE__).freeze
|
42
|
+
|
43
|
+
# Mix in config-loading methods.
|
44
|
+
include Jekyll::DistorteD::Floor
|
45
|
+
include Jekyll::DistorteD::StaticState
|
46
|
+
|
47
|
+
# Enabled media_type drivers. These will be attempted back to front.
|
48
|
+
# TODO: Make this configurable.
|
49
|
+
MEDIA_MOLECULES = [
|
50
|
+
Jekyll::DistorteD::Molecule::LastResort,
|
51
|
+
Jekyll::DistorteD::Molecule::Font,
|
52
|
+
Jekyll::DistorteD::Molecule::Text,
|
53
|
+
Jekyll::DistorteD::Molecule::PDF,
|
54
|
+
Jekyll::DistorteD::Molecule::SVG,
|
55
|
+
Jekyll::DistorteD::Molecule::Video,
|
56
|
+
Jekyll::DistorteD::Molecule::Image,
|
57
|
+
]
|
58
|
+
# Reduce the above to a Hash of Sets of MediaMolecules-per-Type, keyed by Type.
|
59
|
+
TYPE_MOLECULES = MEDIA_MOLECULES.reduce(
|
60
|
+
Hash.new{|hash, key| hash[key] = Set[]}
|
61
|
+
) { |types, molecule|
|
62
|
+
if molecule.const_defined?(:LOWER_WORLD)
|
63
|
+
molecule.const_get(:LOWER_WORLD).each { |t|
|
64
|
+
types.update(t => Set[molecule]) { |k,o,n| o.merge(n) }
|
65
|
+
}
|
66
|
+
end
|
67
|
+
types
|
68
|
+
}
|
69
|
+
|
70
|
+
# Any any attr value will get a to_sym if shorter than this
|
71
|
+
# totally arbitrary length, or if the attr key is in the plugged
|
72
|
+
# Molecule's set of attrs that take only a defined set of values.
|
73
|
+
# My chosen boundary length fits all of the outer-limit tag names I use,
|
74
|
+
# like 'medium'. It fits the longest value of Vips::Interesting too,
|
75
|
+
# though `crop` will be symbolized based on the other condition.
|
76
|
+
ARBITRARY_ATTR_SYMBOL_STRING_LENGTH_BOUNDARY = 13
|
77
|
+
|
78
|
+
|
79
|
+
# 𝘏𝘖𝘞 𝘈𝘙𝘌 𝘠𝘖𝘜 𝘎𝘌𝘕𝘛𝘓𝘌𝘔𝘌𝘕 !!
|
80
|
+
def initialize(tag_name, arguments, liquid_options)
|
81
|
+
super
|
82
|
+
# Tag name as given to Liquid::Template.register_tag().
|
83
|
+
@tag_name = tag_name.to_sym
|
84
|
+
|
85
|
+
# Liquid leaves argument parsing totally up to us.
|
86
|
+
# Use the envygeeks/liquid-tag-parser library to wrangle them.
|
87
|
+
parsed_arguments = Liquid::Tag::Parser.new(arguments)
|
88
|
+
|
89
|
+
# Filename is the only non-keyword argument our tag should ever get.
|
90
|
+
# It's spe-shul and gets its own definition outside the attr loop.
|
91
|
+
if parsed_arguments.key?(:src)
|
92
|
+
@name = parsed_arguments.delete(:src)
|
93
|
+
else
|
94
|
+
@name = parsed_arguments.delete(:argv1)
|
95
|
+
end
|
96
|
+
@liquid_liquid = parsed_arguments.select{ |attr, val|
|
97
|
+
not [nil, ''.freeze].include?(val)
|
98
|
+
}.transform_keys { |attr|
|
99
|
+
attr.length <= ARBITRARY_ATTR_SYMBOL_STRING_LENGTH_BOUNDARY ? attr.to_sym : attr.freeze
|
100
|
+
}.transform_values { |val|
|
101
|
+
if val.respond_to?(:length)
|
102
|
+
val.length <= ARBITRARY_ATTR_SYMBOL_STRING_LENGTH_BOUNDARY ? val.to_sym : val.freeze
|
103
|
+
else
|
104
|
+
val
|
105
|
+
end
|
106
|
+
}
|
107
|
+
|
108
|
+
# If we didn't get one of the two above options there is nothing we
|
109
|
+
# can do but bail.
|
110
|
+
unless @name
|
111
|
+
raise "Failed to get a usable filename from #{arguments}"
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns a Set of DD MIME::Types descriving our file,
|
117
|
+
# optionally falling through to a plain file copy.
|
118
|
+
def type_mars
|
119
|
+
@type_mars ||= begin
|
120
|
+
mime = CHECKING::YOU::OUT(@name)
|
121
|
+
if mime.empty?
|
122
|
+
if Jekyll::DistorteD::Floor::config(Jekyll::DistorteD::Floor::CONFIG_ROOT, :last_resort)
|
123
|
+
mime = Jekyll::DistorteD::Molecule::LastResort::LOWER_WORLD
|
124
|
+
end
|
125
|
+
end
|
126
|
+
mime
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Return any arguments given by the user to our Liquid tag.
|
131
|
+
# This method name is generic across all DD entrypoints so it can be
|
132
|
+
# referenced from lower layers in the pile.
|
133
|
+
def user_arguments
|
134
|
+
@liquid_liquid || Hash[]
|
135
|
+
end
|
136
|
+
|
137
|
+
# Decides which MediaMolecule is most appropriate for our file and returns it.
|
138
|
+
def media_molecule
|
139
|
+
available_molecules = TYPE_MOLECULES.keys.to_set & type_mars
|
140
|
+
# TODO: Handle multiple molecules for the same file
|
141
|
+
case available_molecules.length
|
142
|
+
when 0
|
143
|
+
raise MediaTypeNotImplementedError.new(@name)
|
144
|
+
when 1
|
145
|
+
return TYPE_MOLECULES[available_molecules.first].first
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def plug
|
150
|
+
unless self.singleton_class.instance_variable_defined?(:@media_molecule)
|
151
|
+
self.singleton_class.instance_variable_set(:@media_molecule, media_molecule)
|
152
|
+
self.singleton_class.prepend(media_molecule)
|
153
|
+
Jekyll.logger.info(@name, "Plugging #{media_molecule}")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Called by Jekyll::Renderer
|
158
|
+
# https://github.com/jekyll/jekyll/blob/HEAD/lib/jekyll/renderer.rb
|
159
|
+
# https://jekyllrb.com/tutorials/orderofinterpretation/
|
160
|
+
def render(context)
|
161
|
+
plug
|
162
|
+
render_to_output_buffer(context, '')
|
163
|
+
end
|
164
|
+
|
165
|
+
# A future Liquid version (5.0?) will call this function directly
|
166
|
+
# instead of calling render()
|
167
|
+
def render_to_output_buffer(context, output)
|
168
|
+
plug
|
169
|
+
# Get Jekyll Site object back from tag rendering context registers so we
|
170
|
+
# can get configuration data and path information from it and
|
171
|
+
# then pass it along to our StaticFile subclass.
|
172
|
+
@site = context.registers[:site]
|
173
|
+
|
174
|
+
# The rendering context's `first` page will be the one that invoked us.
|
175
|
+
page_data = context.environments.first['page'.freeze]
|
176
|
+
|
177
|
+
#
|
178
|
+
# Our subclass' additional args:
|
179
|
+
# dest - The String path to the generated `url` folder of the page HTML output
|
180
|
+
@base = @site.source
|
181
|
+
|
182
|
+
# `relative_path` doesn't seem to always exist, but `path` does? idk.
|
183
|
+
# I was testing with `relative_path` only with `_posts`, but it broke
|
184
|
+
# when I invoked DD on a _page. Both have `path`.
|
185
|
+
@dir = File.dirname(page_data['path'.freeze])
|
186
|
+
|
187
|
+
# Every one of Ruby's `File.directory?` / `Pathname.directory?` /
|
188
|
+
# `FileTest.directory?` methods actually tests that path on the
|
189
|
+
# real filesystem, but we shouldn't look at the FS here because
|
190
|
+
# this function gets called when the Site.dest directory does
|
191
|
+
# not exist yet!
|
192
|
+
# Hackily look at the last character to see if the URL is a
|
193
|
+
# directory (like configured on cooltrainer) or a `.html`
|
194
|
+
# (or other extension) like the default Jekyll config.
|
195
|
+
# Get the dirname if the url is not a dir itself.
|
196
|
+
@relative_dest = page_data['url'.freeze]
|
197
|
+
unless @relative_dest[-1] == Jekyll::DistorteD::Floor::PATH_SEPARATOR
|
198
|
+
@relative_dest = File.dirname(@relative_dest)
|
199
|
+
# Append the trailing slash so we don't have to do it
|
200
|
+
# in the Liquid templates.
|
201
|
+
@relative_dest << Jekyll::DistorteD::Floor::PATH_SEPARATOR
|
202
|
+
end
|
203
|
+
|
204
|
+
# Add our new file to the list that will be handled
|
205
|
+
# by Jekyll's built-in StaticFile generator.
|
206
|
+
@site.static_files << self
|
207
|
+
output
|
208
|
+
end
|
209
|
+
|
210
|
+
# Generic Liquid template loader that will be used in every MediaMolecule.
|
211
|
+
# Callers will call `render(**{:template => vars})` on the Object returned
|
212
|
+
# by this method.
|
213
|
+
def parse_template(site: nil, name: nil)
|
214
|
+
site = site || @site || Jekyll.sites.first
|
215
|
+
begin
|
216
|
+
# Use a given filename, or detect one based on media-type.
|
217
|
+
if name.nil?
|
218
|
+
# e.g. Jekyll::DistorteD::Molecule::Image -> 'image.liquid'
|
219
|
+
name = "#{self.singleton_class.instance_variable_get(:@media_molecule).name.gsub(/^.*::/, '').downcase}.liquid".freeze
|
220
|
+
elsif not name.include?('.liquid'.freeze)
|
221
|
+
# Support filename arguments with and without file extension.
|
222
|
+
# The given String might already be frozen, so concatenating
|
223
|
+
# the extension might fail. Just set a new version.
|
224
|
+
name = "#{name}.liquid"
|
225
|
+
end
|
226
|
+
template = File.join(
|
227
|
+
self.singleton_class.const_get(:GEM_ROOT),
|
228
|
+
'template'.freeze,
|
229
|
+
name,
|
230
|
+
)
|
231
|
+
|
232
|
+
# Jekyll's Liquid renderer caches in 4.0+.
|
233
|
+
if Jekyll::DistorteD::Floor::config(
|
234
|
+
Jekyll::DistorteD::Floor::CONFIG_ROOT,
|
235
|
+
:cache_templates,
|
236
|
+
)
|
237
|
+
# file(path) is the caching function, with path as the cache key.
|
238
|
+
# The `template` here will be the full path, so no versions of this
|
239
|
+
# gem should ever conflict. For example, right now during dev it's:
|
240
|
+
# `/home/okeeblow/Works/DistorteD/lib/image.liquid`
|
241
|
+
Jekyll.logger.debug('DistorteD', "Parsing #{template} with caching renderer.")
|
242
|
+
site.liquid_renderer.file(template).parse(File.read(template))
|
243
|
+
else
|
244
|
+
# Re-read the template just for this piece of media.
|
245
|
+
Jekyll.logger.debug('DistorteD', "Parsing #{template} with fresh (uncached) renderer.")
|
246
|
+
Liquid::Template.parse(File.read(template))
|
247
|
+
end
|
248
|
+
|
249
|
+
rescue Liquid::SyntaxError => l
|
250
|
+
# This shouldn't ever happen unless a new version of Liquid
|
251
|
+
# breaks syntax compatibility with our templates somehow.
|
252
|
+
l.message
|
253
|
+
end
|
254
|
+
end # parse_template
|
255
|
+
|
256
|
+
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
@@ -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
|