dotstrings 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +24 -0
- data/Gemfile.lock +9 -1
- data/README.md +3 -2
- data/dotstrings.gemspec +1 -0
- data/lib/dotstrings/errors.rb +2 -0
- data/lib/dotstrings/file.rb +94 -2
- data/lib/dotstrings/item.rb +16 -5
- data/lib/dotstrings/parser.rb +65 -21
- data/lib/dotstrings/version.rb +1 -1
- data/lib/dotstrings.rb +14 -0
- data/test/fixtures/escaped_unicode~bad_surrogate_order.strings +1 -0
- data/test/fixtures/escaped_unicode~duplicated_high_surrogate.strings +1 -0
- data/test/fixtures/escaped_unicode~incomplete_surrogate_pair.strings +1 -0
- data/test/fixtures/escaped_unicode~non_surrogate_after_high_surrogate.strings +1 -0
- data/test/test_dotstrings.rb +20 -1
- data/test/test_file.rb +56 -6
- data/test/{helper.rb → test_helper.rb} +3 -1
- data/test/test_item.rb +1 -1
- data/test/test_parser.rb +50 -0
- metadata +29 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a7f215ea68d4b6ce65de6f15811800b976eb722b89a4618bce4503338508df51
|
4
|
+
data.tar.gz: 51a72c22c7233b00df29f97d876abb3e59517de4bf0b78797ab8897d122250fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d9247c103eb79e3e908a55af5661de97cae316cc36660c7c872a869f077a9875060eab7bab7caa0a9aa8c2ef82d6ce7c5b9acede49fa1227790b4aad53b205d
|
7
|
+
data.tar.gz: af144c8d6b3f9f552fc71db2df31db95f1189575fa8eb9e2c5f97ab0a15234024bcd97692218be4bbd04f7352f6a3d63966b4ff4948df84379158733583920ad
|
data/.github/workflows/ci.yml
CHANGED
data/.rubocop.yml
CHANGED
@@ -10,6 +10,14 @@ Style/StringLiterals:
|
|
10
10
|
Enabled: true
|
11
11
|
EnforcedStyle: single_quotes
|
12
12
|
|
13
|
+
Layout/FirstHashElementIndentation:
|
14
|
+
Enabled: true
|
15
|
+
EnforcedStyle: consistent
|
16
|
+
|
17
|
+
Layout/FirstArrayElementIndentation:
|
18
|
+
Enabled: true
|
19
|
+
EnforcedStyle: consistent
|
20
|
+
|
13
21
|
Metrics/ClassLength:
|
14
22
|
Enabled: true
|
15
23
|
Max: 150
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## [v0.3.0] - 2022-08-07
|
4
|
+
### Changed
|
5
|
+
* Improved unicode code point parsing and validation.
|
6
|
+
* Added `DotStrings::File#sort`, `DotStrings::File#sort!`, and `DotStrings::File#delete_if` methods.
|
7
|
+
* Improved documentation.
|
8
|
+
|
9
|
+
## [v0.2.0] - 2022-07-17
|
10
|
+
### Changed
|
11
|
+
* Made some state transitions more strict.
|
12
|
+
* Added option to ignore comments when serializing.
|
13
|
+
|
14
|
+
## [v0.1.1] - 2022-07-12
|
15
|
+
### Changed
|
16
|
+
* Escaping single quotes is now optional.
|
17
|
+
|
18
|
+
## [v0.1.0] - 2022-07-06
|
19
|
+
### Added
|
20
|
+
* Initial release.
|
21
|
+
|
22
|
+
[v0.2.0]: https://github.com/raymondjavaxx/dotstrings/releases/tag/v0.2.0
|
23
|
+
[v0.1.1]: https://github.com/raymondjavaxx/dotstrings/releases/tag/v0.1.1
|
24
|
+
[v0.1.0]: https://github.com/raymondjavaxx/dotstrings/releases/tag/v0.1.0
|
data/Gemfile.lock
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
dotstrings (0.
|
4
|
+
dotstrings (0.3.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
9
|
ast (2.4.2)
|
10
|
+
docile (1.4.0)
|
10
11
|
minitest (5.15.0)
|
11
12
|
parallel (1.22.1)
|
12
13
|
parser (3.1.2.0)
|
@@ -31,6 +32,12 @@ GEM
|
|
31
32
|
rubocop-rake (0.6.0)
|
32
33
|
rubocop (~> 1.0)
|
33
34
|
ruby-progressbar (1.11.0)
|
35
|
+
simplecov (0.21.2)
|
36
|
+
docile (~> 1.1)
|
37
|
+
simplecov-html (~> 0.11)
|
38
|
+
simplecov_json_formatter (~> 0.1)
|
39
|
+
simplecov-html (0.12.3)
|
40
|
+
simplecov_json_formatter (0.1.4)
|
34
41
|
unicode-display_width (2.2.0)
|
35
42
|
|
36
43
|
PLATFORMS
|
@@ -44,6 +51,7 @@ DEPENDENCIES
|
|
44
51
|
rubocop
|
45
52
|
rubocop-minitest
|
46
53
|
rubocop-rake
|
54
|
+
simplecov
|
47
55
|
|
48
56
|
BUNDLED WITH
|
49
57
|
2.2.18
|
data/README.md
CHANGED
@@ -7,6 +7,7 @@ A parser for Apple *strings* files (`.strings`) written in Ruby. Some of the fea
|
|
7
7
|
* An API for creating strings files programmatically.
|
8
8
|
* Handles Unicode and escaped characters.
|
9
9
|
* Helpful error messages: know which line and column fail to parse and why.
|
10
|
+
* Well [tested](test) and [documented](https://www.rubydoc.info/gems/dotstrings/DotStrings).
|
10
11
|
|
11
12
|
## Installing
|
12
13
|
|
@@ -19,12 +20,12 @@ $ gem install dotstrings
|
|
19
20
|
Or by adding the following entry to your [Gemfile](https://guides.cocoapods.org/using/a-gemfile.html), then running `$ bundle install`.
|
20
21
|
|
21
22
|
```ruby
|
22
|
-
gem
|
23
|
+
gem 'dotstrings'
|
23
24
|
```
|
24
25
|
|
25
26
|
## Usage
|
26
27
|
|
27
|
-
You can load `.strings` files using the `DotString.parse()` utility method. This method returns a `DotStrings::File` object or raises an exception if the file
|
28
|
+
You can load `.strings` files using the `DotString.parse()` utility method. This method returns a `DotStrings::File` object or raises an exception if the file cannot be parsed.
|
28
29
|
|
29
30
|
```ruby
|
30
31
|
file = DotStrings.parse_file('en-US/Localizable.strings')
|
data/dotstrings.gemspec
CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |s|
|
|
22
22
|
s.add_development_dependency 'rubocop'
|
23
23
|
s.add_development_dependency 'rubocop-minitest'
|
24
24
|
s.add_development_dependency 'rubocop-rake'
|
25
|
+
s.add_development_dependency 'simplecov'
|
25
26
|
|
26
27
|
s.required_ruby_version = '>= 2.5.0'
|
27
28
|
s.metadata['rubygems_mfa_required'] = 'true'
|
data/lib/dotstrings/errors.rb
CHANGED
data/lib/dotstrings/file.rb
CHANGED
@@ -5,13 +5,48 @@ require 'dotstrings/errors'
|
|
5
5
|
require 'dotstrings/item'
|
6
6
|
|
7
7
|
module DotStrings
|
8
|
+
##
|
9
|
+
# Represents a .strings file.
|
10
|
+
#
|
11
|
+
# It provides methods to parse .strings, as well as methods for accessing and
|
12
|
+
# manipulating localized string items.
|
8
13
|
class File
|
14
|
+
##
|
15
|
+
# All items in the file.
|
9
16
|
attr_reader :items
|
10
17
|
|
11
18
|
def initialize(items = [])
|
12
19
|
@items = items
|
13
20
|
end
|
14
21
|
|
22
|
+
##
|
23
|
+
# Returns a new File with the items sorted using the given comparator block.
|
24
|
+
#
|
25
|
+
# If no block is given, the items will be sorted by key.
|
26
|
+
def sort(&block)
|
27
|
+
new_file = dup
|
28
|
+
new_file.sort!(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Sort the items using the given block.
|
33
|
+
#
|
34
|
+
# If no block is given, the items will be sorted by key.
|
35
|
+
def sort!(&block)
|
36
|
+
@items.sort!(&block || ->(a, b) { a.key <=> b.key })
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Parses a file from the given IO object.
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# io = Zlib::GzipReader.open('path/to/en.lproj/Localizable.strings.gz')
|
45
|
+
# file = DotStrings::File.parse(io)
|
46
|
+
#
|
47
|
+
# @param io [IO] The IO object to parse.
|
48
|
+
# @return [DotStrings::File] The parsed file.
|
49
|
+
# @raise [DotStrings::ParsingError] if the file could not be parsed.
|
15
50
|
def self.parse(io)
|
16
51
|
items = []
|
17
52
|
|
@@ -22,37 +57,94 @@ module DotStrings
|
|
22
57
|
File.new(items)
|
23
58
|
end
|
24
59
|
|
60
|
+
##
|
61
|
+
# Parses the file at the given path.
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# file = DotStrings::File.parse_file('path/to/en.lproj/Localizable.strings')
|
65
|
+
#
|
66
|
+
# @param path [String] The path to the file to parse.
|
67
|
+
# @return [DotStrings::File] The parsed file.
|
68
|
+
# @raise [DotStrings::ParsingError] if the file could not be parsed.
|
25
69
|
def self.parse_file(path)
|
26
70
|
::File.open(path, 'r') do |file|
|
27
71
|
parse(file)
|
28
72
|
end
|
29
73
|
end
|
30
74
|
|
75
|
+
##
|
76
|
+
# Returns all keys in the file.
|
31
77
|
def keys
|
32
78
|
@items.map(&:key)
|
33
79
|
end
|
34
80
|
|
81
|
+
##
|
82
|
+
# Returns an item by key, if it exists, otherwise nil.
|
83
|
+
#
|
84
|
+
# @example
|
85
|
+
# item = file['button.title']
|
86
|
+
# unless item.nil?
|
87
|
+
# puts item.value # => 'Submit'
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# @param key [String] The key of the item to return.
|
91
|
+
# @return [DotStrings::Item] The item, if it exists.
|
35
92
|
def [](key)
|
36
93
|
@items.find { |item| item.key == key }
|
37
94
|
end
|
38
95
|
|
96
|
+
##
|
97
|
+
# Appends an item to the file.
|
98
|
+
#
|
99
|
+
# @example
|
100
|
+
# file << DotStrings::Item.new(key: 'button.title', value: 'Submit')
|
101
|
+
#
|
102
|
+
# @param item [DotStrings::Item] The item to append.
|
103
|
+
# @return [DotStrings::Item] The item that was appended.
|
39
104
|
def <<(item)
|
40
105
|
@items << item
|
106
|
+
self
|
41
107
|
end
|
42
108
|
|
109
|
+
##
|
110
|
+
# Appends an item to the file.
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# file.append(DotStrings::Item.new(key: 'button.title', value: 'Submit'))
|
43
114
|
def append(item)
|
44
115
|
self << item
|
45
116
|
end
|
46
117
|
|
118
|
+
##
|
119
|
+
# Deletes an item by key.
|
47
120
|
def delete(key)
|
48
121
|
@items.delete_if { |item| item.key == key }
|
49
122
|
end
|
50
123
|
|
51
|
-
|
124
|
+
##
|
125
|
+
# Deletes all items for which the block returns true.
|
126
|
+
#
|
127
|
+
# @example
|
128
|
+
# file.delete_if { |item| item.key.start_with?('button.') }
|
129
|
+
def delete_if(&block)
|
130
|
+
@items.delete_if(&block)
|
131
|
+
self
|
132
|
+
end
|
133
|
+
|
134
|
+
##
|
135
|
+
# Serializes the file to a string.
|
136
|
+
#
|
137
|
+
# @param escape_single_quotes [Boolean] whether to escape single quotes.
|
138
|
+
# @param comments [Boolean] whether to include comments.
|
139
|
+
def to_s(escape_single_quotes: false, comments: true)
|
52
140
|
result = []
|
53
141
|
|
54
142
|
@items.each do |item|
|
55
|
-
result << item.to_s
|
143
|
+
result << item.to_s(
|
144
|
+
escape_single_quotes: escape_single_quotes,
|
145
|
+
include_comment: comments
|
146
|
+
)
|
147
|
+
|
56
148
|
result << ''
|
57
149
|
end
|
58
150
|
|
data/lib/dotstrings/item.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module DotStrings
|
4
|
+
##
|
5
|
+
# Represents a localized string item.
|
4
6
|
class Item
|
5
7
|
attr_reader :comment, :key, :value
|
6
8
|
|
@@ -10,27 +12,36 @@ module DotStrings
|
|
10
12
|
@value = value
|
11
13
|
end
|
12
14
|
|
13
|
-
|
15
|
+
##
|
16
|
+
# Serializes the item to string.
|
17
|
+
#
|
18
|
+
# @param escape_single_quotes [Boolean] Whether to escape single quotes.
|
19
|
+
# @param include_comment [Boolean] Whether to include the comment.
|
20
|
+
def to_s(escape_single_quotes: false, include_comment: true)
|
14
21
|
result = []
|
15
22
|
|
16
|
-
result << "/* #{comment} */" unless comment.nil?
|
17
|
-
result << "
|
23
|
+
result << "/* #{comment} */" unless comment.nil? || !include_comment
|
24
|
+
result << format('"%<key>s" = "%<value>s";', {
|
25
|
+
key: serialize_string(key, escape_single_quotes: escape_single_quotes),
|
26
|
+
value: serialize_string(value, escape_single_quotes: escape_single_quotes)
|
27
|
+
})
|
18
28
|
|
19
29
|
result.join("\n")
|
20
30
|
end
|
21
31
|
|
22
32
|
private
|
23
33
|
|
24
|
-
def serialize_string(string)
|
34
|
+
def serialize_string(string, escape_single_quotes:)
|
25
35
|
replacements = [
|
26
36
|
['"', '\\"'],
|
27
|
-
["'", "\\'"],
|
28
37
|
["\t", '\t'],
|
29
38
|
["\n", '\n'],
|
30
39
|
["\r", '\r'],
|
31
40
|
["\0", '\\0']
|
32
41
|
]
|
33
42
|
|
43
|
+
replacements << ["'", "\\'"] if escape_single_quotes
|
44
|
+
|
34
45
|
replacements.each do |replacement|
|
35
46
|
string = string.gsub(replacement[0]) { replacement[1] }
|
36
47
|
end
|
data/lib/dotstrings/parser.rb
CHANGED
@@ -4,6 +4,9 @@ require 'dotstrings/errors'
|
|
4
4
|
|
5
5
|
module DotStrings
|
6
6
|
# rubocop:disable Metrics/ClassLength
|
7
|
+
|
8
|
+
##
|
9
|
+
# Parser for .strings files.
|
7
10
|
class Parser
|
8
11
|
# Special tokens
|
9
12
|
TOK_SLASH = '/'
|
@@ -36,8 +39,6 @@ module DotStrings
|
|
36
39
|
STATE_UNICODE_SURROGATE = 11
|
37
40
|
STATE_UNICODE_SURROGATE_U = 12
|
38
41
|
|
39
|
-
attr_reader :items
|
40
|
-
|
41
42
|
def initialize
|
42
43
|
@state = STATE_START
|
43
44
|
@temp_state = nil
|
@@ -59,11 +60,16 @@ module DotStrings
|
|
59
60
|
@column = 1
|
60
61
|
end
|
61
62
|
|
63
|
+
##
|
64
|
+
# Specifies a block to be called when a new item is parsed.
|
62
65
|
def on_item(&block)
|
63
66
|
@item_block = block
|
64
67
|
end
|
65
68
|
|
66
69
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
|
70
|
+
|
71
|
+
##
|
72
|
+
# Feeds data to the parser.
|
67
73
|
def <<(data)
|
68
74
|
data.each_char do |ch|
|
69
75
|
case @state
|
@@ -95,19 +101,27 @@ module DotStrings
|
|
95
101
|
@buffer << ch
|
96
102
|
end
|
97
103
|
when STATE_COMMENT_END
|
98
|
-
|
104
|
+
if ch == TOK_QUOTE
|
105
|
+
@state = STATE_KEY
|
106
|
+
else
|
107
|
+
raise_error("Unexpected character '#{ch}'") unless whitespace?(ch)
|
108
|
+
end
|
99
109
|
when STATE_KEY
|
100
110
|
parse_string(ch) do |key|
|
101
111
|
@current_key = key
|
102
112
|
@state = STATE_KEY_END
|
103
113
|
end
|
104
114
|
when STATE_KEY_END
|
105
|
-
|
115
|
+
if ch == TOK_EQUALS
|
116
|
+
@state = STATE_VALUE_SEPARATOR
|
117
|
+
else
|
118
|
+
raise_error("Unexpected character '#{ch}', expecting '#{TOK_EQUALS}'") unless whitespace?(ch)
|
119
|
+
end
|
106
120
|
when STATE_VALUE_SEPARATOR
|
107
121
|
if ch == TOK_QUOTE
|
108
122
|
@state = STATE_VALUE
|
109
123
|
else
|
110
|
-
raise_error("Unexpected character '#{ch}'") unless ch
|
124
|
+
raise_error("Unexpected character '#{ch}'") unless whitespace?(ch)
|
111
125
|
end
|
112
126
|
when STATE_VALUE
|
113
127
|
parse_string(ch) do |value|
|
@@ -121,7 +135,11 @@ module DotStrings
|
|
121
135
|
))
|
122
136
|
end
|
123
137
|
when STATE_VALUE_END
|
124
|
-
|
138
|
+
if ch == TOK_SEMICOLON
|
139
|
+
@state = STATE_START
|
140
|
+
else
|
141
|
+
raise_error("Unexpected character '#{ch}', expecting '#{TOK_SEMICOLON}'") unless whitespace?(ch)
|
142
|
+
end
|
125
143
|
when STATE_UNICODE
|
126
144
|
parse_unicode(ch) do |unicode_ch|
|
127
145
|
@buffer << unicode_ch
|
@@ -145,6 +163,7 @@ module DotStrings
|
|
145
163
|
update_position(ch)
|
146
164
|
end
|
147
165
|
end
|
166
|
+
|
148
167
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
|
149
168
|
|
150
169
|
private
|
@@ -191,30 +210,51 @@ module DotStrings
|
|
191
210
|
end
|
192
211
|
end
|
193
212
|
|
194
|
-
# rubocop:disable
|
213
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
195
214
|
def parse_unicode(ch, &block)
|
196
215
|
raise_error("Unexpected character '#{ch}', expecting a hex digit") unless ch =~ TOK_HEX_DIGIT
|
197
216
|
|
198
217
|
@unicode_buffer << ch
|
199
218
|
|
200
|
-
if
|
201
|
-
|
219
|
+
# Check if we have enough digits to form a codepoint.
|
220
|
+
return if @unicode_buffer.length < 4
|
202
221
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
222
|
+
codepoint = @unicode_buffer.join.hex
|
223
|
+
|
224
|
+
if codepoint >= 0xD800 && codepoint <= 0xDBFF
|
225
|
+
unless @high_surrogate.nil?
|
226
|
+
raise_error(
|
227
|
+
'Found a high surrogate code point after another high surrogate'
|
228
|
+
)
|
229
|
+
end
|
230
|
+
|
231
|
+
@high_surrogate = codepoint
|
232
|
+
@state = STATE_UNICODE_SURROGATE
|
233
|
+
elsif codepoint >= 0xDC00 && codepoint <= 0xDFFF
|
234
|
+
if @high_surrogate.nil?
|
235
|
+
raise_error(
|
236
|
+
'Found a low surrogate code point before a high surrogate'
|
237
|
+
)
|
211
238
|
end
|
212
239
|
|
213
|
-
|
214
|
-
@
|
240
|
+
character = ((@high_surrogate - 0xD800) * 0x400) + (codepoint - 0xDC00) + 0x10000
|
241
|
+
@high_surrogate = nil
|
242
|
+
|
243
|
+
block.call(character.chr('UTF-8'))
|
244
|
+
else
|
245
|
+
unless @high_surrogate.nil?
|
246
|
+
raise_error(
|
247
|
+
"Invalid unicode codepoint '\\U#{codepoint.to_s(16).upcase}' after a high surrogate code point"
|
248
|
+
)
|
249
|
+
end
|
250
|
+
|
251
|
+
block.call(codepoint.chr('UTF-8'))
|
215
252
|
end
|
253
|
+
|
254
|
+
# Clear buffer after codepoint is parsed
|
255
|
+
@unicode_buffer.clear
|
216
256
|
end
|
217
|
-
# rubocop:enable
|
257
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
218
258
|
|
219
259
|
def update_position(ch)
|
220
260
|
@offset += 1
|
@@ -236,7 +276,7 @@ module DotStrings
|
|
236
276
|
@state = STATE_KEY
|
237
277
|
reset_state
|
238
278
|
else
|
239
|
-
raise_error("Unexpected character '#{ch}'") unless ch
|
279
|
+
raise_error("Unexpected character '#{ch}'") unless whitespace?(ch)
|
240
280
|
end
|
241
281
|
end
|
242
282
|
|
@@ -245,6 +285,10 @@ module DotStrings
|
|
245
285
|
@current_key = nil
|
246
286
|
@current_value = nil
|
247
287
|
end
|
288
|
+
|
289
|
+
def whitespace?(ch)
|
290
|
+
ch.strip.empty?
|
291
|
+
end
|
248
292
|
end
|
249
293
|
# rubocop:enable Metrics/ClassLength
|
250
294
|
end
|
data/lib/dotstrings/version.rb
CHANGED
data/lib/dotstrings.rb
CHANGED
@@ -6,10 +6,24 @@ require 'dotstrings/item'
|
|
6
6
|
require 'dotstrings/errors'
|
7
7
|
|
8
8
|
module DotStrings
|
9
|
+
##
|
10
|
+
# Parses a file from the given IO object.
|
11
|
+
#
|
12
|
+
# This is a convenience method for {DotStrings::File.parse}.
|
13
|
+
#
|
14
|
+
# @param io [IO] The IO object to parse.
|
15
|
+
# @return [DotStrings::File] The parsed file.
|
9
16
|
def self.parse(io)
|
10
17
|
File.parse(io)
|
11
18
|
end
|
12
19
|
|
20
|
+
##
|
21
|
+
# Parses a .strings file at the given path.
|
22
|
+
#
|
23
|
+
# This is a convenience method for {DotStrings::File.parse_file}.
|
24
|
+
#
|
25
|
+
# @param path [String] The path to the .strings file to parse.
|
26
|
+
# @return [DotStrings::File] The parsed file.
|
13
27
|
def self.parse_file(path)
|
14
28
|
File.parse_file(path)
|
15
29
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
"key" = "\UDC7B\UD83D";
|
@@ -0,0 +1 @@
|
|
1
|
+
"key" = "\UD83D\UD83D";
|
@@ -0,0 +1 @@
|
|
1
|
+
"key" = "\UD83D";
|
@@ -0,0 +1 @@
|
|
1
|
+
"key" = "\UD83D\U26A1";
|
data/test/test_dotstrings.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative 'test_helper'
|
4
4
|
|
5
5
|
class TestDotStrings < MiniTest::Test
|
6
6
|
def test_parse_can_parse_valid_files
|
@@ -81,6 +81,25 @@ class TestDotStrings < MiniTest::Test
|
|
81
81
|
assert_equal '⚡👻', file.items[0].value
|
82
82
|
end
|
83
83
|
|
84
|
+
def test_raises_error_when_bad_surrogate_pair_is_found
|
85
|
+
# rubocop:disable Layout/LineLength
|
86
|
+
test_cases = {
|
87
|
+
'escaped_unicode~bad_surrogate_order.strings' => 'Found a low surrogate code point before a high surrogate at line 1, column 15 (offset: 14)',
|
88
|
+
'escaped_unicode~duplicated_high_surrogate.strings' => 'Found a high surrogate code point after another high surrogate at line 1, column 21 (offset: 20)',
|
89
|
+
'escaped_unicode~incomplete_surrogate_pair.strings' => 'Unexpected character \'"\', expecting another unicode codepoint at line 1, column 16 (offset: 15)',
|
90
|
+
'escaped_unicode~non_surrogate_after_high_surrogate.strings' => 'Invalid unicode codepoint \'\U26A1\' after a high surrogate code point at line 1, column 21 (offset: 20)'
|
91
|
+
}
|
92
|
+
# rubocop:enable Layout/LineLength
|
93
|
+
|
94
|
+
test_cases.each do |filename, error_message|
|
95
|
+
error = assert_raises DotStrings::ParsingError do
|
96
|
+
DotStrings.parse_file("test/fixtures/#{filename}")
|
97
|
+
end
|
98
|
+
|
99
|
+
assert_equal error_message, error.message
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
84
103
|
def test_can_parse_utf16le_files_with_bom
|
85
104
|
file = DotStrings.parse_file('test/fixtures/utf16le_bom.strings')
|
86
105
|
|
data/test/test_file.rb
CHANGED
@@ -1,8 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative 'test_helper'
|
4
4
|
|
5
5
|
class TestFile < MiniTest::Test
|
6
|
+
def test_sort
|
7
|
+
file = DotStrings::File.new([
|
8
|
+
DotStrings::Item.new(key: 'key 3', value: 'value 3'),
|
9
|
+
DotStrings::Item.new(key: 'key 2', value: 'value 2'),
|
10
|
+
DotStrings::Item.new(key: 'key 1', value: 'value 1')
|
11
|
+
])
|
12
|
+
|
13
|
+
sorted = file.sort
|
14
|
+
assert_equal ['key 1', 'key 2', 'key 3'], sorted.keys
|
15
|
+
end
|
16
|
+
|
6
17
|
def test_delete
|
7
18
|
items = [
|
8
19
|
DotStrings::Item.new(key: 'key 1', value: 'value 1'),
|
@@ -17,6 +28,17 @@ class TestFile < MiniTest::Test
|
|
17
28
|
assert_equal ['key 1', 'key 3'], file.keys
|
18
29
|
end
|
19
30
|
|
31
|
+
def test_delete_if
|
32
|
+
file = DotStrings::File.new([
|
33
|
+
DotStrings::Item.new(key: 'key 1', value: 'value 1'),
|
34
|
+
DotStrings::Item.new(key: 'key 2', value: 'value 2'),
|
35
|
+
DotStrings::Item.new(key: 'key 3', value: 'value 3')
|
36
|
+
])
|
37
|
+
|
38
|
+
file.delete_if { |item| item.key == 'key 2' }
|
39
|
+
assert_equal ['key 1', 'key 3'], file.keys
|
40
|
+
end
|
41
|
+
|
20
42
|
def test_access_by_key
|
21
43
|
items = [
|
22
44
|
DotStrings::Item.new(key: 'key 1', value: 'value 1'),
|
@@ -38,14 +60,12 @@ class TestFile < MiniTest::Test
|
|
38
60
|
end
|
39
61
|
|
40
62
|
def test_to_string
|
41
|
-
|
63
|
+
file = DotStrings::File.new([
|
42
64
|
DotStrings::Item.new(comment: 'Comment 1', key: 'key 1', value: 'value 1'),
|
43
65
|
DotStrings::Item.new(comment: 'Comment 2', key: 'key 2', value: 'value 2'),
|
44
66
|
DotStrings::Item.new(comment: 'Comment 3', key: 'key 3', value: '👻'),
|
45
67
|
DotStrings::Item.new(comment: 'Comment 4', key: "\"'\t\n\r\0", value: "\"'\t\n\r\0")
|
46
|
-
]
|
47
|
-
|
48
|
-
file = DotStrings::File.new(items)
|
68
|
+
])
|
49
69
|
|
50
70
|
expected = <<~'END_OF_DOCUMENT'
|
51
71
|
/* Comment 1 */
|
@@ -58,9 +78,39 @@ class TestFile < MiniTest::Test
|
|
58
78
|
"key 3" = "👻";
|
59
79
|
|
60
80
|
/* Comment 4 */
|
61
|
-
"\"
|
81
|
+
"\"'\t\n\r\0" = "\"'\t\n\r\0";
|
62
82
|
END_OF_DOCUMENT
|
63
83
|
|
64
84
|
assert_equal expected, file.to_s
|
65
85
|
end
|
86
|
+
|
87
|
+
def test_to_string_no_comments
|
88
|
+
file = DotStrings::File.new([
|
89
|
+
DotStrings::Item.new(comment: 'Comment 1', key: 'key 1', value: 'value 1'),
|
90
|
+
DotStrings::Item.new(comment: 'Comment 2', key: 'key 2', value: 'value 2')
|
91
|
+
])
|
92
|
+
|
93
|
+
expected = <<~'END_OF_DOCUMENT'
|
94
|
+
"key 1" = "value 1";
|
95
|
+
|
96
|
+
"key 2" = "value 2";
|
97
|
+
END_OF_DOCUMENT
|
98
|
+
|
99
|
+
assert_equal expected, file.to_s(comments: false)
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_to_string_can_escape_single_quotes
|
103
|
+
items = [
|
104
|
+
DotStrings::Item.new(comment: 'Comment', key: "key'", value: "value'")
|
105
|
+
]
|
106
|
+
|
107
|
+
file = DotStrings::File.new(items)
|
108
|
+
|
109
|
+
expected = <<~'END_OF_DOCUMENT'
|
110
|
+
/* Comment */
|
111
|
+
"key\'" = "value\'";
|
112
|
+
END_OF_DOCUMENT
|
113
|
+
|
114
|
+
assert_equal expected, file.to_s(escape_single_quotes: true)
|
115
|
+
end
|
66
116
|
end
|
data/test/test_item.rb
CHANGED
data/test/test_parser.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'test_helper'
|
4
|
+
|
5
|
+
class TestParser < MiniTest::Test
|
6
|
+
def test_handles_extraneous_characters_at_start_of_file
|
7
|
+
error = assert_raises DotStrings::ParsingError do
|
8
|
+
parser = DotStrings::Parser.new
|
9
|
+
parser << '$'
|
10
|
+
end
|
11
|
+
|
12
|
+
assert_equal "Unexpected character '$' at line 1, column 1 (offset: 0)", error.message
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_handles_malformed_comments
|
16
|
+
error = assert_raises DotStrings::ParsingError do
|
17
|
+
parser = DotStrings::Parser.new
|
18
|
+
parser << '/@ test'
|
19
|
+
end
|
20
|
+
|
21
|
+
assert_equal "Unexpected character '@' at line 1, column 2 (offset: 1)", error.message
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_raises_error_when_escaping_invalid_character
|
25
|
+
error = assert_raises DotStrings::ParsingError do
|
26
|
+
parser = DotStrings::Parser.new
|
27
|
+
parser << '"\\z" = "value";'
|
28
|
+
end
|
29
|
+
|
30
|
+
assert_equal "Unexpected character 'z' at line 1, column 3 (offset: 2)", error.message
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_raises_error_when_items_are_not_separated_by_semicolon
|
34
|
+
error = assert_raises DotStrings::ParsingError do
|
35
|
+
parser = DotStrings::Parser.new
|
36
|
+
parser << '"key_1" = "value_1" "key_2" = "value_2"'
|
37
|
+
end
|
38
|
+
|
39
|
+
assert_equal "Unexpected character '\"', expecting ';' at line 1, column 21 (offset: 20)", error.message
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_raises_error_if_low_surrogate_is_not_formatted_correctly
|
43
|
+
error = assert_raises DotStrings::ParsingError do
|
44
|
+
parser = DotStrings::Parser.new
|
45
|
+
parser << '"key" = "\UD83D\$DC7B";'
|
46
|
+
end
|
47
|
+
|
48
|
+
assert_equal "Unexpected character '$', expecting 'U' at line 1, column 17 (offset: 16)", error.message
|
49
|
+
end
|
50
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dotstrings
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ramon Torres
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-07
|
11
|
+
date: 2022-08-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: simplecov
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
97
111
|
description: Parse and create .strings files used in localization of iOS and macOS
|
98
112
|
apps.
|
99
113
|
email:
|
@@ -106,6 +120,7 @@ files:
|
|
106
120
|
- ".github/workflows/ci.yml"
|
107
121
|
- ".gitignore"
|
108
122
|
- ".rubocop.yml"
|
123
|
+
- CHANGELOG.md
|
109
124
|
- Gemfile
|
110
125
|
- Gemfile.lock
|
111
126
|
- LICENSE
|
@@ -126,14 +141,19 @@ files:
|
|
126
141
|
- test/fixtures/escaped_single_quotes.strings
|
127
142
|
- test/fixtures/escaped_tabs.strings
|
128
143
|
- test/fixtures/escaped_unicode.strings
|
144
|
+
- test/fixtures/escaped_unicode~bad_surrogate_order.strings
|
145
|
+
- test/fixtures/escaped_unicode~duplicated_high_surrogate.strings
|
146
|
+
- test/fixtures/escaped_unicode~incomplete_surrogate_pair.strings
|
147
|
+
- test/fixtures/escaped_unicode~non_surrogate_after_high_surrogate.strings
|
129
148
|
- test/fixtures/utf16be_bom.strings
|
130
149
|
- test/fixtures/utf16le_bom.strings
|
131
150
|
- test/fixtures/utf8_bom.strings
|
132
151
|
- test/fixtures/valid.strings
|
133
|
-
- test/helper.rb
|
134
152
|
- test/test_dotstrings.rb
|
135
153
|
- test/test_file.rb
|
154
|
+
- test/test_helper.rb
|
136
155
|
- test/test_item.rb
|
156
|
+
- test/test_parser.rb
|
137
157
|
homepage: https://github.com/raymondjavaxx/dotstrings
|
138
158
|
licenses:
|
139
159
|
- MIT
|
@@ -167,11 +187,16 @@ test_files:
|
|
167
187
|
- test/fixtures/escaped_single_quotes.strings
|
168
188
|
- test/fixtures/escaped_tabs.strings
|
169
189
|
- test/fixtures/escaped_unicode.strings
|
190
|
+
- test/fixtures/escaped_unicode~bad_surrogate_order.strings
|
191
|
+
- test/fixtures/escaped_unicode~duplicated_high_surrogate.strings
|
192
|
+
- test/fixtures/escaped_unicode~incomplete_surrogate_pair.strings
|
193
|
+
- test/fixtures/escaped_unicode~non_surrogate_after_high_surrogate.strings
|
170
194
|
- test/fixtures/utf16be_bom.strings
|
171
195
|
- test/fixtures/utf16le_bom.strings
|
172
196
|
- test/fixtures/utf8_bom.strings
|
173
197
|
- test/fixtures/valid.strings
|
174
|
-
- test/helper.rb
|
175
198
|
- test/test_dotstrings.rb
|
176
199
|
- test/test_file.rb
|
200
|
+
- test/test_helper.rb
|
177
201
|
- test/test_item.rb
|
202
|
+
- test/test_parser.rb
|