distorted-jekyll 0.5.4 → 0.7.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 +7 -11
- data/lib/distorted-jekyll.rb +75 -0
- data/lib/distorted-jekyll/13th-style.css +79 -0
- data/lib/distorted-jekyll/13th-style.rb +58 -0
- data/lib/distorted-jekyll/_config_default.yml +63 -0
- data/lib/distorted-jekyll/blocks.rb +16 -0
- data/lib/distorted-jekyll/invoker.rb +234 -0
- data/lib/distorted-jekyll/liquid_liquid.rb +255 -0
- data/lib/distorted-jekyll/liquid_liquid/anchor.liquid +5 -0
- data/lib/distorted-jekyll/liquid_liquid/anchor_inline.liquid +1 -0
- data/lib/distorted-jekyll/liquid_liquid/embed.liquid +1 -0
- data/lib/distorted-jekyll/liquid_liquid/img.liquid +1 -0
- data/lib/distorted-jekyll/liquid_liquid/object.liquid +5 -0
- data/lib/distorted-jekyll/liquid_liquid/picture.liquid +15 -0
- data/lib/distorted-jekyll/liquid_liquid/picture.rb +48 -0
- data/lib/distorted-jekyll/liquid_liquid/picture_source.liquid +1 -0
- data/lib/distorted-jekyll/liquid_liquid/root.liquid +5 -0
- data/lib/distorted-jekyll/liquid_liquid/video.liquid +5 -0
- data/lib/distorted-jekyll/liquid_liquid/video_source.liquid +1 -0
- data/lib/distorted-jekyll/md_injection.rb +310 -0
- data/lib/distorted-jekyll/media_molecule.rb +20 -0
- data/lib/distorted-jekyll/media_molecule/font.rb +21 -0
- data/lib/distorted-jekyll/media_molecule/image.rb +15 -0
- data/lib/distorted-jekyll/media_molecule/never_let_you_down.rb +28 -0
- data/lib/distorted-jekyll/media_molecule/pdf.rb +108 -0
- data/lib/distorted-jekyll/media_molecule/svg.rb +20 -0
- data/lib/distorted-jekyll/media_molecule/text.rb +23 -0
- data/lib/distorted-jekyll/media_molecule/video.rb +45 -0
- data/lib/distorted-jekyll/monkey_business/jekyll/cleaner.rb +121 -0
- data/lib/distorted-jekyll/static_state.rb +160 -0
- data/lib/distorted-jekyll/the_setting_sun.rb +179 -0
- metadata +37 -34
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'distorted/media_molecule'
|
3
|
+
|
4
|
+
module Jekyll::DistorteD
|
5
|
+
# Load Jekyll Molecules which will implicitly also load
|
6
|
+
# the Floor Molecules they're based on if they aren't already.
|
7
|
+
@@loaded_molecules rescue begin
|
8
|
+
Dir[File.join(__dir__, 'media_molecule', '*.rb')].each { |molecule| require molecule }
|
9
|
+
@@loaded_molecules = true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Cooltrainer::DistorteD
|
14
|
+
# Override default Molecule Set with their Liquid-rendering submolecules.
|
15
|
+
def self.media_molecules
|
16
|
+
Jekyll::DistorteD::Molecule.constants.map { |molecule|
|
17
|
+
Jekyll::DistorteD::Molecule::const_get(molecule)
|
18
|
+
}.to_set
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'distorted/media_molecule/font'
|
4
|
+
require 'distorted-jekyll/liquid_liquid/picture'
|
5
|
+
|
6
|
+
|
7
|
+
module Jekyll; end
|
8
|
+
module Jekyll::DistorteD; end
|
9
|
+
module Jekyll::DistorteD::Molecule; end
|
10
|
+
module Jekyll::DistorteD::Molecule::Font
|
11
|
+
|
12
|
+
include Cooltrainer::DistorteD::Molecule::Font
|
13
|
+
include Jekyll::DistorteD::LiquidLiquid::Picture
|
14
|
+
|
15
|
+
Cooltrainer::DistorteD::IMPLANTATION(:LOWER_WORLD, Cooltrainer::DistorteD::Molecule::Font).each_key { |type|
|
16
|
+
define_method(type.distorted_template_method) { |change|
|
17
|
+
Cooltrainer::ElementalCreation.new(:anchor_inline, change, **{})
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'distorted-jekyll/liquid_liquid/picture'
|
4
|
+
require 'distorted/media_molecule/image'
|
5
|
+
|
6
|
+
|
7
|
+
module Jekyll; end
|
8
|
+
module Jekyll::DistorteD; end
|
9
|
+
module Jekyll::DistorteD::Molecule; end
|
10
|
+
module Jekyll::DistorteD::Molecule::Image
|
11
|
+
|
12
|
+
include Cooltrainer::DistorteD::Molecule::Image
|
13
|
+
include Jekyll::DistorteD::LiquidLiquid::Picture
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'distorted/checking_you_out'
|
4
|
+
require 'distorted-jekyll/liquid_liquid'
|
5
|
+
|
6
|
+
module Jekyll; end
|
7
|
+
module Jekyll::DistorteD; end
|
8
|
+
module Jekyll::DistorteD::Molecule; end
|
9
|
+
module Jekyll::DistorteD::Molecule::NeverLetYouDown
|
10
|
+
|
11
|
+
FALLBACK_TYPE = CHECKING::YOU::OUT['application/x.distorted.never-let-you-down']
|
12
|
+
LOWER_WORLD = Hash[
|
13
|
+
FALLBACK_TYPE => Hash[
|
14
|
+
:alt => Cooltrainer::Compound.new(:alt, blurb: 'Alternate text to display when this element cannot be rendered.'),
|
15
|
+
:title => Cooltrainer::Compound.new(:title, blurb: 'Extra information about this element — usually displayed as tooltip text.'),
|
16
|
+
:href => Cooltrainer::Compound.new(:href, blurb: 'Hyperlink reference for this element.')
|
17
|
+
]
|
18
|
+
]
|
19
|
+
OUTER_LIMITS = Hash[FALLBACK_TYPE => nil]
|
20
|
+
|
21
|
+
define_method(FALLBACK_TYPE.distorted_file_method) { |dest_root, change|
|
22
|
+
copy_file(change.path(dest_root))
|
23
|
+
}
|
24
|
+
define_method(FALLBACK_TYPE.distorted_template_method) { |change|
|
25
|
+
Cooltrainer::ElementalCreation.new(:anchor_inline, change, **{})
|
26
|
+
}
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'distorted/media_molecule/pdf'
|
4
|
+
|
5
|
+
|
6
|
+
module Jekyll; end
|
7
|
+
module Jekyll::DistorteD; end
|
8
|
+
module Jekyll::DistorteD::Molecule; end
|
9
|
+
module Jekyll::DistorteD::Molecule::PDF
|
10
|
+
|
11
|
+
include Cooltrainer::DistorteD::Molecule::PDF
|
12
|
+
|
13
|
+
# https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdf_open_parameters.pdf
|
14
|
+
#
|
15
|
+
# Adobe's PDF Open Parameters documentation sez:
|
16
|
+
# "Individual parameters, together with their values (separated by & or #),
|
17
|
+
# can be no greater then 32 characters in length."
|
18
|
+
# …but then goes on to show some examples (like `comment`)
|
19
|
+
# that are clearly longer than 32 characters.
|
20
|
+
# Dunno. I'll err on the side of giving you a footgun.
|
21
|
+
#
|
22
|
+
# Keep the PDF Open Params in the order they are defined
|
23
|
+
# in the Adobe documentation, since it says they should
|
24
|
+
# be specified in the URL in that same order.
|
25
|
+
#
|
26
|
+
# "You cannot use the reserved characters =, #, and &.
|
27
|
+
# There is no way to escape these special characters."
|
28
|
+
RESERVED_CHARACTERS_FRAGMENT = '[^=#&]+'.freeze
|
29
|
+
FLOAT_INT_FRAGMENT = '[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)'.freeze
|
30
|
+
ZERO_TO_ONE_HUNDRED = /^(([1-9]\d?|1\d{1})([.,]\d{0,1})?|100([.,]0{1})?)$/
|
31
|
+
PDF_OPEN_PARAMS = Array[
|
32
|
+
Cooltrainer::Compound.new(:nameddest, valid: /^#{RESERVED_CHARACTERS_FRAGMENT}$/, blurb: 'Jump to a named destination in the document.'),
|
33
|
+
Cooltrainer::Compound.new(:page, valid: Integer, default: 1, blurb: 'Jump to a numbered page in the document.'),
|
34
|
+
Cooltrainer::Compound.new(:comment, valid: /^#{RESERVED_CHARACTERS_FRAGMENT}$/, blurb: 'Jump to a comment on a given page.'),
|
35
|
+
Cooltrainer::Compound.new(:collab, valid: /^(DAVFDF|FSFDF|DB)@#{RESERVED_CHARACTERS_FRAGMENT}$/, blurb: 'Sets the comment repository to be used to supply and store comments for the document.'),
|
36
|
+
Cooltrainer::Compound.new(:zoom, valid: /^#{FLOAT_INT_FRAGMENT}(,#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT})?$/, blurb: 'Sets the zoom and scroll factors, using float or integer values.'),
|
37
|
+
Cooltrainer::Compound.new(:view, valid: /^Fit(H|V|B|BH|BV(,#{FLOAT_INT_FRAGMENT})?)?$/, default: :Fit, blurb: 'Set the view of the displayed page, using the keyword values defined in the PDF language specification. For more information, see the PDF Reference.'),
|
38
|
+
Cooltrainer::Compound.new(:viewrect, valid: /^#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT}$/, blurb: 'Sets the view rectangle using float or integer values in a coordinate system where 0,0 represents the top left corner of the visible page, regardless of document rotation.'),
|
39
|
+
Cooltrainer::Compound.new(:pagemode, valid: Set[:none, :thumbs, :bookmarks], default: :none, blurb: 'Displays bookmarks or thumbnails.'),
|
40
|
+
Cooltrainer::Compound.new(:scrollbar, valid: Cooltrainer::BOOLEAN_VALUES, default: true, blurb: 'Turns scrollbars on or off.'),
|
41
|
+
Cooltrainer::Compound.new(:search, valid: /^#{RESERVED_CHARACTERS_FRAGMENT}(,\s#{RESERVED_CHARACTERS_FRAGMENT})*$/ , blurb: 'Opens the Search panel and performs a search for any of the words in the specified word list. The first matching word is highlighted in the document.'),
|
42
|
+
Cooltrainer::Compound.new(:toolbar, valid: Cooltrainer::BOOLEAN_VALUES, default: true, blurb: 'Turns the toolbar on or off.'),
|
43
|
+
Cooltrainer::Compound.new(:statusbar, valid: Cooltrainer::BOOLEAN_VALUES, default: true, blurb: 'Turns the status bar on or off.'),
|
44
|
+
Cooltrainer::Compound.new(:messages, valid: Cooltrainer::BOOLEAN_VALUES, default: false, blurb: 'Turns the document message bar on or off.'),
|
45
|
+
Cooltrainer::Compound.new(:navpanes, valid: Cooltrainer::BOOLEAN_VALUES, default: true, blurb: 'Turns the navigation panes and tabs on or off.'),
|
46
|
+
Cooltrainer::Compound.new(:highlight, valid: /^#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT},#{FLOAT_INT_FRAGMENT}$/, blurb: 'Highlights a specified rectangle on the displayed page. Use the `page` command before this command.'),
|
47
|
+
Cooltrainer::Compound.new(:fdf, valid: /^#{RESERVED_CHARACTERS_FRAGMENT}$/, blurb: 'Specifies an FDF file to populate form fields in the PDF file being
|
48
|
+
opened.'),
|
49
|
+
]
|
50
|
+
|
51
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object#Attributes
|
52
|
+
CONTAINER_ATTRIBUTES = Array[
|
53
|
+
Cooltrainer::Compound.new(:alt, valid: String),
|
54
|
+
Cooltrainer::Compound.new(:caption, valid: String),
|
55
|
+
Cooltrainer::Compound.new(:height, valid: String, default: '100%'.freeze, blurb: '<object> viewer container height.'),
|
56
|
+
Cooltrainer::Compound.new(:width, valid: String, default: '100%'.freeze, blurb: '<object> viewer container width.'),
|
57
|
+
]
|
58
|
+
|
59
|
+
OUTER_LIMITS = Hash[
|
60
|
+
CHECKING::YOU::OUT['application/pdf'] => PDF_OPEN_PARAMS.concat(CONTAINER_ATTRIBUTES).reduce(Hash[]) {|aka, compound|
|
61
|
+
aka.tap { |a| a.store(compound.element, compound) }
|
62
|
+
}
|
63
|
+
]
|
64
|
+
|
65
|
+
# Generate a Hash of our PDF Open Params based on any given to the Liquid tag
|
66
|
+
# and any loaded from the defaults.
|
67
|
+
# https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdf_open_parameters.pdf
|
68
|
+
def pdf_open_params
|
69
|
+
PDF_OPEN_PARAMS.reduce(Hash[]) {|params, compound|
|
70
|
+
# Only include those params whose user-given value exists and differs from its default.
|
71
|
+
params.tap { |p|
|
72
|
+
p.store(compound.element, abstract(compound.element)) unless [
|
73
|
+
nil, ''.freeze, compound.default,
|
74
|
+
].include?(abstract(compound.element))
|
75
|
+
}
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
# Generate the URL fragment version of the PDF Open Params.
|
80
|
+
# This would be difficult / impossible to construct within Liquid
|
81
|
+
# from the individual variables, so let's just do it out here.
|
82
|
+
def pdf_open_params_url
|
83
|
+
pdf_open_params.map{ |(k,v)|
|
84
|
+
case
|
85
|
+
when k == :search
|
86
|
+
# The PDF Open Params docs specify `search` should be quoted.
|
87
|
+
"#{k}=\"#{v}\""
|
88
|
+
when Cooltrainer::BOOLEAN_VALUES.include?(v)
|
89
|
+
# Convert booleans to the numeric representation Adobe use here.
|
90
|
+
"#{k}=#{v ? 1 : 0}"
|
91
|
+
else
|
92
|
+
"#{k}=#{v}"
|
93
|
+
end
|
94
|
+
}.join('&')
|
95
|
+
end
|
96
|
+
|
97
|
+
# http://joliclic.free.fr/html/object-tag/en/
|
98
|
+
# TODO: iOS treats our <object> like an <img>,
|
99
|
+
# showing only the first page with transparency and stretched to the
|
100
|
+
# size of the container element.
|
101
|
+
# We will need something like PDF.js in an <iframe> to handle this.
|
102
|
+
define_method(CHECKING::YOU::OUT['application/pdf'].distorted_template_method) { |change|
|
103
|
+
Cooltrainer::ElementalCreation.new(:object, change, children: [:embed, :anchor_inline]).tap { |e|
|
104
|
+
e.fragment = pdf_open_params.empty? ? ''.freeze : "##{pdf_open_params_url}"
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
end # PDF
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'distorted/media_molecule/svg'
|
4
|
+
require 'distorted-jekyll/liquid_liquid/picture'
|
5
|
+
|
6
|
+
|
7
|
+
module Jekyll; end
|
8
|
+
module Jekyll::DistorteD; end
|
9
|
+
module Jekyll::DistorteD::Molecule; end
|
10
|
+
module Jekyll::DistorteD::Molecule::SVG
|
11
|
+
|
12
|
+
include Cooltrainer::DistorteD::Molecule::SVG
|
13
|
+
include Jekyll::DistorteD::LiquidLiquid::Picture
|
14
|
+
|
15
|
+
define_method(
|
16
|
+
CHECKING::YOU::OUT['image/svg+xml'].distorted_template_method,
|
17
|
+
Jekyll::DistorteD::LiquidLiquid::Picture::render_picture_source,
|
18
|
+
)
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'distorted/media_molecule/text'
|
4
|
+
require 'distorted-jekyll/liquid_liquid/picture'
|
5
|
+
|
6
|
+
|
7
|
+
module Jekyll; end
|
8
|
+
module Jekyll::DistorteD; end
|
9
|
+
module Jekyll::DistorteD::Molecule; end
|
10
|
+
module Jekyll::DistorteD::Molecule::Text
|
11
|
+
|
12
|
+
include Cooltrainer::DistorteD::Molecule::Text
|
13
|
+
include Jekyll::DistorteD::LiquidLiquid::Picture
|
14
|
+
|
15
|
+
Cooltrainer::DistorteD::IMPLANTATION(:LOWER_WORLD, Cooltrainer::DistorteD::Molecule::Text).each_key { |type|
|
16
|
+
define_method(type.distorted_template_method) { |change|
|
17
|
+
# Remove the destructured empty Hash once we drop Ruby 2.7
|
18
|
+
# so we don't get auto-destructured due to Change#to_hash.
|
19
|
+
Cooltrainer::ElementalCreation.new(:anchor_inline, change, **{})
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
end # Text
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'distorted/media_molecule/video'
|
2
|
+
|
3
|
+
|
4
|
+
module Jekyll; end
|
5
|
+
module Jekyll::DistorteD; end
|
6
|
+
module Jekyll::DistorteD::Molecule; end
|
7
|
+
module Jekyll::DistorteD::Molecule::Video
|
8
|
+
|
9
|
+
include Cooltrainer::DistorteD::Molecule::Video
|
10
|
+
|
11
|
+
Cooltrainer::DistorteD::IMPLANTATION(:LOWER_WORLD, Cooltrainer::DistorteD::Molecule::Video).each_key { |type|
|
12
|
+
define_method(type.distorted_template_method) { |change|
|
13
|
+
Cooltrainer::ElementalCreation.new(:video_source, change, parents: Array[:video])
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
# Override wanted-filenames method from StaticState with one that prevents our generated
|
18
|
+
# video segments from being deleted.
|
19
|
+
# This is still very hacky until I can guarantee/control the number of segments we get.
|
20
|
+
def wanted_files
|
21
|
+
dd_dest = File.join(the_setting_sun(:jekyll, :destination).to_s, @relative_dest)
|
22
|
+
changes.each_with_object(Set[]) { |change, wanted|
|
23
|
+
case change.type
|
24
|
+
# Treat HLS and MPEG-DASH the same, with slightly different naming conventions.
|
25
|
+
# Add their main playlist file, but then also glob any segments that happen to exist.
|
26
|
+
when CHECKING::YOU::OUT['application/dash+xml']
|
27
|
+
hls_dir = File.join(dd_dest, "#{basename}.hls")
|
28
|
+
wanted.add(File.join(hls_dir, "#{basename}.m3u8"))
|
29
|
+
if Dir.exist?(hls_dir)
|
30
|
+
Dir.entries(hls_dir).to_set.subtract(Set["#{basename}.m3u8"]).each { |hls| wanted.add(File.join(hls_dir, hls)) }
|
31
|
+
end
|
32
|
+
when CHECKING::YOU::OUT['application/vnd.apple.mpegurl']
|
33
|
+
dash_dir = File.join(dd_dest, "#{basename}.dash")
|
34
|
+
wanted.add(File.join(dash_dir, "#{basename}.mpd"))
|
35
|
+
if Dir.exist?(dash_dir)
|
36
|
+
Dir.entries(dash_dir).to_set.subtract(Set["#{basename}.mpd"]).each { |dash| wanted.add(File.join(dash_dir, dash)) }
|
37
|
+
end
|
38
|
+
else
|
39
|
+
# Treat any other type (including single-file video types) like normal.
|
40
|
+
wanted.add(change.name)
|
41
|
+
end
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
end # Video
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'distorted-jekyll/media_molecule'
|
3
|
+
|
4
|
+
module Jekyll
|
5
|
+
# Handles the cleanup of a site's destination before it is built or re-built.
|
6
|
+
class Cleaner
|
7
|
+
|
8
|
+
# Private: The list of files to be created when site is built.
|
9
|
+
#
|
10
|
+
# Returns a Set with the file paths
|
11
|
+
#
|
12
|
+
# Monkey-patch this to look for DD's unique `destinations` which is similar
|
13
|
+
# to the original `destination` method except it returns a Set of destination
|
14
|
+
# paths instead of a single destination path.
|
15
|
+
# Do the patch with `define_method` instead of just `def` because the block's
|
16
|
+
# closure of the local scope lets it carry a binding to the original overriden
|
17
|
+
# method which I use to bail out iff the monkey-patch fails.
|
18
|
+
# This is an attempt to avoid breaking future Jekyll versions as much as
|
19
|
+
# possible, since any Exception in the monkey-patched code will just cause
|
20
|
+
# the original Jekyll implementation to be called instead.
|
21
|
+
# The new worst case scenario is slow site builds due to media variation generation!
|
22
|
+
#
|
23
|
+
# If a StaticFile responds to `destinations` then use it and merge the result.
|
24
|
+
# I'm defining my own separate method for multi-destinations for now,
|
25
|
+
# but I also considered just overriding `destination` to return the Set and
|
26
|
+
# then doing this as a one-liner that handles either case (single or
|
27
|
+
# multiple destinations) with `files.merge(Set[*(item.destination(site.dest))])`.
|
28
|
+
# This is the safer choice though since we avoid changing the outout type of the
|
29
|
+
# regular `:destination` method.
|
30
|
+
the_old_new_thing = instance_method(:new_files)
|
31
|
+
define_method(:new_files) do
|
32
|
+
begin
|
33
|
+
@new_files ||= Set.new.tap do |files|
|
34
|
+
site.each_site_file { |item|
|
35
|
+
if item.respond_to?(:destinations)
|
36
|
+
files.merge(item.destinations(site.dest))
|
37
|
+
elsif item.respond_to?(:destination)
|
38
|
+
files << item.destination(site.dest)
|
39
|
+
else
|
40
|
+
# Something unrelated has gone wrong for us to end up sending
|
41
|
+
# `destination` to something that doesn't respond to it.
|
42
|
+
# We should fall back to the original implementation of `new_files`
|
43
|
+
# in this case so the failure doesn't appear to be here.
|
44
|
+
the_old_new_thing.bind(self).()
|
45
|
+
end
|
46
|
+
}
|
47
|
+
end
|
48
|
+
rescue RuntimeError => e
|
49
|
+
Jekyll.logger.warn('DistorteD', "Monkey-patching Jekyll::Cleaner#new_files failed: #{e.message}")
|
50
|
+
Jekyll.logger.debug('DistorteD', "Monkey-patched Jekyll::Cleaner#new_files backtrace: #{e.backtrace}")
|
51
|
+
the_old_new_thing.bind(self).()
|
52
|
+
end
|
53
|
+
end # define_method :new_files
|
54
|
+
|
55
|
+
|
56
|
+
# Private: Creates a regular expression from the config's keep_files array
|
57
|
+
#
|
58
|
+
# Examples
|
59
|
+
# ['.git','.svn'] with site.dest "/myblog/_site" creates
|
60
|
+
# the following regex: /\A\/myblog\/_site\/(\.git|\/.svn)/
|
61
|
+
#
|
62
|
+
# Returns the regular expression
|
63
|
+
#
|
64
|
+
# Monkey-patch this to protect DistorteD-generated files from destruction
|
65
|
+
# https://jekyllrb.com/docs/configuration/incremental-regeneration/
|
66
|
+
# when running Jekyll in Incremental mode twice in a row.
|
67
|
+
#
|
68
|
+
# The first Incremental build will process our Liquid tags on every post/page
|
69
|
+
# which will add our generated files to Jekyll::Cleaner's :new_files (See above!)
|
70
|
+
# A second build, however, will not re-process any posts/pages that haven't changed.
|
71
|
+
# Our Tags never get initialized, so their previously-generated files now appear
|
72
|
+
# to be spurious and will get purged.
|
73
|
+
#
|
74
|
+
# Work around this by merging Jekyll::Cleaner#keep_file_regex with a second Regexp
|
75
|
+
# based on the :preferred_extension for every MIME::Type DistorteD can output.
|
76
|
+
mr_regular = instance_method(:keep_file_regex)
|
77
|
+
define_method(:keep_file_regex) do
|
78
|
+
begin
|
79
|
+
# We're going to use it either way, so go ahead and get what the :keep_file_regex
|
80
|
+
# would have been in unpatched Jekyll, e.g.:
|
81
|
+
# (?-mix:\A/home/okeeblow/Works/cooltrainer/_site\/(\.git|\.svn))
|
82
|
+
super_regexp = mr_regular.bind(self).()
|
83
|
+
|
84
|
+
# If we aren't in Incremental mode then each Tag will explicitly declare
|
85
|
+
# the files they write, and that's preferrable to this shotgun approach
|
86
|
+
# since the Regexp approach may preserve unwanted files, but "Some unwanted files"
|
87
|
+
# is way nicer than "fifteen minutes rebuilding everything" rofl
|
88
|
+
if site&.incremental?
|
89
|
+
# Discover every supported output MIME::Type based on every loaded MediaMolecule.
|
90
|
+
outer_limits = Cooltrainer::DistorteD::IMPLANTATION(
|
91
|
+
:OUTER_LIMITS,
|
92
|
+
Cooltrainer::DistorteD::media_molecules,
|
93
|
+
).values.flat_map(&:keys)
|
94
|
+
|
95
|
+
# Build a new Regexp globbing the preferred extension of every Type we support, e.g.:
|
96
|
+
# (?-mix:\A/home/okeeblow/Works/cooltrainer/_site/.*(txt|nfo|v|ppm|pgm|pbm|hdr|png|jpg|webp|tiff|fits|gif|bmp|ttf|svg|pdf|mpd|m3u8|mp4))
|
97
|
+
#
|
98
|
+
# Some Types may have duplicate preferred_extensions, and some might have nil
|
99
|
+
# (e.g. our own application/x.distorted.never-let-you-down), so :uniq and :compact them out.
|
100
|
+
outer_regexp = %r!\A#{Regexp.quote(site.dest)}/.*(#{Regexp.union(outer_limits&.map(&:preferred_extension).uniq.compact).source})!
|
101
|
+
|
102
|
+
# Do the thing.
|
103
|
+
combined_regexp = Regexp.union(outer_regexp, super_regexp)
|
104
|
+
Jekyll.logger.debug(
|
105
|
+
'Protecting DistorteD-generated files from Incremental-mode destruction with new Jekyll::Cleaner#keep_file_regex',
|
106
|
+
combined_regexp.source)
|
107
|
+
return combined_regexp
|
108
|
+
else
|
109
|
+
# Feels like I'm patching nothin' at all… nothin' at all… nothin' at all!
|
110
|
+
return super_regexp
|
111
|
+
end
|
112
|
+
rescue RuntimeError => e
|
113
|
+
Jekyll.logger.warn('DistorteD', "Monkey-patching Jekyll::Cleaner#keep_file_regex failed: #{e.message}")
|
114
|
+
Jekyll.logger.debug('DistorteD', "Monkey-patched Jekyll::Cleaner#keep_file_regex backtrace: #{e.backtrace}")
|
115
|
+
# Bail out by returning what the :keep_file_regex would have been without this patch.
|
116
|
+
mr_regular.bind(self).()
|
117
|
+
end
|
118
|
+
end # define_method :keep_file_regex
|
119
|
+
|
120
|
+
end # Cleaner
|
121
|
+
end # Jekyll
|
@@ -0,0 +1,160 @@
|
|
1
|
+
|
2
|
+
require 'fileutils'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
require 'distorted/error_code'
|
6
|
+
|
7
|
+
|
8
|
+
module Jekyll; end
|
9
|
+
module Jekyll::DistorteD; end
|
10
|
+
|
11
|
+
# This module implements the methods our tag needs in order to
|
12
|
+
# pretend to be a Jekyll::StaticFile so we don't need to
|
13
|
+
# redundantly re-implement a Generator and Jekyll::Cleaner.
|
14
|
+
module Jekyll::DistorteD::StaticState
|
15
|
+
|
16
|
+
|
17
|
+
# Returns the to-be-written path of a single standard StaticFile.
|
18
|
+
# The value returned by this method is only the 'main' or 'original'
|
19
|
+
# (even if modified somehow) file and does not include the
|
20
|
+
# path/filenames of any variations.
|
21
|
+
# This method will be called by jekyll/lib/cleaner#new_files
|
22
|
+
# to generate the list of files that need to be build or rebuilt
|
23
|
+
# for a site. For this reason, this method shouldn't do any kind
|
24
|
+
# of checking the real filesystem, since e.g. its URL-based
|
25
|
+
# destdir might not exist yet if the Site.dest is completely blank.
|
26
|
+
def destination(dest_root)
|
27
|
+
File.join(dest_root, @relative_dest, @name)
|
28
|
+
end
|
29
|
+
|
30
|
+
# This method will be called by our monkey-patched Jekyll::Cleaner#new_files
|
31
|
+
# in place of the single-destination method usually used.
|
32
|
+
# This allows us to tell Jekyll about more than a single file
|
33
|
+
# that should be kept when regenerating the site.
|
34
|
+
# This makes DistorteD fast!
|
35
|
+
def destinations(dest_root)
|
36
|
+
changes&.flat_map { |change| change.paths(dest_root) }
|
37
|
+
end
|
38
|
+
|
39
|
+
# HACK HACK HACK
|
40
|
+
# Jekyll does not pass this method a site.dest like it does write() and
|
41
|
+
# others, but I want to be able to short-circuit here if all the
|
42
|
+
# to-be-generated files already exist.
|
43
|
+
def modified?
|
44
|
+
# Assume modified for the sake of freshness :)
|
45
|
+
modified = true
|
46
|
+
|
47
|
+
site_dest = the_setting_sun(:jekyll, :destination).to_s
|
48
|
+
if Dir.exist?(site_dest)
|
49
|
+
if Dir.exist?(File.join(site_dest, @relative_dest))
|
50
|
+
extant_files = Dir.entries(File.join(site_dest, @relative_dest)).to_set
|
51
|
+
|
52
|
+
# TODO: Make this smarter. It's not enough that all the generated
|
53
|
+
# filenames should exist. Try a few more ways to detect subtler
|
54
|
+
# "changes to the source file since generation of variations.
|
55
|
+
if wanted_files.subset?(extant_files)
|
56
|
+
Jekyll.logger.debug(@name, "All variations present: #{wanted_files}")
|
57
|
+
modified = false
|
58
|
+
else
|
59
|
+
Jekyll.logger.debug(@name, "Missing variations: #{wanted_files - extant_files}")
|
60
|
+
end
|
61
|
+
|
62
|
+
end # relative_dest.exists?
|
63
|
+
end # site_dest.exists?
|
64
|
+
Jekyll.logger.debug("#{@name} modified?", modified)
|
65
|
+
return modified
|
66
|
+
end # modified?
|
67
|
+
|
68
|
+
# Whether to write the file to the filesystem
|
69
|
+
#
|
70
|
+
# Returns true unless the defaults for the destination path from
|
71
|
+
# _config.yml contain `published: false`.
|
72
|
+
def write?
|
73
|
+
publishable = defaults.fetch('published'.freeze, true)
|
74
|
+
return publishable unless @collection
|
75
|
+
|
76
|
+
publishable && @collection.write?
|
77
|
+
end
|
78
|
+
|
79
|
+
# Write the static file to the destination directory (if modified).
|
80
|
+
#
|
81
|
+
# dest - The String path to the destination dir.
|
82
|
+
#
|
83
|
+
# Returns false if the file was not modified since last time (no-op).
|
84
|
+
def write(dest_root)
|
85
|
+
return false if File.exist?(path) && !modified?
|
86
|
+
|
87
|
+
# Create any directories to the depth of the intended destination.
|
88
|
+
FileUtils.mkdir_p(File.join(dest_root, @relative_dest))
|
89
|
+
# Save every desired variation of this image.
|
90
|
+
# This will be a Set of Hashes each describing the name, type,
|
91
|
+
# dimensions, attributes, etc of each output variation we want.
|
92
|
+
# Full-size outputs will have the special tag `:full`.
|
93
|
+
changes&.each { |change|
|
94
|
+
if self.respond_to?(change.type.distorted_file_method)
|
95
|
+
Jekyll.logger.debug("DistorteD::#{change.type.distorted_file_method}", change.name)
|
96
|
+
# WISHLIST: Remove the empty final positional Hash argument once we require a Ruby version
|
97
|
+
# that will not perform the implicit Change-to-Hash conversion due to Change's
|
98
|
+
# implementation of :to_hash. Ruby 2.7 will complain but still do the conversion,
|
99
|
+
# breaking downstream callers that want a Struct they can call arbitrary key methods on.
|
100
|
+
# https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
|
101
|
+
self.send(change.type.distorted_file_method, dest_root, change, **{})
|
102
|
+
elsif extname == ".#{change.type.preferred_extension}"
|
103
|
+
Jekyll.logger.debug(@name, <<~RAWCOPY
|
104
|
+
No #{change.type.distorted_file_method} method is defined,
|
105
|
+
but the intended output type #{change.type.to_s} is the same
|
106
|
+
as the input type, so I will fall back to copying the raw file.
|
107
|
+
RAWCOPY
|
108
|
+
)
|
109
|
+
copy_file(change.paths(dest_root).first)
|
110
|
+
else
|
111
|
+
Jekyll.logger.error(@name, "Missing write method #{change.type.distorted_file_method}")
|
112
|
+
raise MediaTypeOutputNotImplementedError.new(change.path(dest_root), type_mars, self.class.name)
|
113
|
+
end
|
114
|
+
}
|
115
|
+
end # write
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def copy_file(dest_path, *a, **k)
|
120
|
+
if @site.safe || Jekyll.env == "production"
|
121
|
+
FileUtils.cp(path, dest_path)
|
122
|
+
else
|
123
|
+
FileUtils.copy_entry(path, dest_path)
|
124
|
+
end
|
125
|
+
end # copy_file
|
126
|
+
|
127
|
+
# Basic file properties
|
128
|
+
|
129
|
+
# Returns the extname /!\ including the dot /!\
|
130
|
+
def extname
|
131
|
+
File.extname(@name)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns last modification time for this file.
|
135
|
+
def mtime
|
136
|
+
(@modified_time ||= File.stat(path).mtime).to_i
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns source file path.
|
140
|
+
def path
|
141
|
+
@path ||= begin
|
142
|
+
# Static file is from a collection inside custom collections directory
|
143
|
+
if !@collection.nil? && !@site.config['collections_dir'.freeze].empty?
|
144
|
+
File.join(*[@base, @site.config['collections_dir'.freeze], @dir, @name].compact)
|
145
|
+
else
|
146
|
+
File.join(*[@base, @dir, @name].compact)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
# Returns a Set of just the String filenames we want for this media.
|
153
|
+
# This will be used by `modified?` among others.
|
154
|
+
def wanted_files
|
155
|
+
# Cooltrainer::Change#names returns an Array[String], so we must concat every Change into one.
|
156
|
+
changes.map(&:names).reduce(&:concat).to_set
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
end
|