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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +661 -0
  3. data/README.md +7 -11
  4. data/lib/distorted-jekyll.rb +75 -0
  5. data/lib/distorted-jekyll/13th-style.css +79 -0
  6. data/lib/distorted-jekyll/13th-style.rb +58 -0
  7. data/lib/distorted-jekyll/_config_default.yml +63 -0
  8. data/lib/distorted-jekyll/blocks.rb +16 -0
  9. data/lib/distorted-jekyll/invoker.rb +234 -0
  10. data/lib/distorted-jekyll/liquid_liquid.rb +255 -0
  11. data/lib/distorted-jekyll/liquid_liquid/anchor.liquid +5 -0
  12. data/lib/distorted-jekyll/liquid_liquid/anchor_inline.liquid +1 -0
  13. data/lib/distorted-jekyll/liquid_liquid/embed.liquid +1 -0
  14. data/lib/distorted-jekyll/liquid_liquid/img.liquid +1 -0
  15. data/lib/distorted-jekyll/liquid_liquid/object.liquid +5 -0
  16. data/lib/distorted-jekyll/liquid_liquid/picture.liquid +15 -0
  17. data/lib/distorted-jekyll/liquid_liquid/picture.rb +48 -0
  18. data/lib/distorted-jekyll/liquid_liquid/picture_source.liquid +1 -0
  19. data/lib/distorted-jekyll/liquid_liquid/root.liquid +5 -0
  20. data/lib/distorted-jekyll/liquid_liquid/video.liquid +5 -0
  21. data/lib/distorted-jekyll/liquid_liquid/video_source.liquid +1 -0
  22. data/lib/distorted-jekyll/md_injection.rb +310 -0
  23. data/lib/distorted-jekyll/media_molecule.rb +20 -0
  24. data/lib/distorted-jekyll/media_molecule/font.rb +21 -0
  25. data/lib/distorted-jekyll/media_molecule/image.rb +15 -0
  26. data/lib/distorted-jekyll/media_molecule/never_let_you_down.rb +28 -0
  27. data/lib/distorted-jekyll/media_molecule/pdf.rb +108 -0
  28. data/lib/distorted-jekyll/media_molecule/svg.rb +20 -0
  29. data/lib/distorted-jekyll/media_molecule/text.rb +23 -0
  30. data/lib/distorted-jekyll/media_molecule/video.rb +45 -0
  31. data/lib/distorted-jekyll/monkey_business/jekyll/cleaner.rb +121 -0
  32. data/lib/distorted-jekyll/static_state.rb +160 -0
  33. data/lib/distorted-jekyll/the_setting_sun.rb +179 -0
  34. 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