distorted 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +661 -0
  3. data/README.md +4 -139
  4. data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/Less_Perfect_DOS_VGA.png +0 -0
  5. data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/More_Perfect_DOS_VGA.png +0 -0
  6. data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/img/Perfect_DOS_VGA.png +0 -0
  7. data/font/1252/LICENSE/MoreLessPerfectDOSVGA437/less_more_perfect_dos_vga_437.html +52 -0
  8. data/font/1252/LICENSE/PerfectDOSVGA437/font-comment.php@file=perfect_dos_vga_437.html +5 -0
  9. data/font/1252/LessPerfectDOSVGA.ttf +0 -0
  10. data/font/1252/MorePerfectDOSVGA.ttf +0 -0
  11. data/font/1252/Perfect DOS VGA 437 Win.ttf +0 -0
  12. data/font/437/Perfect DOS VGA 437.ttf +0 -0
  13. data/font/437/dos437.txt +72 -0
  14. data/font/65001/Anonymous Pro B.ttf +0 -0
  15. data/font/65001/Anonymous Pro BI.ttf +0 -0
  16. data/font/65001/Anonymous Pro I.ttf +0 -0
  17. data/font/65001/Anonymous Pro.ttf +0 -0
  18. data/font/65001/LICENSE/AnonymousPro/FONTLOG.txt +45 -0
  19. data/font/65001/LICENSE/AnonymousPro/OFL-FAQ.txt +235 -0
  20. data/font/65001/LICENSE/AnonymousPro/OFL.txt +94 -0
  21. data/font/65001/LICENSE/AnonymousPro/README.txt +55 -0
  22. data/font/850/ProFont-Bold-01/LICENSE +22 -0
  23. data/font/850/ProFont-Bold-01/readme.txt +28 -0
  24. data/font/850/ProFontWindows-Bold.ttf +0 -0
  25. data/font/850/ProFontWindows.ttf +0 -0
  26. data/font/850/Profont/LICENSE +22 -0
  27. data/font/850/Profont/readme.txt +31 -0
  28. data/font/932/LICENSE/README-ttf.txt +213 -0
  29. data/font/932/mona.ttf +0 -0
  30. data/lib/distorted/checking_you_out.rb +116 -0
  31. data/lib/distorted/error_code.rb +51 -0
  32. data/lib/distorted/injection_of_love.rb +247 -0
  33. data/lib/distorted/modular_technology/pango.rb +90 -0
  34. data/lib/distorted/modular_technology/triple_counter.rb +45 -0
  35. data/lib/distorted/modular_technology/ttfunk.rb +48 -0
  36. data/lib/distorted/modular_technology/vips.rb +17 -0
  37. data/lib/distorted/modular_technology/vips_load.rb +77 -0
  38. data/lib/distorted/modular_technology/vips_save.rb +172 -0
  39. data/lib/distorted/molecule/C18H27NO3.rb +10 -0
  40. data/lib/distorted/molecule/font.rb +198 -0
  41. data/lib/distorted/molecule/image.rb +36 -0
  42. data/lib/distorted/molecule/pdf.rb +119 -0
  43. data/lib/distorted/molecule/svg.rb +60 -0
  44. data/lib/distorted/molecule/text.rb +225 -0
  45. data/lib/distorted/molecule/video.rb +195 -0
  46. data/lib/distorted/monkey_business/hash.rb +33 -0
  47. data/lib/distorted/monkey_business/mnemoniq.rb +8 -0
  48. data/lib/distorted/monkey_business/set.rb +15 -0
  49. data/lib/distorted/monkey_business/string.rb +6 -0
  50. data/lib/distorted/types/README +4 -0
  51. data/lib/distorted/types/application.yaml +8 -0
  52. data/lib/distorted/types/font.yaml +29 -0
  53. data/lib/distorted/version.rb +22 -0
  54. data/test/distorted_test.rb +11 -0
  55. data/test/test_helper.rb +4 -0
  56. metadata +102 -5
Binary file
@@ -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
@@ -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,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
@@ -0,0 +1,90 @@
1
+ module Cooltrainer
2
+ module DistorteD
3
+ module Technology
4
+ module Pango
5
+
6
+ # Escape text as necessary for Pango Markup, which is what Vips::Image.text()
7
+ # expects for its argv. This code should be in GLib but is unimplemented in Ruby's:
8
+ #
9
+ # https://ruby-gnome2.osdn.jp/hiki.cgi?Gtk%3A%3ALabel#Markup+%28styled+text%29
10
+ # "The markup passed to Gtk::Label#set_markup() must be valid; for example,
11
+ # literal </>/& characters must be escaped as &lt;, &gt;, and &amp;.
12
+ # If you pass text obtained from the user, file, or a network to
13
+ # Gtk::Label#set_markup(), you'll want to escape it
14
+ # with GLib::Markup.escape_text?(not implemented yet)."
15
+ #
16
+ # Base my own implementation on the original C version found in gmarkup:
17
+ # https://gitlab.gnome.org/GNOME/glib/-/blob/master/glib/gmarkup.c
18
+ def g_markup_escape_text(text)
19
+ text.map{ |c| g_markup_escape_char(c) }
20
+ end
21
+
22
+ # Returns a Pango-escaped Carriage Return.
23
+ # Use this for linebreaking Pango Markup output.
24
+ def cr
25
+ g_markup_escape_char(0x0D)
26
+ end
27
+
28
+ # Returns a Pango-escapped Line Feed.
29
+ # This isn't used/needed for anything with Pango
30
+ # but it felt weird to include CR and not LF lmao
31
+ def lf
32
+ g_markup_escape_char(0x0A)
33
+ end
34
+
35
+ # Returns a Pango'escaped CRLF pair.
36
+ # Also not needed for anything.
37
+ def crlf
38
+ cr << lf
39
+ end
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
+
48
+ # The char-by-char actual function used by g_markup_escape_text
49
+ def g_markup_escape_char(c)
50
+ # I think a fully-working version of this function would
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
56
+ # the Unicode control characters (> 0x7f) even though three's no
57
+ # need to in Ruby.
58
+ case c.ord
59
+ when '&'.ord
60
+ '&amp;'
61
+ when '<'.ord
62
+ '&lt;'
63
+ when '>'.ord
64
+ '&gt;'
65
+ when '\''.ord
66
+ '&apos;'
67
+ when '"'.ord
68
+ '&quot;'
69
+ when 0x1..0x8, 0xb..0xc, 0xe..0x1f, 0x7f
70
+ sprintf('&#x%x;', c.ord)
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 :)
78
+ sprintf('&#x%x;', c.ord)
79
+ when 0x0 # what's this…?
80
+ # Avoid a `ArgumentError: string contains null byte`
81
+ # by not printing one :)
82
+ else
83
+ c
84
+ end
85
+ end
86
+
87
+ end # Pango
88
+ end # Tech
89
+ end # DistorteD
90
+ end # Cooltrainer