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 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