polytypo 1.0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +109 -0
- data/lib/polytypo/data/README.md +20 -0
- data/lib/polytypo/data/UNICODE +1 -0
- data/lib/polytypo/data/VERSION +1 -0
- data/lib/polytypo/data/fixtures/de-CH.json +501 -0
- data/lib/polytypo/data/fixtures/de-DE.json +547 -0
- data/lib/polytypo/data/fixtures/el.json +239 -0
- data/lib/polytypo/data/fixtures/en-GB.json +1274 -0
- data/lib/polytypo/data/fixtures/en-US.json +1807 -0
- data/lib/polytypo/data/fixtures/fi.json +1306 -0
- data/lib/polytypo/data/fixtures/fr-CA.json +268 -0
- data/lib/polytypo/data/fixtures/fr.json +603 -0
- data/lib/polytypo/data/fixtures/locale-resolution.json +209 -0
- data/lib/polytypo/data/fixtures/ru.json +688 -0
- data/lib/polytypo/data/fixtures/sv.json +1290 -0
- data/lib/polytypo/data/locales/de-CH.json +77 -0
- data/lib/polytypo/data/locales/de-DE.json +76 -0
- data/lib/polytypo/data/locales/el.json +90 -0
- data/lib/polytypo/data/locales/en-GB.json +115 -0
- data/lib/polytypo/data/locales/en-US.json +133 -0
- data/lib/polytypo/data/locales/fi.json +136 -0
- data/lib/polytypo/data/locales/fr-CA.json +78 -0
- data/lib/polytypo/data/locales/fr.json +84 -0
- data/lib/polytypo/data/locales/registry.json +9 -0
- data/lib/polytypo/data/locales/ru.json +112 -0
- data/lib/polytypo/data/locales/sv.json +124 -0
- data/lib/polytypo/data/rules/dashes.md +1238 -0
- data/lib/polytypo/data/rules/order.json +78 -0
- data/lib/polytypo/data/schema/fixtures.schema.json +79 -0
- data/lib/polytypo/data/schema/locale.schema.json +235 -0
- data/lib/polytypo/data/schema/registry.schema.json +29 -0
- data/lib/polytypo/data/schema/resolution.schema.json +50 -0
- data/lib/polytypo/engine/codepoints.rb +24 -0
- data/lib/polytypo/engine/edits.rb +64 -0
- data/lib/polytypo/engine/locale.rb +138 -0
- data/lib/polytypo/engine/pipeline.rb +61 -0
- data/lib/polytypo/engine/registry.rb +47 -0
- data/lib/polytypo/engine/rules/apostrophe.rb +127 -0
- data/lib/polytypo/engine/rules/dash_shared.rb +342 -0
- data/lib/polytypo/engine/rules/dashes.rb +125 -0
- data/lib/polytypo/engine/rules/ellipsis.rb +100 -0
- data/lib/polytypo/engine/rules/hyphen.rb +207 -0
- data/lib/polytypo/engine/rules/nbsp.rb +616 -0
- data/lib/polytypo/engine/rules/quote_ambiguity.rb +241 -0
- data/lib/polytypo/engine/rules/quotes.rb +420 -0
- data/lib/polytypo/engine/rules/ranges.rb +124 -0
- data/lib/polytypo/engine/rules/spaces.rb +232 -0
- data/lib/polytypo/engine/rules/symbols.rb +291 -0
- data/lib/polytypo/engine/rules.rb +19 -0
- data/lib/polytypo/engine/sentinels.rb +23 -0
- data/lib/polytypo/engine/unicode_util.rb +390 -0
- data/lib/polytypo/errors.rb +24 -0
- data/lib/polytypo/modes/html.rb +233 -0
- data/lib/polytypo/modes/markdown.rb +187 -0
- data/lib/polytypo/modes/parse_error.rb +19 -0
- data/lib/polytypo/modes/runner.rb +57 -0
- data/lib/polytypo/modes/spans.rb +132 -0
- data/lib/polytypo/version.rb +5 -0
- data/lib/polytypo.rb +91 -0
- data/polytypo.gemspec +37 -0
- metadata +122 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../edits"
|
|
4
|
+
require_relative "../registry"
|
|
5
|
+
require_relative "../sentinels"
|
|
6
|
+
require_relative "../unicode_util"
|
|
7
|
+
require_relative "../../errors"
|
|
8
|
+
|
|
9
|
+
module Polytypo
|
|
10
|
+
module Engine
|
|
11
|
+
module Rules
|
|
12
|
+
# `hyphen` -- spec/rules/hyphen.md (spec 0.1.0), order 35.
|
|
13
|
+
#
|
|
14
|
+
# Replaces U+002D with U+2011 inside a closed list of locale-listed morphological forms
|
|
15
|
+
# (hyphen.prefixes/suffixes/compounds). Explicit index-based scanning over the code-point
|
|
16
|
+
# array only: no regex, no native-string indexing (docs/ARCHITECTURE.md section 4.1, 4.2).
|
|
17
|
+
# All identifiers here live under Rules::Hyphen so they cannot collide with sibling rule
|
|
18
|
+
# files in this same directory.
|
|
19
|
+
module Hyphen
|
|
20
|
+
HYPHEN_MINUS = 0x2D
|
|
21
|
+
NON_BREAKING_HYPHEN = 0x2011
|
|
22
|
+
DIGIT_ZERO = 0x30
|
|
23
|
+
DIGIT_NINE = 0x39
|
|
24
|
+
|
|
25
|
+
# 3.4: the three lists, in the order that breaks a length tie.
|
|
26
|
+
COMPOUND = 0
|
|
27
|
+
PREFIX = 1
|
|
28
|
+
SUFFIX = 2
|
|
29
|
+
|
|
30
|
+
# Out-of-range reads yield NONE, the spec's own boundary value (3.1).
|
|
31
|
+
def self.at(cp, i)
|
|
32
|
+
return Engine::NONE if i.negative? || i >= cp.length
|
|
33
|
+
|
|
34
|
+
cp[i]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.digit?(cp)
|
|
38
|
+
cp >= DIGIT_ZERO && cp <= DIGIT_NINE
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 3.1 HYPHENISH. U+2010, U+00AD, U+2012, U+2013, U+2014 are deliberately absent.
|
|
42
|
+
def self.hyphenish?(cp)
|
|
43
|
+
cp == HYPHEN_MINUS || cp == NON_BREAKING_HYPHEN
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 3.1 WORDISH = ALNUM u HYPHENISH. U+2011 is a member on purpose: without it a converted
|
|
47
|
+
# hyphen would flip a neighbouring form's boundary verdict between runs (5).
|
|
48
|
+
def self.wordish?(cp)
|
|
49
|
+
digit?(cp) || UnicodeUtil.letter?(cp) || hyphenish?(cp)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
Pattern = Struct.new(:cps, :kind)
|
|
53
|
+
|
|
54
|
+
# Converts one locale word list into match patterns of the given kind. hyphen.md 2
|
|
55
|
+
# requires every entry to contain at least one U+002D; an entry with none is meaningless
|
|
56
|
+
# and raises POLYTYPO_MALFORMED_LOCALE_DATA directly -- Ruby exceptions propagate through
|
|
57
|
+
# the call chain without the panic/recover workaround the Go port needs for the same case.
|
|
58
|
+
def self.prepare_patterns(entries, field, kind, out)
|
|
59
|
+
entries.each do |entry|
|
|
60
|
+
cps = entry.codepoints
|
|
61
|
+
has_hyphen = cps.any? { |cp| cp == HYPHEN_MINUS }
|
|
62
|
+
unless has_hyphen
|
|
63
|
+
raise Polytypo::Error.new(
|
|
64
|
+
Polytypo::CODE_MALFORMED_LOCALE_DATA,
|
|
65
|
+
"hyphen.#{field} entry #{entry.inspect} contains no U+002D; there is nothing " \
|
|
66
|
+
"for the rule to convert (spec/rules/hyphen.md section 2).",
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
out << Pattern.new(cps, kind)
|
|
70
|
+
end
|
|
71
|
+
out
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# 3.3 -- hyphen-lenient, first-character-lenient literal matching. The hyphen leniency
|
|
75
|
+
# covers j = 0 too, because a suffix entry begins with its own hyphen and must keep
|
|
76
|
+
# matching its own output once that hyphen has already been converted to U+2011 (5's
|
|
77
|
+
# idempotency argument depends on this). The first-character case leniency uses the
|
|
78
|
+
# Unicode simple uppercase mapping of the *pattern*, never a locale-dependent case fold
|
|
79
|
+
# of the input (ARCHITECTURE.md section 4.4).
|
|
80
|
+
def self.matches_at?(cp, a, w)
|
|
81
|
+
return false if a + w.length > cp.length
|
|
82
|
+
|
|
83
|
+
w.each_with_index do |p, j|
|
|
84
|
+
c = cp[a + j]
|
|
85
|
+
if p == HYPHEN_MINUS
|
|
86
|
+
return false unless hyphenish?(c)
|
|
87
|
+
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
next if c == p
|
|
91
|
+
next if j.zero? && UnicodeUtil.letter?(p) && c == UnicodeUtil.simple_uppercase(p)
|
|
92
|
+
|
|
93
|
+
return false
|
|
94
|
+
end
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# 3.4 -- the longest entry that matches at `a`, ties broken compounds, prefixes,
|
|
99
|
+
# suffixes. `patterns` is built compounds-first, prefixes-second, suffixes-third, so
|
|
100
|
+
# keeping the first-seen entry on a length tie already yields that order.
|
|
101
|
+
def self.select(cp, a, patterns)
|
|
102
|
+
best = nil
|
|
103
|
+
patterns.each do |pattern|
|
|
104
|
+
next unless matches_at?(cp, a, pattern.cps)
|
|
105
|
+
|
|
106
|
+
best = pattern if best.nil? || pattern.cps.length > best.cps.length
|
|
107
|
+
end
|
|
108
|
+
best
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# 3.4 C -- a compound must be a whole word.
|
|
112
|
+
def self.guard_compound?(cp, a, k)
|
|
113
|
+
before = at(cp, a - 1)
|
|
114
|
+
return false if before != Engine::NONE && wordish?(before)
|
|
115
|
+
|
|
116
|
+
after = at(cp, a + k)
|
|
117
|
+
after == Engine::NONE || !wordish?(after)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# 3.4 P -- a prefix starts a word and must actually prefix something.
|
|
121
|
+
def self.guard_prefix?(cp, a, k)
|
|
122
|
+
before = at(cp, a - 1)
|
|
123
|
+
return false if before != Engine::NONE && wordish?(before)
|
|
124
|
+
|
|
125
|
+
UnicodeUtil.letter?(at(cp, a + k))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# 3.4 S -- a suffix must actually suffix something and must end the word.
|
|
129
|
+
def self.guard_suffix?(cp, a, k)
|
|
130
|
+
return false unless UnicodeUtil.letter?(at(cp, a - 1))
|
|
131
|
+
|
|
132
|
+
after = at(cp, a + k)
|
|
133
|
+
after == Engine::NONE || !wordish?(after)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.bind(index)
|
|
137
|
+
Edit.new(index, index + 1, [NON_BREAKING_HYPHEN], "hyphen")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.scan(cp, locale_data, _ctx)
|
|
141
|
+
hyphen = locale_data["hyphen"]
|
|
142
|
+
prefixes = hyphen["prefixes"]
|
|
143
|
+
suffixes = hyphen["suffixes"]
|
|
144
|
+
compounds = hyphen["compounds"]
|
|
145
|
+
|
|
146
|
+
# 2: with all three lists empty the rule emits nothing for any input -- the common
|
|
147
|
+
# case for every v1 locale except ru.
|
|
148
|
+
return [] if prefixes.empty? && suffixes.empty? && compounds.empty?
|
|
149
|
+
|
|
150
|
+
patterns = []
|
|
151
|
+
prepare_patterns(compounds, "compounds", COMPOUND, patterns)
|
|
152
|
+
prepare_patterns(prefixes, "prefixes", PREFIX, patterns)
|
|
153
|
+
prepare_patterns(suffixes, "suffixes", SUFFIX, patterns)
|
|
154
|
+
|
|
155
|
+
n = cp.length
|
|
156
|
+
edits = []
|
|
157
|
+
a = 0
|
|
158
|
+
while a < n
|
|
159
|
+
selected = select(cp, a, patterns)
|
|
160
|
+
if selected.nil?
|
|
161
|
+
a += 1
|
|
162
|
+
next
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# 3.4: matching and guarding are separate steps, and there is no backtracking. A
|
|
166
|
+
# guard failure ends the position; no shorter entry is tried at `a`.
|
|
167
|
+
w = selected.cps
|
|
168
|
+
k = w.length
|
|
169
|
+
case selected.kind
|
|
170
|
+
when COMPOUND
|
|
171
|
+
unless guard_compound?(cp, a, k)
|
|
172
|
+
a += 1
|
|
173
|
+
next
|
|
174
|
+
end
|
|
175
|
+
w.each_with_index do |p, j|
|
|
176
|
+
edits << bind(a + j) if p == HYPHEN_MINUS && cp[a + j] != NON_BREAKING_HYPHEN
|
|
177
|
+
end
|
|
178
|
+
when PREFIX
|
|
179
|
+
unless guard_prefix?(cp, a, k)
|
|
180
|
+
a += 1
|
|
181
|
+
next
|
|
182
|
+
end
|
|
183
|
+
# The entry's own last code point is its hyphen (hyphen.md 2).
|
|
184
|
+
if w[k - 1] == HYPHEN_MINUS && cp[a + k - 1] != NON_BREAKING_HYPHEN
|
|
185
|
+
edits << bind(a + k - 1)
|
|
186
|
+
end
|
|
187
|
+
else # SUFFIX
|
|
188
|
+
unless guard_suffix?(cp, a, k)
|
|
189
|
+
a += 1
|
|
190
|
+
next
|
|
191
|
+
end
|
|
192
|
+
# A suffix is matched at its own hyphen, which is its first code point.
|
|
193
|
+
edits << bind(a) if w[0] == HYPHEN_MINUS && cp[a] != NON_BREAKING_HYPHEN
|
|
194
|
+
end
|
|
195
|
+
a += k
|
|
196
|
+
end
|
|
197
|
+
edits
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
Polytypo::Engine::Registry.register(
|
|
205
|
+
"hyphen",
|
|
206
|
+
->(cp, locale_data, ctx) { Polytypo::Engine::Rules::Hyphen.scan(cp, locale_data, ctx) },
|
|
207
|
+
)
|