nanaimo 0.1.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.
@@ -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