nanaimo 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.rubocop.yml +13 -0
- data/.rubocop_todo.yml +7 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +8 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +49 -0
- data/LICENSE.txt +21 -0
- data/README.md +55 -0
- data/Rakefile +72 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/nanaimo.rb +21 -0
- data/lib/nanaimo/object.rb +100 -0
- data/lib/nanaimo/plist.rb +33 -0
- data/lib/nanaimo/reader.rb +252 -0
- data/lib/nanaimo/unicode.rb +88 -0
- data/lib/nanaimo/unicode/next_step_mapping.rb +136 -0
- data/lib/nanaimo/unicode/quote_maps.rb +56 -0
- data/lib/nanaimo/version.rb +3 -0
- data/lib/nanaimo/writer.rb +174 -0
- data/lib/nanaimo/writer/xml.rb +131 -0
- data/lib/nanaimo/xcode_project_writer.rb +76 -0
- data/nanaimo.gemspec +24 -0
- metadata +114 -0
@@ -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
|
+
'&' => '&',
|
79
|
+
'<' => '<',
|
80
|
+
'>' => '>'
|
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
|