distorted-jekyll 0.5.3 → 0.6.0
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 +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)? ![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
|