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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +109 -0
  4. data/lib/polytypo/data/README.md +20 -0
  5. data/lib/polytypo/data/UNICODE +1 -0
  6. data/lib/polytypo/data/VERSION +1 -0
  7. data/lib/polytypo/data/fixtures/de-CH.json +501 -0
  8. data/lib/polytypo/data/fixtures/de-DE.json +547 -0
  9. data/lib/polytypo/data/fixtures/el.json +239 -0
  10. data/lib/polytypo/data/fixtures/en-GB.json +1274 -0
  11. data/lib/polytypo/data/fixtures/en-US.json +1807 -0
  12. data/lib/polytypo/data/fixtures/fi.json +1306 -0
  13. data/lib/polytypo/data/fixtures/fr-CA.json +268 -0
  14. data/lib/polytypo/data/fixtures/fr.json +603 -0
  15. data/lib/polytypo/data/fixtures/locale-resolution.json +209 -0
  16. data/lib/polytypo/data/fixtures/ru.json +688 -0
  17. data/lib/polytypo/data/fixtures/sv.json +1290 -0
  18. data/lib/polytypo/data/locales/de-CH.json +77 -0
  19. data/lib/polytypo/data/locales/de-DE.json +76 -0
  20. data/lib/polytypo/data/locales/el.json +90 -0
  21. data/lib/polytypo/data/locales/en-GB.json +115 -0
  22. data/lib/polytypo/data/locales/en-US.json +133 -0
  23. data/lib/polytypo/data/locales/fi.json +136 -0
  24. data/lib/polytypo/data/locales/fr-CA.json +78 -0
  25. data/lib/polytypo/data/locales/fr.json +84 -0
  26. data/lib/polytypo/data/locales/registry.json +9 -0
  27. data/lib/polytypo/data/locales/ru.json +112 -0
  28. data/lib/polytypo/data/locales/sv.json +124 -0
  29. data/lib/polytypo/data/rules/dashes.md +1238 -0
  30. data/lib/polytypo/data/rules/order.json +78 -0
  31. data/lib/polytypo/data/schema/fixtures.schema.json +79 -0
  32. data/lib/polytypo/data/schema/locale.schema.json +235 -0
  33. data/lib/polytypo/data/schema/registry.schema.json +29 -0
  34. data/lib/polytypo/data/schema/resolution.schema.json +50 -0
  35. data/lib/polytypo/engine/codepoints.rb +24 -0
  36. data/lib/polytypo/engine/edits.rb +64 -0
  37. data/lib/polytypo/engine/locale.rb +138 -0
  38. data/lib/polytypo/engine/pipeline.rb +61 -0
  39. data/lib/polytypo/engine/registry.rb +47 -0
  40. data/lib/polytypo/engine/rules/apostrophe.rb +127 -0
  41. data/lib/polytypo/engine/rules/dash_shared.rb +342 -0
  42. data/lib/polytypo/engine/rules/dashes.rb +125 -0
  43. data/lib/polytypo/engine/rules/ellipsis.rb +100 -0
  44. data/lib/polytypo/engine/rules/hyphen.rb +207 -0
  45. data/lib/polytypo/engine/rules/nbsp.rb +616 -0
  46. data/lib/polytypo/engine/rules/quote_ambiguity.rb +241 -0
  47. data/lib/polytypo/engine/rules/quotes.rb +420 -0
  48. data/lib/polytypo/engine/rules/ranges.rb +124 -0
  49. data/lib/polytypo/engine/rules/spaces.rb +232 -0
  50. data/lib/polytypo/engine/rules/symbols.rb +291 -0
  51. data/lib/polytypo/engine/rules.rb +19 -0
  52. data/lib/polytypo/engine/sentinels.rb +23 -0
  53. data/lib/polytypo/engine/unicode_util.rb +390 -0
  54. data/lib/polytypo/errors.rb +24 -0
  55. data/lib/polytypo/modes/html.rb +233 -0
  56. data/lib/polytypo/modes/markdown.rb +187 -0
  57. data/lib/polytypo/modes/parse_error.rb +19 -0
  58. data/lib/polytypo/modes/runner.rb +57 -0
  59. data/lib/polytypo/modes/spans.rb +132 -0
  60. data/lib/polytypo/version.rb +5 -0
  61. data/lib/polytypo.rb +91 -0
  62. data/polytypo.gemspec +37 -0
  63. 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
+ )