asciidoc-pubkit 0.8.0 → 0.8.2
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 +31 -0
- data/README.md +220 -107
- data/lib/asciidoc_pubkit/replacement.rb +168 -50
- 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: 3dd09ef810a52e64bd286b34f4f59cda2cf357214abd94a1255ee44d77a96e27
|
|
4
|
+
data.tar.gz: fec7d11ca4f985b41aa1d2e5d37c8b126d56a1dc1ee86874330d2239634207c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e56e14f6abd189a621a3f4b918d9e24697749bc1a777816b2cdfa9a02bfc662f1b53e831e291af99b5254ac976a6fa72a2703c224d98ce979e0d9d7eac5be6d5
|
|
7
|
+
data.tar.gz: 19361ba968e9738a736f4452eb6590b90cbc1ca998e128d0cdd952f1da527c66d97817f6913f9957092c8dfbae55b5019e90909e1709337671bb57384031da06
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.2 — 2026-10-02
|
|
4
|
+
|
|
5
|
+
- Report replacement rule errors with the original YAML filename and one-based
|
|
6
|
+
line number on the first line and the existing reason on the following line.
|
|
7
|
+
Track imported rules, pattern arrays, specs, and runtime replacement references
|
|
8
|
+
without changing exit codes or applying invalid plans.
|
|
9
|
+
|
|
10
|
+
## 0.8.1 — 2026-10-01
|
|
11
|
+
|
|
12
|
+
- Add an opening README feature table and describe prose replacements before
|
|
13
|
+
scoring and review.
|
|
14
|
+
|
|
15
|
+
- Support replacement regex `/i` with Ruby case-insensitive matching and translate
|
|
16
|
+
`\b`/`\B` assertions to limited ECMAScript Unicode word boundaries. Preserve
|
|
17
|
+
capture numbering, source positions, protected spans, and backspace-in-class
|
|
18
|
+
semantics; retain Ruby behavior elsewhere and reject other flags.
|
|
19
|
+
|
|
20
|
+
- Treat null replacement `rules`, including bare `rules:`, as an empty local
|
|
21
|
+
rule set while retaining imported rules and rejecting other non-array values.
|
|
22
|
+
|
|
23
|
+
- Accept nonempty string arrays in replacement `pattern` as an alias for
|
|
24
|
+
`patterns`, retaining strict validation, specs, protection, and conflict checks.
|
|
25
|
+
|
|
26
|
+
- Document omitted replacement `rules` as an empty set, including version-only
|
|
27
|
+
files, and verify CLI no-op behavior and application through import-only chains.
|
|
28
|
+
|
|
29
|
+
- Support nested local `imports` in replacement rules, resolving paths relative
|
|
30
|
+
to each importing file and accepting import-only files and `path` mappings.
|
|
31
|
+
Deduplicate physical files, reject cycles and unsupported import options, and
|
|
32
|
+
preserve validation, conflict detection, and prose-only replacement scope.
|
|
33
|
+
|
|
3
34
|
## 0.8.0 — 2026-10-01
|
|
4
35
|
|
|
5
36
|
- Add explicit `replace check`, `replace diff`, and `replace apply` commands for
|
data/README.md
CHANGED
|
@@ -7,13 +7,34 @@
|
|
|
7
7
|
|
|
8
8
|
A toolkit for authoring and reviewing AsciiDoc books.
|
|
9
9
|
|
|
10
|
-
Version 0.8.
|
|
10
|
+
Version 0.8.2 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.
|
|
14
14
|
The CLI, diagnostics, documentation, and generated instructions are in English.
|
|
15
15
|
Japanese text is retained in manuscript excerpts, rule dictionaries, and fixtures.
|
|
16
16
|
|
|
17
|
+
## Available features
|
|
18
|
+
|
|
19
|
+
The following commands are available in version 0.8.2.
|
|
20
|
+
|
|
21
|
+
| Feature | Commands | Behavior |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| [Writing criteria and prompts](#writing-commands) | `writing criteria`, `writing prompt` | Print Japanese prose criteria or generate a writing prompt without a manuscript or MeCab |
|
|
24
|
+
| [Replacement preview](#prose-replacements) | `replace check`, `replace diff` | Report mechanical replacement candidates or preview a diff for mapped running prose; preserve source files |
|
|
25
|
+
| [Replacement application](#prose-replacements) | `replace apply` | Apply author-supplied rules after checking protected content and document structure; no MeCab or AI required |
|
|
26
|
+
| [Prose review](#review-workflow) | `review scan` | Collect mapped Japanese running prose and contextual review candidates with MeCab/IPADIC by default |
|
|
27
|
+
| [Heading review](#heading-review) | `review scan --scope headings` | Collect editable section titles separately, retaining the outline and read-only body evidence |
|
|
28
|
+
| [Review prompts](#prompt) | `review prompt` | Generate one Markdown prompt from a saved session for review and editing by an external reviewer |
|
|
29
|
+
| [Baseline verification](#verify) | `review verify` | Check edited sources against a saved baseline for mechanical preservation; meaning is not verified |
|
|
30
|
+
| [Manuscript scoring](#score-an-asciidoc-manuscript) | `review score` | Report a prose candidate-density score; explicit `--agent codex` or `--agent claude` optionally invokes a local AI CLI for readability ratings |
|
|
31
|
+
|
|
32
|
+
Version 0.8.1 additionally supports nested replacement-rule imports,
|
|
33
|
+
omitted/null `rules`, arrays in `pattern`, `/i`, and limited ECMAScript word
|
|
34
|
+
boundaries. See [Prose replacements](#prose-replacements) for supported syntax
|
|
35
|
+
and preservation limits. Only `replace apply` edits manuscripts directly;
|
|
36
|
+
review prompts supply instructions for an external reviewer.
|
|
37
|
+
|
|
17
38
|
## Status
|
|
18
39
|
|
|
19
40
|
Version 0.6.3 adds separate heading review with `review scan --scope headings`.
|
|
@@ -86,7 +107,7 @@ Add the gem to your project's `Gemfile` to manage its version with Bundler:
|
|
|
86
107
|
|
|
87
108
|
```ruby
|
|
88
109
|
source 'https://rubygems.org'
|
|
89
|
-
gem 'asciidoc-pubkit', '~> 0.8.
|
|
110
|
+
gem 'asciidoc-pubkit', '~> 0.8.2'
|
|
90
111
|
```
|
|
91
112
|
|
|
92
113
|
Then install dependencies and run the CLI through Bundler:
|
|
@@ -108,7 +129,7 @@ git clone https://github.com/cybergarage/asciidoc-pubkit.git
|
|
|
108
129
|
cd asciidoc-pubkit
|
|
109
130
|
bundle install
|
|
110
131
|
gem build asciidoc-pubkit.gemspec
|
|
111
|
-
gem install ./asciidoc-pubkit-0.8.
|
|
132
|
+
gem install ./asciidoc-pubkit-0.8.2.gem
|
|
112
133
|
asciidoc-pubkit --version
|
|
113
134
|
```
|
|
114
135
|
|
|
@@ -132,12 +153,205 @@ asciidoc-pubkit writing prompt --lang ja --output writing-prompt.md
|
|
|
132
153
|
```
|
|
133
154
|
|
|
134
155
|
For example, `--lang en` exits with an unsupported-language error. No English
|
|
135
|
-
review or writing criteria are shipped in 0.8.
|
|
156
|
+
review or writing criteria are shipped in 0.8.2.
|
|
136
157
|
|
|
137
158
|
For a small trial, use `examples/book.adoc` as the scan input. Its Japanese
|
|
138
159
|
paragraphs deliberately contain review candidates; its code block must remain
|
|
139
160
|
unchanged.
|
|
140
161
|
|
|
162
|
+
## Prose replacements
|
|
163
|
+
|
|
164
|
+
Available starting with version 0.8.0.
|
|
165
|
+
It applies author-supplied mechanical replacement rules without MeCab or an AI
|
|
166
|
+
CLI. Review candidates and glossary variants remain advisory and are never
|
|
167
|
+
implicitly applied. Replacement rules are separate from review rules.
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
asciidoc-pubkit replace check book.adoc --rules replacements.yml
|
|
171
|
+
asciidoc-pubkit replace diff book.adoc --rules replacements.yml
|
|
172
|
+
asciidoc-pubkit replace apply book.adoc --rules replacements.yml
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`check` reports original text, proposed text, rule IDs (`rule-1`, etc.), source
|
|
176
|
+
paths, one-based Unicode lines/columns, and a coverage-notice count. Its exit code
|
|
177
|
+
is 1 when replacements are available, 0 otherwise. `diff` previews unified diffs
|
|
178
|
+
without editing; it requires the external `diff` command. `apply` explicitly
|
|
179
|
+
authorizes source replacement without prompting. Successful diff/apply commands
|
|
180
|
+
return 0; invalid rules, conflicts, unsafe edits, and input/output failures return
|
|
181
|
+
2. Check/diff accept `--output` for a new file only; apply rejects `--output`.
|
|
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
|
+
|
|
196
|
+
All three accept `--lang ja`, `--base-dir`, `--config`, and `--only` (an included
|
|
197
|
+
source file). Normal configuration discovery and `review.base_dir`, `language`,
|
|
198
|
+
`attributes`, and `exclude` control parsing and selection. `--rules` is required;
|
|
199
|
+
`review.rules` and glossary entries do not supply replacement rules. There are
|
|
200
|
+
no built-in replacement rules and no automatic search for `prh.yml`.
|
|
201
|
+
|
|
202
|
+
### Limited prh-format rules
|
|
203
|
+
|
|
204
|
+
The supported format is a strict subset of [prh](https://github.com/prh/prh):
|
|
205
|
+
|
|
206
|
+
```yaml
|
|
207
|
+
version: 1
|
|
208
|
+
rules:
|
|
209
|
+
- expected: '®'
|
|
210
|
+
pattern: '®'
|
|
211
|
+
- expected: '($1)'
|
|
212
|
+
pattern: '/(([^()\r\n]+))/'
|
|
213
|
+
specs:
|
|
214
|
+
- from: '設定(任意)'
|
|
215
|
+
to: '設定(任意)'
|
|
216
|
+
- expected: '/'
|
|
217
|
+
patterns:
|
|
218
|
+
- '/'
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The root requires `version: 1` and accepts optional `rules` and `imports` arrays.
|
|
222
|
+
Starting with version 0.8.1, omitted arrays default to empty. Version 0.8.0
|
|
223
|
+
requires `rules` and rejects `imports`. Each rule requires a string
|
|
224
|
+
`expected` (empty strings permit deletion) and exactly one of `pattern` (a
|
|
225
|
+
nonempty string or, starting with version 0.8.1, a nonempty array of nonempty
|
|
226
|
+
strings) or `patterns` (a nonempty array of nonempty strings). Array-valued
|
|
227
|
+
`pattern` is equivalent to `patterns`; specifying both keys is rejected. Optional
|
|
228
|
+
`specs` is an array of exact `from`/`to` string pairs, validated on load. Specs
|
|
229
|
+
exercise the rule on plain text, not AsciiDoc selection or inline protection.
|
|
230
|
+
|
|
231
|
+
Strings not beginning with `/` match literally. `/.../` denotes a regex; the
|
|
232
|
+
version 0.8.1 also accepts `/.../i` for Ruby case-insensitive matching.
|
|
233
|
+
Version 0.8.0 rejects all flags. All occurrences are collected. The implementation
|
|
234
|
+
uses Ruby regexes with
|
|
235
|
+
a timeout and accepts a limited common syntax: character classes, ordinary
|
|
236
|
+
captures, noncapturing groups, lookarounds, alternatives, anchors, quantifiers,
|
|
237
|
+
and the escapes `\n`, `\r`, `\t`, `\d`, `\D`, `\s`, `\S`, `\w`, `\W`
|
|
238
|
+
and escaped punctuation. Ruby regex character-class behavior applies; this is
|
|
239
|
+
not a JavaScript regex engine or a promise of full prh equivalence. Engine-specific
|
|
240
|
+
groups/escapes and flags other than a single `i` are rejected. A leading literal slash must be expressed
|
|
241
|
+
as an escaped regex, for example `/\//`. Zero-length matches are rejected.
|
|
242
|
+
|
|
243
|
+
Replacement strings support `$1` through `$99` for existing capture groups and
|
|
244
|
+
`$$` for a literal dollar sign. Unsupported dollar references are rejected when
|
|
245
|
+
matched. Missing optional captures expand to empty strings. YAML single quotes
|
|
246
|
+
are recommended to keep backslashes literal. Unknown fields, including
|
|
247
|
+
`options` and `regexpMustEmpty`, and omitted patterns are rejected rather than
|
|
248
|
+
ignored. prh's automatic pattern generation is not supported.
|
|
249
|
+
|
|
250
|
+
For example, version 0.8.1 also accepts:
|
|
251
|
+
|
|
252
|
+
```yaml
|
|
253
|
+
version: 1
|
|
254
|
+
rules:
|
|
255
|
+
- expected: 'ハードウェア'
|
|
256
|
+
pattern:
|
|
257
|
+
- 'ハードウエア'
|
|
258
|
+
- 'ハードウェアー'
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Each array entry uses the same literal/regex syntax and validation as a single
|
|
262
|
+
pattern. Arrays do not change supported flags or overlap handling.
|
|
263
|
+
|
|
264
|
+
### Limited ECMAScript boundary compatibility
|
|
265
|
+
|
|
266
|
+
Version 0.8.1 translates `\b` and `\B` outside character classes into Ruby
|
|
267
|
+
lookarounds with ECMAScript word-character semantics, following prh's default
|
|
268
|
+
Unicode mode. Word characters are ASCII letters, digits, and underscore; Japanese
|
|
269
|
+
characters are non-word characters. Thus `/\bTips\b/` matches `Tips` in
|
|
270
|
+
`前Tips後`, but not in `Tips2`, `_Tips`, or `ATips`.
|
|
271
|
+
With `/i`, long s (`ſ`) and Kelvin sign (`K`) also count as word characters under
|
|
272
|
+
ECMAScript Unicode case folding. The generated assertions add no capture groups
|
|
273
|
+
and preserve original match offsets. Inside a character class, `\b` means a
|
|
274
|
+
backspace character; `\B` there is rejected. Escaped literal backslashes are
|
|
275
|
+
preserved.
|
|
276
|
+
|
|
277
|
+
Only the boundary assertions emulate ECMAScript. `/i` uses Ruby's
|
|
278
|
+
`Regexp::IGNORECASE`; Unicode case folding can differ from JavaScript (for
|
|
279
|
+
example multi-character folds). Other constructs, including whitespace classes,
|
|
280
|
+
retain Ruby semantics. This is limited prh compatibility, not full ECMAScript
|
|
281
|
+
conformance. Flags `g`, `m`, `s`, `u`, `y`, `d`, and `v` and duplicate `i` are
|
|
282
|
+
rejected; all occurrences are already collected independently of `g`.
|
|
283
|
+
|
|
284
|
+
### Importing replacement rules
|
|
285
|
+
|
|
286
|
+
Version 0.8.1 supports nested `imports`; version 0.8.0 does not. For example:
|
|
287
|
+
|
|
288
|
+
```yaml
|
|
289
|
+
version: 1
|
|
290
|
+
imports:
|
|
291
|
+
- ../../rules/prh.yml
|
|
292
|
+
- path: ./terminology.yml
|
|
293
|
+
rules:
|
|
294
|
+
- expected: '®'
|
|
295
|
+
pattern: '®'
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Each entry is a nonempty local file path string or a mapping containing exactly
|
|
299
|
+
`path`. Relative paths resolve from the importing YAML file, including nested
|
|
300
|
+
imports, independently of the working directory and manuscript base directory.
|
|
301
|
+
Absolute local paths are also accepted; URL imports are rejected. An import-only
|
|
302
|
+
file may omit `rules`. A file containing only `version: 1` is also valid and
|
|
303
|
+
contributes no rules. With no imported or local rules, `check`, `diff`, and
|
|
304
|
+
`apply` succeed without changing manuscript sources, and `diff` emits no patch.
|
|
305
|
+
Starting with version 0.8.1, a bare `rules:`, `rules: null`, and `rules: ~`
|
|
306
|
+
also contribute no local rules, just like omission or `rules: []`; imported rules
|
|
307
|
+
are still loaded. Other non-array values remain invalid. This normalization
|
|
308
|
+
applies only to `rules`, not to `imports` or individual rule fields.
|
|
309
|
+
Every imported file must use version 1 and pass the same
|
|
310
|
+
strict rule validation and specs as the entry file.
|
|
311
|
+
|
|
312
|
+
Imports are loaded in listed order, recursively before each file's own rules.
|
|
313
|
+
Each physical file is loaded once per plan, including repeated paths, symlink
|
|
314
|
+
aliases, and shared dependencies. Rule IDs are assigned across the flattened
|
|
315
|
+
set. Cycles (including symlink aliases), missing files, and import chains deeper
|
|
316
|
+
than 100 files fail rather than silently omitting rules. Imported and local rules
|
|
317
|
+
are combined, not overridden: overlapping replacement candidates remain errors
|
|
318
|
+
and replacements are still calculated only against the original text.
|
|
319
|
+
|
|
320
|
+
Import options such as `ignoreRules` and `disableImports` remain unsupported and
|
|
321
|
+
are rejected. Source selection and protection are unchanged by imports.
|
|
322
|
+
|
|
323
|
+
### Selection and preservation
|
|
324
|
+
|
|
325
|
+
Only unambiguously source-mapped running prose in the existing default review
|
|
326
|
+
scope is eligible. Headings, lists, tables, quotations, listing/source, literal,
|
|
327
|
+
and passthrough blocks are excluded. Recognized inline code, quotations, URLs,
|
|
328
|
+
macros, and attribute references are protected by the existing conservative
|
|
329
|
+
inline heuristic, not a complete inline parser. A match overlapping any protected
|
|
330
|
+
span is skipped; matching is performed on the original text, never on blanked
|
|
331
|
+
placeholders. Backtick-to-bold and list-marker conversions are outside this scope.
|
|
332
|
+
|
|
333
|
+
Candidates are calculated once against the original sources; replacement results
|
|
334
|
+
are not searched again. Overlapping edits are conflicts, not resolved by rule
|
|
335
|
+
order. Matches/results containing line breaks are rejected. Source files retain
|
|
336
|
+
UTF-8 bytes, original line endings, and unrelated content; repeated include
|
|
337
|
+
occurrences must not create overlapping edits.
|
|
338
|
+
|
|
339
|
+
Before presenting a plan, the complete captured source set is copied to temporary
|
|
340
|
+
staging and reparsed. Parsing diagnostics, structural changes, new/deleted protected
|
|
341
|
+
inline tokens, and protected-content/source-membership changes reject the plan.
|
|
342
|
+
Relative includes within the base directory are supported. Absolute includes and
|
|
343
|
+
includes whose paths depend on the original filesystem may fail staged validation;
|
|
344
|
+
they are not rewritten or silently accepted. Symlink entrypoints are rejected.
|
|
345
|
+
|
|
346
|
+
Apply stages all changed files beside their destinations, checks all captured
|
|
347
|
+
source bytes against the plan, and renames the prepared files into place while
|
|
348
|
+
preserving permission bits. An ordinary installation error restores sources
|
|
349
|
+
already installed. This is not a crash-atomic transaction across multiple files;
|
|
350
|
+
keep manuscripts under version control and avoid concurrent edits during apply.
|
|
351
|
+
No review session is created or replaced. Existing sessions retain their original
|
|
352
|
+
baselines; applying replacements can make a session stale for prompt generation.
|
|
353
|
+
Mechanical validation does not establish semantic correctness.
|
|
354
|
+
|
|
141
355
|
## Score an AsciiDoc manuscript
|
|
142
356
|
|
|
143
357
|
Pass any AsciiDoc entry file directly; no review session or gold annotations are
|
|
@@ -753,107 +967,6 @@ Regex patterns are not interpreted in glossary or allow-list entries. Unknown
|
|
|
753
967
|
configuration keys are rejected. A bare `mecab_command` is resolved through PATH;
|
|
754
968
|
use an absolute path for an explicit executable override.
|
|
755
969
|
|
|
756
|
-
## Prose replacements
|
|
757
|
-
|
|
758
|
-
Available starting with version 0.8.0.
|
|
759
|
-
It applies author-supplied mechanical replacement rules without MeCab or an AI
|
|
760
|
-
CLI. Review candidates and glossary variants remain advisory and are never
|
|
761
|
-
implicitly applied. Replacement rules are separate from review rules.
|
|
762
|
-
|
|
763
|
-
```sh
|
|
764
|
-
asciidoc-pubkit replace check book.adoc --rules replacements.yml
|
|
765
|
-
asciidoc-pubkit replace diff book.adoc --rules replacements.yml
|
|
766
|
-
asciidoc-pubkit replace apply book.adoc --rules replacements.yml
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
`check` reports original text, proposed text, rule IDs (`rule-1`, etc.), source
|
|
770
|
-
paths, one-based Unicode lines/columns, and a coverage-notice count. Its exit code
|
|
771
|
-
is 1 when replacements are available, 0 otherwise. `diff` previews unified diffs
|
|
772
|
-
without editing; it requires the external `diff` command. `apply` explicitly
|
|
773
|
-
authorizes source replacement without prompting. Successful diff/apply commands
|
|
774
|
-
return 0; invalid rules, conflicts, unsafe edits, and input/output failures return
|
|
775
|
-
2. Check/diff accept `--output` for a new file only; apply rejects `--output`.
|
|
776
|
-
|
|
777
|
-
All three accept `--lang ja`, `--base-dir`, `--config`, and `--only` (an included
|
|
778
|
-
source file). Normal configuration discovery and `review.base_dir`, `language`,
|
|
779
|
-
`attributes`, and `exclude` control parsing and selection. `--rules` is required;
|
|
780
|
-
`review.rules` and glossary entries do not supply replacement rules. There are
|
|
781
|
-
no built-in replacement rules and no automatic search for `prh.yml`.
|
|
782
|
-
|
|
783
|
-
### Limited prh-format rules
|
|
784
|
-
|
|
785
|
-
The supported format is a strict subset of [prh](https://github.com/prh/prh):
|
|
786
|
-
|
|
787
|
-
```yaml
|
|
788
|
-
version: 1
|
|
789
|
-
rules:
|
|
790
|
-
- expected: '®'
|
|
791
|
-
pattern: '®'
|
|
792
|
-
- expected: '($1)'
|
|
793
|
-
pattern: '/(([^()\r\n]+))/'
|
|
794
|
-
specs:
|
|
795
|
-
- from: '設定(任意)'
|
|
796
|
-
to: '設定(任意)'
|
|
797
|
-
- expected: '/'
|
|
798
|
-
patterns:
|
|
799
|
-
- '/'
|
|
800
|
-
```
|
|
801
|
-
|
|
802
|
-
The root contains exactly `version: 1` and `rules`. Each rule requires a string
|
|
803
|
-
`expected` (empty strings permit deletion) and exactly one of `pattern` (a
|
|
804
|
-
nonempty string) or `patterns` (a nonempty array of nonempty strings). Optional
|
|
805
|
-
`specs` is an array of exact `from`/`to` string pairs, validated on load. Specs
|
|
806
|
-
exercise the rule on plain text, not AsciiDoc selection or inline protection.
|
|
807
|
-
|
|
808
|
-
Strings not beginning with `/` match literally. `/.../` denotes a regex with no
|
|
809
|
-
flags; all occurrences are collected. The implementation uses Ruby regexes with
|
|
810
|
-
a timeout and accepts a limited common syntax: character classes, ordinary
|
|
811
|
-
captures, noncapturing groups, lookarounds, alternatives, anchors, quantifiers,
|
|
812
|
-
and the escapes `\n`, `\r`, `\t`, `\d`, `\D`, `\s`, `\S`, `\w`, `\W`
|
|
813
|
-
and escaped punctuation. Ruby regex character-class behavior applies; this is
|
|
814
|
-
not a JavaScript regex engine or a promise of full prh equivalence. Engine-specific
|
|
815
|
-
groups/escapes and flags are rejected. A leading literal slash must be expressed
|
|
816
|
-
as an escaped regex, for example `/\//`. Zero-length matches are rejected.
|
|
817
|
-
|
|
818
|
-
Replacement strings support `$1` through `$99` for existing capture groups and
|
|
819
|
-
`$$` for a literal dollar sign. Unsupported dollar references are rejected when
|
|
820
|
-
matched. Missing optional captures expand to empty strings. YAML single quotes
|
|
821
|
-
are recommended to keep backslashes literal. Unknown fields, including `imports`,
|
|
822
|
-
`options`, and `regexpMustEmpty`, and omitted patterns are rejected rather than
|
|
823
|
-
ignored. Regex flags and prh's automatic pattern generation are not supported.
|
|
824
|
-
|
|
825
|
-
### Selection and preservation
|
|
826
|
-
|
|
827
|
-
Only unambiguously source-mapped running prose in the existing default review
|
|
828
|
-
scope is eligible. Headings, lists, tables, quotations, listing/source, literal,
|
|
829
|
-
and passthrough blocks are excluded. Recognized inline code, quotations, URLs,
|
|
830
|
-
macros, and attribute references are protected by the existing conservative
|
|
831
|
-
inline heuristic, not a complete inline parser. A match overlapping any protected
|
|
832
|
-
span is skipped; matching is performed on the original text, never on blanked
|
|
833
|
-
placeholders. Backtick-to-bold and list-marker conversions are outside this scope.
|
|
834
|
-
|
|
835
|
-
Candidates are calculated once against the original sources; replacement results
|
|
836
|
-
are not searched again. Overlapping edits are conflicts, not resolved by rule
|
|
837
|
-
order. Matches/results containing line breaks are rejected. Source files retain
|
|
838
|
-
UTF-8 bytes, original line endings, and unrelated content; repeated include
|
|
839
|
-
occurrences must not create overlapping edits.
|
|
840
|
-
|
|
841
|
-
Before presenting a plan, the complete captured source set is copied to temporary
|
|
842
|
-
staging and reparsed. Parsing diagnostics, structural changes, new/deleted protected
|
|
843
|
-
inline tokens, and protected-content/source-membership changes reject the plan.
|
|
844
|
-
Relative includes within the base directory are supported. Absolute includes and
|
|
845
|
-
includes whose paths depend on the original filesystem may fail staged validation;
|
|
846
|
-
they are not rewritten or silently accepted. Symlink entrypoints are rejected.
|
|
847
|
-
|
|
848
|
-
Apply stages all changed files beside their destinations, checks all captured
|
|
849
|
-
source bytes against the plan, and renames the prepared files into place while
|
|
850
|
-
preserving permission bits. An ordinary installation error restores sources
|
|
851
|
-
already installed. This is not a crash-atomic transaction across multiple files;
|
|
852
|
-
keep manuscripts under version control and avoid concurrent edits during apply.
|
|
853
|
-
No review session is created or replaced. Existing sessions retain their original
|
|
854
|
-
baselines; applying replacements can make a session stale for prompt generation.
|
|
855
|
-
Mechanical validation does not establish semantic correctness.
|
|
856
|
-
|
|
857
970
|
## Rules and coverage
|
|
858
971
|
|
|
859
972
|
| Rule | Severity | Purpose |
|
|
@@ -944,12 +1057,12 @@ reports analyzer changes instead of treating results from different dictionaries
|
|
|
944
1057
|
as directly comparable. Prompt generation uses saved evidence and does not need
|
|
945
1058
|
MeCab. Changed rules or dictionary settings require a new scan.
|
|
946
1059
|
|
|
947
|
-
Sessions from earlier tool versions are not compatible with 0.8.
|
|
1060
|
+
Sessions from earlier tool versions are not compatible with 0.8.2 (session schema 2). Keep the original baseline for
|
|
948
1061
|
an ongoing review and finish it with the original version, or start a new review
|
|
949
1062
|
pass in a different directory:
|
|
950
1063
|
|
|
951
1064
|
```sh
|
|
952
|
-
asciidoc-pubkit review scan book.adoc --output .pubkit/review-0.8.
|
|
1065
|
+
asciidoc-pubkit review scan book.adoc --output .pubkit/review-0.8.2
|
|
953
1066
|
```
|
|
954
1067
|
|
|
955
1068
|
Severity describes review priority, not proof of an error. There is no AI-authorship
|
|
@@ -5,55 +5,166 @@ 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.'
|
|
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 }
|
|
80
|
+
rescue RegexpError, Regexp::TimeoutError => e
|
|
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)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# prh uses Unicode JS regexes: word characters are ASCII letters/digits/_;
|
|
87
|
+
# Unicode ignore-case additionally folds long s and Kelvin sign to ASCII.
|
|
88
|
+
# Scope out Ruby's ignore-case so these assertion classes stay exact.
|
|
89
|
+
def self.javascript_boundaries(body, ignore_case:)
|
|
90
|
+
word = ignore_case ? '(?-i:[A-Za-z0-9_ſK])' : '[A-Za-z0-9_]'
|
|
91
|
+
boundary = "(?:(?<=#{word})(?!#{word})|(?<!#{word})(?=#{word}))"
|
|
92
|
+
nonboundary = "(?:(?<=#{word})(?=#{word})|(?<!#{word})(?!#{word}))"
|
|
93
|
+
chars = body.chars
|
|
94
|
+
result = +''
|
|
95
|
+
in_class = false
|
|
96
|
+
index = 0
|
|
97
|
+
while index < chars.length
|
|
98
|
+
char = chars[index]
|
|
99
|
+
if char == '\\' && index + 1 < chars.length
|
|
100
|
+
following = chars[index + 1]
|
|
101
|
+
if following == 'b'
|
|
102
|
+
result << (in_class ? '\x08' : boundary)
|
|
103
|
+
elsif following == 'B'
|
|
104
|
+
raise Error, '\B inside a character class is unsupported.' if in_class
|
|
105
|
+
result << nonboundary
|
|
34
106
|
else
|
|
35
|
-
|
|
107
|
+
result << char << following
|
|
36
108
|
end
|
|
109
|
+
index += 2
|
|
110
|
+
next
|
|
37
111
|
end
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
112
|
+
in_class = true if char == '['
|
|
113
|
+
in_class = false if char == ']'
|
|
114
|
+
result << char
|
|
115
|
+
index += 1
|
|
116
|
+
end
|
|
117
|
+
result
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def self.collect(path, active, visited)
|
|
121
|
+
path = File.expand_path(path)
|
|
122
|
+
location = nil
|
|
123
|
+
path = File.realpath(path)
|
|
124
|
+
if active.include?(path)
|
|
125
|
+
raise Error, "Circular replacement imports: #{(active + [path]).join(' -> ')}"
|
|
126
|
+
end
|
|
127
|
+
return [] if visited[path]
|
|
128
|
+
raise Error, 'Replacement imports exceed the maximum depth of 100 files.' if active.length >= 100
|
|
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)
|
|
134
|
+
unless data.is_a?(Hash) && data.key?('version') && (data.keys - %w[version rules imports]).empty?
|
|
135
|
+
raise Error, "Replacement rules in #{path} require version and accept only rules and imports."
|
|
136
|
+
end
|
|
137
|
+
location = field(root, 'version')
|
|
138
|
+
unless data['version'].is_a?(Integer) && data['version'] == 1
|
|
139
|
+
raise Error, "Replacement version in #{path} must be integer 1."
|
|
140
|
+
end
|
|
141
|
+
rules = data.fetch('rules', [])
|
|
142
|
+
rules = [] if rules.nil?
|
|
143
|
+
imports = data.fetch('imports', [])
|
|
144
|
+
location = field(root, 'rules')
|
|
145
|
+
raise Error, "rules in #{path} must be an array." unless rules.is_a?(Array)
|
|
146
|
+
location = field(root, 'imports')
|
|
147
|
+
raise Error, "imports in #{path} must be an array." unless imports.is_a?(Array)
|
|
148
|
+
imported = imports.each_with_index.flat_map do |entry, index|
|
|
149
|
+
location = item(field(root, 'imports'), index)
|
|
150
|
+
if entry.is_a?(Hash)
|
|
151
|
+
RuleSet.mapping(entry, ['path'], "Replacement import in #{path}")
|
|
152
|
+
entry = entry['path']
|
|
41
153
|
end
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
specs.each do |spec|
|
|
45
|
-
RuleSet.mapping(spec, %w[from to], 'Replacement spec')
|
|
46
|
-
raise Error, 'Spec from/to must be strings.' unless spec.values.all? { |v| v.is_a?(String) }
|
|
47
|
-
result = spec['from'].dup
|
|
48
|
-
edits = matches(spec['from'], compiled, rule['expected'])
|
|
49
|
-
reject_overlaps!(edits)
|
|
50
|
-
edits.reverse_each { |edit| result[edit[:start]...edit[:finish]] = edit[:after] }
|
|
51
|
-
raise Error, "Replacement spec failed for rule #{index + 1}." unless result == spec['to']
|
|
154
|
+
unless entry.is_a?(String) && !entry.strip.empty? && !entry.match?(/\A[a-z][a-z0-9+.-]*:/i)
|
|
155
|
+
raise Error, "Replacement imports in #{path} must specify nonempty local file paths."
|
|
52
156
|
end
|
|
53
|
-
|
|
157
|
+
collect(File.expand_path(entry, File.dirname(path)), active + [path], visited)
|
|
54
158
|
end
|
|
55
|
-
|
|
56
|
-
|
|
159
|
+
visited[path] = true
|
|
160
|
+
imported + rules.each_with_index.map { |rule, index| { value: rule, path: path, node: item(field(root, 'rules'), index) } }
|
|
161
|
+
rescue ReplacementRuleError
|
|
162
|
+
raise
|
|
163
|
+
rescue Psych::Exception => e
|
|
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)
|
|
57
168
|
end
|
|
58
169
|
|
|
59
170
|
def self.expand(template, match)
|
|
@@ -68,16 +179,23 @@ module AsciidocPubkit
|
|
|
68
179
|
end
|
|
69
180
|
end
|
|
70
181
|
|
|
71
|
-
def self.matches(text, patterns, expected)
|
|
72
|
-
patterns.flat_map do |regex|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
77
197
|
end
|
|
78
198
|
end.reject { |edit| edit[:before] == edit[:after] }.uniq.sort_by { |edit| edit[:start] }
|
|
79
|
-
rescue Regexp::TimeoutError
|
|
80
|
-
raise Error, 'Replacement regex timed out.'
|
|
81
199
|
end
|
|
82
200
|
|
|
83
201
|
def self.reject_overlaps!(edits)
|
|
@@ -111,7 +229,7 @@ module AsciidocPubkit
|
|
|
111
229
|
line_offsets = [0]
|
|
112
230
|
lines.each { |line| line_offsets << line_offsets.last + line.length }
|
|
113
231
|
rules.each do |rule|
|
|
114
|
-
ReplacementRules.matches(text, rule[:patterns], rule[:expected]).each do |edit|
|
|
232
|
+
ReplacementRules.matches(text, rule[:patterns], rule[:expected], source: rule).each do |edit|
|
|
115
233
|
next if protected_ranges.any? { |range| edit[:start] < range.end && edit[:finish] > range.begin }
|
|
116
234
|
# Keep physical line boundaries intact, including CRLF.
|
|
117
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