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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8340a490160e7c0c2ca089657884c882eca5861dd27206dc9a3513ab1b5cfa2d
4
- data.tar.gz: b4c2428c0408f2301563ee3e4586e9422c7b4875f927779146d9ebacec678568
3
+ metadata.gz: 1302c0c833ffbfdb14eff663bc8a30c80449ddf6ab065fc440c85e103189a87d
4
+ data.tar.gz: 47c5bc1c86b79a3130406f562cd099cd4b76cf4e7a6aa95c17122c14835fcee1
5
5
  SHA512:
6
- metadata.gz: b919c53366fb83912221bf4a642b23db13725535c82449e54f6859d6a6bc76a04582c7b859eb4fb4f9cde814637dd535625eea31ffd62af48f02a6a71642c4f5
7
- data.tar.gz: 18d123157532330403229b1365b97e2dcba308de2ed5d07aa16583c973d801bfa1e6aac001f299be7ab445beaf7a1600f204ff058362f1cd84dc7362f7466aca
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.1 provides shared Japanese technical writing criteria, an authoring
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.1.
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.1'
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.1.gem
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.1.
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.1 (session schema 2). Keep the original baseline for
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.1
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
@@ -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: Check whether the phrase adds a condition that changes the reader's
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.
@@ -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
- - Replace unnecessary English words with established Japanese terms, but retain
174
- identifiers and official names.
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 do |rule, index|
11
- unless rule.is_a?(Hash) && (rule.keys - %w[expected pattern patterns specs]).empty? &&
12
- rule['expected'].is_a?(String) && (rule.key?('pattern') ^ rule.key?('patterns'))
13
- raise Error, "Rule #{index + 1} requires expected and exactly one of pattern or patterns; unsupported keys are rejected."
14
- end
15
- patterns = rule.key?('pattern') ? rule['pattern'] : rule['patterns']
16
- patterns = [patterns] if rule.key?('pattern') && patterns.is_a?(String)
17
- unless patterns.is_a?(Array) && !patterns.empty? && patterns.all? { |p| p.is_a?(String) && !p.empty? }
18
- raise Error, 'Replacement patterns must be nonempty strings.'
19
- end
20
- compiled = patterns.map do |pattern|
21
- if pattern.start_with?('/')
22
- match = pattern.match(%r{\A/(.*)/([a-z]*)\z}m)
23
- unless match && ['', 'i'].include?(match[2])
24
- raise Error, 'Unsupported regex flags; only optional i is supported (all matches are collected).'
25
- end
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 Error, "Invalid replacement regex: #{e.message}"
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
- data = YAML.safe_load(AsciidocPubkit.read_text(path), permitted_classes: [], aliases: false)
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 Error, "Invalid replacement YAML in #{path}: #{e.message}"
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
- text.to_enum(:scan, regex).map do
144
- match = Regexp.last_match
145
- raise Error, 'Zero-length replacement matches are unsupported.' if match.begin(0) == match.end(0)
146
- { start: match.begin(0), finish: match.end(0), before: match[0], after: expand(expected, match) }
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]/)
@@ -9,7 +9,7 @@ require 'pathname'
9
9
  require 'optparse'
10
10
 
11
11
  module AsciidocPubkit
12
- VERSION = '0.8.1'
12
+ VERSION = '0.8.3'
13
13
  class Error < StandardError; end
14
14
 
15
15
  def self.hash_text(text)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asciidoc-pubkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - CyberGarage