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.
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