polytypo 1.9.0 → 1.11.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.
@@ -34,9 +34,32 @@ module Polytypo
34
34
  spans = []
35
35
  li = 0
36
36
  li = scan_line(chars, lines, li, keys, spans) while li < lines.length
37
- spans
37
+ drop_carriage_return_regions(chars, lines, spans)
38
38
  end
39
39
 
40
+ # 3.8.4's carriage-return region: a line containing a U+000D yields no spans, and neither does
41
+ # any later line up to the first one that is non-empty, does not begin with U+0020 and
42
+ # contains none. It removes spans and never adds one.
43
+ def self.drop_carriage_return_regions(chars, lines, spans)
44
+ in_region = false
45
+ declined = lines.map do |line|
46
+ if lone_cr?(chars, line)
47
+ in_region = true
48
+ elsif line.end > line.start && chars[line.start] != " "
49
+ in_region = false
50
+ end
51
+ in_region
52
+ end
53
+ return spans unless declined.any?
54
+
55
+ li = 0
56
+ spans.reject do |span|
57
+ li += 1 while li + 1 < lines.length && span.start >= lines[li + 1].start
58
+ declined[li]
59
+ end
60
+ end
61
+ private_class_method :drop_carriage_return_regions
62
+
40
63
  # 3.8.4: a line ends at U+000A, and A U+000D IMMEDIATELY BEFORE IT IS NOT PART OF THE LINE
41
64
  # -- it is a terminator like the U+000A itself, so it lies outside every span and comes back
42
65
  # untouched. Without that clause a CRLF file behaves differently from the same bytes with
@@ -70,11 +93,24 @@ module Polytypo
70
93
  end
71
94
  private_class_method :blank?
72
95
 
96
+ # 3.8.4 step 7: a line of the value run that is not blank. Blank lines alone are not one.
97
+ def self.continuation?(chars, lines, li, run_end)
98
+ ((li + 1)...run_end).any? { |k| !blank?(chars, lines[k]) }
99
+ end
100
+ private_class_method :continuation?
101
+
73
102
  def self.tab?(chars, line)
74
103
  (line.start...line.end).any? { |i| chars[i] == TAB }
75
104
  end
76
105
  private_class_method :tab?
77
106
 
107
+ # A U+000D the line definition did not take for a terminator, i.e. one that is not part of
108
+ # a CRLF and does not end the input. It would otherwise land inside a span.
109
+ def self.lone_cr?(chars, line)
110
+ (line.start...line.end).any? { |i| chars[i] == CR }
111
+ end
112
+ private_class_method :lone_cr?
113
+
78
114
  # 3.8.4 step 3. "---" and "..." at the head of a line, bare or introducing a node: the
79
115
  # trailing-content form is declined too, so "--- key: value" never yields a key of
80
116
  # "--- key".
@@ -122,59 +158,171 @@ module Polytypo
122
158
  end
123
159
  private_class_method :value_run_end
124
160
 
