distorted 0.6.0 → 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/README.md +2 -2
- data/bin/console +14 -0
- data/bin/distorted +6 -0
- data/bin/setup +8 -0
- data/lib/distorted.rb +2 -0
- data/lib/distorted/checking_you_out.rb +116 -13
- data/lib/distorted/{types → checking_you_out}/README +0 -0
- data/lib/distorted/checking_you_out/application.yaml +33 -0
- data/lib/distorted/{types → checking_you_out}/font.yaml +0 -0
- data/lib/distorted/checking_you_out/image.yaml +108 -0
- data/lib/distorted/click_again.rb +333 -0
- data/lib/distorted/element_of_media.rb +2 -0
- data/lib/distorted/element_of_media/change.rb +119 -0
- data/lib/distorted/element_of_media/compound.rb +120 -0
- data/lib/distorted/floor.rb +17 -0
- data/lib/distorted/invoker.rb +97 -0
- data/lib/distorted/media_molecule.rb +58 -0
- data/lib/distorted/media_molecule/font.rb +195 -0
- data/lib/distorted/media_molecule/image.rb +33 -0
- data/lib/distorted/media_molecule/pdf.rb +44 -0
- data/lib/distorted/media_molecule/svg.rb +45 -0
- data/lib/distorted/media_molecule/text.rb +203 -0
- data/lib/distorted/media_molecule/video.rb +18 -0
- data/lib/distorted/modular_technology/gstreamer.rb +174 -0
- data/lib/distorted/modular_technology/vips.rb +4 -4
- data/lib/distorted/modular_technology/vips/foreign.rb +489 -0
- data/lib/distorted/modular_technology/vips/load.rb +133 -0
- data/lib/distorted/modular_technology/{vips_save.rb → vips/save.rb} +23 -34
- data/lib/distorted/monkey_business/encoding.rb +317 -0
- data/lib/distorted/monkey_business/hash.rb +0 -15
- data/lib/distorted/{modular_technology/triple_counter.rb → triple_counter.rb} +8 -1
- data/lib/distorted/version.rb +16 -16
- metadata +59 -46
- data/lib/distorted/injection_of_love.rb +0 -247
- data/lib/distorted/modular_technology/vips_load.rb +0 -77
- data/lib/distorted/molecule/C18H27NO3.rb +0 -10
- data/lib/distorted/molecule/font.rb +0 -198
- data/lib/distorted/molecule/image.rb +0 -36
- data/lib/distorted/molecule/pdf.rb +0 -119
- data/lib/distorted/molecule/svg.rb +0 -60
- data/lib/distorted/molecule/text.rb +0 -225
- data/lib/distorted/molecule/video.rb +0 -195
- data/lib/distorted/monkey_business/mnemoniq.rb +0 -8
- data/lib/distorted/types/application.yaml +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14d224830d6e35072b97427b9a07d8a97061510259c7fa94be189509e52cf453
|
4
|
+
data.tar.gz: 724f0479db6c93ec9f00cfae008524fb51cd519db0865915bd3f7e11f97eec77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82886187c077e5f61cf976546ea2c15e77f5b3dabc79a69071b78e0153f69a6f9a41250d4ea3a2deb66e5caea67c8f5abeb41ef7ff2b6f50af171d3e2232c821
|
7
|
+
data.tar.gz: b2f17ab2f0807cc067bbb035c0bbe559e8e3df30cbd2e29feb27d79784fcadbb035ec9016b785dc29ace3ebc234c81b71a7b6879de0c0ed76b6956bcb90f128d
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Cooltrainer::DistorteD
|
2
2
|
|
3
|
-
`DistorteD-
|
3
|
+
`DistorteD-Floor` is the core file-handling code for `DistorteD-Jekyll`.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -24,7 +24,7 @@ Or install it yourself as:
|
|
24
24
|
Clone the DistorteD repository and modify your Jekyll `Gemfile` to refer to your local path instead of to the newest published version of the gem:
|
25
25
|
|
26
26
|
```
|
27
|
-
gem 'distorted', :path => '~/repos/DistorteD/DistorteD-
|
27
|
+
gem 'distorted', :path => '~/repos/DistorteD/DistorteD-Floor/'[, :branch => 'NEW-SENSATION']
|
28
28
|
```
|
29
29
|
|
30
30
|
## License
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "distorted"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/distorted
ADDED
data/bin/setup
ADDED
data/lib/distorted.rb
ADDED
@@ -1,12 +1,83 @@
|
|
1
1
|
require 'set'
|
2
2
|
|
3
|
+
# CYO encapsulate all concepts related to media file identification for DistorteD!
|
4
|
+
|
5
|
+
# General media type resources:
|
6
|
+
# https://www.iana.org/assignments/media-types/media-types.xhtml
|
7
|
+
|
8
|
+
|
9
|
+
# The Gem `ruby-mime-types` and its associated `mime-types-data` provide our core classes:
|
10
|
+
# https://github.com/mime-types/ruby-mime-types
|
3
11
|
require 'mime/types'
|
12
|
+
#
|
13
|
+
# - Its MIME Types database ensures I don't have to constantly update DD with
|
14
|
+
# new filetypes as they are invented, like how AVIF is still currently brand-new as of 2021.
|
15
|
+
# - Our main Type search method — :OUT() — wraps `MIME::Types.type_for`/`MIME::Types[]`
|
16
|
+
# to identify media types based on filename alone (e.g. even for hypothetical files).
|
17
|
+
# - The Object we give callers will be a `MIME::Type`, not anything wrapped/renamed.
|
18
|
+
# - I use its Loader class and YAML structure to ship additional Type definitions local to DD.
|
19
|
+
#
|
20
|
+
# NOTE: Type objects returned from the unwrapped MIME::Types interfaces will not have equality
|
21
|
+
# with instances of the same media type from CYO! This is because we load our own database.
|
22
|
+
# irb(main)> MIME::Types['image/jpeg'].object_id
|
23
|
+
# => 136400
|
24
|
+
# irb(main)> CHECKING::YOU::OUT['image/jpeg'].object_id
|
25
|
+
# => 90800
|
26
|
+
|
27
|
+
|
28
|
+
# The Gem `ruby-filemagic` provides the ability to inspect the magic bytes of actual on-disk files.
|
29
|
+
# https://github.com/blackwinter/ruby-filemagic
|
30
|
+
# http://blackwinter.github.io/ruby-filemagic/
|
31
|
+
#
|
32
|
+
# NOTE: Unmaintained!
|
33
|
+
# https://github.com/blackwinter/ruby-filemagic/commit/e1f2efd07da4130484f06f58fed016d9eddb4818
|
34
|
+
#
|
35
|
+
# Might consider replacing this with an FFI filemagic to eliminate the native-code compilation.
|
36
|
+
# https://rubygems.org/gems/glongman-ffiruby-filemagic/
|
37
|
+
# https://stuart.com/blog/ruby-bindings-extensions/
|
4
38
|
require 'ruby-filemagic'
|
5
39
|
|
40
|
+
|
41
|
+
# Monkey-patch some DistorteD-specific methods into MIME::Type objects.
|
6
42
|
module MIME
|
7
43
|
class Type
|
8
|
-
|
9
|
-
|
44
|
+
|
45
|
+
# Provide a few variations on the base :distorted_method for mixed workflows
|
46
|
+
# where it isn't feasible to overload a single method name and call :super.
|
47
|
+
# Jekyll, for example, renders its output markup upfront, collects all of
|
48
|
+
# the StaticFiles (or StaticStatic-includers, in our case), then calls their
|
49
|
+
# :write methods all at once after the rest of the site is built,
|
50
|
+
# and this precludes us from easily sharing method names between layers.
|
51
|
+
DISTORTED_METHOD_PREFIXES = Hash[
|
52
|
+
:buffer => 'to'.freeze,
|
53
|
+
:file => 'write'.freeze,
|
54
|
+
:template => 'render'.freeze,
|
55
|
+
]
|
56
|
+
SUB_TYPE_SEPARATORS = /[-_+\.]/
|
57
|
+
|
58
|
+
# Returns a Symbol name of the method that should return a String buffer containing the file in this Type.
|
59
|
+
def distorted_buffer_method; "#{DISTORTED_METHOD_PREFIXES[:buffer]}_#{distorted_method_suffix}".to_sym; end
|
60
|
+
|
61
|
+
# Returns a Symbol name of the method that should write a file of this Type to a given path on a filesystem.
|
62
|
+
def distorted_file_method; "#{DISTORTED_METHOD_PREFIXES[:file]}_#{distorted_method_suffix}".to_sym; end
|
63
|
+
|
64
|
+
# Returns a Symbol name of the method that should returns a context-appropriate Object
|
65
|
+
# for displaying the file as this Type.
|
66
|
+
# Might be e.g. a String buffer containing Rendered Liquid in Jekylland,
|
67
|
+
# or a Type-appropriate frame in some GUI toolkit in DD-Booth.
|
68
|
+
def distorted_template_method; "#{DISTORTED_METHOD_PREFIXES[:template]}_#{distorted_method_suffix}".to_sym; end
|
69
|
+
|
70
|
+
# Returns an Array[Array[String]] of human-readable keys we can use for our YAML config,
|
71
|
+
# e.g. :media_type 'image' & :sub_type 'svg+xml' would be split to ['image', 'svg'].
|
72
|
+
# `nil` `:sub_type`s will just be compacted out.
|
73
|
+
# Every non-nil :media_type will also request a key path [media_type, '*']
|
74
|
+
# to allow for similar-type defaults, e.g. every image type outputting a fallback.
|
75
|
+
def settings_paths; [[self.media_type, '*'.freeze], [self.media_type, self.sub_type&.split('+'.freeze)&.first].compact]; end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Provide a consistent base method name for context-specific DistorteD operations.
|
80
|
+
def distorted_method_suffix
|
10
81
|
# Standardize MIME::Types' media_type+sub_type to DistorteD method mapping
|
11
82
|
# by replacing all the combining characters with underscores (snake case)
|
12
83
|
# to match Ruby conventions:
|
@@ -18,20 +89,29 @@ module MIME
|
|
18
89
|
# :to_application_vnd_openxmlformats_officedocument_wordprocessingml_document
|
19
90
|
# which would most likely be defined by the :included method of a library-specific
|
20
91
|
# module for handling OpenXML MS Office documents.
|
21
|
-
"
|
22
|
-
end
|
92
|
+
"#{self.media_type}_#{self.sub_type.gsub(SUB_TYPE_SEPARATORS, '_'.freeze)}"
|
93
|
+
end # distorted_method_suffix
|
23
94
|
end
|
24
95
|
end
|
25
96
|
|
97
|
+
|
26
98
|
module CHECKING
|
27
99
|
class YOU
|
28
100
|
|
101
|
+
# Returns a single Type with Array-style access.
|
102
|
+
class OUT
|
103
|
+
def self.[](type)
|
104
|
+
CHECKING::YOU::types[type].first
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
29
108
|
# Returns a Set of MIME::Type for a given file path, by default only
|
30
109
|
# based on the file extension. If the file extension is unavailable—
|
31
110
|
# or if `so_deep` is enabled—the `path` will be used as an actual
|
32
111
|
# path to look at the magic bytes with ruby-filemagic.
|
33
|
-
def self.OUT(path, so_deep: false)
|
34
|
-
|
112
|
+
def self.OUT(path, so_deep: false, only_one_test: false)
|
113
|
+
return Set[] if path.nil?
|
114
|
+
if not (only_one_test || types.type_for(path).empty?)
|
35
115
|
# NOTE: `type_for`'s return order is supposed to be deterministic:
|
36
116
|
# https://github.com/mime-types/ruby-mime-types/issues/148
|
37
117
|
# My use case so far has never required order but has required
|
@@ -43,7 +123,7 @@ module CHECKING
|
|
43
123
|
# irb(main)> MIME::Types.type_for('lol.ttf')).to_set
|
44
124
|
# => #<Set: {#<MIME::Type: font/ttf>, #<MIME::Type: application/font-sfnt>, #<MIME::Type: application/x-font-truetype>, #<MIME::Type: application/x-font-ttf>}>
|
45
125
|
return types.type_for(path).to_set
|
46
|
-
|
126
|
+
elsif (so_deep && path[0] != '.'.freeze) # Support taking hypothetical file extensions (e.g. '.jpg') without stat()ing anything.
|
47
127
|
# Did we fail to guess any MIME::Types from the given filename?
|
48
128
|
# We're going to have to look at the actual file
|
49
129
|
# (or at least its first four bytes).
|
@@ -63,18 +143,31 @@ module CHECKING
|
|
63
143
|
# irb(main)> "image/svg+xml; charset=us-ascii".split(';').first
|
64
144
|
# => "image/svg+xml"
|
65
145
|
mime = types[fm.file(path, false).split(';'.freeze).first].to_set
|
66
|
-
end
|
67
|
-
|
68
|
-
|
146
|
+
end # FileMagic.open
|
147
|
+
else
|
148
|
+
# TODO: Warn here that we may need a custom type!
|
149
|
+
#p "NO MATCH FOR #{path}"
|
150
|
+
Set[]
|
151
|
+
end # if
|
152
|
+
end # self.OUT()
|
69
153
|
|
70
154
|
# Returns a Set of MIME::Type objects matching a String search key of the
|
71
155
|
# format MEDIA_TYPE/SUB_TYPE.
|
72
156
|
# This can return multiple Types, e.g. 'font/collection' TTC/OTC variations:
|
73
157
|
# [#<MIME::Type: font/collection>, #<MIME::Type: font/collection>]
|
74
|
-
def self.IN(
|
75
|
-
|
158
|
+
def self.IN(wanted_type_or_types)
|
159
|
+
if wanted_type_or_types.is_a?(Enumerable)
|
160
|
+
# Support taking a list of String types for Molecules whose Type support
|
161
|
+
# isn't easily expressable as a single Regexp.
|
162
|
+
types.select{ |type| wanted_type_or_types.include?(type.to_s) }
|
163
|
+
else
|
164
|
+
# Might be a single String or Regexp
|
165
|
+
types[wanted_type_or_types, :complete => wanted_type_or_types.is_a?(Regexp)].to_set
|
166
|
+
end
|
76
167
|
end
|
77
168
|
|
169
|
+
protected
|
170
|
+
|
78
171
|
# Returns the MIME::Types container or loads one
|
79
172
|
def self.types
|
80
173
|
@@types ||= types_loader
|
@@ -88,17 +181,27 @@ module CHECKING
|
|
88
181
|
# Load the upstream mime-types-data by providing a nil `path`:
|
89
182
|
# path || ENV['RUBY_MIME_TYPES_DATA'] || MIME::Types::Data::PATH
|
90
183
|
loader = MIME::Types::Loader.new(nil, container)
|
184
|
+
# TODO: Log this once I figure out a nice way to wrap Jekyll logger too.
|
185
|
+
# irb> loader.load_columnar => #<MIME::Types: 2277 variants, 1195 extensions>
|
91
186
|
loader.load_columnar
|
92
187
|
|
93
188
|
# Change default JPEG file extension from .jpeg to .jpg
|
94
189
|
# because it pisses me off lol
|
95
190
|
container['image/jpeg'].last.preferred_extension = 'jpg'
|
96
191
|
|
192
|
+
# Add a missing extension to MPEG-DASH manifests:
|
193
|
+
# irb> MIME::Types['application/dash+xml'].first
|
194
|
+
# => #<MIME::Type: application/dash+xml>
|
195
|
+
# irb> MIME::Types['application/dash+xml'].first.preferred_extension
|
196
|
+
# => nil
|
197
|
+
# https://www.iana.org/assignments/media-types/application/dash+xml
|
198
|
+
container['application/dash+xml'].last.preferred_extension = 'mpd'
|
199
|
+
|
97
200
|
# Override the loader's path with the path to our local data directory
|
98
201
|
# after we've loaded the upstream data.
|
99
202
|
# :@path is set up in Loader::initialize and only has an attr_reader
|
100
203
|
# but we can reach in and change it.
|
101
|
-
loader.instance_variable_set(:@path, File.join(__dir__, '
|
204
|
+
loader.instance_variable_set(:@path, File.join(__dir__, 'checking_you_out'.freeze))
|
102
205
|
|
103
206
|
# Load our local types data. The YAML files are separated by type,
|
104
207
|
# and :load_yaml will load all of them in the :@path we just set.
|
File without changes
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Define our own type to trigger fallback copy instead of abusing application/x-imagemap :)
|
2
|
+
- !ruby/object:MIME::Type
|
3
|
+
content-type: application/x.distorted.never-let-you-down
|
4
|
+
xrefs:
|
5
|
+
person:
|
6
|
+
- okeeblow
|
7
|
+
registered: false
|
8
|
+
|
9
|
+
|
10
|
+
# MATLAB files supported by VIPS Magick foreign loader.
|
11
|
+
- !ruby/object:MIME::Type
|
12
|
+
content-type: application/x-matlab-data
|
13
|
+
encoding: 8bit
|
14
|
+
extensions:
|
15
|
+
- mat
|
16
|
+
xrefs_urls:
|
17
|
+
- "http://justsolve.archiveteam.org/wiki/MAT"
|
18
|
+
registered: false
|
19
|
+
|
20
|
+
|
21
|
+
# Microsoft SilverLight multiple-zoom tiled-raster image archive formats.
|
22
|
+
# I made this content-type up based on the convention of other 'vnd.microsoft' Types,
|
23
|
+
# e.g. #<MIME::Type: application/vnd.microsoft.windows.thumbnail-cache>
|
24
|
+
- !ruby/object:MIME::Type
|
25
|
+
content-type: application/vnd.microsoft.deep-zoom
|
26
|
+
extensions:
|
27
|
+
- dz
|
28
|
+
- dzi
|
29
|
+
- dzc
|
30
|
+
- xml
|
31
|
+
xrefs_urls:
|
32
|
+
- "http://msdn.microsoft.com/en-us/library/cc645077(VS.95).aspx"
|
33
|
+
registered: false
|
File without changes
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# Define our own type to trigger a legacy <img> element src for <picture>
|
2
|
+
- !ruby/object:MIME::Type
|
3
|
+
content-type: image/x.distorted.fallback
|
4
|
+
xrefs:
|
5
|
+
person:
|
6
|
+
- okeeblow
|
7
|
+
registered: false
|
8
|
+
|
9
|
+
|
10
|
+
# libvips' internal format, unlikely to be used but may as well be supported :)
|
11
|
+
- !ruby/object:MIME::Type
|
12
|
+
content-type: image/vips
|
13
|
+
extensions:
|
14
|
+
- v
|
15
|
+
- vips
|
16
|
+
xrefs_urls:
|
17
|
+
- "http://fileformats.archiveteam.org/wiki/VIPS"
|
18
|
+
registered: false
|
19
|
+
|
20
|
+
|
21
|
+
# OpenEXR stuff
|
22
|
+
- !ruby/object:MIME::Type
|
23
|
+
content-type: image/x-exr
|
24
|
+
extensions:
|
25
|
+
- exr
|
26
|
+
xrefs_urls:
|
27
|
+
- "https://www.nationalarchives.gov.uk/PRONOM/fmt/1001"
|
28
|
+
- "https://en.wikipedia.org/wiki/OpenEXR"
|
29
|
+
- "http://fileformats.archiveteam.org/wiki/OpenEXR"
|
30
|
+
|
31
|
+
- !ruby/object:MIME::Type
|
32
|
+
content-type: image/vnd.radiance
|
33
|
+
extensions:
|
34
|
+
- hdr
|
35
|
+
xrefs_urls:
|
36
|
+
- "https://en.wikipedia.org/wiki/RGBE_image_format"
|
37
|
+
registered: false
|
38
|
+
|
39
|
+
- !ruby/object:MIME::Type
|
40
|
+
content-type: image/fits
|
41
|
+
extensions:
|
42
|
+
- fits
|
43
|
+
- fit
|
44
|
+
- fts
|
45
|
+
xrefs_urls:
|
46
|
+
- "https://www.iana.org/assignments/media-types/image/fits"
|
47
|
+
- "https://www.cv.nrao.edu/fits/"
|
48
|
+
registered: true
|
49
|
+
|
50
|
+
# End OpenEXR stuff
|
51
|
+
|
52
|
+
|
53
|
+
# OpenSlide stuff — https://openslide.org/formats/
|
54
|
+
|
55
|
+
- !ruby/object:MIME::Type
|
56
|
+
content-type: image/vnd.scanscope.virtual.slide
|
57
|
+
extensions:
|
58
|
+
- svs # Excluding tiff since that has a generic entry.
|
59
|
+
xrefs_urls:
|
60
|
+
- "https://openslide.org/formats/aperio/"
|
61
|
+
- "https://docs.openmicroscopy.org/bio-formats/latest/formats/aperio-svs-tiff.html"
|
62
|
+
- "http://justsolve.archiveteam.org/wiki/Aperio_SVS"
|
63
|
+
registered: false
|
64
|
+
|
65
|
+
- !ruby/object:MIME::Type
|
66
|
+
content-type: image/vnd.hamamatsu
|
67
|
+
extensions:
|
68
|
+
- vms
|
69
|
+
- vmu
|
70
|
+
- ndpi
|
71
|
+
xrefs_urls:
|
72
|
+
- "https://openslide.org/formats/hamamatsu/"
|
73
|
+
registered: false
|
74
|
+
|
75
|
+
- !ruby/object:MIME::Type
|
76
|
+
content-type: image/vnd.sakura
|
77
|
+
extensions:
|
78
|
+
- svslide
|
79
|
+
xrefs_urls:
|
80
|
+
- "https://openslide.org/formats/sakura/"
|
81
|
+
registered: false
|
82
|
+
|
83
|
+
- !ruby/object:MIME::Type
|
84
|
+
content-type: image/vnd.mirax
|
85
|
+
extensions:
|
86
|
+
- mrxs
|
87
|
+
xrefs_urls:
|
88
|
+
- "https://openslide.org/formats/mirax/"
|
89
|
+
registered: false
|
90
|
+
|
91
|
+
- !ruby/object:MIME::Type
|
92
|
+
content-type: image/vnd.leica
|
93
|
+
extensions:
|
94
|
+
- scn
|
95
|
+
xrefs_urls:
|
96
|
+
- "https://openslide.org/formats/leica/"
|
97
|
+
registered: false
|
98
|
+
|
99
|
+
- !ruby/object:MIME::Type
|
100
|
+
content-type: image/vnd.ventana
|
101
|
+
extensions:
|
102
|
+
- bif
|
103
|
+
xrefs_urls:
|
104
|
+
- "https://openslide.org/formats/ventana/"
|
105
|
+
registered: false
|
106
|
+
|
107
|
+
|
108
|
+
# End OpenSlide stuff
|
@@ -0,0 +1,333 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'distorted/monkey_business/set'
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require 'shellwords' # Necessary for inclusion in OptionParser coercions list.
|
6
|
+
|
7
|
+
require 'distorted/invoker'
|
8
|
+
require 'distorted/checking_you_out'
|
9
|
+
require 'distorted/element_of_media/compound'
|
10
|
+
|
11
|
+
|
12
|
+
module Cooltrainer; end
|
13
|
+
module Cooltrainer::DistorteD; end
|
14
|
+
|
15
|
+
class Cooltrainer::DistorteD::ClickAgain
|
16
|
+
|
17
|
+
include Cooltrainer::DistorteD::Invoker # MediaMolecule plugger
|
18
|
+
|
19
|
+
attr_reader :global_options, :lower_options, :outer_options
|
20
|
+
|
21
|
+
|
22
|
+
# Set up and parse a given Array of command-line switches based on
|
23
|
+
# our global OptionParser and its Type/Molecule-specific sub-commands.
|
24
|
+
#
|
25
|
+
# :argv will be operated on destructively!
|
26
|
+
# Consider passing a duplicate of ARGV instead of passing it directly.
|
27
|
+
def initialize(argv, exe_name)
|
28
|
+
|
29
|
+
# Partition argv into (switches and their arguments) and (filenames or wanted type Strings)
|
30
|
+
switches, @get_out = partition_argv(argv)
|
31
|
+
|
32
|
+
# Initialize Hashes to store our three types of Options using a small
|
33
|
+
# custom subclass that will store items as a Set but won't store :nil alone.
|
34
|
+
@global_options = Hash.new { |h,k| h[k] = h.class.new(&h.default_proc) }
|
35
|
+
@lower_options = Hash.new { |h,k| h[k] = h.class.new(&h.default_proc) }
|
36
|
+
@outer_options = Hash.new { |h,k| h[k] = h.class.new(&h.default_proc) }
|
37
|
+
# Temporary Array for unmatched Switches when parsing subcommands.
|
38
|
+
sorry_try_again = Array.new
|
39
|
+
|
40
|
+
# Pass our executable name in for the global OptionParser's banner String,
|
41
|
+
# then parse the complete/raw user-given-arguments-list first with this Parser.
|
42
|
+
#
|
43
|
+
# I am intentionally using OptionParser's non-POSIXy :permute! method
|
44
|
+
# instead of the POSIX-compatible :order! method,
|
45
|
+
# because I want to :)
|
46
|
+
# Otherwise users would have to define all switch arguments
|
47
|
+
# ahead of all positional arguments in the command,
|
48
|
+
# and I think that would be frustrating and silly.
|
49
|
+
#
|
50
|
+
# In strictly-POSIX mode, one would have to call e.g.
|
51
|
+
# `distorted -o image/png inputfile.webp outfilewithnofileextension`
|
52
|
+
# instead of
|
53
|
+
# `distorted inputfile.webp -o image/png outfilewithnofileextension`,
|
54
|
+
# which I find to be much more intuitive.
|
55
|
+
#
|
56
|
+
# Note that `:parse!` would call one of the other of :order!/:permute! based on
|
57
|
+
# an invironment variable `POSIXLY_CORRECT`. Talk about a footgun!
|
58
|
+
# Be explicit!!
|
59
|
+
global = global_options(exe_name)
|
60
|
+
begin
|
61
|
+
switches = global.permute!(switches, into: @global_options)
|
62
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument, OptionParser::ParseError => nope
|
63
|
+
nope.recover(sorry_try_again) # Will :unshift the :nope value to the recovery Array.
|
64
|
+
#if switches&.first&.chr == '-'.freeze
|
65
|
+
# sorry_try_again.unshift(switches.shift)
|
66
|
+
#end
|
67
|
+
retry
|
68
|
+
end
|
69
|
+
switches.unshift(*sorry_try_again.reverse)
|
70
|
+
|
71
|
+
# The global OptionParser#permute! call will strip our `:argv` Array of
|
72
|
+
# any `--help` or Molecule-picking switches.
|
73
|
+
# Molecule-specific switches (both 'lower' and 'outer') and positional
|
74
|
+
# file-name arguments remain.
|
75
|
+
#
|
76
|
+
# The first remaining `argv` will be our input filename if one was given!
|
77
|
+
#
|
78
|
+
# NOTE: Never assume this filename will be a complete, absolute, usable path.
|
79
|
+
# POSIX shells do not do tilde expansion, for example, on quoted switch arguments,
|
80
|
+
# so a quoted filename argument '~/cover.png' will come through to Ruby-land
|
81
|
+
# as the literal String '~/cover.png' while the same filename argument sans-quotes
|
82
|
+
# will be expanded to e.g. '/home/okeeblow/cover.png' (based on `$HOME` env var).
|
83
|
+
# Additional Ruby-side path validation will almost certainly be needed!
|
84
|
+
# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_01
|
85
|
+
@name = @get_out&.shift
|
86
|
+
|
87
|
+
# Print some sort of help message or list of supported input/output Types
|
88
|
+
# if no source filename was given.
|
89
|
+
unless @name
|
90
|
+
puts case
|
91
|
+
when @global_options.has_key?(:help) then global
|
92
|
+
when @global_options.has_key?(:"lower-world")
|
93
|
+
"Supported input media types:\n#{lower_world.keys.join("\n")}"
|
94
|
+
when @global_options.has_key?(:"outer-limits")
|
95
|
+
"Supported output media types:\n#{outer_limits(all: true).values.map{|m| m.keys}.join("\n")}"
|
96
|
+
else global
|
97
|
+
end
|
98
|
+
exit
|
99
|
+
end
|
100
|
+
|
101
|
+
# Here's that additional filename validation I was talking about.
|
102
|
+
# I don't do this as a one-shot with the argv.shift because
|
103
|
+
# File::expand_path raises an error on :nil argument,
|
104
|
+
# and we already checked for that when we checked for 'help' switches.
|
105
|
+
@name = File.expand_path(@name)
|
106
|
+
|
107
|
+
# Check for 'help' switches *again* now that we have a source file path,
|
108
|
+
# because the output can be file-specific instead of generic.
|
109
|
+
# This is where we display subcommands' help!
|
110
|
+
specific_help = case
|
111
|
+
when @get_out.empty?
|
112
|
+
# Only input filename given; no outputs; nothing left to do!
|
113
|
+
lower_subcommands.merge(outer_subcommands).values.unshift(Hash[:DistorteD => [global]]).map { |l|
|
114
|
+
l.values.join("\n")
|
115
|
+
}.join("\n")
|
116
|
+
when @global_options.has_key?(:help), @global_options.has_key?(:"lower-world")
|
117
|
+
lower_subcommands.values.map { |l|
|
118
|
+
l.values.join("\n")
|
119
|
+
}.join("\n")
|
120
|
+
when @global_options.has_key?(:"outer-limits")
|
121
|
+
# Trigger this help message on `-o` iff that switch is used bare.
|
122
|
+
# If `-o` is given an argument it will inform the MIME::Type
|
123
|
+
# of the same-index output file, e.g.
|
124
|
+
# `-o image/png -o image/webp pngnoextension webpnoextension`
|
125
|
+
# will work exactly as that example implies.
|
126
|
+
@global_options.dig(:"outer-limits")&.empty? ?
|
127
|
+
outer_subcommands.values.map { |o|
|
128
|
+
o.values.join("\n")
|
129
|
+
}.join("\n") : nil
|
130
|
+
else nil
|
131
|
+
end
|
132
|
+
if specific_help
|
133
|
+
puts specific_help
|
134
|
+
exit
|
135
|
+
end
|
136
|
+
|
137
|
+
# Our "subcommands" are additional instances of OptionParser,
|
138
|
+
# one for every MediaMolecule that can load the source file,
|
139
|
+
# and one for every intended output variation.
|
140
|
+
lower_subcommands.each_pair { |type, molecule_commands|
|
141
|
+
molecule_commands.each_pair { |molecule, subcommand|
|
142
|
+
begin
|
143
|
+
switches = subcommand.permute!(switches, into: @lower_options[type][molecule])
|
144
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument, OptionParser::ParseError => nope
|
145
|
+
nope.recover(sorry_try_again) # Will :unshift the :nope value to the recovery Array.
|
146
|
+
retry
|
147
|
+
end
|
148
|
+
switches.unshift(*sorry_try_again.reverse)
|
149
|
+
@lower_options[type][molecule].store(:molecule, molecule)
|
150
|
+
}
|
151
|
+
}
|
152
|
+
outer_subcommands.each_pair { |molecule, type_commands|
|
153
|
+
type_commands.each_pair { |type, subcommand|
|
154
|
+
begin
|
155
|
+
switches = subcommand.permute!(switches, into: @outer_options[molecule][type])
|
156
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument, OptionParser::ParseError => nope
|
157
|
+
nope.recover(sorry_try_again) # Will :unshift the :nope value to the recovery Array.
|
158
|
+
retry
|
159
|
+
end
|
160
|
+
switches.unshift(*sorry_try_again.reverse)
|
161
|
+
@outer_options[molecule][type].store(:molecule, molecule)
|
162
|
+
}
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
# Writes all intended output files to a given directory.
|
167
|
+
def write(dest_root)
|
168
|
+
changes.each { |change|
|
169
|
+
if self.respond_to?(change.type.distorted_file_method)
|
170
|
+
# WISHLIST: Remove the empty final positional Hash argument once we require a Ruby version
|
171
|
+
# that will not perform the implicit Change-to-Hash conversion due to Change's
|
172
|
+
# implementation of :to_hash. Ruby 2.7 will complain but still do the conversion,
|
173
|
+
# breaking downstream callers that want a Struct they can call arbitrary key methods on.
|
174
|
+
# https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
|
175
|
+
self.send(change.type.distorted_file_method, dest_root, change, **{})
|
176
|
+
else
|
177
|
+
raise MediaTypeOutputNotImplementedError.new(change.name, change.type, self.class.name)
|
178
|
+
end
|
179
|
+
}
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
# Partitions the raw `argv` into two buckets — outer_limits (String relative filenames or "media/type" Strings) and switches/arguments.
|
185
|
+
#
|
186
|
+
# References:
|
187
|
+
# - glibc Program Argument Syntax Conventions https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
|
188
|
+
# - POSIX Utility Argument Syntax https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_01
|
189
|
+
# - Windows Command-line syntax key https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/command-line-syntax-key
|
190
|
+
#
|
191
|
+
# The filenames will be used as the source file (first member) and destination file(s) (any others).
|
192
|
+
# The switches/arguments will be passed to our global OptionParser's `:parse_in_order`
|
193
|
+
# which will return the unused remainder.
|
194
|
+
#
|
195
|
+
# I think it should be possible to achieve this same effect with our global OptionParser alone
|
196
|
+
# by specifying two required NoArgument Switches (source and first destination filename),
|
197
|
+
# specifying multiple optional NoArgument Switches (additional destinations),
|
198
|
+
# and parsing the unmodified `:argv` in permutation mode.
|
199
|
+
# I can't figure out how to wrangle OptionParser into doing that rn tho so welp here we are.
|
200
|
+
#
|
201
|
+
# There is a built-in Enumeraable#partition, but:
|
202
|
+
# - It doesn't take an accumulator variable natively.
|
203
|
+
# - I'm bad at chaining Enumerators and idk how to chain in a `:with_object` without returning only that object.
|
204
|
+
# - `:with_object` treats scalar types as immutable which precludes cleanly passing a boolean flag variable between iterations.
|
205
|
+
def partition_argv(argv)
|
206
|
+
switches, @get_out = argv.each_with_object(
|
207
|
+
# Accumulate to a three-key Hash containing the two wanted buckets and the flag that will be discarded.
|
208
|
+
Hash[:switches => Array.new, :get_out => Array.new, :want_value => false]
|
209
|
+
) { |arg, partition|
|
210
|
+
# Switches and their values will be:
|
211
|
+
# - Any argument beginning with a single dash, e.g. long switches like '--crop' or short switches like '-Q90'.
|
212
|
+
# - Any non-dash argument if :want_value is flagged, e.g. the 'attention' value for the '--crop' switch.
|
213
|
+
# Filenames will be:
|
214
|
+
# - Anything else :)
|
215
|
+
if partition.fetch(:want_value) and not arg[0] == '-'.freeze
|
216
|
+
# `ARGV` Strings are frozen, so we have to replace instead of directly concat
|
217
|
+
partition[:switches].push(partition[:switches].pop.yield_self { |last| "#{last}#{'='.freeze unless arg[0] == '='.freeze}#{arg}" })
|
218
|
+
else
|
219
|
+
partition[(arg[0] == '-'.freeze or partition.fetch(:want_value)) ? :switches : :get_out].push(arg)
|
220
|
+
end
|
221
|
+
# The *next* argument should be a value for this iteration's argument iff:
|
222
|
+
# - This iteration is a long switch with no included value, e.g. '--crop' but not '--crop=attention'.
|
223
|
+
# - This iteration is a short switch with no included value, e.g. '-Q' but not '-Q90' or '-Q=90'.
|
224
|
+
partition.store(:want_value, [
|
225
|
+
arg[0] == '-'.freeze, # e.g. '--crop' or '-Q'
|
226
|
+
!arg.include?('='.freeze), # e.g. not '--crop=attention' or '-Q=90'
|
227
|
+
[
|
228
|
+
arg[1] == '-'.freeze, # e.g. '--crop'
|
229
|
+
[arg[1] == '-'.freeze, arg.length > 2].none? # e.g. not '-Q90'
|
230
|
+
].any?
|
231
|
+
].all?)
|
232
|
+
}.values.select(&Array.method(:===)) # Return only the Array members of the Hash
|
233
|
+
end
|
234
|
+
|
235
|
+
# Generic top-level OptionParser
|
236
|
+
def global_options(exe_name)
|
237
|
+
OptionParser.new do |opts|
|
238
|
+
opts.banner = "Usage: #{exe_name} [OPTION]… SOURCE DEST [DEST]…"
|
239
|
+
opts.on_tail('-h', '--help', 'Show this message')
|
240
|
+
opts.on('-v', '--[no-]verbose', 'Run verbosely')
|
241
|
+
opts.on('-l', '--lower-world', 'Show supported input media types')
|
242
|
+
opts.on('-o', '--outer-limits', 'Show supported output media types')
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Returns an Array[Change] for every intended output variation.
|
247
|
+
def changes
|
248
|
+
@changes ||= begin
|
249
|
+
# TODO: Consume @lower_options as well, and figure out how to specify Molecule
|
250
|
+
# for future situations where multiple Molecules may overlap.
|
251
|
+
# Until then, just collapse @outer_options to one Hash and take anything we find for our Type.
|
252
|
+
combined_outer_options = @outer_options.each_with_object(Array.new) { |(molecule,type_options),combined| combined.push(type_options) }.reduce(&:merge)
|
253
|
+
@get_out.each_with_object(Array[]) { |out, wanted|
|
254
|
+
# TODO: Nice way to check format for Type string here.
|
255
|
+
# Should be e.g. "image/png"
|
256
|
+
if CHECKING::YOU::OUT[out].nil?
|
257
|
+
name = out
|
258
|
+
type = CHECKING::YOU::OUT(out).first
|
259
|
+
else
|
260
|
+
name = @name
|
261
|
+
type = CHECKING::YOU::OUT[out]
|
262
|
+
end
|
263
|
+
wanted.push(Cooltrainer::Change.new(type, src: name, **(combined_outer_options.fetch(type, {}))))
|
264
|
+
}
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Returns an absolute String path to the source file.
|
269
|
+
def path
|
270
|
+
File.expand_path(@name)
|
271
|
+
end
|
272
|
+
|
273
|
+
# This is a CLI, so we always want to write new files when called.
|
274
|
+
def modified?
|
275
|
+
true
|
276
|
+
end
|
277
|
+
|
278
|
+
# And again.
|
279
|
+
def write?
|
280
|
+
true
|
281
|
+
end
|
282
|
+
|
283
|
+
# Generate an OptionParser for a flat Enumerable of Compounds
|
284
|
+
COMPOUND_OPTIONPARSER = Proc.new { |compounds, from, to|
|
285
|
+
OptionParser.new(banner = "#{from.to_s} ⟹ #{to.to_s}:") { |subopt|
|
286
|
+
compounds.map { |compound|
|
287
|
+
next if compound.nil?
|
288
|
+
parts = Array[
|
289
|
+
*compound.to_options,
|
290
|
+
]
|
291
|
+
if compound.default == true
|
292
|
+
parts.append(TrueClass)
|
293
|
+
elsif compound.default == false
|
294
|
+
parts.append(FalseClass)
|
295
|
+
elsif compound.valid.is_a?(Range)
|
296
|
+
# TODO: decide how to handle Ranges that might be hundreds/thousands of items in length.
|
297
|
+
elsif compound.valid.is_a?(Enumerable)
|
298
|
+
parts.append(compound.valid.to_a)
|
299
|
+
elsif Cooltrainer::OPTIONPARSER_COERSIONS.include?(compound.valid.class)
|
300
|
+
parts.append(compound.valid)
|
301
|
+
end
|
302
|
+
parts.append(compound.blurb)
|
303
|
+
subopt.on(*parts)
|
304
|
+
}
|
305
|
+
subopt.on_tail('-h', '--help', 'Show this message')
|
306
|
+
}
|
307
|
+
}
|
308
|
+
|
309
|
+
# Generate a Hash[MIME::Type] => Hash[MediaMolecule] => OptionParser
|
310
|
+
# for file-specific input options.
|
311
|
+
def lower_subcommands
|
312
|
+
type_mars.each_with_object(Hash[]) { |type, commands|
|
313
|
+
lower_world[type].each_pair { |molecule, aka|
|
314
|
+
commands.update(type => {
|
315
|
+
molecule => COMPOUND_OPTIONPARSER.call(aka.values.to_set, type, molecule)
|
316
|
+
}) { |k,o,n| o.merge(n) }
|
317
|
+
}
|
318
|
+
}
|
319
|
+
end
|
320
|
+
|
321
|
+
# Generate a Hash[MediaMolecule] => Hash[MIME::Type] => OptionParser
|
322
|
+
# for file-specific output options.
|
323
|
+
def outer_subcommands(all: false)
|
324
|
+
outer_limits(all: all).each_with_object(Hash[]) { |(molecule, types), commands|
|
325
|
+
types.each_pair { |type, aka|
|
326
|
+
commands.update(molecule => {
|
327
|
+
type => COMPOUND_OPTIONPARSER.call(aka.values.to_set, molecule, type)
|
328
|
+
}) { |k,o,n| o.merge(n) }
|
329
|
+
}
|
330
|
+
}
|
331
|
+
end
|
332
|
+
|
333
|
+
end
|