distorted 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 +5 -140
- data/bin/console +14 -0
- data/bin/distorted +6 -0
- data/bin/setup +8 -0
- data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/Less_Perfect_DOS_VGA.png +0 -0
- data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/More_Perfect_DOS_VGA.png +0 -0
- data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/Perfect_DOS_VGA.png +0 -0
- data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/less_more_perfect_dos_vga_437.html +52 -0
- data/font/1252/LICENSE/PerfectDOSVGA437/font-comment.php@file=perfect_dos_vga_437.html +5 -0
- data/font/1252/LessPerfectDOSVGA.ttf +0 -0
- data/font/1252/MorePerfectDOSVGA.ttf +0 -0
- data/font/1252/Perfect DOS VGA 437 Win.ttf +0 -0
- data/font/437/Perfect DOS VGA 437.ttf +0 -0
- data/font/437/dos437.txt +72 -0
- data/font/65001/Anonymous Pro B.ttf +0 -0
- data/font/65001/Anonymous Pro BI.ttf +0 -0
- data/font/65001/Anonymous Pro I.ttf +0 -0
- data/font/65001/Anonymous Pro.ttf +0 -0
- data/font/65001/LICENSE/AnonymousPro/FONTLOG.txt +45 -0
- data/font/65001/LICENSE/AnonymousPro/OFL-FAQ.txt +235 -0
- data/font/65001/LICENSE/AnonymousPro/OFL.txt +94 -0
- data/font/65001/LICENSE/AnonymousPro/README.txt +55 -0
- data/font/850/ProFont-Bold-01/LICENSE +22 -0
- data/font/850/ProFont-Bold-01/readme.txt +28 -0
- data/font/850/ProFontWindows-Bold.ttf +0 -0
- data/font/850/ProFontWindows.ttf +0 -0
- data/font/850/Profont/LICENSE +22 -0
- data/font/850/Profont/readme.txt +31 -0
- data/font/932/LICENSE/README-ttf.txt +213 -0
- data/font/932/mona.ttf +0 -0
- data/lib/distorted.rb +2 -0
- data/lib/distorted/checking_you_out.rb +219 -0
- data/lib/distorted/checking_you_out/README +4 -0
- data/lib/distorted/checking_you_out/application.yaml +33 -0
- data/lib/distorted/checking_you_out/font.yaml +29 -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/error_code.rb +51 -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/pango.rb +90 -0
- data/lib/distorted/modular_technology/ttfunk.rb +48 -0
- data/lib/distorted/modular_technology/vips.rb +17 -0
- 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 +161 -0
- data/lib/distorted/monkey_business/encoding.rb +317 -0
- data/lib/distorted/monkey_business/hash.rb +18 -0
- data/lib/distorted/monkey_business/set.rb +15 -0
- data/lib/distorted/monkey_business/string.rb +6 -0
- data/lib/distorted/triple_counter.rb +52 -0
- data/lib/distorted/version.rb +22 -0
- data/test/distorted_test.rb +11 -0
- data/test/test_helper.rb +4 -0
- metadata +130 -20
@@ -0,0 +1,119 @@
|
|
1
|
+
module Cooltrainer
|
2
|
+
|
3
|
+
# Fun Ruby Fact™: `false` is always object_id 0
|
4
|
+
# https://skorks.com/2009/09/true-false-and-nil-objects-in-ruby/
|
5
|
+
# irb(main):650:0> true.object_id
|
6
|
+
# => 20
|
7
|
+
# irb(main):651:0> false.object_id
|
8
|
+
# => 0
|
9
|
+
BOOLEAN_VALUES = Set[false, true]
|
10
|
+
|
11
|
+
|
12
|
+
# Struct to encapsulate all the data needed to perform one (1) MIME::Type transformation
|
13
|
+
# of a source media file into any supported MIME::Type, possibly even the same type as input.
|
14
|
+
Change = Struct.new(:type, :src, :basename, :molecule, :tag, :breaks, :atoms, keyword_init: true) do
|
15
|
+
|
16
|
+
# Customize the destination filename and other values before doing the normal Struct setup.
|
17
|
+
def initialize(type, src: nil, molecule: nil, tag: nil, breaks: Array.new, **atoms)
|
18
|
+
# `name` might have a leading slash if referenced as an absolute path as the Tag.
|
19
|
+
basename = File.basename(src, '.*'.freeze).reverse.chomp('/'.freeze).reverse
|
20
|
+
|
21
|
+
# Set the &default_proc on the kwarg-glob Hash instead of making a new Hash,
|
22
|
+
atoms.default_proc = lambda { |h,k| h[k] = Cooltrainer::Atom.new }
|
23
|
+
atoms.transform_values {
|
24
|
+
# We might get Atoms already instantiated, but do it for any that aren't.
|
25
|
+
# We won't have a default value for them in that case.
|
26
|
+
|v| v.is_a?(Cooltrainer::Atom) ? atom : Cooltrainer::Atom.new(v, nil)
|
27
|
+
}.each_key { |k|
|
28
|
+
# Define accessors for context-specific :atoms keys/values that aren't normal Struct members.
|
29
|
+
self.singleton_class.define_method(k) { self[:atoms]&.fetch(k, nil)&.get }
|
30
|
+
self.singleton_class.define_method("#{k}=".to_sym) { |v| self[:atoms][k] = v }
|
31
|
+
}
|
32
|
+
|
33
|
+
# And now back to your regularly-scheduled Struct
|
34
|
+
super(type: type, src: src, basename: basename, molecule: molecule, tag: tag, breaks: breaks, atoms: atoms)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the Change Type's :preferred_extension as a String with leading dot (.)
|
38
|
+
def extname
|
39
|
+
@extname ||= begin
|
40
|
+
dot = '.'.freeze unless type.preferred_extension.nil? || type.preferred_extension&.empty?
|
41
|
+
"#{dot}#{type.preferred_extension}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns an Array[String] of filenames this Change should generate,
|
46
|
+
# one 'full'/'original' plus any limit-breaks,
|
47
|
+
# e.g. ["DistorteD.png", "DistorteD-333.png", "DistorteD-555.png", "DistorteD-888.png", "DistorteD-1111.png"]
|
48
|
+
def names
|
49
|
+
Array[''.freeze].concat(self[:breaks]).map { |b|
|
50
|
+
filetag = (b.nil? || b&.to_s.empty?) ? ''.freeze : '-'.concat(b.to_s)
|
51
|
+
"#{self[:basename]}#{"-#{self.tag}" unless self.tag.nil?}#{filetag}#{extname}"
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns a String describing the :names but rolled into one,
|
56
|
+
# e.g. "IIDX-turntable-(400|800|1500).png"
|
57
|
+
def name
|
58
|
+
tags = self[:breaks].length > 1 ? "-(#{self[:breaks].join('|'.freeze)})" : ''.freeze
|
59
|
+
"#{self.basename}#{tags}#{self.extname}"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns an Array[String] of all absolute destination paths this Change should generate,
|
63
|
+
# given a root destination directory.
|
64
|
+
def paths(dest_root = ''.freeze) # Empty String will expand to current working directory
|
65
|
+
output_dir = self[:atoms]&.fetch(:dir, ''.freeze)
|
66
|
+
return self.names.map { |n| File.join(File.expand_path(dest_root), output_dir, n) }
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a String absolute destination path for only one limit-break.
|
70
|
+
def path(dest_root, break_value)
|
71
|
+
output_dir = self[:atoms]&.fetch(:dir, ''.freeze)
|
72
|
+
return File.join(File.expand_path(dest_root), output_dir, "#{self.basename}#{"-#{self.tag}" unless self.tag.nil?}-#{break_value}#{self.extname}")
|
73
|
+
end
|
74
|
+
|
75
|
+
# A generic version of Struct#to_hash was rejected with good reason,
|
76
|
+
# but I'm going to use it here because I want the implicit Struct-to-Hash
|
77
|
+
# conversion to let me destructure these Structs with a double-splat:
|
78
|
+
# https://bugs.ruby-lang.org/issues/4862
|
79
|
+
#
|
80
|
+
# Defining this method causes Ruby 2.7 to emit a "Using the last argument as keyword parameters is deprecated" warning
|
81
|
+
# if this Struct is passed to a method as the final positional argument! Ruby 2.7 will actually do the
|
82
|
+
# conversion when calling the method in that scenario, causing incorrect behavior to methods expecting Struct.
|
83
|
+
# This is why DD-Floor's `:write` and DD-Jekyll's `:render_to_output_buffer` pass an empty kwargs Hash.
|
84
|
+
# https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
|
85
|
+
def to_hash # Implicit
|
86
|
+
Hash[self.members.reject{|m| m == :atoms}.zip(self.values.reject{|v| v.is_a?(Hash)})].merge(self[:atoms].transform_values(&:get))
|
87
|
+
end
|
88
|
+
# Struct#to_h does exist in stdlib, but redefine its behavior to match our `:to_hash`.
|
89
|
+
def to_h # Explicit
|
90
|
+
Hash[self.members.reject{|m| m == :atoms}.zip(self.values.reject{|v| v.is_a?(Hash)})].merge(self[:atoms].transform_values(&:get))
|
91
|
+
end
|
92
|
+
def dig(*keys); self.to_hash.dig(*keys); end
|
93
|
+
|
94
|
+
# Support setting Atoms that were not defined at instantiation.
|
95
|
+
def method_missing(meth, *a, **k, &b)
|
96
|
+
# Are we a setter?
|
97
|
+
if meth.to_s[-1] == '='.freeze
|
98
|
+
# Set the :value of an existing Atom Struct
|
99
|
+
self[:atoms][meth.to_s.chomp('='.freeze).to_sym].value = a.first
|
100
|
+
else
|
101
|
+
self[:atoms]&.fetch(meth, nil)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end # Struct Change
|
106
|
+
|
107
|
+
|
108
|
+
# Struct to wrap just the user and default values for a Compound or just for freeform usage.
|
109
|
+
Atom = Struct.new(:value, :default) do
|
110
|
+
# Return a value if set, otherwise a default. Both can be `nil`.
|
111
|
+
def get; self.value || self.default; end
|
112
|
+
# Override these default Struct methods with ones that reference our :get
|
113
|
+
def to_s; self.get.to_s; end # Explicit
|
114
|
+
def to_str; self.get.to_s; end # Implicit
|
115
|
+
# Send any unknown message through to a value/default.
|
116
|
+
def method_missing(meth, *a, **k, &b); self.get.send(meth, *a, **k, &b); end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module Cooltrainer
|
5
|
+
|
6
|
+
# This is defined in writing in a comment in optparse.rb's RDoc,
|
7
|
+
# but I can't seem to find anywhere it's available directly in code,
|
8
|
+
# so I am going to build my own.
|
9
|
+
# Maybe I am missing something obvious, in which case I should use that
|
10
|
+
# and get rid of this :)
|
11
|
+
# Based on https://ruby-doc.org/stdlib/libdoc/optparse/rdoc/OptionParser.html#class-OptionParser-label-Type+Coercion
|
12
|
+
OPTIONPARSER_COERSIONS = [
|
13
|
+
Date,
|
14
|
+
DateTime,
|
15
|
+
Time,
|
16
|
+
URI,
|
17
|
+
#Shellwords, # Stock in optparse, but under autoload. I don't want it.
|
18
|
+
String,
|
19
|
+
Integer,
|
20
|
+
Float,
|
21
|
+
Numeric,
|
22
|
+
TrueClass,
|
23
|
+
FalseClass,
|
24
|
+
Array,
|
25
|
+
Regexp,
|
26
|
+
].concat(OptionParser::Acceptables::constants)
|
27
|
+
|
28
|
+
|
29
|
+
# Struct to wrap a MediaMolecule option/attribute datum.
|
30
|
+
Compound = Struct.new(:isotopes, :molecule, :valid, :default, :blurb, keyword_init: true) do
|
31
|
+
|
32
|
+
# Massage the data then call `super` to set the members/values and create the accessors.
|
33
|
+
def initialize(isotopes, molecule: nil, valid: nil, default: nil, blurb: nil)
|
34
|
+
super(
|
35
|
+
# The first argument defines the aliases for a single option and may be just a Symbol
|
36
|
+
# or may be an Enumerable[Symbol] in which case all items after the first are aliases for the first.
|
37
|
+
isotopes: isotopes.is_a?(Enumerable) ? isotopes.to_a : Array[isotopes],
|
38
|
+
# Hint the MediaMolecule that should execute this Change.
|
39
|
+
molecule: molecule,
|
40
|
+
# Valid values for this option may be expressed as a Class a value must be an instance of,
|
41
|
+
# a Regexp a String value must match, an Enumerable that a valid value must be in,
|
42
|
+
# a Range a Float/Integer must be within, a special Boolean Set, etc etc.
|
43
|
+
valid: case valid
|
44
|
+
when Set then valid.to_a
|
45
|
+
else valid
|
46
|
+
end,
|
47
|
+
# Optional default value to use when unset.
|
48
|
+
default: default,
|
49
|
+
# String description of this option's effect.
|
50
|
+
blurb: blurb,
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
# The first isotope is the """real""" option name. Any others are aliases for it.
|
55
|
+
def element; self.isotopes&.first; end
|
56
|
+
def to_s; self.element.to_s; end
|
57
|
+
|
58
|
+
# Returns a longform String representation of one option.
|
59
|
+
def inspect
|
60
|
+
# Intentionally not including the blurb here since they are pretty long and messy.
|
61
|
+
"#{self.isotopes.length > 1 ? self.isotopes : self.element}: #{"#{self.valid} " if self.valid}#{"(#{self.default})" if self.default}"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns an Array of properly-formatted OptionParser::Switch strings for this Compound.
|
65
|
+
def to_options
|
66
|
+
# @isotopes is a Hash[Symbol] => Compound, allowing for Compound aliasing
|
67
|
+
# to multiple Hash keys, e.g. libvips' `:Q` and `:quality` are two Hash keys
|
68
|
+
# referencing the same Compound object.
|
69
|
+
self.isotopes.each_with_object(Array[]) { |aka, commands|
|
70
|
+
# Every Switch has at least one leading dash, and longer ones have two,
|
71
|
+
# e.g. `-Q` vs `--quality`.
|
72
|
+
command = "-"
|
73
|
+
if aka.length > 1
|
74
|
+
command << '-'.freeze
|
75
|
+
end
|
76
|
+
|
77
|
+
# Compounds that take a boolean should format their Switch string
|
78
|
+
# as `--[no]-whatever` (including the brackets!) instead of taking
|
79
|
+
# any kind of boolean-ish argument like true/false/yes/no.
|
80
|
+
#
|
81
|
+
# TODO: There seems to be a bug with Ruby optparse and multiple of these
|
82
|
+
# "--[no]-whatever"-style Switches where only the final Switch will display,
|
83
|
+
# so disable this for now in favor of separate --whatever/--no-whatever.
|
84
|
+
# I have a very basic standalone repro case that fails, so it's not just DD.
|
85
|
+
#
|
86
|
+
#if @valid == BOOLEAN_VALUES or @valid == BOOLEAN_VALUES.to_a
|
87
|
+
# command << '[no]-'.freeze
|
88
|
+
#end
|
89
|
+
|
90
|
+
# Add the alias to form the command.
|
91
|
+
command << aka.to_s
|
92
|
+
|
93
|
+
# Format the valid values and/or default value and stick it on the end.
|
94
|
+
# https://ruby-doc.org/stdlib/libdoc/optparse/rdoc/OptionParser.html#class-OptionParser-label-Type+Coercion
|
95
|
+
if self.valid.is_a?(Range)
|
96
|
+
command << " [#{self.valid.to_s}]"
|
97
|
+
elsif self.valid == BOOLEAN_VALUES or self.valid == BOOLEAN_VALUES.to_a
|
98
|
+
# Intentional no-op
|
99
|
+
elsif self.valid.is_a?(Enumerable)
|
100
|
+
command << " [#{self.valid.join(', '.freeze)}]"
|
101
|
+
elsif not default.nil?
|
102
|
+
command << " [#{self.default}]"
|
103
|
+
else
|
104
|
+
command << " [#{self.element.upcase}]"
|
105
|
+
end
|
106
|
+
|
107
|
+
commands << command
|
108
|
+
|
109
|
+
# HACK around issue with multiple "--[no]-whatever"-style long arguments.
|
110
|
+
# See above note and the commented-out implementation I'd like to use
|
111
|
+
# instead of this. Remove this iff I can figure out what's wrong there.
|
112
|
+
if self.valid == BOOLEAN_VALUES or self.valid == BOOLEAN_VALUES.to_a
|
113
|
+
commands << "--no-#{aka}"
|
114
|
+
end
|
115
|
+
}
|
116
|
+
end # to_options
|
117
|
+
|
118
|
+
end # Compound
|
119
|
+
|
120
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# https://ruby-doc.org/core/Exception.html sez:
|
2
|
+
# "It is recommended that a library should have one subclass of StandardError
|
3
|
+
# or RuntimeError and have specific exception types inherit from it.
|
4
|
+
# This allows the user to rescue a generic exception type to catch
|
5
|
+
# all exceptions the library may raise even if future versions of
|
6
|
+
# the library add new exception subclasses."
|
7
|
+
class StandardDistorteDError < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
# The built-in NotImplementedError is for "when a feature is not implemented
|
11
|
+
# on the current platform", so make our own more appropriate ones.
|
12
|
+
class MediaTypeNotImplementedError < StandardDistorteDError
|
13
|
+
attr_reader :name
|
14
|
+
def initialize(name)
|
15
|
+
super
|
16
|
+
@name = name
|
17
|
+
end
|
18
|
+
|
19
|
+
def message
|
20
|
+
"No supported media type for #{name}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class MediaTypeOutputNotImplementedError < MediaTypeNotImplementedError
|
25
|
+
attr_reader :type, :context
|
26
|
+
def initialize(name, type, context)
|
27
|
+
super(name)
|
28
|
+
@type = type
|
29
|
+
@context = context
|
30
|
+
end
|
31
|
+
|
32
|
+
def message
|
33
|
+
"Unable to save #{name} as #{type.to_s} from #{context}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class MediaTypeNotFoundError < StandardDistorteDError
|
38
|
+
attr_reader :name
|
39
|
+
def initialize(name)
|
40
|
+
super
|
41
|
+
@name = name
|
42
|
+
end
|
43
|
+
|
44
|
+
def message
|
45
|
+
"Failed to detect media type for #{name}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
class OutOfDateLibraryError < LoadError
|
51
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
require 'set'
|
3
|
+
require 'distorted/monkey_business/set'
|
4
|
+
|
5
|
+
require 'distorted/invoker'
|
6
|
+
require 'distorted/click_again'
|
7
|
+
require 'distorted/checking_you_out'
|
8
|
+
|
9
|
+
|
10
|
+
module Cooltrainer; end
|
11
|
+
module Cooltrainer::DistorteD; end
|
12
|
+
|
13
|
+
class Cooltrainer::DistorteD::Floor
|
14
|
+
def initialize(src, dest, type: nil)
|
15
|
+
#TODO: Library-use entry-point
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
|
2
|
+
# Our custom Exceptions
|
3
|
+
require 'distorted/error_code'
|
4
|
+
|
5
|
+
# MIME::Typer
|
6
|
+
require 'distorted/checking_you_out'
|
7
|
+
require 'distorted/media_molecule'
|
8
|
+
|
9
|
+
# Set.to_hash
|
10
|
+
require 'distorted/monkey_business/set'
|
11
|
+
require 'set'
|
12
|
+
|
13
|
+
|
14
|
+
module Cooltrainer::DistorteD::Invoker
|
15
|
+
# Returns a Hash[MIME::Type] => Hash[MediaMolecule] => Hash[param_alias] => Compound
|
16
|
+
def lower_world
|
17
|
+
Cooltrainer::DistorteD::IMPLANTATION(:LOWER_WORLD).each_with_object(
|
18
|
+
Hash.new { |pile, type| pile[type] = Hash[] }
|
19
|
+
) { |(key, types), pile|
|
20
|
+
types.each { |type, elements| pile.update(type => {key.molecule => elements}) { |k,o,n| o.merge(n) }}
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns a Hash[MediaMolecule] => Hash[MIME::Type] => Hash[param_alias] => Compound
|
25
|
+
def outer_limits(all: false)
|
26
|
+
Cooltrainer::DistorteD::IMPLANTATION(
|
27
|
+
:OUTER_LIMITS,
|
28
|
+
(all || type_mars.empty?) ? Cooltrainer::DistorteD::media_molecules : type_mars.each_with_object(Set[]) { |type, molecules|
|
29
|
+
molecules.merge(lower_world[type].keys)
|
30
|
+
},
|
31
|
+
).each_with_object(Hash.new { |pile, type| pile[type] = Hash[] }) { |(key, types), pile|
|
32
|
+
types.each { |type, elements| pile.update(key.molecule => {type => elements}) { |k,o,n| o.merge(n) }}
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Filename without the dot-and-extension.
|
37
|
+
def basename
|
38
|
+
File.basename(@name, '.*')
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns a Set of MIME::Types common to the source file and our supported MediaMolecules.
|
42
|
+
# Each of these Molecules will be plugged to the current instance.
|
43
|
+
def type_mars
|
44
|
+
@type_mars ||= CHECKING::YOU::OUT(path, so_deep: true) & lower_world.keys.to_set
|
45
|
+
raise MediaTypeNotImplementedError.new(@name) if @type_mars.empty?
|
46
|
+
@type_mars
|
47
|
+
end
|
48
|
+
|
49
|
+
# MediaMolecule file-type plugger.
|
50
|
+
# Any call to a MIME::Type's distorted_method will end up here unless
|
51
|
+
# the Molecule that defines it has been `prepend`ed to our instance.
|
52
|
+
def method_missing(meth, *args, **kwargs, &block)
|
53
|
+
# Only consider method names with our prefixes.
|
54
|
+
if MIME::Type::DISTORTED_METHOD_PREFIXES.values.map(&:to_s).include?(meth.to_s.split(MIME::Type::SUB_TYPE_SEPARATORS)[0])
|
55
|
+
# TODO: Might need to handle cases here where the Set[Molecule]
|
56
|
+
# exists but none of them defined our method.
|
57
|
+
unless self.singleton_class.instance_variable_get(:@media_molecules)
|
58
|
+
unless outer_limits.empty?
|
59
|
+
self.singleton_class.instance_variable_set(
|
60
|
+
:@media_molecules,
|
61
|
+
outer_limits.keys.each_with_object(Set[]) { |molecule, molecules|
|
62
|
+
self.singleton_class.prepend(molecule)
|
63
|
+
molecules.add(molecule)
|
64
|
+
}
|
65
|
+
)
|
66
|
+
# `return` to ensure we don't fall through to #method_missing:super
|
67
|
+
# if we are going to do any work, otherwise a NoMethodError will
|
68
|
+
# still be raised despite the distorted_method :sends suceeding.
|
69
|
+
#
|
70
|
+
# Use :__send__ in case a Molecule defines a `:send` method.
|
71
|
+
# https://ruby-doc.org/core/Object.html#method-i-send
|
72
|
+
return self.send(meth, *args, **kwargs, &block)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
# …and I still haven't found it! — What I'm looking for, that is.
|
77
|
+
# https://www.youtube.com/watch?v=xqse3vYcnaU
|
78
|
+
super
|
79
|
+
end
|
80
|
+
|
81
|
+
# Make sure :respond_to? works for yet-unplugged distorted_methods.
|
82
|
+
# http://blog.marc-andre.ca/2010/11/15/methodmissing-politely/
|
83
|
+
def respond_to_missing?(meth, *a)
|
84
|
+
# We can tell if a method looks like one of ours if it has at least 3 (maybe more!)
|
85
|
+
# underscore-separated components with a valid prefix as the first component
|
86
|
+
# and the media-type and sub-type as the rest, e.g.
|
87
|
+
#
|
88
|
+
# irb(main)> 'to_application_pdf'.split('_')
|
89
|
+
# => ["to", "application", "pdf"]
|
90
|
+
#
|
91
|
+
# irb(main)> CHECKING::YOU::OUT('.docx').first.distorted_file_method.to_s.split('_')
|
92
|
+
# => ["write", "application", "vnd", "openxmlformats", "officedocument", "wordprocessingml", "document"]
|
93
|
+
parts = meth.to_s.split(MIME::Type::SUB_TYPE_SEPARATORS)
|
94
|
+
MIME::Type::DISTORTED_METHOD_PREFIXES.values.map(&:to_s).include?(parts[0]) && parts.length > 2 || super(meth, *a)
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Cooltrainer; end
|
4
|
+
module Cooltrainer::DistorteD
|
5
|
+
|
6
|
+
# Discover DistorteD MediaMolecules bundled with this Gem
|
7
|
+
# TODO: and any installed as separate Gems.
|
8
|
+
@@loaded_molecules rescue begin
|
9
|
+
Dir[File.join(__dir__, 'media_molecule', '*.rb')].each { |molecule| require molecule }
|
10
|
+
@@loaded_molecules = true
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns a Set[Module] of our discovered MediaMolecules.
|
14
|
+
def self.media_molecules
|
15
|
+
Cooltrainer::DistorteD::Molecule.constants.map { |molecule|
|
16
|
+
Cooltrainer::DistorteD::Molecule::const_get(molecule)
|
17
|
+
}.to_set
|
18
|
+
end
|
19
|
+
|
20
|
+
# Reusable IMPLANTATION Hash key, since instances of the same Struct subclass are equal:
|
21
|
+
# irb> Pair = Struct.new(:uno, :dos)
|
22
|
+
# irb> lol = Pair.new(:a, 1)
|
23
|
+
# irb> rofl = Pair.new(:a, 1)
|
24
|
+
# irb> lol === rofl
|
25
|
+
# => true
|
26
|
+
KEY = Struct.new(:molecule, :constant, :inherit) do
|
27
|
+
# Descend into ancestor Modules by default.
|
28
|
+
def initialize(molecule, constant, inherit = true); super(molecule, constant, inherit); end
|
29
|
+
def inspect; "#{molecule}#{'∫'.freeze if inherit}::#{constant}"; end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Check and create attribute-memoizing Hash whose default_proc will fetch
|
33
|
+
# and collate the data for a given KEY.
|
34
|
+
@@implantation rescue begin
|
35
|
+
@@implantation = Hash.new { |piles, key|
|
36
|
+
# Optionally limit search to top-level Module like `:const_defined?` with `inherit`
|
37
|
+
piles[key] = Set[key.molecule].merge(key.inherit ? key.molecule.ancestors : []).each_with_object(Hash.new) { |mod, pile|
|
38
|
+
mod.const_get(key.constant).each { |target, elements|
|
39
|
+
pile.update(target => elements) { |_key, existing, new| existing.merge(new) }
|
40
|
+
} rescue nil
|
41
|
+
}
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Generic entry-point for attribute-collation of a given constant
|
46
|
+
# over a given Molecule or Enumerable of Molecules.
|
47
|
+
def self.IMPLANTATION(constant, corpus = self.media_molecules)
|
48
|
+
(corpus.is_a?(Enumerable) ? corpus : Array[corpus]).map { |molecule|
|
49
|
+
KEY.new(molecule, constant)
|
50
|
+
}.each_with_object(Hash[]) { |key, piles|
|
51
|
+
# Hash#slice doesn't trigger the default_proc, so access each directly.
|
52
|
+
piles.store(key, @@implantation[key])
|
53
|
+
}.yield_self { |piles|
|
54
|
+
# Return just the data when we were given a single Molecule to search.
|
55
|
+
corpus.is_a?(Enumerable) ? piles : piles.shift[1]
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|