161
+ # 3.8.4 step 5: the index of the quote that closes the one at q, or -1. In a double-quoted
162
+ # scalar a U+005C and the code point after it never close the quote, which decodes nothing:
163
+ # no YAML escape ends in anything that would be a closing quote. A quote not closed on the
164
+ # line returns -1.
165
+ def self.closing_quote(chars, line, q)
166
+ quote = chars[q]
167
+ j = q + 1
168
+ while j < line.end
169
+ c = chars[j]
170
+ if (quote == '"' && c == "\\") || (quote == "'" && c == quote && j + 1 < line.end && chars[j + 1] == "'")
171
+ j += 2
172
+ elsif c != quote
173
+ j += 1
174
+ else
175
+ return j
176
+ end
177
+ end
178
+ -1
179
+ end
180
+ private_class_method :closing_quote
181
+
182
+ # 3.8.4 step 3: the next line that begins in column 0 with a document marker.
183
+ def self.document_run_end(chars, lines, li)
184
+ k = li + 1
185
+ k += 1 while k < lines.length && !document_marker?(chars, lines[k].start, lines[k].end)
186
+ k
187
+ end
188
+ private_class_method :document_run_end
189
+
190
+ # 3.8.4 step 5's own-line node: everything up to the first later line that is non-empty and
191
+ # does not begin with U+0020, because the scan does not track the key above.
192
+ def self.own_line_run_end(chars, lines, li, indent)
193
+ return document_run_end(chars, lines, li) if indent.zero?
194
+
195
+ k = li + 1
196
+ while k < lines.length
197
+ nxt = lines[k]
198
+ break if nxt.end > nxt.start && chars[nxt.start] != SPACE
199
+
200
+ k += 1
201
+ end
202
+ k
203
+ end
204
+ private_class_method :own_line_run_end
205
+
206
+ # 3.8.4 step 4: "-", "?" or ":" followed by U+0020 or the line end.
207
+ def self.node_marker?(chars, j, to)
208
+ return false unless ["-", "?", ":"].include?(chars[j])
209
+
210
+ j + 1 == to || chars[j + 1] == SPACE
211
+ end
212
+ private_class_method :node_marker?
213
+
214
+ # 3.8.4 step 5. Returns [colon, key_end, declined, open_quote]; colon is -1 for a line with
215
+ # no key, which is a node line.
216
+ def self.locate_key(chars, line, i)
217
+ first = chars[i]
218
+ if ['"', "'"].include?(first)
219
+ close = closing_quote(chars, line, i)
220
+ return [-1, -1, false, true] if close.negative?
221
+
222
+ p = close + 1
223
+ p += 1 while p < line.end && chars[p] == SPACE
224
+ return [p, close + 1, true, false] if p < line.end && indicator_colon?(chars, p, line.end)
225
+
226
+ return [-1, -1, false, false]
227
+ end
228
+ return [-1, -1, false, false] if ["{", "[", "&", "*", "!"].include?(first)
229
+
230
+ declined = false
231
+ (i...line.end).each do |j|
232
+ c = chars[j]
233
+ break if c == "#" && chars[j - 1] == SPACE
234
+
235
+ if indicator_colon?(chars, j, line.end)
236
+ key_end = j
237
+ key_end -= 1 while key_end > i && chars[key_end - 1] == SPACE
238
+ return [j, key_end, declined, false]
239
+ end
240
+ declined = true if DECLINING_KEY_CHARS.include?(c)
241
+ end
242
+ [-1, -1, false, false]
243
+ end
244
+ private_class_method :locate_key
245
+
246
+ # 3.8.4 step 7: whether a declined key's value can continue on the next line.
247
+ def self.declined_value_open?(chars, line, v)
248
+ value = chars[v]
249
+ return true if ["|", ">", "{", "[", "&", "!"].include?(value)
250
+ return false unless ['"', "'"].include?(value)
251
+
252
+ close = closing_quote(chars, line, v)
253
+ return true if close.negative?
254
+
255
+ p = close + 1
256
+ p += 1 while p < line.end && chars[p] == SPACE
257
+ p < line.end && chars[p] != "#"
258
+ end
259
+ private_class_method :declined_value_open?
260
+
125
261
  # One step of 3.8.4. Returns the index of the next line to scan.
126
262
  def self.scan_line(chars, lines, li, keys, spans)
127
263
  line = lines[li]
128
264
  skip_line = li + 1
129
265
 
130
- # step 1 -- blank, or a tab anywhere, which makes indentation undecidable.
266
+ # step 1 -- blank. A tab makes indentation undecidable, so the line and whatever it
267
+ # absorbs yield no spans, but steps 2 to 7 still decide what it absorbs.
131
268
  start = first_non_space(chars, line)
132
- return skip_line if start == line.end || tab?(chars, line)
269
+ return skip_line if start == line.end
133
270
 
271
+ quiet = tab?(chars, line)
134
272
  indent = start - line.start
135
273
 
136
- # steps 2 and 3 -- comment, directive, document marker.
274
+ # steps 2 and 3 -- comment, directive, document marker. A marker with a node after it
275
+ # absorbs the document.
137
276
  return skip_line if ["#", "%"].include?(chars[start])
138
- return skip_line if document_marker?(chars, start, line.end)
139
277
 
140
- # step 4 -- block sequence entries are consumed, not skipped; "- - key: v" nests.
278
+ if document_marker?(chars, start, line.end)
279
+ p = start + 3
280
+ p += 1 while p < line.end && chars[p] == SPACE
281
+ return skip_line if p >= line.end || chars[p] == "#"
282
+
283
+ return indent.zero? ? document_run_end(chars, lines, li) : skip_line
284
+ end
285
+
286
+ # step 4 -- node markers are consumed, not skipped; "- - key: v" nests.
141
287
  i = start
142
- while i < line.end && sequence_dash?(chars, i, line.end)
288
+ while i < line.end && node_marker?(chars, i, line.end)
143
289
  i += 2
144
290
  i += 1 while i < line.end && chars[i] == SPACE
145
291
  end
146
- return skip_line if i >= line.end
292
+ return skip_line if i >= line.end || chars[i] == "#"
147
293
 
148
294
  # step 5 -- find the key. A colon NOT followed by U+0020 or the line end is an ordinary
149
295
  # key character, so "a:b: v" has the key "a:b"; stating that is what keeps five scanners
150
296
  # agreeing.
151
- key_start = i
152
- colon = -1
153
- (i...line.end).each do |j|
154
- return skip_line if DECLINING_KEY_CHARS.include?(chars[j])
297
+ colon, key_end, declined, open_quote = locate_key(chars, line, i)
155
298
 
