polytypo 1.17.1 → 1.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/polytypo/data/VERSION +1 -1
- data/lib/polytypo/data/fixtures/bg.json +1 -1
- data/lib/polytypo/data/fixtures/cs.json +1 -1
- data/lib/polytypo/data/fixtures/de-CH.json +1 -1
- data/lib/polytypo/data/fixtures/de-DE.json +9 -26
- data/lib/polytypo/data/fixtures/el.json +1 -1
- data/lib/polytypo/data/fixtures/en-GB.json +172 -1
- data/lib/polytypo/data/fixtures/en-US.json +181 -516
- data/lib/polytypo/data/fixtures/es.json +1 -1
- data/lib/polytypo/data/fixtures/fi.json +1 -1
- data/lib/polytypo/data/fixtures/fr-CA.json +1 -1
- data/lib/polytypo/data/fixtures/fr.json +48 -37
- data/lib/polytypo/data/fixtures/hr.json +1 -1
- data/lib/polytypo/data/fixtures/hu.json +1 -1
- data/lib/polytypo/data/fixtures/it.json +1 -1
- data/lib/polytypo/data/fixtures/locale-resolution.json +1 -1
- data/lib/polytypo/data/fixtures/lt.json +1 -1
- data/lib/polytypo/data/fixtures/nl.json +1 -1
- data/lib/polytypo/data/fixtures/pl.json +1 -1
- data/lib/polytypo/data/fixtures/pt-BR.json +1 -1
- data/lib/polytypo/data/fixtures/pt-PT.json +1 -1
- data/lib/polytypo/data/fixtures/ro.json +1 -1
- data/lib/polytypo/data/fixtures/ru.json +1 -1
- data/lib/polytypo/data/fixtures/sk.json +1 -1
- data/lib/polytypo/data/fixtures/sl.json +1 -1
- data/lib/polytypo/data/fixtures/sr.json +1 -1
- data/lib/polytypo/data/fixtures/sv.json +1 -1
- data/lib/polytypo/data/fixtures/tr.json +1 -1
- data/lib/polytypo/data/fixtures/uk.json +1 -1
- data/lib/polytypo/data/locales/registry.json +1 -1
- data/lib/polytypo/data/rules/modes.md +124 -9
- data/lib/polytypo/data/rules/order.json +1 -1
- data/lib/polytypo/modes/markdown.rb +109 -5
- data/lib/polytypo/modes/runner.rb +13 -4
- data/lib/polytypo/modes/spans.rb +45 -3
- data/lib/polytypo/version.rb +1 -1
- metadata +1 -1
|
@@ -287,12 +287,108 @@ module Polytypo
|
|
|
287
287
|
parser_source = normalize_lone_cr(mask_frontmatter(source, block))
|
|
288
288
|
|
|
289
289
|
spans = []
|
|
290
|
+
state = { stack: [], frontmatter_end: block.nil? ? 0 : block.end, labels: [], url_ends: Set.new }
|
|
290
291
|
ParseError.wrap do
|
|
291
|
-
doc = Commonmarker.parse(
|
|
292
|
-
|
|
292
|
+
doc = Commonmarker.parse(
|
|
293
|
+
parser_source,
|
|
294
|
+
options: { parse: { sourcepos_chars: true }, extension: { footnotes: true } }
|
|
295
|
+
)
|
|
296
|
+
walk(doc, chars, line_starts, spans, state)
|
|
297
|
+
end
|
|
298
|
+
trim_url_tails(chars, Spans.normalize_spans(remove_ranges(spans, state[:labels])), state[:url_ends])
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
URL_PREFIXES = ["www.", "http://", "https://"].freeze
|
|
302
|
+
|
|
303
|
+
# modes.md 3.7.3 (spec 1.17.2): does a bare web address begin at +i+ and fit before +limit+?
|
|
304
|
+
# ASCII case-insensitive, and no test on the character before it.
|
|
305
|
+
def self.starts_url?(chars, i, limit)
|
|
306
|
+
URL_PREFIXES.any? do |word|
|
|
307
|
+
next false if i + word.length > limit
|
|
308
|
+
|
|
309
|
+
word.each_char.with_index.all? do |ch, k|
|
|
310
|
+
c = chars[i + k]
|
|
311
|
+
(c.ord >= 0x41 && c.ord <= 0x5A ? c.downcase : c) == ch
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
private_class_method :starts_url?
|
|
316
|
+
|
|
317
|
+
URL_TERMINATORS = [SPACE, TAB, LF, CR, "<"].freeze
|
|
318
|
+
|
|
319
|
+
# modes.md 3.7.3 (spec 1.17.2): the tail of a bare web address, found by scanning the source
|
|
320
|
+
# rather than asking a parser, because parsers disagree about where one may start and end. A
|
|
321
|
+
# span that begins at the U+0020 or U+0009 ending such an address is flagged +url_tail+, which
|
|
322
|
+
# guards its first code point (modes.md 3.4, Spans.filter_boundary_edits).
|
|
323
|
+
def self.trim_url_tails(chars, spans, url_ends)
|
|
324
|
+
out = []
|
|
325
|
+
spans.each do |span|
|
|
326
|
+
from = span.start
|
|
327
|
+
i = span.start
|
|
328
|
+
guard = span.url_tail == true ||
|
|
329
|
+
(url_ends.include?(span.start) && [SPACE, TAB].include?(chars[span.start]))
|
|
330
|
+
while i < span.end
|
|
331
|
+
unless (i == span.start && url_ends.include?(i)) || starts_url?(chars, i, span.end)
|
|
332
|
+
i += 1
|
|
333
|
+
next
|
|
334
|
+
end
|
|
335
|
+
e = i
|
|
336
|
+
e += 1 while e < span.end && !URL_TERMINATORS.include?(chars[e])
|
|
337
|
+
if e == i
|
|
338
|
+
i += 1
|
|
339
|
+
next
|
|
340
|
+
end
|
|
341
|
+
out << Spans::Span.new(from, i, guard ? true : nil) if i > from
|
|
342
|
+
guard = e < span.end && [SPACE, TAB].include?(chars[e])
|
|
343
|
+
from = e
|
|
344
|
+
i = e
|
|
345
|
+
end
|
|
346
|
+
out << Spans::Span.new(from, span.end, guard ? true : nil) if from < span.end
|
|
293
347
|
end
|
|
294
|
-
|
|
348
|
+
out
|
|
349
|
+
end
|
|
350
|
+
private_class_method :trim_url_tails
|
|
351
|
+
|
|
352
|
+
def self.remove_ranges(spans, ranges)
|
|
353
|
+
return spans if ranges.empty?
|
|
354
|
+
|
|
355
|
+
spans.flat_map do |span|
|
|
356
|
+
ranges.reduce([span]) do |pieces, r|
|
|
357
|
+
pieces.flat_map do |q|
|
|
358
|
+
next [q] if r.end <= q.start || r.start >= q.end
|
|
359
|
+
|
|
360
|
+
res = []
|
|
361
|
+
res << Spans::Span.new(q.start, r.start) if q.start < r.start
|
|
362
|
+
res << Spans::Span.new(r.end, q.end) if r.end < q.end
|
|
363
|
+
res
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
private_class_method :remove_ranges
|
|
369
|
+
|
|
370
|
+
# modes.md 3.7.3 (spec 1.17.2): the text of a shortcut or collapsed reference is its label.
|
|
371
|
+
# A node exists only when the parser formed it from a definition, so the kind is read off the
|
|
372
|
+
# source after the closing bracket of the text: nothing (shortcut), "[]" (collapsed); a
|
|
373
|
+
# resource "(...)" or a non-empty "[label]" (full) leaves the text processable.
|
|
374
|
+
def self.reference_label_range(node, chars, line_starts)
|
|
375
|
+
sp = node.source_position
|
|
376
|
+
start_offset = offset_of(line_starts, sp[:start_line], sp[:start_column])
|
|
377
|
+
end_offset = offset_of(line_starts, sp[:end_line], sp[:end_column])
|
|
378
|
+
last = node.to_a.last
|
|
379
|
+
return nil if last.nil?
|
|
380
|
+
|
|
381
|
+
lsp = last.source_position
|
|
382
|
+
close = offset_of(line_starts, lsp[:end_line], lsp[:end_column]) + 1
|
|
383
|
+
return nil unless chars[close] == "]"
|
|
384
|
+
|
|
385
|
+
shortcut = end_offset == close
|
|
386
|
+
collapsed = end_offset == close + 2 && chars[close + 1] == "[" && chars[close + 2] == "]"
|
|
387
|
+
return nil unless shortcut || collapsed
|
|
388
|
+
|
|
389
|
+
Spans::Span.new(start_offset + (node.type == :image ? 2 : 1), close)
|
|
295
390
|
end
|
|
391
|
+
private_class_method :reference_label_range
|
|
296
392
|
|
|
297
393
|
# line_starts[i] is the 0-based code-point index at which comrak's 1-based line i begins.
|
|
298
394
|
# comrak counts CommonMark line endings, which are U+000A, U+000D, and U+000D U+000A --
|
|
@@ -347,12 +443,20 @@ module Polytypo
|
|
|
347
443
|
if type == :link || type == :image
|
|
348
444
|
sp = node.source_position
|
|
349
445
|
start_offset = offset_of(line_starts, sp[:start_line], sp[:start_column])
|
|
350
|
-
|
|
446
|
+
opener = type == :image ? start_offset + 1 : start_offset
|
|
447
|
+
unless chars[opener] == "[" && (type == :link || chars[start_offset] == "!")
|
|
351
448
|
# No leading "[" at the link's own source position: this is an autolink
|
|
352
449
|
# (<https://…> or a bare GFM autolink literal), not a real [text](url) link --
|
|
353
|
-
# skipped whole, per modes.md 3.7.3.
|
|
450
|
+
# skipped whole, per modes.md 3.7.3. The end of a literal that begins with a web
|
|
451
|
+
# prefix is where the tail scan resumes.
|
|
452
|
+
if type == :link && chars[start_offset] != "<" &&
|
|
453
|
+
starts_url?(chars, start_offset, chars.length)
|
|
454
|
+
state[:url_ends] << (offset_of(line_starts, sp[:end_line], sp[:end_column]) + 1)
|
|
455
|
+
end
|
|
354
456
|
return
|
|
355
457
|
end
|
|
458
|
+
label = reference_label_range(node, chars, line_starts)
|
|
459
|
+
state[:labels] << label unless label.nil?
|
|
356
460
|
# A real link/image: fall through to the generic child walk below, which emits the
|
|
357
461
|
# link text (or image alt text) as processable prose. The destination/title are never
|
|
358
462
|
# represented as child nodes at all, so there is nothing further to exclude.
|
|
@@ -13,12 +13,16 @@ module Polytypo
|
|
|
13
13
|
module Runner
|
|
14
14
|
# The same sequence as Engine::Pipeline.run_rules, with the two boundary filters of
|
|
15
15
|
# modes.md 3.4 interposed.
|
|
16
|
-
def self.run_rules_over_spans(cp, plan, locale_data, ctx)
|
|
16
|
+
def self.run_rules_over_spans(cp, plan, locale_data, ctx, guarded = [])
|
|
17
17
|
current = cp
|
|
18
|
+
markdown = ctx.mode == "markdown"
|
|
18
19
|
plan.each do |rule_id|
|
|
19
20
|
fn = Engine::Registry.rule(rule_id)
|
|
20
21
|
edits = fn.call(current, locale_data, ctx)
|
|
21
|
-
filtered = Spans.filter_boundary_edits(
|
|
22
|
+
filtered = Spans.filter_boundary_edits(
|
|
23
|
+
current, edits, Spans.span_ranges_of(current),
|
|
24
|
+
delimiter_neighbours: markdown, guarded: guarded
|
|
25
|
+
)
|
|
22
26
|
next if filtered.empty?
|
|
23
27
|
|
|
24
28
|
current = Engine::Edits.apply_edits(current, filtered, rule_id)
|
|
@@ -61,7 +65,8 @@ module Polytypo
|
|
|
61
65
|
return [] if normalized.empty?
|
|
62
66
|
|
|
63
67
|
concatenated = Spans.concatenate_spans(source_cp, normalized)
|
|
64
|
-
|
|
68
|
+
guarded = normalized.map { |s| s.url_tail == true }
|
|
69
|
+
transformed = run_rules_over_spans(concatenated, plan, locale_data, ctx, guarded)
|
|
65
70
|
pieces = Spans.split_on_marker(transformed, normalized.length)
|
|
66
71
|
normalized.each_with_index.map { |span, i| [span, pieces[i]] }
|
|
67
72
|
end
|
|
@@ -91,12 +96,16 @@ module Polytypo
|
|
|
91
96
|
normalized = Spans.normalize_spans(spans)
|
|
92
97
|
return [] if normalized.empty?
|
|
93
98
|
|
|
99
|
+
guarded = normalized.map { |s| s.url_tail == true }
|
|
94
100
|
Engine::Pipeline.run_rules_recording(
|
|
95
101
|
Spans.concatenate_spans(source_cp, normalized),
|
|
96
102
|
plan, locale_data, ctx,
|
|
97
103
|
Spans.origin_of_spans(normalized),
|
|
98
104
|
source_cp.length
|
|
99
|
-
)
|
|
105
|
+
) do |current, edits|
|
|
106
|
+
Spans.filter_boundary_edits(current, edits, Spans.span_ranges_of(current),
|
|
107
|
+
delimiter_neighbours: ctx.mode == "markdown", guarded: guarded)
|
|
108
|
+
end
|
|
100
109
|
end
|
|
101
110
|
end
|
|
102
111
|
end
|
data/lib/polytypo/modes/spans.rb
CHANGED
|
@@ -18,7 +18,10 @@ module Polytypo
|
|
|
18
18
|
# A processable span, identified by its offsets in the original source, addressed as
|
|
19
19
|
# code-point indices (ARCHITECTURE.md section 4.2) -- Ruby String indices are already
|
|
20
20
|
# code-point indices for a UTF-8 String, no separate byte/char distinction to manage here.
|
|
21
|
-
|
|
21
|
+
#
|
|
22
|
+
# +url_tail+ is true (or nil) on a span that begins at the U+0020 or U+0009 ending a bare web
|
|
23
|
+
# address, so the filter can guard its first code point (modes.md 3.7.3, spec 1.17.2).
|
|
24
|
+
Span = Struct.new(:start, :end, :url_tail)
|
|
22
25
|
|
|
23
26
|
# A span's extent in the concatenated code-point array: s0/s1 of modes.md 3.4. `:first`
|
|
24
27
|
# deliberately shadows Struct#first with the equivalent one-argument-less accessor -- this
|
|
@@ -50,7 +53,7 @@ module Polytypo
|
|
|
50
53
|
)
|
|
51
54
|
end
|
|
52
55
|
if span.start == last.end
|
|
53
|
-
out[-1] = Span.new(last.start, span.end)
|
|
56
|
+
out[-1] = Span.new(last.start, span.end, last.url_tail)
|
|
54
57
|
else
|
|
55
58
|
out << span
|
|
56
59
|
end
|
|
@@ -106,6 +109,35 @@ module Polytypo
|
|
|
106
109
|
end
|
|
107
110
|
private_class_method :span_containing
|
|
108
111
|
|
|
112
|
+
DELIMITERS = [0x2A, 0x5F, 0x7E].freeze
|
|
113
|
+
|
|
114
|
+
def self.markdown_whitespace?(c)
|
|
115
|
+
return false if c.nil?
|
|
116
|
+
|
|
117
|
+
[0x09, 0x0A, 0x0C, 0x0D, 0x20, 0xA0, 0x1680, 0x202F, 0x205F, 0x3000].include?(c) ||
|
|
118
|
+
(c >= 0x2000 && c <= 0x200A)
|
|
119
|
+
end
|
|
120
|
+
private_class_method :markdown_whitespace?
|
|
121
|
+
|
|
122
|
+
# modes.md 3.4 delimiter-neighbour rule (markdown only, spec 1.17.2): an edit is discarded
|
|
123
|
+
# when, beside U+002A, U+005F or U+007E, exactly one of the old and new neighbour is
|
|
124
|
+
# CommonMark whitespace. A marker belongs to neither class, nor does a position outside the
|
|
125
|
+
# array. An insertion has q = p - 1.
|
|
126
|
+
def self.changes_delimiter_flank?(cp, p, q, replacement)
|
|
127
|
+
r = replacement.length
|
|
128
|
+
before_edit = p.positive? ? cp[p - 1] : nil
|
|
129
|
+
if DELIMITERS.include?(before_edit)
|
|
130
|
+
after = r.positive? ? replacement[0] : cp[q + 1]
|
|
131
|
+
return true if markdown_whitespace?(cp[p]) != markdown_whitespace?(after)
|
|
132
|
+
end
|
|
133
|
+
if q + 1 < cp.length && DELIMITERS.include?(cp[q + 1])
|
|
134
|
+
after = r.positive? ? replacement[r - 1] : before_edit
|
|
135
|
+
return true if markdown_whitespace?(q.negative? ? nil : cp[q]) != markdown_whitespace?(after)
|
|
136
|
+
end
|
|
137
|
+
false
|
|
138
|
+
end
|
|
139
|
+
private_class_method :changes_delimiter_flank?
|
|
140
|
+
|
|
109
141
|
# modes.md 3.4, two safety nets, both pure functions of (p, q, r, s0, s1):
|
|
110
142
|
#
|
|
111
143
|
# 1. No edit may contain a marker -- one that does is a bug, discarded rather than
|
|
@@ -118,7 +150,11 @@ module Polytypo
|
|
|
118
150
|
# clause tests the CHARACTER, and only U+0020 needs testing -- it is the one code
|
|
119
151
|
# point any rule emits whose meaning comes from its position rather than from itself
|
|
120
152
|
# (modes.md 5 item 2).
|
|
121
|
-
|
|
153
|
+
#
|
|
154
|
+
# +delimiter_neighbours+ (markdown only) enables the delimiter-neighbour rule of modes.md 3.4,
|
|
155
|
+
# and +guarded+ holds, per span, whether its first code point is the terminator of a bare web
|
|
156
|
+
# address (modes.md 3.7.3); both spec 1.17.2.
|
|
157
|
+
def self.filter_boundary_edits(cp, edits, ranges, delimiter_neighbours: false, guarded: [])
|
|
122
158
|
edits.select do |edit|
|
|
123
159
|
next false if (edit.start...edit.end).any? { |i| Engine.marker?(cp[i]) }
|
|
124
160
|
|
|
@@ -126,7 +162,13 @@ module Polytypo
|
|
|
126
162
|
q = edit.end - 1
|
|
127
163
|
d = edit.end - edit.start
|
|
128
164
|
r = edit.replacement.length
|
|
165
|
+
next false if delimiter_neighbours && changes_delimiter_flank?(cp, p, q, edit.replacement)
|
|
166
|
+
|
|
129
167
|
span = span_containing(ranges, p)
|
|
168
|
+
if span && p == span.first && guarded[ranges.index(span)] == true &&
|
|
169
|
+
(r.zero? || edit.replacement[0] != SPACE)
|
|
170
|
+
next false
|
|
171
|
+
end
|
|
130
172
|
next true unless span && (p == span.first || q == span.last)
|
|
131
173
|
next false if r > d
|
|
132
174
|
next false if r.positive? && p == span.first && edit.replacement[0] == SPACE && cp[p] != SPACE
|
data/lib/polytypo/version.rb
CHANGED