nanaimo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,33 @@
1
+ module Nanaimo
2
+ # A Plist.
3
+ #
4
+ class Plist
5
+ # @return [Nanaimo::Object] The root level object in the plist.
6
+ #
7
+ attr_accessor :root_object
8
+
9
+ # @return [String] The encoding of the plist.
10
+ #
11
+ attr_accessor :file_type
12
+
13
+ def initialize(root_object = nil, file_type = nil)
14
+ @root_object = root_object
15
+ @file_type = file_type
16
+ end
17
+
18
+ def ==(other)
19
+ return unless other.is_a?(Nanaimo::Plist)
20
+ file_type == other.file_type && root_object == other.root_object
21
+ end
22
+
23
+ def hash
24
+ root_object.hash
25
+ end
26
+
27
+ # @return A native Ruby object representation of the plist.
28
+ #
29
+ def as_ruby
30
+ root_object.as_ruby
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,252 @@
1
+ # frozen-string-literal: true
2
+ module Nanaimo
3
+ # Transforms plist strings into Plist objects.
4
+ #
5
+ class Reader
6
+ # Raised when attempting to read a plist with an unsupported file format.
7
+ #
8
+ class UnsupportedPlistFormatError < Error
9
+ # @return [Symbol] The unsupported format.
10
+ #
11
+ attr_reader :format
12
+
13
+ def initialize(format)
14
+ @format = format
15
+ end
16
+
17
+ def to_s
18
+ "#{format} plists are currently unsupported"
19
+ end
20
+ end
21
+
22
+ # Raised when parsing fails.
23
+ #
24
+ class ParseError < Error
25
+ # @return [[Integer, Integer]] The (line, column) offset into the plist
26
+ # where the error occurred
27
+ #
28
+ attr_accessor :location
29
+
30
+ # @return [String] The contents of the plist.
31
+ #
32
+ attr_accessor :plist_string
33
+ end
34
+
35
+ # @param plist_contents [String]
36
+ #
37
+ # @return [Symbol] The file format of the plist in the given string.
38
+ #
39
+ def self.plist_type(plist_contents)
40
+ case plist_contents
41
+ when /\Abplist/
42
+ :binary
43
+ when /\A<\?xml/
44
+ :xml
45
+ else
46
+ :ascii
47
+ end
48
+ end
49
+
50
+ # @param file_path [String]
51
+ #
52
+ # @return [Plist] A parsed plist from the given file
53
+ #
54
+ def self.from_file(file_path)
55
+ new(File.read(file_path))
56
+ end
57
+
58
+ # @param contents [String] The plist to be parsed
59
+ #
60
+ def initialize(contents)
61
+ @scanner = StringScanner.new(contents)
62
+ end
63
+
64
+ # Parses the contents of the plist
65
+ #
66
+ # @return [Plist] The parsed Plist object.
67
+ #
68
+ def parse!
69
+ plist_format = ensure_ascii_plist!
70
+ read_string_encoding
71
+ root_object = parse_object
72
+
73
+ eat_whitespace!
74
+ raise_parser_error ParseError, "unrecognized characters #{@scanner.rest.inspect} after parsing" unless @scanner.eos?
75
+
76
+ Nanaimo::Plist.new(root_object, plist_format)
77
+ end
78
+
79
+ private
80
+
81
+ def ensure_ascii_plist!
82
+ self.class.plist_type(@scanner.string).tap do |plist_format|
83
+ raise UnsupportedPlistFormatError, plist_format unless plist_format == :ascii
84
+ end
85
+ end
86
+
87
+ def read_string_encoding
88
+ # TODO
89
+ end
90
+
91
+ def parse_object
92
+ _comment = skip_to_non_space_matching_annotations
93
+ start_pos = @scanner.pos
94
+ raise_parser_error ParseError, 'Unexpected eos while parsing' if @scanner.eos?
95
+ if @scanner.skip(/\{/)
96
+ parse_dictionary
97
+ elsif @scanner.skip(/\(/)
98
+ parse_array
99
+ elsif @scanner.skip(/</)
100
+ parse_data
101
+ elsif quote = @scanner.scan(/['"]/)
102
+ parse_quotedstring(quote)
103
+ else
104
+ parse_string
105
+ end.tap do |o|
106
+ o.annotation = skip_to_non_space_matching_annotations
107
+ Nanaimo.debug { "parsed #{o.inspect} from #{start_pos}..#{@scanner.pos}" }
108
+ end
109
+ end
110
+
111
+ def parse_string
112
+ eat_whitespace!
113
+ unless match = @scanner.scan(%r{[\w/.]+})
114
+ raise_parser_error ParseError, "not a valid string at index #{@scanner.pos} (char is #{current_character.inspect})"
115
+ end
116
+ Nanaimo::String.new(match, nil)
117
+ end
118
+
119
+ def parse_quotedstring(quote)
120
+ unless string = @scanner.scan(/(?:([^#{quote}\\]|\\.)*)#{quote}/)
121
+ raise_parser_error ParseError, "unterminated quoted string started at #{@scanner.pos}, expected #{quote} but never found it"
122
+ end
123
+ string = Unicode.unquotify_string(string.chomp!(quote))
124
+ Nanaimo::QuotedString.new(string, nil)
125
+ end
126
+
127
+ def parse_array
128
+ objects = []
129
+ until @scanner.eos?
130
+ eat_whitespace!
131
+ break if @scanner.skip(/\)/)
132
+
133
+ objects << parse_object
134
+
135
+ eat_whitespace!
136
+ break if @scanner.skip(/\)/)
137
+ unless @scanner.skip(/,/)
138
+ raise_parser_error ParseError, "Array #{objects} missing ',' in between objects"
139
+ end
140
+ end
141
+
142
+ Nanaimo::Array.new(objects, nil)
143
+ end
144
+
145
+ def parse_dictionary
146
+ objects = {}
147
+ until @scanner.eos?
148
+ skip_to_non_space_matching_annotations
149
+ break if @scanner.skip(/}/)
150
+
151
+ key = parse_object
152
+ eat_whitespace!
153
+ unless @scanner.skip(/=/)
154
+ raise_parser_error ParseError, "Dictionary missing value after key #{key.inspect} at index #{@scanner.pos}, expected '=' and got #{current_character.inspect}"
155
+ end
156
+
157
+ value = parse_object
158
+ objects[key] = value
159
+
160
+ eat_whitespace!
161
+ break if @scanner.skip(/}/)
162
+ unless @scanner.skip(/;/)
163
+ raise_parser_error ParseError, "Dictionary (#{objects}) missing ';' after key-value pair (#{key} = #{value}) at index #{@scanner.pos} (got #{current_character})"
164
+ end
165
+ end
166
+
167
+ Nanaimo::Dictionary.new(objects, nil)
168
+ end
169
+
170
+ def parse_data
171
+ unless data = @scanner.scan(/[\h ]*>/)
172
+ raise_parser_error ParseError, "Data missing closing '>'"
173
+ end
174
+ data.chomp!('>')
175
+ data.delete!(' ')
176
+ unless data.size.even?
177
+ @scanner.unscan
178
+ raise_parser_error ParseError, 'Data has an uneven number of hex digits'
179
+ end
180
+ data = [data].pack('H*')
181
+ Nanaimo::Data.new(data, nil)
182
+ end
183
+
184
+ def current_character
185
+ @scanner.peek(1)
186
+ end
187
+
188
+ def read_singleline_comment
189
+ unless comment = @scanner.scan_until(NEWLINE)
190
+ raise_parser_error ParseError, "failed to terminate single line comment #{@scanner.rest.inspect}"
191
+ end
192
+ comment
193
+ end
194
+
195
+ def eat_whitespace!
196
+ @scanner.skip(MANY_WHITESPACES)
197
+ end
198
+
199
+ NEWLINE_CHARACTERS = %W(\x0A \x0D \u2028 \u2029).freeze
200
+ NEWLINE = Regexp.union(*NEWLINE_CHARACTERS)
201
+
202
+ WHITESPACE_CHARACTERS = NEWLINE_CHARACTERS + %W(\x09 \x0B \x0C \x20)
203
+ WHITESPACE = Regexp.union(*WHITESPACE_CHARACTERS)
204
+
205
+ MANY_WHITESPACES = /#{WHITESPACE}+/
206
+
207
+ def read_multiline_comment
208
+ unless annotation = @scanner.scan(%r{(?:.+?)(?=\*/)}m)
209
+ raise_parser_error ParseError, "#{@scanner.rest.inspect} failed to terminate multiline comment"
210
+ end
211
+ @scanner.skip(%r{\*/})
212
+
213
+ annotation
214
+ end
215
+
216
+ def skip_to_non_space_matching_annotations
217
+ annotation = ''.freeze
218
+ until @scanner.eos?
219
+ eat_whitespace!
220
+
221
+ # Comment Detection
222
+ if @scanner.skip(%r{//})
223
+ annotation = read_singleline_comment
224
+ next
225
+ elsif @scanner.skip(%r{/\*})
226
+ annotation = read_multiline_comment
227
+ next
228
+ end
229
+
230
+ eat_whitespace!
231
+
232
+ break
233
+ end
234
+ annotation
235
+ end
236
+
237
+ def location_in(scanner)
238
+ pos = scanner.charpos
239
+ line = scanner.string[0..scanner.charpos].scan(NEWLINE).size + 1
240
+ column = pos - (scanner.string.rindex(NEWLINE, pos - 1) || -1)
241
+ [line, column]
242
+ end
243
+
244
+ def raise_parser_error(klass, message)
245
+ exception = klass.new(message).tap do |error|
246
+ error.location = location_in(@scanner)
247
+ error.plist_string = @scanner.string
248
+ end
249
+ raise(exception)
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,88 @@
1
+ # frozen-string-literal: true
2
+ require 'nanaimo/unicode/next_step_mapping'
3
+ require 'nanaimo/unicode/quote_maps'
4
+ module Nanaimo
5
+ # @!visibility private
6
+ #
7
+ module Unicode
8
+ class UnsupportedEscapeSequenceError < Error; end
9
+ class InvalidEscapeSequenceError < Error; end
10
+
11
+ module_function
12
+
13
+ def quotify_string(string)
14
+ string.gsub(QUOTE_REGEXP) { |s| QUOTE_MAP[s] }
15
+ end
16
+
17
+ ESCAPE_PREFIXES = %W(
18
+ 0 1 2 3 4 5 6 7 a b f n r t v \n U
19
+ ).freeze
20
+
21
+ OCTAL_DIGITS = (0..7).map(&:to_s).freeze
22
+
23
+ # Credit to Samantha Marshall
24
+ # Taken from https://github.com/samdmarshall/pbPlist/blob/346c29f91f913d35d0e24f6722ec19edb24e5707/pbPlist/StrParse.py#L197
25
+ # Licensed under https://raw.githubusercontent.com/samdmarshall/pbPlist/blob/346c29f91f913d35d0e24f6722ec19edb24e5707/LICENSE
26
+ #
27
+ # Originally from: http://www.opensource.apple.com/source/CF/CF-744.19/CFOldStylePList.c See `getSlashedChar()`
28
+ def unquotify_string(string)
29
+ formatted_string = ::String.new
30
+ extracted_string = string
31
+ string_length = string.size
32
+ index = 0
33
+ while index < string_length
34
+ if escape_index = extracted_string.index('\\', index)
35
+ formatted_string << extracted_string[index..escape_index - 1] unless index == escape_index
36
+ index = escape_index + 1
37
+ next_char = extracted_string[index]
38
+ if ESCAPE_PREFIXES.include?(next_char)
39
+ index += 1
40
+ if unquoted = UNQUOTE_MAP[next_char]
41
+ formatted_string << unquoted
42
+ elsif next_char == 'U'
43
+ length = 4
44
+ unicode_numbers = extracted_string[index, length]
45
+ unless unicode_numbers =~ /\A\h{4}\z/
46
+ raise InvalidEscapeSequenceError, "Unicode '\\U' escape sequence terminated without 4 following hex characters"
47
+ end
48
+ index += length
49
+ formatted_string << [unicode_numbers.to_i(16)].pack('U')
50
+ elsif OCTAL_DIGITS.include?(next_char) # https://twitter.com/Catfish_Man/status/658014170055507968
51
+ octal_string = extracted_string[index - 1, 3]
52
+ if octal_string =~ /\A[0-7]{3}\z/
53
+ index += 2
54
+ code_point = octal_string.to_i(8)
55
+ unless code_point <= 0x80 || converted = NEXT_STEP_MAPPING[code_point]
56
+ raise InvalidEscapeSequenceError, "Invalid octal escape sequence #{octal_string}"
57
+ end
58
+ formatted_string << [converted].pack('U')
59
+ else
60
+ formatted_string << next_char
61
+ end
62
+ else
63
+ raise UnsupportedEscapeSequenceError, "Failed to handle #{next_char} which is in the list of possible escapes"
64
+ end
65
+ else
66
+ index += 1
67
+ formatted_string << next_char
68
+ end
69
+ else
70
+ formatted_string << extracted_string[index..-1]
71
+ index = string_length
72
+ end
73
+ end
74
+ formatted_string
75
+ end
76
+
77
+ XML_STRING_ESCAPES = {
78
+ '&' => '&amp;',
79
+ '<' => '&lt;',
80
+ '>' => '&gt;'
81
+ }.freeze
82
+ XML_STRING_ESCAPE_REGEXP = Regexp.union(XML_STRING_ESCAPES.keys)
83
+
84
+ def xml_escape_string(string)
85
+ string.to_s.gsub(XML_STRING_ESCAPE_REGEXP) { |m| XML_STRING_ESCAPES[m] }
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,136 @@
1
+ # frozen-string-literal: true
2
+ module Nanaimo
3
+ module Unicode
4
+ # Taken from http://ftp.unicode.org/Public/MAPPINGS/VENDORS/NEXT/NEXTSTEP.TXT
5
+ NEXT_STEP_MAPPING = {
6
+ 0x80 => 0x00a0, # NO-BREAK SPACE
7
+ 0x81 => 0x00c0, # LATIN CAPITAL LETTER A WITH GRAVE
8
+ 0x82 => 0x00c1, # LATIN CAPITAL LETTER A WITH ACUTE
9
+ 0x83 => 0x00c2, # LATIN CAPITAL LETTER A WITH CIRCUMFLEX
10
+ 0x84 => 0x00c3, # LATIN CAPITAL LETTER A WITH TILDE
11
+ 0x85 => 0x00c4, # LATIN CAPITAL LETTER A WITH DIAERESIS
12
+ 0x86 => 0x00c5, # LATIN CAPITAL LETTER A WITH RING
13
+ 0x87 => 0x00c7, # LATIN CAPITAL LETTER C WITH CEDILLA
14
+ 0x88 => 0x00c8, # LATIN CAPITAL LETTER E WITH GRAVE
15
+ 0x89 => 0x00c9, # LATIN CAPITAL LETTER E WITH ACUTE
16
+ 0x8a => 0x00ca, # LATIN CAPITAL LETTER E WITH CIRCUMFLEX
17
+ 0x8b => 0x00cb, # LATIN CAPITAL LETTER E WITH DIAERESIS
18
+ 0x8c => 0x00cc, # LATIN CAPITAL LETTER I WITH GRAVE
19
+ 0x8d => 0x00cd, # LATIN CAPITAL LETTER I WITH ACUTE
20
+ 0x8e => 0x00ce, # LATIN CAPITAL LETTER I WITH CIRCUMFLEX
21
+ 0x8f => 0x00cf, # LATIN CAPITAL LETTER I WITH DIAERESIS
22
+ 0x90 => 0x00d0, # LATIN CAPITAL LETTER ETH
23
+ 0x91 => 0x00d1, # LATIN CAPITAL LETTER N WITH TILDE
24
+ 0x92 => 0x00d2, # LATIN CAPITAL LETTER O WITH GRAVE
25
+ 0x93 => 0x00d3, # LATIN CAPITAL LETTER O WITH ACUTE
26
+ 0x94 => 0x00d4, # LATIN CAPITAL LETTER O WITH CIRCUMFLEX
27
+ 0x95 => 0x00d5, # LATIN CAPITAL LETTER O WITH TILDE
28
+ 0x96 => 0x00d6, # LATIN CAPITAL LETTER O WITH DIAERESIS
29
+ 0x97 => 0x00d9, # LATIN CAPITAL LETTER U WITH GRAVE
30
+ 0x98 => 0x00da, # LATIN CAPITAL LETTER U WITH ACUTE
31
+ 0x99 => 0x00db, # LATIN CAPITAL LETTER U WITH CIRCUMFLEX
32
+ 0x9a => 0x00dc, # LATIN CAPITAL LETTER U WITH DIAERESIS
33
+ 0x9b => 0x00dd, # LATIN CAPITAL LETTER Y WITH ACUTE
34
+ 0x9c => 0x00de, # LATIN CAPITAL LETTER THORN
35
+ 0x9d => 0x00b5, # MICRO SIGN
36
+ 0x9e => 0x00d7, # MULTIPLICATION SIGN
37
+ 0x9f => 0x00f7, # DIVISION SIGN
38
+ 0xa0 => 0x00a9, # COPYRIGHT SIGN
39
+ 0xa1 => 0x00a1, # INVERTED EXCLAMATION MARK
40
+ 0xa2 => 0x00a2, # CENT SIGN
41
+ 0xa3 => 0x00a3, # POUND SIGN
42
+ 0xa4 => 0x2044, # FRACTION SLASH
43
+ 0xa5 => 0x00a5, # YEN SIGN
44
+ 0xa6 => 0x0192, # LATIN SMALL LETTER F WITH HOOK
45
+ 0xa7 => 0x00a7, # SECTION SIGN
46
+ 0xa8 => 0x00a4, # CURRENCY SIGN
47
+ 0xa9 => 0x2019, # RIGHT SINGLE QUOTATION MARK
48
+ 0xaa => 0x201c, # LEFT DOUBLE QUOTATION MARK
49
+ 0xab => 0x00ab, # LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
50
+ 0xac => 0x2039, # SINGLE LEFT-POINTING ANGLE QUOTATION MARK
51
+ 0xad => 0x203a, # SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
52
+ 0xae => 0xfb01, # LATIN SMALL LIGATURE FI
53
+ 0xaf => 0xfb02, # LATIN SMALL LIGATURE FL
54
+ 0xb0 => 0x00ae, # REGISTERED SIGN
55
+ 0xb1 => 0x2013, # EN DASH
56
+ 0xb2 => 0x2020, # DAGGER
57
+ 0xb3 => 0x2021, # DOUBLE DAGGER
58
+ 0xb4 => 0x00b7, # MIDDLE DOT
59
+ 0xb5 => 0x00a6, # BROKEN BAR
60
+ 0xb6 => 0x00b6, # PILCROW SIGN
61
+ 0xb7 => 0x2022, # BULLET
62
+ 0xb8 => 0x201a, # SINGLE LOW-9 QUOTATION MARK
63
+ 0xb9 => 0x201e, # DOUBLE LOW-9 QUOTATION MARK
64
+ 0xba => 0x201d, # RIGHT DOUBLE QUOTATION MARK
65
+ 0xbb => 0x00bb, # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
66
+ 0xbc => 0x2026, # HORIZONTAL ELLIPSIS
67
+ 0xbd => 0x2030, # PER MILLE SIGN
68
+ 0xbe => 0x00ac, # NOT SIGN
69
+ 0xbf => 0x00bf, # INVERTED QUESTION MARK
70
+ 0xc0 => 0x00b9, # SUPERSCRIPT ONE
71
+ 0xc1 => 0x02cb, # MODIFIER LETTER GRAVE ACCENT
72
+ 0xc2 => 0x00b4, # ACUTE ACCENT
73
+ 0xc3 => 0x02c6, # MODIFIER LETTER CIRCUMFLEX ACCENT
74
+ 0xc4 => 0x02dc, # SMALL TILDE
75
+ 0xc5 => 0x00af, # MACRON
76
+ 0xc6 => 0x02d8, # BREVE
77
+ 0xc7 => 0x02d9, # DOT ABOVE
78
+ 0xc8 => 0x00a8, # DIAERESIS
79
+ 0xc9 => 0x00b2, # SUPERSCRIPT TWO
80
+ 0xca => 0x02da, # RING ABOVE
81
+ 0xcb => 0x00b8, # CEDILLA
82
+ 0xcc => 0x00b3, # SUPERSCRIPT THREE
83
+ 0xcd => 0x02dd, # DOUBLE ACUTE ACCENT
84
+ 0xce => 0x02db, # OGONEK
85
+ 0xcf => 0x02c7, # CARON
86
+ 0xd0 => 0x2014, # EM DASH
87
+ 0xd1 => 0x00b1, # PLUS-MINUS SIGN
88
+ 0xd2 => 0x00bc, # VULGAR FRACTION ONE QUARTER
89
+ 0xd3 => 0x00bd, # VULGAR FRACTION ONE HALF
90
+ 0xd4 => 0x00be, # VULGAR FRACTION THREE QUARTERS
91
+ 0xd5 => 0x00e0, # LATIN SMALL LETTER A WITH GRAVE
92
+ 0xd6 => 0x00e1, # LATIN SMALL LETTER A WITH ACUTE
93
+ 0xd7 => 0x00e2, # LATIN SMALL LETTER A WITH CIRCUMFLEX
94
+ 0xd8 => 0x00e3, # LATIN SMALL LETTER A WITH TILDE
95
+ 0xd9 => 0x00e4, # LATIN SMALL LETTER A WITH DIAERESIS
96
+ 0xda => 0x00e5, # LATIN SMALL LETTER A WITH RING ABOVE
97
+ 0xdb => 0x00e7, # LATIN SMALL LETTER C WITH CEDILLA
98
+ 0xdc => 0x00e8, # LATIN SMALL LETTER E WITH GRAVE
99
+ 0xdd => 0x00e9, # LATIN SMALL LETTER E WITH ACUTE
100
+ 0xde => 0x00ea, # LATIN SMALL LETTER E WITH CIRCUMFLEX
101
+ 0xdf => 0x00eb, # LATIN SMALL LETTER E WITH DIAERESIS
102
+ 0xe0 => 0x00ec, # LATIN SMALL LETTER I WITH GRAVE
103
+ 0xe1 => 0x00c6, # LATIN CAPITAL LETTER AE
104
+ 0xe2 => 0x00ed, # LATIN SMALL LETTER I WITH ACUTE
105
+ 0xe3 => 0x00aa, # FEMININE ORDINAL INDICATOR
106
+ 0xe4 => 0x00ee, # LATIN SMALL LETTER I WITH CIRCUMFLEX
107
+ 0xe5 => 0x00ef, # LATIN SMALL LETTER I WITH DIAERESIS
108
+ 0xe6 => 0x00f0, # LATIN SMALL LETTER ETH
109
+ 0xe7 => 0x00f1, # LATIN SMALL LETTER N WITH TILDE
110
+ 0xe8 => 0x0141, # LATIN CAPITAL LETTER L WITH STROKE
111
+ 0xe9 => 0x00d8, # LATIN CAPITAL LETTER O WITH STROKE
112
+ 0xea => 0x0152, # LATIN CAPITAL LIGATURE OE
113
+ 0xeb => 0x00ba, # MASCULINE ORDINAL INDICATOR
114
+ 0xec => 0x00f2, # LATIN SMALL LETTER O WITH GRAVE
115
+ 0xed => 0x00f3, # LATIN SMALL LETTER O WITH ACUTE
116
+ 0xee => 0x00f4, # LATIN SMALL LETTER O WITH CIRCUMFLEX
117
+ 0xef => 0x00f5, # LATIN SMALL LETTER O WITH TILDE
118
+ 0xf0 => 0x00f6, # LATIN SMALL LETTER O WITH DIAERESIS
119
+ 0xf1 => 0x00e6, # LATIN SMALL LETTER AE
120
+ 0xf2 => 0x00f9, # LATIN SMALL LETTER U WITH GRAVE
121
+ 0xf3 => 0x00fa, # LATIN SMALL LETTER U WITH ACUTE
122
+ 0xf4 => 0x00fb, # LATIN SMALL LETTER U WITH CIRCUMFLEX
123
+ 0xf5 => 0x0131, # LATIN SMALL LETTER DOTLESS I
124
+ 0xf6 => 0x00fc, # LATIN SMALL LETTER U WITH DIAERESIS
125
+ 0xf7 => 0x00fd, # LATIN SMALL LETTER Y WITH ACUTE
126
+ 0xf8 => 0x0142, # LATIN SMALL LETTER L WITH STROKE
127
+ 0xf9 => 0x00f8, # LATIN SMALL LETTER O WITH STROKE
128
+ 0xfa => 0x0153, # LATIN SMALL LIGATURE OE
129
+ 0xfb => 0x00df, # LATIN SMALL LETTER SHARP S
130
+ 0xfc => 0x00fe, # LATIN SMALL LETTER THORN
131
+ 0xfd => 0x00ff, # LATIN SMALL LETTER Y WITH DIAERESIS
132
+ 0xfe => 0xfffd, # .notdef, REPLACEMENT CHARACTER
133
+ 0xff => 0xfffd, # .notdef, REPLACEMENT CHARACTER
134
+ }.freeze
135
+ end
136
+ end