156
- if indicator_colon?(chars, j, line.end)
157
- colon = j
158
- break
159
- end
160
- end
161
- return skip_line if colon.negative?
299
+ # No key: the line is a node of its own, and it absorbs its value run. One that opens a
300
+ # flow collection, a block scalar or a quote and sits on a line of its own absorbs the
301
+ # region.
302
+ if colon.negative?
303
+ own_line = i == start && (open_quote || ["{", "[", "|", ">"].include?(chars[i]))
304
+ return own_line_run_end(chars, lines, li, indent) if own_line
162
305
 
163
- key_end = colon
164
- key_end -= 1 while key_end > key_start && chars[key_end - 1] == SPACE
165
- return skip_line if key_end <= key_start
306
+ return value_run_end(chars, lines, li, indent)
307
+ end
166
308
 
167
309
  # step 6 -- an empty value means a nested node, whose lines ARE scanned on their own.
168
310
  v = colon + 1
169
311
  v += 1 while v < line.end && chars[v] == SPACE
170
312
  return skip_line if v >= line.end
171
313
 
172
- # step 7 -- the line carries an inline value, so its continuation lines belong to it.
314
+ # step 7 -- the line carries an inline value, so its continuation lines belong to it. A
315
+ # declined key's value absorbs them only if it can continue on the next line.
316
+ if declined
317
+ return declined_value_open?(chars, line, v) ? value_run_end(chars, lines, li, indent) : skip_line
318
+ end
319
+
173
320
  nxt = value_run_end(chars, lines, li, indent)
174
321
 
175
322
  # step 8 -- the key must be listed. Checked before the value's form, so an unlisted key
176
323
  # costs nothing to decline: this is what makes "run:", "if:" and "image:" unreachable.
177
- return nxt unless keys.include?(chars[key_start...key_end].join)
324
+ return nxt unless keys.include?(chars[i...key_end].join)
325
+ return nxt if quiet
178
326
 
179
327
  # step 9 -- the scalar form.
180
328
  value = chars[v]
@@ -183,8 +331,8 @@ module Polytypo
183
331
  if ["|", ">"].include?(value)
184
332
  block_scalar(chars, line, lines[(li + 1)...nxt], indent, v, spans)
185
333
  elsif ['"', "'"].include?(value)
186
- quoted_scalar(chars, line, v, spans) if nxt == li + 1
187
- elsif nxt == li + 1
334
+ quoted_scalar(chars, line, v, spans) unless continuation?(chars, lines, li, nxt)
335
+ elsif !continuation?(chars, lines, li, nxt)
188
336
  plain_scalar(chars, line, v, spans)
189
337
  end
190
338
  nxt
@@ -217,17 +365,18 @@ module Polytypo
217
365
  h += 1 while h < line.end && chars[h] == SPACE
218
366
  return if h < line.end && chars[h] != "#"
219
367
 
220
- # One definition of the run, and three conditions that make the whole block yield no
368
+ # One definition of the run, and four conditions that make the whole block yield no
221
369
  # spans.
222
370
  content = []
223
371
  content_indent = -1
224
372
  run_lines.each do |nxt|
225
373
  next if blank?(chars, nxt) # blank lines belong to the block and yield no span
226
374
 
227
- # A tab makes indentation undecidable; an explicit indicator that disagrees with the
228
- # block as written, or a later line dedented inside it, is ambiguous rather than
229
- # guessable. All three make the WHOLE block yield no spans -- bail rather than choose.
230
- break content_indent = -1 if tab?(chars, nxt)
375
+ # A tab makes indentation undecidable, and a lone U+000D would land inside a span; an
376
+ # explicit indicator that disagrees with the block as written, or a later line dedented
377
+ # inside it, is ambiguous rather than guessable. All of them make the WHOLE block yield
378
+ # no spans -- bail rather than choose.
379
+ break content_indent = -1 if tab?(chars, nxt) || lone_cr?(chars, nxt)
231
380
 
232
381
  next_indent = first_non_space(chars, nxt) - nxt.start
233
382
  if content_indent.negative?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polytypo
4
- VERSION = "1.9.0"
4
+ VERSION = "1.11.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polytypo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Iurii Rogulia
@@ -57,6 +57,7 @@ files:
57
57
  - lib/polytypo/data/fixtures/pt-BR.json
58
58
  - lib/polytypo/data/fixtures/pt-PT.json
59
59
  - lib/polytypo/data/fixtures/ru.json
60
+ - lib/polytypo/data/fixtures/sk.json
60
61
  - lib/polytypo/data/fixtures/sv.json
61
62
  - lib/polytypo/data/fixtures/tr.json
62
63
  - lib/polytypo/data/fixtures/uk.json
@@ -77,6 +78,7 @@ files:
77
78
  - lib/polytypo/data/locales/pt-PT.json
78
79
  - lib/polytypo/data/locales/registry.json
79
80
  - lib/polytypo/data/locales/ru.json
81
+ - lib/polytypo/data/locales/sk.json
80
82
  - lib/polytypo/data/locales/sv.json
81
83
  - lib/polytypo/data/locales/tr.json
82
84
  - lib/polytypo/data/locales/uk.json