distorted 0.5.7 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/distorted/checking_you_out.rb +116 -0
- data/lib/distorted/error_code.rb +43 -0
- data/lib/distorted/injection_of_love.rb +247 -0
- data/lib/distorted/modular_technology/pango.rb +20 -5
- data/lib/distorted/modular_technology/triple_counter.rb +45 -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_load.rb +77 -0
- data/lib/distorted/modular_technology/vips_save.rb +172 -0
- data/lib/distorted/molecule/C18H27NO3.rb +10 -0
- data/lib/distorted/{font.rb → molecule/font.rb} +33 -27
- data/lib/distorted/molecule/image.rb +36 -0
- data/lib/distorted/{pdf.rb → molecule/pdf.rb} +23 -14
- data/lib/distorted/molecule/svg.rb +60 -0
- data/lib/distorted/{text.rb → molecule/text.rb} +70 -86
- data/lib/distorted/{video.rb → molecule/video.rb} +9 -7
- data/lib/distorted/types/README +4 -0
- data/lib/distorted/types/application.yaml +8 -0
- data/lib/distorted/types/font.yaml +29 -0
- data/lib/distorted/version.rb +3 -1
- metadata +33 -8
- data/lib/distorted/image.rb +0 -121
- data/lib/distorted/svg.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a96a3b49df5a194c2d18bae2a71d208e02aaad05c6af3eb5399c0f5c87167bd0
|
4
|
+
data.tar.gz: 7260d18a57ab0a14008bb92712c9dec322c5be4469329a656d5ef287c09e52bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e290cb8d83991ad9391bf3cfd1c495142692af34b3ff424da78be54439e85f38507612df20814d5cb98e935db20ea970095a6890ca3291a6d3e0e6805948d928
|
7
|
+
data.tar.gz: a5388b7db8e91e16ad74080295882ad9bf106257c062ad0b54fdd60813c6300073ec25a1c031f8356c0f4b46dae3150e3bbf14a9c32466faaa848b2c8b10e445
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'mime/types'
|
4
|
+
require 'ruby-filemagic'
|
5
|
+
|
6
|
+
module MIME
|
7
|
+
class Type
|
8
|
+
# Give MIME::Type objects an easy way to get the DistorteD saver method name.
|
9
|
+
def distorted_method
|
10
|
+
# Standardize MIME::Types' media_type+sub_type to DistorteD method mapping
|
11
|
+
# by replacing all the combining characters with underscores (snake case)
|
12
|
+
# to match Ruby conventions:
|
13
|
+
# https://rubystyle.guide/#snake-case-symbols-methods-vars
|
14
|
+
#
|
15
|
+
# For the worst possible example, an intended outout Type of
|
16
|
+
# "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
17
|
+
# (a.k.a. a MSWord `docx` file) would map to a DistorteD saver method
|
18
|
+
# :to_application_vnd_openxmlformats_officedocument_wordprocessingml_document
|
19
|
+
# which would most likely be defined by the :included method of a library-specific
|
20
|
+
# module for handling OpenXML MS Office documents.
|
21
|
+
"to_#{self.media_type}_#{self.sub_type.gsub(/[-+\.]/, '_'.freeze)}".to_sym
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module CHECKING
|
27
|
+
class YOU
|
28
|
+
|
29
|
+
# Returns a Set of MIME::Type for a given file path, by default only
|
30
|
+
# based on the file extension. If the file extension is unavailable—
|
31
|
+
# or if `so_deep` is enabled—the `path` will be used as an actual
|
32
|
+
# path to look at the magic bytes with ruby-filemagic.
|
33
|
+
def self.OUT(path, so_deep: false)
|
34
|
+
unless so_deep || types.type_for(path).empty?
|
35
|
+
# NOTE: `type_for`'s return order is supposed to be deterministic:
|
36
|
+
# https://github.com/mime-types/ruby-mime-types/issues/148
|
37
|
+
# My use case so far has never required order but has required
|
38
|
+
# many Set comparisons, so I am going to return a Set here
|
39
|
+
# and possibly throw the order away.
|
40
|
+
# In my experience the order is usually preserved anyway:
|
41
|
+
# irb(main)> MIME::Types.type_for(File.expand_path('lol.ttf'))
|
42
|
+
# => [#<MIME::Type: font/ttf>, #<MIME::Type: application/font-sfnt>, #<MIME::Type: application/x-font-truetype>, #<MIME::Type: application/x-font-ttf>]
|
43
|
+
# irb(main)> MIME::Types.type_for('lol.ttf')).to_set
|
44
|
+
# => #<Set: {#<MIME::Type: font/ttf>, #<MIME::Type: application/font-sfnt>, #<MIME::Type: application/x-font-truetype>, #<MIME::Type: application/x-font-ttf>}>
|
45
|
+
return types.type_for(path).to_set
|
46
|
+
else
|
47
|
+
# Did we fail to guess any MIME::Types from the given filename?
|
48
|
+
# We're going to have to look at the actual file
|
49
|
+
# (or at least its first four bytes).
|
50
|
+
FileMagic.open(:mime) do |fm|
|
51
|
+
# The second argument makes fm.file return just the simple
|
52
|
+
# MIME::Type String, e.g.:
|
53
|
+
#
|
54
|
+
# irb(main)> fm.file('/home/okeeblow/IIDX-turntable.svg')
|
55
|
+
# => "image/svg+xml; charset=us-ascii"
|
56
|
+
# irb(main)> fm.file('/home/okeeblow/IIDX-turntable.svg', true)
|
57
|
+
# => "image/svg"
|
58
|
+
#
|
59
|
+
# However MIME::Types won't take short variants like 'image/svg',
|
60
|
+
# so explicitly have FM return long types and split it ourself
|
61
|
+
# on the semicolon:
|
62
|
+
#
|
63
|
+
# irb(main)> "image/svg+xml; charset=us-ascii".split(';').first
|
64
|
+
# => "image/svg+xml"
|
65
|
+
mime = types[fm.file(path, false).split(';'.freeze).first].to_set
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns a Set of MIME::Type objects matching a String search key of the
|
71
|
+
# format MEDIA_TYPE/SUB_TYPE.
|
72
|
+
# This can return multiple Types, e.g. 'font/collection' TTC/OTC variations:
|
73
|
+
# [#<MIME::Type: font/collection>, #<MIME::Type: font/collection>]
|
74
|
+
def self.IN(type)
|
75
|
+
types[type, :complete => type.is_a?(Regexp)].to_set
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the MIME::Types container or loads one
|
79
|
+
def self.types
|
80
|
+
@@types ||= types_loader
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns a loaded MIME::Types container containing both the upstream
|
84
|
+
# mime-types-data and our own local data.
|
85
|
+
def self.types_loader
|
86
|
+
container = MIME::Types.new
|
87
|
+
|
88
|
+
# Load the upstream mime-types-data by providing a nil `path`:
|
89
|
+
# path || ENV['RUBY_MIME_TYPES_DATA'] || MIME::Types::Data::PATH
|
90
|
+
loader = MIME::Types::Loader.new(nil, container)
|
91
|
+
loader.load_columnar
|
92
|
+
|
93
|
+
# Change default JPEG file extension from .jpeg to .jpg
|
94
|
+
# because it pisses me off lol
|
95
|
+
container['image/jpeg'].last.preferred_extension = 'jpg'
|
96
|
+
|
97
|
+
# Override the loader's path with the path to our local data directory
|
98
|
+
# after we've loaded the upstream data.
|
99
|
+
# :@path is set up in Loader::initialize and only has an attr_reader
|
100
|
+
# but we can reach in and change it.
|
101
|
+
loader.instance_variable_set(:@path, File.join(__dir__, 'types'.freeze))
|
102
|
+
|
103
|
+
# Load our local types data. The YAML files are separated by type,
|
104
|
+
# and :load_yaml will load all of them in the :@path we just set.
|
105
|
+
# MAYBE: Integrate MIME::Types YAML conversion scripts and commit
|
106
|
+
# JSON/Columnar artifacts for SPEEEEEED, but YAML is probably fine
|
107
|
+
# since we will have so few custom types compared to upstream.
|
108
|
+
# Convert.from_yaml_to_json
|
109
|
+
# Convert::Columnar.from_yaml_to_columnar
|
110
|
+
loader.load_yaml
|
111
|
+
|
112
|
+
container
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
data/lib/distorted/error_code.rb
CHANGED
@@ -6,3 +6,46 @@
|
|
6
6
|
# the library add new exception subclasses."
|
7
7
|
class StandardDistorteDError < StandardError
|
8
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,247 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'distorted/monkey_business/set'
|
3
|
+
|
4
|
+
|
5
|
+
# This Module supports Module "Piles"* in DistorteD by merging
|
6
|
+
# an arbitrarily-deep nest of attribute-definition constants
|
7
|
+
# into a single combined datastructure per constant at include-/
|
8
|
+
# extend-/prepend-time.
|
9
|
+
# [*] 'Monad' doesn't feel quite right, but http://www.geekculture.com/joyoftech/joyimages/469.gif
|
10
|
+
#
|
11
|
+
# The combined structures can be accessed in one shot and trusted as
|
12
|
+
# a source of truth, versus inspecting Module::nesting or whatever
|
13
|
+
# to iterate over the masked same-Symbol constants we'd get when
|
14
|
+
# including/extending/prepending in Ruby normally:
|
15
|
+
# - https://ruby-doc.org/core/Module.html#method-c-nesting
|
16
|
+
# - http://valve.github.io/blog/2013/10/26/constant-resolution-in-ruby/
|
17
|
+
|
18
|
+
# There's some general redundancy here with Bundler's const_get_safely:
|
19
|
+
# https://ruby-doc.org/stdlib/libdoc/bundler/rdoc/Bundler/SharedHelpers.html#method-i-const_get_safely
|
20
|
+
#
|
21
|
+
# …but even though I use (and enjoy using) Bundler it feels Wrong™ to me to have
|
22
|
+
# that method in stdlib and especially in Core but not as part of Module since
|
23
|
+
# 'Bundler' still feels like a third-party namespace to me v(._. )v
|
24
|
+
|
25
|
+
|
26
|
+
module Cooltrainer; end
|
27
|
+
module Cooltrainer::DistorteD; end
|
28
|
+
module Cooltrainer::DistorteD::InjectionOfLove
|
29
|
+
|
30
|
+
# These hold (possibly-runtime-generated) Sets of MIME::Types (from our loader)*
|
31
|
+
# describing any supported input media-types (:LOWER_WORLD)
|
32
|
+
# and any supported output media-types (:OUTER_LIMITS).
|
33
|
+
TYPE_CONSTANTS = Set[
|
34
|
+
:LOWER_WORLD,
|
35
|
+
:OUTER_LIMITS,
|
36
|
+
]
|
37
|
+
# These hold Hashes or Sets describing supported attributes,
|
38
|
+
# supported attribute-values (otherwise freeform),
|
39
|
+
# attribute defaults if any, and mappings for any of those things
|
40
|
+
# to any aliased equivalents for normalization and localization.
|
41
|
+
ATTRIBUTE_CONSTANTS = Set[
|
42
|
+
:ATTRIBUTES,
|
43
|
+
:ATTRIBUTES_DEFAULT,
|
44
|
+
:ATTRIBUTES_VALUES,
|
45
|
+
]
|
46
|
+
# 🄒 All of the above.
|
47
|
+
DISTORTED_CONSTANTS = Set[].merge(TYPE_CONSTANTS).merge(ATTRIBUTE_CONSTANTS)
|
48
|
+
|
49
|
+
# Name of our fully-merged-Hash's class variable.
|
50
|
+
AFTERPARTY = :@@DistorteD
|
51
|
+
|
52
|
+
|
53
|
+
# Activate this module when it's included.
|
54
|
+
# We will merge DistorteD attributes to the singleton class from
|
55
|
+
# our including context and from out including context's included_modules,
|
56
|
+
# then we will define methods in the including context to perpetuate
|
57
|
+
# the merging process when that context is included/extended/prepended.
|
58
|
+
def self.included(otra)
|
59
|
+
self::Injection_Of_Love.call(otra)
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
# "Attribute" fragments are processed in one additional step to support
|
64
|
+
# aliased/equivalent attribute names and values.
|
65
|
+
# This is a quality-of-life feature to help normalize/localize attributes
|
66
|
+
# defined in a wide range of places by multiple unrelated upstream devs.
|
67
|
+
#
|
68
|
+
# For example, libvips savers expect a single-character upper-case
|
69
|
+
# `Q` argument for their 1–100 integer quality factor,
|
70
|
+
# and my VipsSave module's `:ATTRIBUTES` additionally aliases it
|
71
|
+
# to the more typical `quality` to provide consistent UX
|
72
|
+
# with other attributes from VIPS and other sources.
|
73
|
+
# https://libvips.github.io/libvips/API/current/VipsForeignSave.html#vips-jpegsave
|
74
|
+
#
|
75
|
+
# VIPS also provides our example of the need for attribute-value equivalents,
|
76
|
+
# such as how it only accepts the spelling of "centre" and not "center"
|
77
|
+
# like myself and many millions of other people will reflexively enter :)
|
78
|
+
# https://libvips.github.io/libvips/API/current/libvips-conversion.html#VipsInteresting
|
79
|
+
def self.so_deep(fragment)
|
80
|
+
fragment.each_with_object(Array[]) { |(attribute, raw), to_merge|
|
81
|
+
# Each attribute's :raw may be an object (probably a Symbol),
|
82
|
+
# a Set (e.g. of aliases), or nil (for all values in a Set.to_hash)
|
83
|
+
case raw
|
84
|
+
when Set then raw.add(attribute)
|
85
|
+
when NilClass then [attribute]
|
86
|
+
else [attribute]
|
87
|
+
end.each{ |equivalent|
|
88
|
+
to_merge << [equivalent, attribute]
|
89
|
+
}
|
90
|
+
}.to_h
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns a block that will define methods in a given context
|
94
|
+
# such that when the given context is included/extended/prepended
|
95
|
+
# we will also merge our DD attributes into the new layer.
|
96
|
+
Injection_Of_Love = Proc.new { |otra|
|
97
|
+
# These are the methods that actively perform the include/extend/prepend process.
|
98
|
+
[:append_features, :prepend_features, :extend_object].each { |m|
|
99
|
+
otra.define_singleton_method(m) do |winter|
|
100
|
+
# Perform the normal include/extend/prepend that will mask our constants.
|
101
|
+
super(winter)
|
102
|
+
|
103
|
+
# Get new values to override masked constants.
|
104
|
+
pile = Cooltrainer::DistorteD::InjectionOfLove::trip_machine(winter)
|
105
|
+
|
106
|
+
# Record each constant individually as well as the entire pile.
|
107
|
+
# This doesn't currently get used, as this :class_variable_set call
|
108
|
+
# is broken in KRI:
|
109
|
+
# - https://bugs.ruby-lang.org/issues/7475
|
110
|
+
# - https://bugs.ruby-lang.org/issues/8297
|
111
|
+
# - https://bugs.ruby-lang.org/issues/11022
|
112
|
+
winter.singleton_class.class_variable_set(AFTERPARTY, pile)
|
113
|
+
pile.each_pair{ |k, v|
|
114
|
+
if winter.singleton_class.const_defined?(k, false)
|
115
|
+
# Since we are setting constants in the singleton_class
|
116
|
+
# we must remove any old ones first to avoid a warning.
|
117
|
+
winter.singleton_class.send(:remove_const, k)
|
118
|
+
end
|
119
|
+
winter.singleton_class.const_set(k, v)
|
120
|
+
}
|
121
|
+
end
|
122
|
+
}
|
123
|
+
# These are the callback methods called after the above methods fire.
|
124
|
+
# Use them to perpetuate our merge by calling the thing that calls us :)
|
125
|
+
[:included, :prepended, :extended].each { |m|
|
126
|
+
otra.define_singleton_method(m) do |winter|
|
127
|
+
Cooltrainer::DistorteD::InjectionOfLove::Injection_Of_Love.call(winter)
|
128
|
+
super(winter)
|
129
|
+
end
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
# Returns an instance-level copy of the complete attribute pile.
|
134
|
+
def trip_machine
|
135
|
+
@DistorteD ||= Cooltrainer::DistorteD::InjectionOfLove::trip_machine(self.singleton_class)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Builds the attribute pile (e.g. suported atrs, values, defaults, etc) for any given scope.
|
139
|
+
def self.trip_machine(scope)
|
140
|
+
attribute_aliases = Hash[]
|
141
|
+
alias_attributes = Hash[]
|
142
|
+
values = Hash[]
|
143
|
+
defaults = Hash[]
|
144
|
+
|
145
|
+
scope&.ancestors.each { |otra|
|
146
|
+
DISTORTED_CONSTANTS.each { |invitation| # OUT OF CONTROL / MY WHEELS IN CONSTANT MOTION
|
147
|
+
if otra.const_defined?(invitation)
|
148
|
+
part = otra.const_get(invitation) rescue Hash[]
|
149
|
+
|
150
|
+
if invitation == :ATTRIBUTES
|
151
|
+
# Support both alias-to-attribute and attribute-to-aliases
|
152
|
+
attribute_aliases.merge!(part) { |invitation, old, new|
|
153
|
+
if old.nil?
|
154
|
+
new.nil? ? Set[invitation] : Set[new]
|
155
|
+
elsif new.nil?
|
156
|
+
old
|
157
|
+
elsif new.is_a?(Enumerable)
|
158
|
+
old.merge(new)
|
159
|
+
else
|
160
|
+
old << new
|
161
|
+
end
|
162
|
+
}
|
163
|
+
alias_attributes.merge!(Cooltrainer::DistorteD::InjectionOfLove::so_deep(part))
|
164
|
+
elsif invitation == :ATTRIBUTES_VALUES
|
165
|
+
# Regexes currently override Enumerables
|
166
|
+
to_merge = {}
|
167
|
+
part.each_pair { |attribute, values|
|
168
|
+
if values.is_a?(Regexp)
|
169
|
+
to_merge.update(attribute => values)
|
170
|
+
else
|
171
|
+
to_merge.update(attribute => Cooltrainer::DistorteD::InjectionOfLove::so_deep(values))
|
172
|
+
end
|
173
|
+
}
|
174
|
+
values.merge!(to_merge)
|
175
|
+
elsif invitation == :ATTRIBUTES_DEFAULT
|
176
|
+
defaults.merge!(part)
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
return {
|
184
|
+
:ATTRIBUTE_ALIASES => attribute_aliases,
|
185
|
+
:ALIAS_ATTRIBUTES => alias_attributes,
|
186
|
+
:ATTRIBUTES_VALUES => values,
|
187
|
+
:ATTRIBUTES_DEFAULT => defaults,
|
188
|
+
}
|
189
|
+
end
|
190
|
+
|
191
|
+
# Returns a value for any attribute.
|
192
|
+
# In order of priority, that means:
|
193
|
+
# - A user-given value (Liquid, CLI, etc) iff it passes a validity check,
|
194
|
+
# - the default value if the given value is not in the accepted Set,
|
195
|
+
# - nil for unset attributes with no default defined.
|
196
|
+
def abstract(argument)
|
197
|
+
# Reject any unknown arguments.
|
198
|
+
if trip_machine.dig(:ATTRIBUTE_ALIASES)&.keys.include?(argument)
|
199
|
+
alias_possibilities = trip_machine.dig(:ATTRIBUTE_ALIASES)&.dig(argument) || Set[]
|
200
|
+
possibilities = user_arguments&.keys.to_set & alias_possibilities
|
201
|
+
|
202
|
+
# How many matching user-defined attributes are there for our aliases?
|
203
|
+
case possibilities.length
|
204
|
+
when 0
|
205
|
+
# None; take the default.
|
206
|
+
trip_machine.dig(:ATTRIBUTES_DEFAULT)&.dig(argument)
|
207
|
+
when 1
|
208
|
+
# One; does it look valid?
|
209
|
+
is_valid = false
|
210
|
+
user_value = user_arguments&.dig(argument)
|
211
|
+
|
212
|
+
# Supported values may be declared as:
|
213
|
+
# - A Hash of values-and-their-aliases to values.
|
214
|
+
# - A Regex.
|
215
|
+
# - nil for freeform input.
|
216
|
+
valid_value = trip_machine.dig(:ATTRIBUTES_VALUES)&.dig(argument)
|
217
|
+
if valid_value.is_a?(Enumerable)
|
218
|
+
if valid_value.include?(user_value)
|
219
|
+
is_valid = true
|
220
|
+
end
|
221
|
+
elsif valid_value.is_a?(Regexp)
|
222
|
+
if valid_value.match(user_value.to_s)
|
223
|
+
is_valid = true
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Return a valid user value, a default, or nil if all else fails.
|
228
|
+
if is_valid
|
229
|
+
# TODO: boolean casting
|
230
|
+
user_value
|
231
|
+
else
|
232
|
+
trip_machine.dig(:ATTRIBUTES_DEFAULT)&.dig(argument)
|
233
|
+
end
|
234
|
+
|
235
|
+
else # case user_values.length
|
236
|
+
# Two or more; what do??
|
237
|
+
raise RuntimeError("Can't have multiple settings for #{argument} and its aliases.")
|
238
|
+
end
|
239
|
+
else
|
240
|
+
# The programmer asked for the value of an attribute that is
|
241
|
+
# not supported by its MediaMolecule. This is most likely a bug.
|
242
|
+
raise RuntimeError("#{argument} is not supported for #{@name}")
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Cooltrainer
|
2
2
|
module DistorteD
|
3
|
-
module
|
3
|
+
module Technology
|
4
4
|
module Pango
|
5
5
|
|
6
6
|
# Escape text as necessary for Pango Markup, which is what Vips::Image.text()
|
@@ -38,12 +38,21 @@ module Cooltrainer
|
|
38
38
|
cr << lf
|
39
39
|
end
|
40
40
|
|
41
|
+
# "Modified UTF-8" uses a normally-illegal byte sequence
|
42
|
+
# to encode the NULL character so 0x00 can exclusively
|
43
|
+
# be a string terminator.
|
44
|
+
def overlong_null
|
45
|
+
[0xC0, 0x80].pack('C*').force_encoding('UTF-8')
|
46
|
+
end
|
47
|
+
|
41
48
|
# The char-by-char actual function used by g_markup_escape_text
|
42
49
|
def g_markup_escape_char(c)
|
43
50
|
# I think a fully-working version of this function would
|
44
|
-
# be as simple `sprintf('&#x%x;', c.ord)
|
45
|
-
#
|
46
|
-
#
|
51
|
+
# be as simple as `sprintf('&#x%x;', c.ord)` ALL THE THINGS,
|
52
|
+
# but I want to copy the structure of the C implementation
|
53
|
+
# as closely as possible, which means using the named escape
|
54
|
+
# sequences for common characters and separating the
|
55
|
+
# Latin-1 Supplement range from the other
|
47
56
|
# the Unicode control characters (> 0x7f) even though three's no
|
48
57
|
# need to in Ruby.
|
49
58
|
case c.ord
|
@@ -59,7 +68,13 @@ module Cooltrainer
|
|
59
68
|
'"'
|
60
69
|
when 0x1..0x8, 0xb..0xc, 0xe..0x1f, 0x7f
|
61
70
|
sprintf('&#x%x;', c.ord)
|
62
|
-
when
|
71
|
+
when 0x80..0x84, 0x86..0x9f
|
72
|
+
# The original C implementation separates this range
|
73
|
+
# from the above range due to its need to handle the
|
74
|
+
# UTF control character bytes with gunichar:
|
75
|
+
# https://wiki.tcl-lang.org/page/UTF%2D8+bit+by+bit
|
76
|
+
# https://www.fileformat.info/info/unicode/utf8.htm
|
77
|
+
# Ruby has already done this for us here :)
|
63
78
|
sprintf('&#x%x;', c.ord)
|
64
79
|
when 0x0 # what's this…?
|
65
80
|
# Avoid a `ArgumentError: string contains null byte`
|