distorted-jekyll 0.5.4 → 0.7.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 +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
|