polytypo 1.6.3 → 1.7.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 +4 -4
- data/README.md +25 -0
- data/lib/polytypo/data/.not-vendored +11 -0
- data/lib/polytypo/data/README.md +16 -11
- data/lib/polytypo/data/VERSION +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 +14 -1
- data/lib/polytypo/data/fixtures/el.json +1 -1
- data/lib/polytypo/data/fixtures/en-GB.json +1 -1
- data/lib/polytypo/data/fixtures/en-US.json +143 -1
- 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 +13 -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/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/ru.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/cs.json +56 -37
- data/lib/polytypo/data/locales/de-CH.json +43 -34
- data/lib/polytypo/data/locales/de-DE.json +47 -32
- data/lib/polytypo/data/locales/el.json +51 -1
- data/lib/polytypo/data/locales/en-GB.json +56 -4
- data/lib/polytypo/data/locales/en-US.json +68 -4
- data/lib/polytypo/data/locales/es.json +66 -29
- data/lib/polytypo/data/locales/fi.json +77 -7
- data/lib/polytypo/data/locales/fr-CA.json +63 -36
- data/lib/polytypo/data/locales/fr.json +70 -41
- data/lib/polytypo/data/locales/it.json +65 -22
- data/lib/polytypo/data/locales/nl.json +61 -29
- data/lib/polytypo/data/locales/pl.json +61 -28
- data/lib/polytypo/data/locales/pt-BR.json +53 -28
- data/lib/polytypo/data/locales/pt-PT.json +55 -28
- data/lib/polytypo/data/locales/registry.json +1 -1
- data/lib/polytypo/data/locales/ru.json +45 -30
- data/lib/polytypo/data/locales/sv.json +63 -4
- data/lib/polytypo/data/locales/tr.json +72 -32
- data/lib/polytypo/data/locales/uk.json +55 -27
- data/lib/polytypo/data/rules/modes.md +243 -11
- data/lib/polytypo/data/rules/order.json +1 -1
- data/lib/polytypo/data/schema/fixtures.schema.json +12 -1
- data/lib/polytypo/engine/pipeline.rb +29 -0
- data/lib/polytypo/modes/markdown.rb +51 -0
- data/lib/polytypo/modes/runner.rb +33 -3
- data/lib/polytypo/version.rb +1 -1
- data/lib/polytypo.rb +27 -12
- metadata +2 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://polytypo.org/schema/fixtures.schema.json",
|
|
4
4
|
"title": "polytypo conformance fixtures",
|
|
5
|
-
"description": "Executable form of the rule semantics. Every case is also an idempotency case: a runner must additionally assert transform(out) == out, passing the case's own options (mode, dialect, keys, rules, narrowNbsp) on that second call exactly as on the first — a case is a fixed point under its own options, not under the defaults.",
|
|
5
|
+
"description": "Executable form of the rule semantics. Every case is also an idempotency case: a runner must additionally assert transform(out) == out, passing the case's own options (mode, dialect, keys, frontmatterKeys, rules, narrowNbsp) on that second call exactly as on the first — a case is a fixed point under its own options, not under the defaults.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": ["spec", "locale", "cases"],
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"if": { "properties": { "mode": { "const": "yaml" } }, "required": ["mode"] },
|
|
30
30
|
"then": { "required": ["keys"] },
|
|
31
31
|
"else": { "not": { "required": ["keys"] } }
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"if": { "properties": { "mode": { "const": "markdown" } }, "required": ["mode"] },
|
|
35
|
+
"then": true,
|
|
36
|
+
"else": { "not": { "required": ["frontmatterKeys"] } }
|
|
32
37
|
}
|
|
33
38
|
],
|
|
34
39
|
"oneOf": [{ "required": ["out"] }, { "required": ["throws"] }],
|
|
@@ -62,6 +67,12 @@
|
|
|
62
67
|
"items": { "type": "string" },
|
|
63
68
|
"description": "Required when mode is \"yaml\" — spec/rules/modes.md §3.8.2 gives the option no default, so a fixture must name the processable keys exactly as a caller would. An empty array is legal and processes nothing. Meaningless for the other modes."
|
|
64
69
|
},
|
|
70
|
+
"frontmatterKeys": {
|
|
71
|
+
"type": "array",
|
|
72
|
+
"items": { "type": "string" },
|
|
73
|
+
"description": "Optional, spec 1.7.0, and only meaningful when mode is \"markdown\" — spec/rules/modes.md §3.7.4's opt-out from the frontmatter skip. Absent means the block is skipped whole, exactly as before 1.7.0; an empty array is legal and yields no spans. A runner passes it through to transform() as it passes dialect.",
|
|
74
|
+
"$comment": "The block is its own text unit (§3.7.4), so this option cannot change a byte of the body — every case released before 1.7.0 keeps its recorded output whatever it is set to."
|
|
75
|
+
},
|
|
65
76
|
"in": { "type": "string" },
|
|
66
77
|
"out": { "type": "string" },
|
|
67
78
|
"throws": {
|
|
@@ -59,6 +59,35 @@ module Polytypo
|
|
|
59
59
|
value.to_set
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# modes.md 3.7.4: "markdown" mode's `frontmatter_keys` option (spec 1.7.0).
|
|
63
|
+
#
|
|
64
|
+
# Optional, unlike `keys` -- nil means the frontmatter block is skipped whole, which is every
|
|
65
|
+
# pre-1.7.0 document's behaviour -- and an empty Array is legal, exactly as it is for `keys`.
|
|
66
|
+
# Checked after dialect and before the parse, which is what decides that a document failing to
|
|
67
|
+
# parse in its dialect still reports the option error rather than CODE_MALFORMED_INPUT.
|
|
68
|
+
def self.resolve_frontmatter_keys(value)
|
|
69
|
+
return nil if value.nil?
|
|
70
|
+
|
|
71
|
+
unless value.is_a?(Array)
|
|
72
|
+
raise Polytypo::Error.new(
|
|
73
|
+
Polytypo::CODE_INVALID_OPTION,
|
|
74
|
+
'"frontmatter_keys" must be an Array of Strings when given (modes.md 3.7.4). ' \
|
|
75
|
+
"Received #{value.inspect}.",
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
value.each do |key|
|
|
80
|
+
next if key.is_a?(String)
|
|
81
|
+
|
|
82
|
+
raise Polytypo::Error.new(
|
|
83
|
+
Polytypo::CODE_INVALID_OPTION,
|
|
84
|
+
"\"frontmatter_keys\" must contain only Strings; received #{key.inspect}.",
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
value.to_set
|
|
89
|
+
end
|
|
90
|
+
|
|
62
91
|
# Resolves the locale, builds the rule plan, and runs each enabled rule in
|
|
63
92
|
# spec/rules/order.json order over a code-point array, applying its edits before the next
|
|
64
93
|
# rule sees it. No module-level mutable state beyond the immutable, load-once-on-first-use
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "spans"
|
|
4
4
|
require_relative "html"
|
|
5
|
+
require_relative "yaml"
|
|
5
6
|
require_relative "parse_error"
|
|
6
7
|
require_relative "../errors"
|
|
7
8
|
|
|
@@ -66,6 +67,56 @@ module Polytypo
|
|
|
66
67
|
end
|
|
67
68
|
private_class_method :detect_frontmatter_delimiter
|
|
68
69
|
|
|
70
|
+
# modes.md 3.7.4, spec 1.7.0. The frontmatter block's own spans, which form a SECOND TEXT
|
|
71
|
+
# UNIT: the pipeline runs over them separately from the body's, so an unbalanced mark in a
|
|
72
|
+
# metadata field can never pair with one in the first paragraph, and the option cannot
|
|
73
|
+
# change a byte outside the block.
|
|
74
|
+
#
|
|
75
|
+
# Spans come from the scan of modes.md 3.8 -- frontmatter IS YAML, and implementing that
|
|
76
|
+
# grammar twice is how two implementations of one spec drift -- with +keys+ as step 8's key
|
|
77
|
+
# predicate. The block is the construct markdown_spans skips (3.7.3, the :frontmatter node),
|
|
78
|
+
# so the option only ever adds spans where the skip removed them: no source position belongs
|
|
79
|
+
# to both units. A TOML block yields nothing, with the option or without it -- its quoting is
|
|
80
|
+
# a second grammar this scan does not claim (modes.md 7.13).
|
|
81
|
+
#
|
|
82
|
+
# The content range comes from the node rather than from a second scan of the text: comrak
|
|
83
|
+
# reports the block from its opening delimiter line through its closing one, so the content
|
|
84
|
+
# is every line between them, and both delimiters and every line terminator stay outside
|
|
85
|
+
# every span.
|
|
86
|
+
def self.frontmatter_spans(source, keys)
|
|
87
|
+
require "commonmarker"
|
|
88
|
+
return [] if keys.empty?
|
|
89
|
+
return [] unless detect_frontmatter_delimiter(source) == "---"
|
|
90
|
+
|
|
91
|
+
cp = Polytypo::Engine::Codepoints.to_codepoints(source)
|
|
92
|
+
chars = cp.map { |c| [c].pack("U") }
|
|
93
|
+
line_starts = build_line_starts(chars)
|
|
94
|
+
|
|
95
|
+
spans = []
|
|
96
|
+
ParseError.wrap do
|
|
97
|
+
doc = Commonmarker.parse(
|
|
98
|
+
source,
|
|
99
|
+
options: { parse: { sourcepos_chars: true },
|
|
100
|
+
extension: { front_matter_delimiter: "---" } },
|
|
101
|
+
)
|
|
102
|
+
doc.each do |node|
|
|
103
|
+
next unless node.type == :frontmatter
|
|
104
|
+
|
|
105
|
+
pos = node.source_position
|
|
106
|
+
content_start = line_starts[pos[:start_line]]
|
|
107
|
+
content_end = line_starts[pos[:end_line] - 1]
|
|
108
|
+
break if content_start.nil? || content_end.nil? || content_end <= content_start
|
|
109
|
+
|
|
110
|
+
content = chars[content_start...content_end].join
|
|
111
|
+
Yaml.yaml_spans(content, keys).each do |span|
|
|
112
|
+
spans << Spans::Span.new(span.start + content_start, span.end + content_start)
|
|
113
|
+
end
|
|
114
|
+
break
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
spans
|
|
118
|
+
end
|
|
119
|
+
|
|
69
120
|
# Locates the processable spans of a Markdown document. dialect must already be validated
|
|
70
121
|
# via resolve_dialect (== "commonmark"); this function does not re-check it.
|
|
71
122
|
def self.markdown_spans(source)
|
|
@@ -34,17 +34,46 @@ module Polytypo
|
|
|
34
34
|
# source_cp is the whole input already converted to code points; spans address it by
|
|
35
35
|
# code-point index.
|
|
36
36
|
def self.run_over_spans(source_cp, spans, plan, locale_data, ctx)
|
|
37
|
+
emit(source_cp, replacements_of_unit(source_cp, spans, plan, locale_data, ctx))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# modes.md 3.1 and 3.5 step 3 (spec 1.7.0). A document has one text unit, except in
|
|
41
|
+
# "markdown" with frontmatter_keys, where the frontmatter block's spans form a unit of their
|
|
42
|
+
# own. The pipeline runs once per unit and the two edit sets are disjoint, because no span of
|
|
43
|
+
# one unit lies inside the other -- which is what the body's walk skipping the block
|
|
44
|
+
# guarantees. Only step 5 is shared: the source is emitted once, in document order.
|
|
45
|
+
def self.run_over_units(source_cp, units, plan, locale_data, ctx)
|
|
46
|
+
replacements = units.flat_map do |spans|
|
|
47
|
+
replacements_of_unit(source_cp, spans, plan, locale_data, ctx)
|
|
48
|
+
end
|
|
49
|
+
emit(source_cp, replacements.sort_by { |span, _piece| span.start })
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# analyze_over_spans per text unit (modes.md 3.1), reported in document order.
|
|
53
|
+
def self.analyze_over_units(source_cp, units, plan, locale_data, ctx)
|
|
54
|
+
units.flat_map { |spans| analyze_over_spans(source_cp, spans, plan, locale_data, ctx) }
|
|
55
|
+
.sort_by(&:start)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# One text unit: the marker-separated concatenation, the pipeline, and the pieces it
|
|
59
|
+
# produced, paired with the spans they replace.
|
|
60
|
+
def self.replacements_of_unit(source_cp, spans, plan, locale_data, ctx)
|
|
37
61
|
normalized = Spans.normalize_spans(spans)
|
|
38
|
-
return
|
|
62
|
+
return [] if normalized.empty?
|
|
39
63
|
|
|
40
64
|
concatenated = Spans.concatenate_spans(source_cp, normalized)
|
|
41
65
|
transformed = run_rules_over_spans(concatenated, plan, locale_data, ctx)
|
|
42
66
|
pieces = Spans.split_on_marker(transformed, normalized.length)
|
|
67
|
+
normalized.each_with_index.map { |span, i| [span, pieces[i]] }
|
|
68
|
+
end
|
|
69
|
+
private_class_method :replacements_of_unit
|
|
43
70
|
|
|
71
|
+
# modes.md 4: the source with disjoint replacements applied at recorded offsets, and nothing
|
|
72
|
+
# else changed.
|
|
73
|
+
def self.emit(source_cp, replacements)
|
|
44
74
|
out = []
|
|
45
75
|
cursor = 0
|
|
46
|
-
|
|
47
|
-
piece = pieces[i]
|
|
76
|
+
replacements.each do |span, piece|
|
|
48
77
|
original = source_cp[span.start...span.end]
|
|
49
78
|
out.concat(source_cp[cursor...span.start])
|
|
50
79
|
out.concat(piece == original ? original : piece)
|
|
@@ -53,6 +82,7 @@ module Polytypo
|
|
|
53
82
|
out.concat(source_cp[cursor..])
|
|
54
83
|
out.pack("U*")
|
|
55
84
|
end
|
|
85
|
+
private_class_method :emit
|
|
56
86
|
|
|
57
87
|
# run_over_spans, reporting instead of applying (analyze.md section 1). The span table
|
|
58
88
|
# supplies the origin map, so every change comes back in DOCUMENT coordinates -- analyze.md
|
data/lib/polytypo/version.rb
CHANGED
data/lib/polytypo.rb
CHANGED
|
@@ -32,7 +32,8 @@ module Polytypo
|
|
|
32
32
|
#
|
|
33
33
|
# Only `mode: "text"`/`"html"` ever touch this file's own requires; `mode: "markdown"` lazily
|
|
34
34
|
# requires "commonmarker" from within Modes::Markdown, never at load time of this file.
|
|
35
|
-
def self.transform(input, locale:, mode: "text", dialect: nil, keys: nil, rules: nil, narrow_nbsp: nil
|
|
35
|
+
def self.transform(input, locale:, mode: "text", dialect: nil, keys: nil, rules: nil, narrow_nbsp: nil,
|
|
36
|
+
frontmatter_keys: nil)
|
|
36
37
|
resolved_mode = resolve_mode(mode)
|
|
37
38
|
narrow_target = Engine.resolve_narrow_target(narrow_nbsp)
|
|
38
39
|
|
|
@@ -53,7 +54,7 @@ module Polytypo
|
|
|
53
54
|
end
|
|
54
55
|
transform_yaml(input, locale, keys, rules, narrow_target)
|
|
55
56
|
else # "markdown"
|
|
56
|
-
transform_markdown(input, locale, dialect, rules, narrow_target)
|
|
57
|
+
transform_markdown(input, locale, dialect, rules, narrow_target, frontmatter_keys)
|
|
57
58
|
end
|
|
58
59
|
end
|
|
59
60
|
|
|
@@ -69,7 +70,8 @@ module Polytypo
|
|
|
69
70
|
# for the text (analyze.md sections 4 and 5).
|
|
70
71
|
#
|
|
71
72
|
# Pure and thread-safe on the same terms as .transform.
|
|
72
|
-
def self.analyze(input, locale:, mode: "text", dialect: nil, keys: nil, rules: nil, narrow_nbsp: nil
|
|
73
|
+
def self.analyze(input, locale:, mode: "text", dialect: nil, keys: nil, rules: nil, narrow_nbsp: nil,
|
|
74
|
+
frontmatter_keys: nil)
|
|
73
75
|
resolved_mode = resolve_mode(mode)
|
|
74
76
|
narrow_target = Engine.resolve_narrow_target(narrow_nbsp)
|
|
75
77
|
|
|
@@ -90,7 +92,7 @@ module Polytypo
|
|
|
90
92
|
end
|
|
91
93
|
analyze_yaml(input, locale, keys, rules, narrow_target)
|
|
92
94
|
else # "markdown"
|
|
93
|
-
analyze_markdown(input, locale, dialect, rules, narrow_target)
|
|
95
|
+
analyze_markdown(input, locale, dialect, rules, narrow_target, frontmatter_keys)
|
|
94
96
|
end
|
|
95
97
|
end
|
|
96
98
|
|
|
@@ -143,18 +145,30 @@ module Polytypo
|
|
|
143
145
|
end
|
|
144
146
|
private_class_method :transform_yaml
|
|
145
147
|
|
|
146
|
-
def self.transform_markdown(input, locale, dialect, rules, narrow_target)
|
|
148
|
+
def self.transform_markdown(input, locale, dialect, rules, narrow_target, frontmatter_keys = nil)
|
|
147
149
|
# Validation order is public, tested behaviour, identical across every runtime: rules (an
|
|
148
|
-
# unknown rule id), then locale (an unknown locale), then dialect
|
|
150
|
+
# unknown rule id), then locale (an unknown locale), then dialect, then frontmatter_keys, then
|
|
151
|
+
# parsing (modes.md 3.7.4).
|
|
149
152
|
resolved_locale, locale_data, plan = Engine::Pipeline.prepare(locale, rules)
|
|
150
153
|
require_relative "polytypo/modes/markdown"
|
|
151
154
|
Modes::Markdown.resolve_dialect(dialect)
|
|
152
|
-
|
|
155
|
+
resolved_keys = Engine.resolve_frontmatter_keys(frontmatter_keys)
|
|
153
156
|
ctx = Engine::RuleContext.new(mode: "markdown", dialect: dialect, locale: resolved_locale,
|
|
154
157
|
narrow_target: narrow_target)
|
|
155
158
|
cp = Engine::Codepoints.to_codepoints(input)
|
|
156
|
-
Modes::Runner.
|
|
159
|
+
Modes::Runner.run_over_units(cp, markdown_units(input, resolved_keys), plan, locale_data, ctx)
|
|
157
160
|
end
|
|
161
|
+
|
|
162
|
+
# modes.md 3.7.4: the body, and -- only when the caller named frontmatter keys -- the
|
|
163
|
+
# frontmatter block as a second text unit. With frontmatter_keys nil this is exactly the single
|
|
164
|
+
# unit every document had before spec 1.7.0, which is why no released output can move.
|
|
165
|
+
def self.markdown_units(input, resolved_keys)
|
|
166
|
+
body = Modes::Markdown.markdown_spans(input)
|
|
167
|
+
return [body] if resolved_keys.nil?
|
|
168
|
+
|
|
169
|
+
[Modes::Markdown.frontmatter_spans(input, resolved_keys), body]
|
|
170
|
+
end
|
|
171
|
+
private_class_method :markdown_units
|
|
158
172
|
private_class_method :transform_markdown
|
|
159
173
|
|
|
160
174
|
def self.analyze_text(input, locale, rules, narrow_target)
|
|
@@ -187,16 +201,17 @@ module Polytypo
|
|
|
187
201
|
end
|
|
188
202
|
private_class_method :analyze_yaml
|
|
189
203
|
|
|
190
|
-
def self.analyze_markdown(input, locale, dialect, rules, narrow_target)
|
|
204
|
+
def self.analyze_markdown(input, locale, dialect, rules, narrow_target, frontmatter_keys = nil)
|
|
191
205
|
# Validation order is public, tested behaviour and is shared with .transform: rules, then
|
|
192
|
-
# locale, then dialect
|
|
206
|
+
# locale, then dialect, then frontmatter_keys, then parsing (analyze.md section 4, A1).
|
|
193
207
|
resolved_locale, locale_data, plan = Engine::Pipeline.prepare(locale, rules)
|
|
194
208
|
require_relative "polytypo/modes/markdown"
|
|
195
209
|
Modes::Markdown.resolve_dialect(dialect)
|
|
196
|
-
|
|
210
|
+
resolved_keys = Engine.resolve_frontmatter_keys(frontmatter_keys)
|
|
197
211
|
ctx = Engine::RuleContext.new(mode: "markdown", dialect: dialect, locale: resolved_locale,
|
|
198
212
|
narrow_target: narrow_target)
|
|
199
|
-
Modes::Runner.
|
|
213
|
+
Modes::Runner.analyze_over_units(Engine::Codepoints.to_codepoints(input),
|
|
214
|
+
markdown_units(input, resolved_keys), plan, locale_data, ctx)
|
|
200
215
|
end
|
|
201
216
|
private_class_method :analyze_markdown
|
|
202
217
|
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.
|
|
4
|
+
version: 1.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Iurii Rogulia
|
|
@@ -36,6 +36,7 @@ files:
|
|
|
36
36
|
- README.md
|
|
37
37
|
- lib/polytypo.rb
|
|
38
38
|
- lib/polytypo/data/.not-canonical
|
|
39
|
+
- lib/polytypo/data/.not-vendored
|
|
39
40
|
- lib/polytypo/data/README.md
|
|
40
41
|
- lib/polytypo/data/UNICODE
|
|
41
42
|
- lib/polytypo/data/VERSION
|