asciidoc-pubkit 0.8.1 → 0.8.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/CHANGELOG.md +16 -0
- data/README.md +32 -7
- data/data/review-rules.ja.yml +25 -1
- data/data/writing/ja/criteria.md +53 -2
- data/lib/asciidoc_pubkit/replacement.rb +107 -59
- data/lib/asciidoc_pubkit.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1302c0c833ffbfdb14eff663bc8a30c80449ddf6ab065fc440c85e103189a87d
|
|
4
|
+
data.tar.gz: 47c5bc1c86b79a3130406f562cd099cd4b76cf4e7a6aa95c17122c14835fcee1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bd1665669a9d3de5c4b27218c3886e6c0de4c03410f533de318278644c26a8ac467665af74f9026cc39b3243c05afba6fb98ede1bf9c7e4fa922ca6db0fb941e
|
|
7
|
+
data.tar.gz: a5090e7e0efbcb464e694f413312e942c6e88bf4d84749763d0e40971ee3b0f4cffe564f174d8c8c5368f81ffd7fa353cbccebf0d84117e81d051c5ff1e44f45
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.3 — 2026-10-02
|
|
4
|
+
|
|
5
|
+
- Refine shared Japanese writing and review criteria for established technical
|
|
6
|
+
terminology, precise operations, direct predicates, and evidence-based tone.
|
|
7
|
+
Preserve distinctions between assumptions, requirements, and capabilities.
|
|
8
|
+
- Add scoped review candidates for unusual translations, everyday operation
|
|
9
|
+
wording, indirect predicates, and author-centered emphasis; reuse existing
|
|
10
|
+
noun, verb, and qualification rules instead of fixed automatic replacements.
|
|
11
|
+
|
|
12
|
+
## 0.8.2 — 2026-10-02
|
|
13
|
+
|
|
14
|
+
- Report replacement rule errors with the original YAML filename and one-based
|
|
15
|
+
line number on the first line and the existing reason on the following line.
|
|
16
|
+
Track imported rules, pattern arrays, specs, and runtime replacement references
|
|
17
|
+
without changing exit codes or applying invalid plans.
|
|
18
|
+
|
|
3
19
|
## 0.8.1 — 2026-10-01
|
|
4
20
|
|
|
5
21
|
- Add an opening README feature table and describe prose replacements before
|
data/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
A toolkit for authoring and reviewing AsciiDoc books.
|
|
9
9
|
|
|
10
|
-
Version 0.8.
|
|
10
|
+
Version 0.8.3 provides shared Japanese technical writing criteria, an authoring
|
|
11
11
|
prompt, separate prose and heading review prompts, manuscript scoring, and
|
|
12
12
|
baseline verification for edited manuscripts, plus explicit prose-only mechanical
|
|
13
13
|
replacements using a limited prh-format rule set.
|
|
@@ -16,7 +16,7 @@ Japanese text is retained in manuscript excerpts, rule dictionaries, and fixture
|
|
|
16
16
|
|
|
17
17
|
## Available features
|
|
18
18
|
|
|
19
|
-
The following commands are available in version 0.8.
|
|
19
|
+
The following commands are available in version 0.8.3.
|
|
20
20
|
|
|
21
21
|
| Feature | Commands | Behavior |
|
|
22
22
|
| --- | --- | --- |
|
|
@@ -107,7 +107,7 @@ Add the gem to your project's `Gemfile` to manage its version with Bundler:
|
|
|
107
107
|
|
|
108
108
|
```ruby
|
|
109
109
|
source 'https://rubygems.org'
|
|
110
|
-
gem 'asciidoc-pubkit', '~> 0.8.
|
|
110
|
+
gem 'asciidoc-pubkit', '~> 0.8.3'
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
Then install dependencies and run the CLI through Bundler:
|
|
@@ -129,7 +129,7 @@ git clone https://github.com/cybergarage/asciidoc-pubkit.git
|
|
|
129
129
|
cd asciidoc-pubkit
|
|
130
130
|
bundle install
|
|
131
131
|
gem build asciidoc-pubkit.gemspec
|
|
132
|
-
gem install ./asciidoc-pubkit-0.8.
|
|
132
|
+
gem install ./asciidoc-pubkit-0.8.3.gem
|
|
133
133
|
asciidoc-pubkit --version
|
|
134
134
|
```
|
|
135
135
|
|
|
@@ -153,7 +153,7 @@ asciidoc-pubkit writing prompt --lang ja --output writing-prompt.md
|
|
|
153
153
|
```
|
|
154
154
|
|
|
155
155
|
For example, `--lang en` exits with an unsupported-language error. No English
|
|
156
|
-
review or writing criteria are shipped in 0.8.
|
|
156
|
+
review or writing criteria are shipped in 0.8.3.
|
|
157
157
|
|
|
158
158
|
For a small trial, use `examples/book.adoc` as the scan input. Its Japanese
|
|
159
159
|
paragraphs deliberately contain review candidates; its code block must remain
|
|
@@ -180,6 +180,19 @@ authorizes source replacement without prompting. Successful diff/apply commands
|
|
|
180
180
|
return 0; invalid rules, conflicts, unsafe edits, and input/output failures return
|
|
181
181
|
2. Check/diff accept `--output` for a new file only; apply rejects `--output`.
|
|
182
182
|
|
|
183
|
+
Starting with version 0.8.2, invalid rule files report their original YAML file
|
|
184
|
+
and one-based line number on the first error line, followed by the existing
|
|
185
|
+
reason on the next line. Imported files and entries in pattern arrays retain
|
|
186
|
+
their own source locations. For example:
|
|
187
|
+
|
|
188
|
+
```text
|
|
189
|
+
Error: /path/to/prh.yml:162
|
|
190
|
+
Unsupported regex flags; only optional i is supported (all matches are collected).
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Rules are validated before any manuscript is replaced. All replacement commands
|
|
194
|
+
stop with exit code 2 on invalid rules, including errors found in imported files.
|
|
195
|
+
|
|
183
196
|
All three accept `--lang ja`, `--base-dir`, `--config`, and `--only` (an included
|
|
184
197
|
source file). Normal configuration discovery and `review.base_dir`, `language`,
|
|
185
198
|
`attributes`, and `exclude` control parsing and selection. `--rules` is required;
|
|
@@ -992,6 +1005,18 @@ These are candidates for contextual review, not banned expressions. A necessary
|
|
|
992
1005
|
condition, uncertainty, or distinction must survive a revision; a redundant
|
|
993
1006
|
disclaimer can instead be removed or folded into a more precise main claim.
|
|
994
1007
|
|
|
1008
|
+
Version 0.8.3 also includes scoped candidates such as `構築入口`,
|
|
1009
|
+
`エラーを回収する`, `ツールを呼ぶ`, `したりできます`, and `設計の肝`.
|
|
1010
|
+
These additions use the exact surfaces listed in the YAML in both modes;
|
|
1011
|
+
they do not add general inflection coverage or ban everyday verbs. Existing
|
|
1012
|
+
`木`, `持つ`, `書く`, `別です`, and `ではありません` rules cover related examples.
|
|
1013
|
+
The shared criteria explain context-dependent terminology, precise operations,
|
|
1014
|
+
direct predicates, and objective tone. They distinguish invocation from
|
|
1015
|
+
execution, configuration text from configuration changes, assumptions from
|
|
1016
|
+
requirements, and collecting errors from catching them. Suggested alternatives
|
|
1017
|
+
are not automatic replacement rules. Start a new review session to include
|
|
1018
|
+
updated rules and criteria; existing sessions retain their saved versions.
|
|
1019
|
+
|
|
995
1020
|
Metaphorical-operation candidates include limited exact surfaces of
|
|
996
1021
|
`地味に効く`, `静かに壊れる`, `時間を溶かす`, and `側に倒す`, including selected polite,
|
|
997
1022
|
past, and connective forms listed in the packaged YAML. They use contextual
|
|
@@ -1044,12 +1069,12 @@ reports analyzer changes instead of treating results from different dictionaries
|
|
|
1044
1069
|
as directly comparable. Prompt generation uses saved evidence and does not need
|
|
1045
1070
|
MeCab. Changed rules or dictionary settings require a new scan.
|
|
1046
1071
|
|
|
1047
|
-
Sessions from earlier tool versions are not compatible with 0.8.
|
|
1072
|
+
Sessions from earlier tool versions are not compatible with 0.8.3 (session schema 2). Keep the original baseline for
|
|
1048
1073
|
an ongoing review and finish it with the original version, or start a new review
|
|
1049
1074
|
pass in a different directory:
|
|
1050
1075
|
|
|
1051
1076
|
```sh
|
|
1052
|
-
asciidoc-pubkit review scan book.adoc --output .pubkit/review-0.8.
|
|
1077
|
+
asciidoc-pubkit review scan book.adoc --output .pubkit/review-0.8.3
|
|
1053
1078
|
```
|
|
1054
1079
|
|
|
1055
1080
|
Severity describes review priority, not proof of an error. There is no AI-authorship
|
data/data/review-rules.ja.yml
CHANGED
|
@@ -135,6 +135,25 @@ terms:
|
|
|
135
135
|
# Retain this specific phrase for existing allow lists and fuller evidence.
|
|
136
136
|
- わけではありません
|
|
137
137
|
- 別です
|
|
138
|
+
# Unusual technical translations; shared criteria own the alternatives.
|
|
139
|
+
- 構築入口
|
|
140
|
+
- 実装の読解
|
|
141
|
+
- エラーを回収する
|
|
142
|
+
- エラーを回収します
|
|
143
|
+
- 介入パターン
|
|
144
|
+
# Object-specific everyday verbs, not general bans on these verbs.
|
|
145
|
+
- ツールを呼ぶ
|
|
146
|
+
- ツールを呼びます
|
|
147
|
+
- 責務を持つ
|
|
148
|
+
- 指示を読み取る
|
|
149
|
+
- 指示を読み取ります
|
|
150
|
+
- 設定を書く
|
|
151
|
+
- 値を拾う
|
|
152
|
+
- 値を拾います
|
|
153
|
+
- 切り出す合図です
|
|
154
|
+
# Indirect capabilities and prerequisites; preserve their modality.
|
|
155
|
+
- したりできます
|
|
156
|
+
- することが前提です
|
|
138
157
|
# Compressed noun relationships; suggestions are not automatic replacements.
|
|
139
158
|
- 明示選択
|
|
140
159
|
- 欠落理由
|
|
@@ -158,7 +177,8 @@ terms:
|
|
|
158
177
|
- でもありません
|
|
159
178
|
- してはいけません
|
|
160
179
|
- 表してはいません
|
|
161
|
-
question:
|
|
180
|
+
question: Apply the shared prose criteria to terminology, operation wording,
|
|
181
|
+
indirect predicates, and qualifications. Check whether the phrase adds a condition that changes the reader's
|
|
162
182
|
interpretation or action. If it only denies an overbroad reading already
|
|
163
183
|
excluded by the main claim, revise that claim or remove the qualification.
|
|
164
184
|
Preserve necessary negation, uncertainty, and technical boundaries. For
|
|
@@ -176,6 +196,10 @@ terms:
|
|
|
176
196
|
- まとめると
|
|
177
197
|
- このように
|
|
178
198
|
- 要するに
|
|
199
|
+
# Author-centered discovery, indirect explanation, and unsupported emphasis.
|
|
200
|
+
- 見えてきます
|
|
201
|
+
- 理由がここにあります
|
|
202
|
+
- 設計の肝
|
|
179
203
|
question: Check whether this opening or recap adds a claim, scope, or consequence
|
|
180
204
|
beyond the surrounding prose. Keep useful framing; otherwise state the
|
|
181
205
|
substantive claim directly. Do not force a uniform sentence pattern.
|
data/data/writing/ja/criteria.md
CHANGED
|
@@ -66,6 +66,21 @@ Prefer a precise action or consequence:
|
|
|
66
66
|
- `結果を残します` becomes `実行したコマンドと結果をプルリクエストへ記録します`
|
|
67
67
|
when that is the storage location.
|
|
68
68
|
|
|
69
|
+
For everyday verbs, identify the operation before choosing terminology:
|
|
70
|
+
|
|
71
|
+
| Wording to inspect | Revision when supported by context |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| ツールを呼ぶ | 呼び出す for invocation; 実行する only for execution |
|
|
74
|
+
| 責務を持つ | 責務を担う when describing responsibility, not stored state or ownership |
|
|
75
|
+
| 指示を読み取る | 解析する for parsing or analysis; 抽出する for selecting information |
|
|
76
|
+
| 設定を書く・設定を書きます | 設定を記述する・定義する for authoring a definition; 設定する for actually configuring something |
|
|
77
|
+
| 値を拾う | 取得する for retrieval; 抽出する for selection from existing data |
|
|
78
|
+
| 切り出す合図です | 切り出す目安です for a judgment criterion; シグナルです only for an actual signal |
|
|
79
|
+
|
|
80
|
+
A familiar verb is not wrong merely because it is simple. Prefer the most direct
|
|
81
|
+
accurate verb; do not introduce jargon or claim parsing, execution, or a state
|
|
82
|
+
change that the evidence does not establish.
|
|
83
|
+
|
|
69
84
|
Do not choose a more impressive synonym merely to vary sentence endings. Keep
|
|
70
85
|
the technically correct verb when it names the real operation.
|
|
71
86
|
|
|
@@ -156,6 +171,27 @@ Common patterns that merit paragraph-level review include:
|
|
|
156
171
|
Keep a contrast when the two alternatives and the difference that affects the
|
|
157
172
|
decision are explicit. Keep necessary repetition when it improves technical
|
|
158
173
|
accuracy.
|
|
174
|
+
Inspect indirect predicates and author-centered emphasis as well:
|
|
175
|
+
|
|
176
|
+
- Simplify `無効化したりできます` to `無効化できます` when it describes a single
|
|
177
|
+
capability. Preserve `たり` when it marks a meaningful non-exhaustive list.
|
|
178
|
+
- For `提供することが前提です`, identify whose prerequisite or assumption is
|
|
179
|
+
stated. Use `提供する必要があります` only when the source establishes an actual
|
|
180
|
+
requirement; do not turn an assumption into an obligation.
|
|
181
|
+
- For `宣言することは別です`, name what is compared and how it differs. A form such
|
|
182
|
+
as `宣言することとは異なります` needs an explicit, correctly oriented comparison.
|
|
183
|
+
- For `そうではありません`, state the specific fact or negate the specific claim.
|
|
184
|
+
`実際には異なります` alone can retain the same missing referent. Preserve negation.
|
|
185
|
+
- For `〜が見えてきます`, state the property the explanation makes identifiable.
|
|
186
|
+
`理解しやすくなります` is still a reader-effect claim that needs support; retain
|
|
187
|
+
literal visibility when discussing a display or visualization.
|
|
188
|
+
- For `理由がここにあります`, connect the cause and result directly, for example
|
|
189
|
+
`これが〜の理由です` when `これ` has a clear referent.
|
|
190
|
+
- For `設計の肝です`, identify the central responsibility or design decision.
|
|
191
|
+
`重要なポイントです` is not sufficient when it merely replaces one emphasis
|
|
192
|
+
phrase with another. Remove emotional, essay-like, or promotional emphasis
|
|
193
|
+
that supplies no technical fact; retain supported judgments and their basis.
|
|
194
|
+
|
|
159
195
|
Do not shorten sentences or vary endings mechanically. Read the sequence aloud
|
|
160
196
|
and repair the missing relationship rather than replacing one stock pattern
|
|
161
197
|
with another.
|
|
@@ -170,12 +206,27 @@ glossary, and primary documentation before changing it.
|
|
|
170
206
|
- Distinguish resource ownership from the responsibility to close or stop it.
|
|
171
207
|
- Use the project's chosen forms consistently, such as `goroutine` versus
|
|
172
208
|
`ゴルーチン`, or `reflection` versus `リフレクション`.
|
|
173
|
-
-
|
|
174
|
-
|
|
209
|
+
- Prefer established terminology in the technical domain, whether Japanese or
|
|
210
|
+
katakana. Avoid forced literal translations and unnecessary English; retain
|
|
211
|
+
identifiers, official names, and the project's chosen terminology.
|
|
175
212
|
- Do not describe a language operation with a loose everyday verb when the
|
|
176
213
|
domain has a precise term, such as name shadowing, method promotion, parsing,
|
|
177
214
|
serialization, cancellation, or resource release.
|
|
178
215
|
|
|
216
|
+
Review unusual translations against the actual concept:
|
|
217
|
+
|
|
218
|
+
| Wording to inspect | Context-dependent terminology |
|
|
219
|
+
| --- | --- |
|
|
220
|
+
| 木 | ツリー when referring to the data structure; keep literal trees and established mathematical usage |
|
|
221
|
+
| 構築入口 | ビルドのエントリポイント only when the entry point starts a build, not object construction or initialization |
|
|
222
|
+
| 実装の読解 | コードリーディング for reading code; 実装の解析 when analysis is the actual task |
|
|
223
|
+
| エラーを回収する | エラーを捕捉する for catching; ハンドリングする for handling; preserve aggregation or collection when that is the behavior |
|
|
224
|
+
| 介入パターン | フックパターン only when a hook mechanism is established, not arbitrary human intervention |
|
|
225
|
+
|
|
226
|
+
These alternatives are not interchangeable replacements. Confirm the operation
|
|
227
|
+
and the domain's established usage from code, the glossary, or primary
|
|
228
|
+
documentation; record uncertainty rather than inventing a mechanism.
|
|
229
|
+
|
|
179
230
|
Natural Japanese must not erase a technical distinction. If the original claim
|
|
180
231
|
is ambiguous, inspect the code or authoritative documentation before editing.
|
|
181
232
|
Preserve numerical values, versions, settings, responsibility, causal claims,
|
|
@@ -5,56 +5,82 @@ require 'tempfile'
|
|
|
5
5
|
require 'open3'
|
|
6
6
|
|
|
7
7
|
module AsciidocPubkit
|
|
8
|
+
class ReplacementRuleError < Error
|
|
9
|
+
def initialize(path, node, message, line: nil)
|
|
10
|
+
super("#{path}:#{line || (node ? node.start_line + 1 : 1)}\n#{message}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
8
14
|
class ReplacementRules
|
|
15
|
+
def self.field(node, name)
|
|
16
|
+
return unless node.is_a?(Psych::Nodes::Mapping)
|
|
17
|
+
pair = node.children.each_slice(2).find { |key, _| key.respond_to?(:value) && key.value == name }
|
|
18
|
+
pair && pair.last
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.item(node, index)
|
|
22
|
+
node.is_a?(Psych::Nodes::Sequence) ? node.children[index] : node
|
|
23
|
+
end
|
|
24
|
+
|
|
9
25
|
def self.load(path)
|
|
10
|
-
collect(path, [], {}).each_with_index.map
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
body = match[1]
|
|
27
|
-
# Deliberately small common JavaScript/Ruby subset; Ruby-only escapes
|
|
28
|
-
# and engine-specific groups must not silently change JS semantics.
|
|
29
|
-
if body.match?(/\\[^\\\/.*+?()\[\]{}^$|nrtdDsSwWbB-]/) || body.match?(/\(\?(?![:=!]|<[=!])/) || body.include?('&&')
|
|
30
|
-
raise Error, 'Unsupported regex syntax in replacement rule.'
|
|
31
|
-
end
|
|
32
|
-
ignore_case = match[2] == 'i'
|
|
33
|
-
body = javascript_boundaries(body, ignore_case: ignore_case)
|
|
34
|
-
Regexp.new(body.gsub('\\/', '/'), ignore_case ? Regexp::IGNORECASE : 0, timeout: 0.1)
|
|
35
|
-
else
|
|
36
|
-
Regexp.new(Regexp.escape(pattern), timeout: 0.1)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
compiled.each do |regex|
|
|
40
|
-
regex.match('') # Detect common empty matches; runtime checks cover contextual ones.
|
|
41
|
-
raise Error, 'Zero-length replacement matches are unsupported.' if regex.match('')
|
|
42
|
-
end
|
|
43
|
-
specs = rule.fetch('specs', [])
|
|
44
|
-
raise Error, 'specs must be an array.' unless specs.is_a?(Array)
|
|
45
|
-
specs.each do |spec|
|
|
46
|
-
RuleSet.mapping(spec, %w[from to], 'Replacement spec')
|
|
47
|
-
raise Error, 'Spec from/to must be strings.' unless spec.values.all? { |v| v.is_a?(String) }
|
|
48
|
-
result = spec['from'].dup
|
|
49
|
-
edits = matches(spec['from'], compiled, rule['expected'])
|
|
50
|
-
reject_overlaps!(edits)
|
|
51
|
-
edits.reverse_each { |edit| result[edit[:start]...edit[:finish]] = edit[:after] }
|
|
52
|
-
raise Error, "Replacement spec failed for rule #{index + 1}." unless result == spec['to']
|
|
53
|
-
end
|
|
54
|
-
{ id: "rule-#{index + 1}", patterns: compiled, expected: rule['expected'] }
|
|
26
|
+
collect(path, [], {}).each_with_index.map { |source, index| compile(source, index) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.compile(source, index)
|
|
30
|
+
rule = source[:value]
|
|
31
|
+
location = source[:node]
|
|
32
|
+
unless rule.is_a?(Hash) && (rule.keys - %w[expected pattern patterns specs]).empty? &&
|
|
33
|
+
rule['expected'].is_a?(String) && (rule.key?('pattern') ^ rule.key?('patterns'))
|
|
34
|
+
raise Error, "Rule #{index + 1} requires expected and exactly one of pattern or patterns; unsupported keys are rejected."
|
|
35
|
+
end
|
|
36
|
+
pattern_node = field(source[:node], rule.key?('pattern') ? 'pattern' : 'patterns')
|
|
37
|
+
location = pattern_node
|
|
38
|
+
patterns = rule.key?('pattern') ? rule['pattern'] : rule['patterns']
|
|
39
|
+
patterns = [patterns] if rule.key?('pattern') && patterns.is_a?(String)
|
|
40
|
+
unless patterns.is_a?(Array) && !patterns.empty? && patterns.all? { |p| p.is_a?(String) && !p.empty? }
|
|
41
|
+
raise Error, 'Replacement patterns must be nonempty strings.'
|
|
55
42
|
end
|
|
43
|
+
compiled = patterns.each_with_index.map do |pattern, pattern_index|
|
|
44
|
+
location = item(pattern_node, pattern_index)
|
|
45
|
+
regex = if pattern.start_with?('/')
|
|
46
|
+
match = pattern.match(%r{\A/(.*)/([a-z]*)\z}m)
|
|
47
|
+
unless match && ['', 'i'].include?(match[2])
|
|
48
|
+
raise Error, 'Unsupported regex flags; only optional i is supported (all matches are collected).'
|
|
49
|
+
end
|
|
50
|
+
body = match[1]
|
|
51
|
+
# Deliberately small common JavaScript/Ruby subset; Ruby-only escapes
|
|
52
|
+
# and engine-specific groups must not silently change JS semantics.
|
|
53
|
+
if body.match?(/\\[^\\\/.*+?()\[\]{}^$|nrtdDsSwWbB-]/) || body.match?(/\(\?(?![:=!]|<[=!])/) || body.include?('&&')
|
|
54
|
+
raise Error, 'Unsupported regex syntax in replacement rule.'
|
|
55
|
+
end
|
|
56
|
+
ignore_case = match[2] == 'i'
|
|
57
|
+
body = javascript_boundaries(body, ignore_case: ignore_case)
|
|
58
|
+
Regexp.new(body.gsub('\\/', '/'), ignore_case ? Regexp::IGNORECASE : 0, timeout: 0.1)
|
|
59
|
+
else
|
|
60
|
+
Regexp.new(Regexp.escape(pattern), timeout: 0.1)
|
|
61
|
+
end
|
|
62
|
+
raise Error, 'Zero-length replacement matches are unsupported.' if regex.match('')
|
|
63
|
+
regex
|
|
64
|
+
end
|
|
65
|
+
location = field(source[:node], 'specs') || source[:node]
|
|
66
|
+
specs = rule.fetch('specs', [])
|
|
67
|
+
raise Error, 'specs must be an array.' unless specs.is_a?(Array)
|
|
68
|
+
specs.each_with_index do |spec, spec_index|
|
|
69
|
+
location = item(field(source[:node], 'specs'), spec_index)
|
|
70
|
+
RuleSet.mapping(spec, %w[from to], 'Replacement spec')
|
|
71
|
+
raise Error, 'Spec from/to must be strings.' unless spec.values.all? { |v| v.is_a?(String) }
|
|
72
|
+
result = spec['from'].dup
|
|
73
|
+
edits = matches(spec['from'], compiled, rule['expected'])
|
|
74
|
+
reject_overlaps!(edits)
|
|
75
|
+
edits.reverse_each { |edit| result[edit[:start]...edit[:finish]] = edit[:after] }
|
|
76
|
+
raise Error, "Replacement spec failed for rule #{index + 1}." unless result == spec['to']
|
|
77
|
+
end
|
|
78
|
+
{ id: "rule-#{index + 1}", patterns: compiled, expected: rule['expected'],
|
|
79
|
+
path: source[:path], node: source[:node], pattern_node: pattern_node }
|
|
56
80
|
rescue RegexpError, Regexp::TimeoutError => e
|
|
57
|
-
raise
|
|
81
|
+
raise ReplacementRuleError.new(source[:path], location, "Invalid replacement regex: #{e.message}")
|
|
82
|
+
rescue Error => e
|
|
83
|
+
raise ReplacementRuleError.new(source[:path], location, e.message)
|
|
58
84
|
end
|
|
59
85
|
|
|
60
86
|
# prh uses Unicode JS regexes: word characters are ASCII letters/digits/_;
|
|
@@ -92,25 +118,35 @@ module AsciidocPubkit
|
|
|
92
118
|
end
|
|
93
119
|
|
|
94
120
|
def self.collect(path, active, visited)
|
|
121
|
+
path = File.expand_path(path)
|
|
122
|
+
location = nil
|
|
95
123
|
path = File.realpath(path)
|
|
96
124
|
if active.include?(path)
|
|
97
125
|
raise Error, "Circular replacement imports: #{(active + [path]).join(' -> ')}"
|
|
98
126
|
end
|
|
99
127
|
return [] if visited[path]
|
|
100
128
|
raise Error, 'Replacement imports exceed the maximum depth of 100 files.' if active.length >= 100
|
|
101
|
-
|
|
129
|
+
text = AsciidocPubkit.read_text(path)
|
|
130
|
+
tree = Psych.parse_stream(text, filename: path)
|
|
131
|
+
root = tree.children.first&.root
|
|
132
|
+
location = root
|
|
133
|
+
data = YAML.safe_load(text, permitted_classes: [], aliases: false)
|
|
102
134
|
unless data.is_a?(Hash) && data.key?('version') && (data.keys - %w[version rules imports]).empty?
|
|
103
135
|
raise Error, "Replacement rules in #{path} require version and accept only rules and imports."
|
|
104
136
|
end
|
|
137
|
+
location = field(root, 'version')
|
|
105
138
|
unless data['version'].is_a?(Integer) && data['version'] == 1
|
|
106
139
|
raise Error, "Replacement version in #{path} must be integer 1."
|
|
107
140
|
end
|
|
108
141
|
rules = data.fetch('rules', [])
|
|
109
142
|
rules = [] if rules.nil?
|
|
110
143
|
imports = data.fetch('imports', [])
|
|
144
|
+
location = field(root, 'rules')
|
|
111
145
|
raise Error, "rules in #{path} must be an array." unless rules.is_a?(Array)
|
|
146
|
+
location = field(root, 'imports')
|
|
112
147
|
raise Error, "imports in #{path} must be an array." unless imports.is_a?(Array)
|
|
113
|
-
imported = imports.flat_map do |entry|
|
|
148
|
+
imported = imports.each_with_index.flat_map do |entry, index|
|
|
149
|
+
location = item(field(root, 'imports'), index)
|
|
114
150
|
if entry.is_a?(Hash)
|
|
115
151
|
RuleSet.mapping(entry, ['path'], "Replacement import in #{path}")
|
|
116
152
|
entry = entry['path']
|
|
@@ -121,9 +157,14 @@ module AsciidocPubkit
|
|
|
121
157
|
collect(File.expand_path(entry, File.dirname(path)), active + [path], visited)
|
|
122
158
|
end
|
|
123
159
|
visited[path] = true
|
|
124
|
-
imported + rules
|
|
160
|
+
imported + rules.each_with_index.map { |rule, index| { value: rule, path: path, node: item(field(root, 'rules'), index) } }
|
|
161
|
+
rescue ReplacementRuleError
|
|
162
|
+
raise
|
|
125
163
|
rescue Psych::Exception => e
|
|
126
|
-
raise
|
|
164
|
+
raise ReplacementRuleError.new(path, location, "Invalid replacement YAML in #{path}: #{e.message}",
|
|
165
|
+
line: e.respond_to?(:line) ? e.line : nil)
|
|
166
|
+
rescue Error, SystemCallError => e
|
|
167
|
+
raise ReplacementRuleError.new(path, location, e.message)
|
|
127
168
|
end
|
|
128
169
|
|
|
129
170
|
def self.expand(template, match)
|
|
@@ -138,16 +179,23 @@ module AsciidocPubkit
|
|
|
138
179
|
end
|
|
139
180
|
end
|
|
140
181
|
|
|
141
|
-
def self.matches(text, patterns, expected)
|
|
142
|
-
patterns.flat_map do |regex|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
182
|
+
def self.matches(text, patterns, expected, source: nil)
|
|
183
|
+
patterns.each_with_index.flat_map do |regex, index|
|
|
184
|
+
begin
|
|
185
|
+
text.to_enum(:scan, regex).map do
|
|
186
|
+
match = Regexp.last_match
|
|
187
|
+
raise Error, 'Zero-length replacement matches are unsupported.' if match.begin(0) == match.end(0)
|
|
188
|
+
{ start: match.begin(0), finish: match.end(0), before: match[0], after: expand(expected, match) }
|
|
189
|
+
end
|
|
190
|
+
rescue Error, Regexp::TimeoutError => e
|
|
191
|
+
message = e.is_a?(Regexp::TimeoutError) ? 'Replacement regex timed out.' : e.message
|
|
192
|
+
if source
|
|
193
|
+
location = message.start_with?('Unsupported replacement reference:') ? field(source[:node], 'expected') : item(source[:pattern_node], index)
|
|
194
|
+
raise ReplacementRuleError.new(source[:path], location, message)
|
|
195
|
+
end
|
|
196
|
+
raise Error, message
|
|
147
197
|
end
|
|
148
198
|
end.reject { |edit| edit[:before] == edit[:after] }.uniq.sort_by { |edit| edit[:start] }
|
|
149
|
-
rescue Regexp::TimeoutError
|
|
150
|
-
raise Error, 'Replacement regex timed out.'
|
|
151
199
|
end
|
|
152
200
|
|
|
153
201
|
def self.reject_overlaps!(edits)
|
|
@@ -181,7 +229,7 @@ module AsciidocPubkit
|
|
|
181
229
|
line_offsets = [0]
|
|
182
230
|
lines.each { |line| line_offsets << line_offsets.last + line.length }
|
|
183
231
|
rules.each do |rule|
|
|
184
|
-
ReplacementRules.matches(text, rule[:patterns], rule[:expected]).each do |edit|
|
|
232
|
+
ReplacementRules.matches(text, rule[:patterns], rule[:expected], source: rule).each do |edit|
|
|
185
233
|
next if protected_ranges.any? { |range| edit[:start] < range.end && edit[:finish] > range.begin }
|
|
186
234
|
# Keep physical line boundaries intact, including CRLF.
|
|
187
235
|
raise Error, 'Replacement matches and results must stay on one line.' if (edit[:before] + edit[:after]).match?(/[\r\n]/)
|
data/lib/asciidoc_pubkit.rb
CHANGED