dotstrings 0.1.0 → 0.3.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.
- 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
|