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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c23d2110a873db44936fc87e1c89e41300a82edfdde9c8a357679c1b378cc27
4
- data.tar.gz: 81363243a7425d43383246594362b23740ee22a03b3d8d74f871cf8ea113809c
3
+ metadata.gz: a7f215ea68d4b6ce65de6f15811800b976eb722b89a4618bce4503338508df51
4
+ data.tar.gz: 51a72c22c7233b00df29f97d876abb3e59517de4bf0b78797ab8897d122250fe
5
5
  SHA512:
6
- metadata.gz: 01a9055615619969260d1012c0a027338d85d73495ac664e6ac72c4a6bb7d3368eb4554496d5622940e660ea01d44fe2949bbf3aa88149ab05b284c021792456
7
- data.tar.gz: ca19e173e63d4b820b9cd2ebbd4030ef70bb33080822738a4e290e4ce01b6f6be64c9578a858dffd8ae46d6423e343f0345611f1f1a5a04ca48a2ad9fc7b145f
6
+ metadata.gz: 0d9247c103eb79e3e908a55af5661de97cae316cc36660c7c872a869f077a9875060eab7bab7caa0a9aa8c2ef82d6ce7c5b9acede49fa1227790b4aad53b205d
7
+ data.tar.gz: af144c8d6b3f9f552fc71db2df31db95f1189575fa8eb9e2c5f97ab0a15234024bcd97692218be4bbd04f7352f6a3d63966b4ff4948df84379158733583920ad
@@ -16,6 +16,7 @@ jobs:
16
16
  - "2.6"
17
17
  - "2.7"
18
18
  - "3.0"
19
+ - "3.1"
19
20
 
20
21
  steps:
21
22
  - uses: actions/checkout@v2
data/.gitignore CHANGED
@@ -1,2 +1,5 @@
1
1
  pkg
2
2
  .vscode
3
+ coverage
4
+ doc
5
+ .yardoc
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.1.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 "dotstrings"
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 is invalid.
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'
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DotStrings
4
+ ##
5
+ # Class for errors raised by the parser.
4
6
  class ParsingError < RuntimeError
5
7
  end
6
8
  end
@@ -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
- def to_s
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
 
@@ -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
- def to_s
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 << "\"#{serialize_string(key)}\" = \"#{serialize_string(value)}\";"
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
@@ -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
- @state = STATE_KEY if ch == TOK_QUOTE
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
- @state = STATE_VALUE_SEPARATOR if ch == TOK_EQUALS
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.strip.empty?
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
- @state = STATE_START if ch == TOK_SEMICOLON
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 Style/GuardClause
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 @unicode_buffer.length == 4
201
- codepoint = @unicode_buffer.join.hex
219
+ # Check if we have enough digits to form a codepoint.
220
+ return if @unicode_buffer.length < 4
202
221
 
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'))
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
- # Clear buffer after codepoint is parsed
214
- @unicode_buffer.clear
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 Style/GuardClause
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.strip.empty?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DotStrings
4
- VERSION = '0.1.0'
4
+ VERSION = '0.3.0'
5
5
  end
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";
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'helper'
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 'helper'
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
- items = [
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
- "\"\'\t\n\r\0" = "\"\'\t\n\r\0";
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
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- gem 'minitest'
3
+ require 'simplecov'
4
+ SimpleCov.start
4
5
 
6
+ require 'minitest'
5
7
  require 'minitest/pride'
6
8
  require 'minitest/autorun'
7
9
 
data/test/test_item.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'helper'
3
+ require_relative 'test_helper'
4
4
 
5
5
  class TestFile < MiniTest::Test
6
6
  def test_to_s
@@ -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.1.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-06 00:00:00.000000000 Z
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