dotstrings 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9c23d2110a873db44936fc87e1c89e41300a82edfdde9c8a357679c1b378cc27
4
+ data.tar.gz: 81363243a7425d43383246594362b23740ee22a03b3d8d74f871cf8ea113809c
5
+ SHA512:
6
+ metadata.gz: 01a9055615619969260d1012c0a027338d85d73495ac664e6ac72c4a6bb7d3368eb4554496d5622940e660ea01d44fe2949bbf3aa88149ab05b284c021792456
7
+ data.tar.gz: ca19e173e63d4b820b9cd2ebbd4030ef70bb33080822738a4e290e4ce01b6f6be64c9578a858dffd8ae46d6423e343f0345611f1f1a5a04ca48a2ad9fc7b145f
data/.editorconfig ADDED
@@ -0,0 +1,13 @@
1
+ # EditorConfig is awesome: https://EditorConfig.org
2
+
3
+ # top-most EditorConfig file
4
+ root = true
5
+
6
+ [*]
7
+ end_of_line = lf
8
+ insert_final_newline = true
9
+
10
+ [{*.rb,Gemfile,Rakefile,*.yml}]
11
+ charset = utf-8
12
+ indent_style = space
13
+ indent_size = 2
@@ -0,0 +1,32 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: "*"
6
+ pull_request:
7
+ branches: "*"
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby-version:
15
+ - "2.5"
16
+ - "2.6"
17
+ - "2.7"
18
+ - "3.0"
19
+
20
+ steps:
21
+ - uses: actions/checkout@v2
22
+ - name: Set up Ruby
23
+ uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: ${{ matrix.ruby-version }}
26
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
27
+ - name: Lint
28
+ run: bundle exec rake rubocop
29
+ - name: Run tests
30
+ run: bundle exec rake
31
+ - name: Install
32
+ run: bundle exec rake install
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ pkg
2
+ .vscode
data/.rubocop.yml ADDED
@@ -0,0 +1,33 @@
1
+ require:
2
+ - rubocop-rake
3
+ - rubocop-minitest
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 2.5
7
+ NewCops: enable
8
+
9
+ Style/StringLiterals:
10
+ Enabled: true
11
+ EnforcedStyle: single_quotes
12
+
13
+ Metrics/ClassLength:
14
+ Enabled: true
15
+ Max: 150
16
+
17
+ Naming/MethodParameterName:
18
+ Enabled: false
19
+
20
+ Metrics/MethodLength:
21
+ Enabled: false
22
+
23
+ Metrics/AbcSize:
24
+ Enabled: false
25
+
26
+ Style/Documentation:
27
+ Enabled: false
28
+
29
+ Style/ParallelAssignment:
30
+ Enabled: false
31
+
32
+ Minitest/AssertInDelta:
33
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,49 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ dotstrings (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.2)
10
+ minitest (5.15.0)
11
+ parallel (1.22.1)
12
+ parser (3.1.2.0)
13
+ ast (~> 2.4.1)
14
+ rainbow (3.1.1)
15
+ rake (13.0.6)
16
+ regexp_parser (2.5.0)
17
+ rexml (3.2.5)
18
+ rubocop (1.28.2)
19
+ parallel (~> 1.10)
20
+ parser (>= 3.1.0.0)
21
+ rainbow (>= 2.2.2, < 4.0)
22
+ regexp_parser (>= 1.8, < 3.0)
23
+ rexml
24
+ rubocop-ast (>= 1.17.0, < 2.0)
25
+ ruby-progressbar (~> 1.7)
26
+ unicode-display_width (>= 1.4.0, < 3.0)
27
+ rubocop-ast (1.17.0)
28
+ parser (>= 3.1.1.0)
29
+ rubocop-minitest (0.19.1)
30
+ rubocop (>= 0.90, < 2.0)
31
+ rubocop-rake (0.6.0)
32
+ rubocop (~> 1.0)
33
+ ruby-progressbar (1.11.0)
34
+ unicode-display_width (2.2.0)
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ bundler
41
+ dotstrings!
42
+ minitest (~> 5.14)
43
+ rake
44
+ rubocop
45
+ rubocop-minitest
46
+ rubocop-rake
47
+
48
+ BUNDLED WITH
49
+ 2.2.18
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2022 Ramon Torres
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # DotStrings
2
+
3
+ A parser for Apple *strings* files (`.strings`) written in Ruby. Some of the features of DotStrings include:
4
+
5
+ * A fast and memory-efficient streaming parser.
6
+ * Support for multiline (`/* ... */`) comments as well as single-line comments (`// ...`).
7
+ * An API for creating strings files programmatically.
8
+ * Handles Unicode and escaped characters.
9
+ * Helpful error messages: know which line and column fail to parse and why.
10
+
11
+ ## Installing
12
+
13
+ You can install DotStrings manually by running:
14
+
15
+ ```shell
16
+ $ gem install dotstrings
17
+ ```
18
+
19
+ Or by adding the following entry to your [Gemfile](https://guides.cocoapods.org/using/a-gemfile.html), then running `$ bundle install`.
20
+
21
+ ```ruby
22
+ gem "dotstrings"
23
+ ```
24
+
25
+ ## Usage
26
+
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 is invalid.
28
+
29
+ ```ruby
30
+ file = DotStrings.parse_file('en-US/Localizable.strings')
31
+ file.items.each do |item|
32
+ puts item.comment
33
+ puts item.key
34
+ puts item.value
35
+ end
36
+ ```
37
+
38
+ ## Examples
39
+
40
+ ### Listing keys
41
+
42
+ ```ruby
43
+ puts file.keys
44
+ # => ["key 1", "key 2", ...]
45
+ ```
46
+
47
+ ### Accessing items by key
48
+
49
+ ```ruby
50
+ puts file['key 1'].value
51
+ # => "value 1"
52
+ ```
53
+
54
+ ### Deleting items by key
55
+
56
+ ```ruby
57
+ file.delete('key 1')
58
+ ```
59
+
60
+ ### Appending items
61
+
62
+ ```ruby
63
+ file << DotStrings::Item(
64
+ comment: 'Title for the cancel button',
65
+ key: 'button.cancel.title',
66
+ value: 'Cancel'
67
+ )
68
+ ```
69
+
70
+ ### Saving a file
71
+
72
+ ```ruby
73
+ File.write('en-US/Localizable.strings', file.to_s)
74
+ ```
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+ require 'rubocop/rake_task'
6
+
7
+ Rake::TestTask.new
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: :test
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './lib/dotstrings/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'dotstrings'
7
+ s.version = DotStrings::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['Ramon Torres']
10
+ s.email = ['raymondjavaxx@gmail.com']
11
+ s.homepage = 'https://github.com/raymondjavaxx/dotstrings'
12
+ s.description = s.summary = 'Parse and create .strings files used in localization of iOS and macOS apps.'
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
16
+ s.require_paths = ['lib']
17
+ s.license = 'MIT'
18
+
19
+ s.add_development_dependency 'bundler'
20
+ s.add_development_dependency 'minitest', '~> 5.14'
21
+ s.add_development_dependency 'rake'
22
+ s.add_development_dependency 'rubocop'
23
+ s.add_development_dependency 'rubocop-minitest'
24
+ s.add_development_dependency 'rubocop-rake'
25
+
26
+ s.required_ruby_version = '>= 2.5.0'
27
+ s.metadata['rubygems_mfa_required'] = 'true'
28
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DotStrings
4
+ class ParsingError < RuntimeError
5
+ end
6
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dotstrings/parser'
4
+ require 'dotstrings/errors'
5
+ require 'dotstrings/item'
6
+
7
+ module DotStrings
8
+ class File
9
+ attr_reader :items
10
+
11
+ def initialize(items = [])
12
+ @items = items
13
+ end
14
+
15
+ def self.parse(io)
16
+ items = []
17
+
18
+ parser = Parser.new
19
+ parser.on_item { |item| items << item }
20
+ parser << normalize_encoding(io.read)
21
+
22
+ File.new(items)
23
+ end
24
+
25
+ def self.parse_file(path)
26
+ ::File.open(path, 'r') do |file|
27
+ parse(file)
28
+ end
29
+ end
30
+
31
+ def keys
32
+ @items.map(&:key)
33
+ end
34
+
35
+ def [](key)
36
+ @items.find { |item| item.key == key }
37
+ end
38
+
39
+ def <<(item)
40
+ @items << item
41
+ end
42
+
43
+ def append(item)
44
+ self << item
45
+ end
46
+
47
+ def delete(key)
48
+ @items.delete_if { |item| item.key == key }
49
+ end
50
+
51
+ def to_s
52
+ result = []
53
+
54
+ @items.each do |item|
55
+ result << item.to_s
56
+ result << ''
57
+ end
58
+
59
+ result.join("\n")
60
+ end
61
+
62
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
63
+ def self.normalize_encoding(str)
64
+ if str.bytesize >= 3 && str.getbyte(0) == 0xEF && str.getbyte(1) == 0xBB && str.getbyte(2) == 0xBF
65
+ # UTF-8 BOM
66
+ str.byteslice(3, str.bytesize - 3)
67
+ elsif str.bytesize >= 2 && str.getbyte(0) == 0xFE && str.getbyte(1) == 0xFF
68
+ # UTF-16 (BE) BOM
69
+ converter = Encoding::Converter.new('UTF-16BE', 'UTF-8')
70
+ str = converter.convert(str)
71
+ str.byteslice(3, str.bytesize - 3)
72
+ elsif str.bytesize >= 2 && str.getbyte(0) == 0xFF && str.getbyte(1) == 0xFE
73
+ # UTF-16 (LE) BOM
74
+ converter = Encoding::Converter.new('UTF-16LE', 'UTF-8')
75
+ str = converter.convert(str)
76
+ str.byteslice(3, str.bytesize - 3)
77
+ else
78
+ str.force_encoding('UTF-8')
79
+ end
80
+ end
81
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
82
+
83
+ private_class_method :normalize_encoding
84
+ end
85
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DotStrings
4
+ class Item
5
+ attr_reader :comment, :key, :value
6
+
7
+ def initialize(key:, value:, comment: nil)
8
+ @comment = comment
9
+ @key = key
10
+ @value = value
11
+ end
12
+
13
+ def to_s
14
+ result = []
15
+
16
+ result << "/* #{comment} */" unless comment.nil?
17
+ result << "\"#{serialize_string(key)}\" = \"#{serialize_string(value)}\";"
18
+
19
+ result.join("\n")
20
+ end
21
+
22
+ private
23
+
24
+ def serialize_string(string)
25
+ replacements = [
26
+ ['"', '\\"'],
27
+ ["'", "\\'"],
28
+ ["\t", '\t'],
29
+ ["\n", '\n'],
30
+ ["\r", '\r'],
31
+ ["\0", '\\0']
32
+ ]
33
+
34
+ replacements.each do |replacement|
35
+ string = string.gsub(replacement[0]) { replacement[1] }
36
+ end
37
+
38
+ string
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dotstrings/errors'
4
+
5
+ module DotStrings
6
+ # rubocop:disable Metrics/ClassLength
7
+ class Parser
8
+ # Special tokens
9
+ TOK_SLASH = '/'
10
+ TOK_ASTERISK = '*'
11
+ TOK_QUOTE = '"'
12
+ TOK_SINGLE_QUOTE = "'"
13
+ TOK_BACKSLASH = '\\'
14
+ TOK_EQUALS = '='
15
+ TOK_SEMICOLON = ';'
16
+ TOK_NEW_LINE = "\n"
17
+ TOK_N = 'n'
18
+ TOK_R = 'r'
19
+ TOK_T = 't'
20
+ TOK_CAP_U = 'U'
21
+ TOK_ZERO = '0'
22
+ TOK_HEX_DIGIT = /[0-9a-fA-F]/.freeze
23
+
24
+ # States
25
+ STATE_START = 0
26
+ STATE_COMMENT_START = 1
27
+ STATE_COMMENT = 2
28
+ STATE_MULTILINE_COMMENT = 3
29
+ STATE_COMMENT_END = 4
30
+ STATE_KEY = 5
31
+ STATE_KEY_END = 6
32
+ STATE_VALUE_SEPARATOR = 7
33
+ STATE_VALUE = 8
34
+ STATE_VALUE_END = 9
35
+ STATE_UNICODE = 10
36
+ STATE_UNICODE_SURROGATE = 11
37
+ STATE_UNICODE_SURROGATE_U = 12
38
+
39
+ attr_reader :items
40
+
41
+ def initialize
42
+ @state = STATE_START
43
+ @temp_state = nil
44
+
45
+ @buffer = []
46
+ @unicode_buffer = []
47
+ @high_surrogate = nil
48
+
49
+ @escaping = false
50
+
51
+ @current_comment = nil
52
+ @current_key = nil
53
+ @current_value = nil
54
+
55
+ @item_block = nil
56
+
57
+ @offset = 0
58
+ @line = 1
59
+ @column = 1
60
+ end
61
+
62
+ def on_item(&block)
63
+ @item_block = block
64
+ end
65
+
66
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
67
+ def <<(data)
68
+ data.each_char do |ch|
69
+ case @state
70
+ when STATE_START
71
+ start_value(ch)
72
+ when STATE_COMMENT_START
73
+ case ch
74
+ when TOK_SLASH
75
+ @state = STATE_COMMENT
76
+ when TOK_ASTERISK
77
+ @state = STATE_MULTILINE_COMMENT
78
+ else
79
+ raise_error("Unexpected character '#{ch}'")
80
+ end
81
+ when STATE_COMMENT
82
+ if ch == TOK_NEW_LINE
83
+ @state = STATE_COMMENT_END
84
+ @current_comment = @buffer.join.strip
85
+ @buffer.clear
86
+ else
87
+ @buffer << ch
88
+ end
89
+ when STATE_MULTILINE_COMMENT
90
+ if ch == TOK_SLASH && @buffer.last == TOK_ASTERISK
91
+ @state = STATE_COMMENT_END
92
+ @current_comment = @buffer.slice(0, @buffer.length - 1).join.strip
93
+ @buffer.clear
94
+ else
95
+ @buffer << ch
96
+ end
97
+ when STATE_COMMENT_END
98
+ @state = STATE_KEY if ch == TOK_QUOTE
99
+ when STATE_KEY
100
+ parse_string(ch) do |key|
101
+ @current_key = key
102
+ @state = STATE_KEY_END
103
+ end
104
+ when STATE_KEY_END
105
+ @state = STATE_VALUE_SEPARATOR if ch == TOK_EQUALS
106
+ when STATE_VALUE_SEPARATOR
107
+ if ch == TOK_QUOTE
108
+ @state = STATE_VALUE
109
+ else
110
+ raise_error("Unexpected character '#{ch}'") unless ch.strip.empty?
111
+ end
112
+ when STATE_VALUE
113
+ parse_string(ch) do |value|
114
+ @current_value = value
115
+ @state = STATE_VALUE_END
116
+
117
+ @item_block&.call(Item.new(
118
+ comment: @current_comment,
119
+ key: @current_key,
120
+ value: @current_value
121
+ ))
122
+ end
123
+ when STATE_VALUE_END
124
+ @state = STATE_START if ch == TOK_SEMICOLON
125
+ when STATE_UNICODE
126
+ parse_unicode(ch) do |unicode_ch|
127
+ @buffer << unicode_ch
128
+ # Restore state
129
+ @state = @temp_state
130
+ end
131
+ when STATE_UNICODE_SURROGATE
132
+ if ch == TOK_BACKSLASH
133
+ @state = STATE_UNICODE_SURROGATE_U
134
+ else
135
+ raise_error("Unexpected character '#{ch}', expecting another unicode codepoint")
136
+ end
137
+ when STATE_UNICODE_SURROGATE_U
138
+ if ch == TOK_CAP_U
139
+ @state = STATE_UNICODE
140
+ else
141
+ raise_error("Unexpected character '#{ch}', expecting '#{TOK_CAP_U}'")
142
+ end
143
+ end
144
+
145
+ update_position(ch)
146
+ end
147
+ end
148
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
149
+
150
+ private
151
+
152
+ def raise_error(message)
153
+ raise ParsingError, "#{message} at line #{@line}, column #{@column} (offset: #{@offset})"
154
+ end
155
+
156
+ def parse_string(ch, &block)
157
+ if @escaping
158
+ parse_escaped_character(ch, &block)
159
+ else
160
+ case ch
161
+ when TOK_BACKSLASH
162
+ @escaping = true
163
+ when TOK_QUOTE
164
+ block.call(@buffer.join)
165
+ @buffer.clear
166
+ else
167
+ @buffer << ch
168
+ end
169
+ end
170
+ end
171
+
172
+ def parse_escaped_character(ch)
173
+ @escaping = false
174
+
175
+ case ch
176
+ when TOK_QUOTE, TOK_SINGLE_QUOTE, TOK_BACKSLASH
177
+ @buffer << ch
178
+ when TOK_N
179
+ @buffer << "\n"
180
+ when TOK_R
181
+ @buffer << "\r"
182
+ when TOK_T
183
+ @buffer << "\t"
184
+ when TOK_CAP_U
185
+ @temp_state = @state
186
+ @state = STATE_UNICODE
187
+ when TOK_ZERO
188
+ @buffer << "\0"
189
+ else
190
+ raise_error("Unexpected character '#{ch}'")
191
+ end
192
+ end
193
+
194
+ # rubocop:disable Style/GuardClause
195
+ def parse_unicode(ch, &block)
196
+ raise_error("Unexpected character '#{ch}', expecting a hex digit") unless ch =~ TOK_HEX_DIGIT
197
+
198
+ @unicode_buffer << ch
199
+
200
+ if @unicode_buffer.length == 4
201
+ codepoint = @unicode_buffer.join.hex
202
+
203
+ if codepoint >= 0xD800 && codepoint <= 0xDBFF
204
+ @high_surrogate = codepoint
205
+ @state = STATE_UNICODE_SURROGATE
206
+ elsif codepoint >= 0xDC00 && codepoint <= 0xDFFF
207
+ character = ((@high_surrogate - 0xD800) * 0x400) + (codepoint - 0xDC00) + 0x10000
208
+ block.call(character.chr('UTF-8'))
209
+ else
210
+ block.call(codepoint.chr('UTF-8'))
211
+ end
212
+
213
+ # Clear buffer after codepoint is parsed
214
+ @unicode_buffer.clear
215
+ end
216
+ end
217
+ # rubocop:enable Style/GuardClause
218
+
219
+ def update_position(ch)
220
+ @offset += 1
221
+
222
+ if ch == TOK_NEW_LINE
223
+ @column = 1
224
+ @line += 1
225
+ else
226
+ @column += 1
227
+ end
228
+ end
229
+
230
+ def start_value(ch)
231
+ case ch
232
+ when TOK_SLASH
233
+ @state = STATE_COMMENT_START
234
+ reset_state
235
+ when TOK_QUOTE
236
+ @state = STATE_KEY
237
+ reset_state
238
+ else
239
+ raise_error("Unexpected character '#{ch}'") unless ch.strip.empty?
240
+ end
241
+ end
242
+
243
+ def reset_state
244
+ @current_comment = nil
245
+ @current_key = nil
246
+ @current_value = nil
247
+ end
248
+ end
249
+ # rubocop:enable Metrics/ClassLength
250
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DotStrings
4
+ VERSION = '0.1.0'
5
+ end
data/lib/dotstrings.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dotstrings/version'
4
+ require 'dotstrings/file'
5
+ require 'dotstrings/item'
6
+ require 'dotstrings/errors'
7
+
8
+ module DotStrings
9
+ def self.parse(io)
10
+ File.parse(io)
11
+ end
12
+
13
+ def self.parse_file(path)
14
+ File.parse_file(path)
15
+ end
16
+ end
@@ -0,0 +1,2 @@
1
+ /* Escaped carriage backslashes */
2
+ "some\\key" = "some\\value";
@@ -0,0 +1,2 @@
1
+ /* Escaped carriage returns */
2
+ "some\rkey" = "some\rvalue";
@@ -0,0 +1,2 @@
1
+ /* Escaped new lines */
2
+ "some\nkey" = "some\nvalue";
@@ -0,0 +1,2 @@
1
+ /* Escaped nil */
2
+ "key\0" = "value\0";
@@ -0,0 +1,2 @@
1
+ /* Escaped quotes */
2
+ "some \"key\"" = "some \"value\"";
@@ -0,0 +1,2 @@
1
+ /* Escaped quotes */
2
+ "some \'key\'" = "some \'value\'";
@@ -0,0 +1,2 @@
1
+ /* Escaped tabs */
2
+ "some\tkey" = "some\tvalue";
@@ -0,0 +1,2 @@
1
+ /* Unicode characters. Key is a dollar sign, value is: ⚡👻 */
2
+ "\U0024" = "\U26A1\UD83D\UDC7B";
Binary file
Binary file
@@ -0,0 +1,2 @@
1
+ /* Comment */
2
+ "key" = "value";
@@ -0,0 +1,8 @@
1
+ // Single line comment
2
+ "key 1" = "value 1";
3
+
4
+ /* Multi line
5
+ comment */
6
+ "key 2" = "value 2";
7
+
8
+ "key 3" = "value 3";
data/test/helper.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem 'minitest'
4
+
5
+ require 'minitest/pride'
6
+ require 'minitest/autorun'
7
+
8
+ require_relative '../lib/dotstrings'
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class TestDotStrings < MiniTest::Test
6
+ def test_parse_can_parse_valid_files
7
+ file = DotStrings.parse_file('test/fixtures/valid.strings')
8
+
9
+ assert_equal 3, file.items.size
10
+
11
+ assert_equal 'Single line comment', file.items[0].comment
12
+ assert_equal 'key 1', file.items[0].key
13
+ assert_equal 'value 1', file.items[0].value
14
+
15
+ assert_equal "Multi line\ncomment", file.items[1].comment
16
+ assert_equal 'key 2', file.items[1].key
17
+ assert_equal 'value 2', file.items[1].value
18
+ end
19
+
20
+ def test_can_parse_file_with_escaped_quotes
21
+ file = DotStrings.parse_file('test/fixtures/escaped_quotes.strings')
22
+
23
+ assert_equal 1, file.items.size
24
+ assert_equal 'some "key"', file.items[0].key
25
+ assert_equal 'some "value"', file.items[0].value
26
+ end
27
+
28
+ def test_can_parse_file_with_escaped_single_quotes
29
+ file = DotStrings.parse_file('test/fixtures/escaped_single_quotes.strings')
30
+
31
+ assert_equal 1, file.items.size
32
+ assert_equal 'some \'key\'', file.items[0].key
33
+ assert_equal 'some \'value\'', file.items[0].value
34
+ end
35
+
36
+ def test_can_parse_file_with_escaped_tabs
37
+ file = DotStrings.parse_file('test/fixtures/escaped_tabs.strings')
38
+
39
+ assert_equal 1, file.items.size
40
+ assert_equal "some\tkey", file.items[0].key
41
+ assert_equal "some\tvalue", file.items[0].value
42
+ end
43
+
44
+ def test_can_parse_files_with_escaped_carriage_returns
45
+ file = DotStrings.parse_file('test/fixtures/escaped_carriage_returns.strings')
46
+
47
+ assert_equal 1, file.items.size
48
+ assert_equal "some\rkey", file.items[0].key
49
+ assert_equal "some\rvalue", file.items[0].value
50
+ end
51
+
52
+ def test_can_parse_files_with_escaped_nil
53
+ file = DotStrings.parse_file('test/fixtures/escaped_nil.strings')
54
+
55
+ assert_equal 1, file.items.size
56
+ assert_equal "key\0", file.items[0].key
57
+ assert_equal "value\0", file.items[0].value
58
+ end
59
+
60
+ def test_can_parse_files_with_escaped_new_lines
61
+ file = DotStrings.parse_file('test/fixtures/escaped_new_lines.strings')
62
+
63
+ assert_equal 1, file.items.size
64
+ assert_equal "some\nkey", file.items[0].key
65
+ assert_equal "some\nvalue", file.items[0].value
66
+ end
67
+
68
+ def test_can_parse_files_with_escaped_backslashes
69
+ file = DotStrings.parse_file('test/fixtures/escaped_backslashes.strings')
70
+
71
+ assert_equal 1, file.items.size
72
+ assert_equal 'some\\key', file.items[0].key
73
+ assert_equal 'some\\value', file.items[0].value
74
+ end
75
+
76
+ def test_can_parse_files_with_escaped_unicode
77
+ file = DotStrings.parse_file('test/fixtures/escaped_unicode.strings')
78
+
79
+ assert_equal 1, file.items.size
80
+ assert_equal '$', file.items[0].key
81
+ assert_equal '⚡👻', file.items[0].value
82
+ end
83
+
84
+ def test_can_parse_utf16le_files_with_bom
85
+ file = DotStrings.parse_file('test/fixtures/utf16le_bom.strings')
86
+
87
+ assert_equal 1, file.items.size
88
+ assert_equal 'key', file.items[0].key
89
+ assert_equal 'value', file.items[0].value
90
+ end
91
+
92
+ def test_can_parse_utf16be_files_with_bom
93
+ file = DotStrings.parse_file('test/fixtures/utf16be_bom.strings')
94
+
95
+ assert_equal 1, file.items.size
96
+ assert_equal 'key', file.items[0].key
97
+ assert_equal 'value', file.items[0].value
98
+ end
99
+
100
+ def test_can_parse_utf8_files_with_bom
101
+ file = DotStrings.parse_file('test/fixtures/utf8_bom.strings')
102
+
103
+ assert_equal 1, file.items.size
104
+ assert_equal 'key', file.items[0].key
105
+ assert_equal 'value', file.items[0].value
106
+ end
107
+ end
data/test/test_file.rb ADDED
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class TestFile < MiniTest::Test
6
+ def test_delete
7
+ items = [
8
+ DotStrings::Item.new(key: 'key 1', value: 'value 1'),
9
+ DotStrings::Item.new(key: 'key 2', value: 'value 2'),
10
+ DotStrings::Item.new(key: 'key 3', value: 'value 3')
11
+ ]
12
+
13
+ file = DotStrings::File.new(items)
14
+
15
+ file.delete('key 2')
16
+ assert_equal 2, file.items.size
17
+ assert_equal ['key 1', 'key 3'], file.keys
18
+ end
19
+
20
+ def test_access_by_key
21
+ items = [
22
+ DotStrings::Item.new(key: 'key 1', value: 'value 1'),
23
+ DotStrings::Item.new(key: 'key 2', value: 'value 2'),
24
+ DotStrings::Item.new(key: 'key 3', value: 'value 3')
25
+ ]
26
+
27
+ file = DotStrings::File.new(items)
28
+
29
+ assert_equal 'value 1', file['key 1'].value
30
+ assert_equal 'value 2', file['key 2'].value
31
+ assert_equal 'value 3', file['key 3'].value
32
+ end
33
+
34
+ def test_append
35
+ file = DotStrings::File.new
36
+ file.append(DotStrings::Item.new(key: 'key 1', value: 'value 1'))
37
+ assert_equal 1, file.items.size
38
+ end
39
+
40
+ def test_to_string
41
+ items = [
42
+ DotStrings::Item.new(comment: 'Comment 1', key: 'key 1', value: 'value 1'),
43
+ DotStrings::Item.new(comment: 'Comment 2', key: 'key 2', value: 'value 2'),
44
+ DotStrings::Item.new(comment: 'Comment 3', key: 'key 3', value: '👻'),
45
+ DotStrings::Item.new(comment: 'Comment 4', key: "\"'\t\n\r\0", value: "\"'\t\n\r\0")
46
+ ]
47
+
48
+ file = DotStrings::File.new(items)
49
+
50
+ expected = <<~'END_OF_DOCUMENT'
51
+ /* Comment 1 */
52
+ "key 1" = "value 1";
53
+
54
+ /* Comment 2 */
55
+ "key 2" = "value 2";
56
+
57
+ /* Comment 3 */
58
+ "key 3" = "👻";
59
+
60
+ /* Comment 4 */
61
+ "\"\'\t\n\r\0" = "\"\'\t\n\r\0";
62
+ END_OF_DOCUMENT
63
+
64
+ assert_equal expected, file.to_s
65
+ end
66
+ end
data/test/test_item.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class TestFile < MiniTest::Test
6
+ def test_to_s
7
+ item = DotStrings::Item.new(comment: 'Comment', key: 'key 1', value: 'value 1')
8
+ assert_equal "/* Comment */\n\"key 1\" = \"value 1\";", item.to_s
9
+ end
10
+
11
+ def test_to_s_with_nil_comment
12
+ item = DotStrings::Item.new(comment: nil, key: 'key 1', value: 'value 1')
13
+ assert_equal '"key 1" = "value 1";', item.to_s
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,177 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dotstrings
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ramon Torres
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-07-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.14'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.14'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Parse and create .strings files used in localization of iOS and macOS
98
+ apps.
99
+ email:
100
+ - raymondjavaxx@gmail.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".editorconfig"
106
+ - ".github/workflows/ci.yml"
107
+ - ".gitignore"
108
+ - ".rubocop.yml"
109
+ - Gemfile
110
+ - Gemfile.lock
111
+ - LICENSE
112
+ - README.md
113
+ - Rakefile
114
+ - dotstrings.gemspec
115
+ - lib/dotstrings.rb
116
+ - lib/dotstrings/errors.rb
117
+ - lib/dotstrings/file.rb
118
+ - lib/dotstrings/item.rb
119
+ - lib/dotstrings/parser.rb
120
+ - lib/dotstrings/version.rb
121
+ - test/fixtures/escaped_backslashes.strings
122
+ - test/fixtures/escaped_carriage_returns.strings
123
+ - test/fixtures/escaped_new_lines.strings
124
+ - test/fixtures/escaped_nil.strings
125
+ - test/fixtures/escaped_quotes.strings
126
+ - test/fixtures/escaped_single_quotes.strings
127
+ - test/fixtures/escaped_tabs.strings
128
+ - test/fixtures/escaped_unicode.strings
129
+ - test/fixtures/utf16be_bom.strings
130
+ - test/fixtures/utf16le_bom.strings
131
+ - test/fixtures/utf8_bom.strings
132
+ - test/fixtures/valid.strings
133
+ - test/helper.rb
134
+ - test/test_dotstrings.rb
135
+ - test/test_file.rb
136
+ - test/test_item.rb
137
+ homepage: https://github.com/raymondjavaxx/dotstrings
138
+ licenses:
139
+ - MIT
140
+ metadata:
141
+ rubygems_mfa_required: 'true'
142
+ post_install_message:
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: 2.5.0
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubygems_version: 3.0.1
158
+ signing_key:
159
+ specification_version: 4
160
+ summary: Parse and create .strings files used in localization of iOS and macOS apps.
161
+ test_files:
162
+ - test/fixtures/escaped_backslashes.strings
163
+ - test/fixtures/escaped_carriage_returns.strings
164
+ - test/fixtures/escaped_new_lines.strings
165
+ - test/fixtures/escaped_nil.strings
166
+ - test/fixtures/escaped_quotes.strings
167
+ - test/fixtures/escaped_single_quotes.strings
168
+ - test/fixtures/escaped_tabs.strings
169
+ - test/fixtures/escaped_unicode.strings
170
+ - test/fixtures/utf16be_bom.strings
171
+ - test/fixtures/utf16le_bom.strings
172
+ - test/fixtures/utf8_bom.strings
173
+ - test/fixtures/valid.strings
174
+ - test/helper.rb
175
+ - test/test_dotstrings.rb
176
+ - test/test_file.rb
177
+ - test/test_item.rb