asciidoc-pubkit 0.1.0 → 0.1.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 +21 -1
- data/README.md +187 -13
- data/data/review-rules.ja.yml +123 -0
- data/lib/asciidoc_pubkit/cli.rb +3 -0
- data/lib/asciidoc_pubkit/morphology.rb +83 -0
- data/lib/asciidoc_pubkit/rule_set.rb +52 -0
- data/lib/asciidoc_pubkit/rules.rb +109 -9
- data/lib/asciidoc_pubkit/session.rb +19 -3
- data/lib/asciidoc_pubkit/settings.rb +9 -2
- data/lib/asciidoc_pubkit.rb +3 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de07af1758fdba08a7099b9e66f63b14084d0dad6cba8437498f0ddd0577b425
|
|
4
|
+
data.tar.gz: 8474d883c10c23f1221ff85cbd023d781c84f93dd55436a542c0d304c0a8f424
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6983524a7967131b469360bebd417125d7eea14cdf8054d14c928940528d685ee50fd0c402ca354073a55e643217ed931ddda3ca3e402d252d02e4030b7b3a9e
|
|
7
|
+
data.tar.gz: 8e8111fea8d32dfb1449c48cfc8d8f2df22e58dec14a44a55c6dcb6596a6bd3e87aa86fbd596cfc332a6ca49c9d1452f5dd34b23126137d9359a081585e1ae56
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.2 — Released
|
|
4
|
+
|
|
5
|
+
- Package default Japanese review rules as a validated UTF-8 YAML file.
|
|
6
|
+
- Support custom rule files through `review scan --rules` and `review.rules`.
|
|
7
|
+
- Save resolved rules in sessions for reproducible prompts and verification.
|
|
8
|
+
- Add concept, distinction, and negative meaning review candidates.
|
|
9
|
+
- Add a Make target for running a local checkout with arbitrary CLI arguments.
|
|
10
|
+
- Require a new review session when upgrading from earlier tool versions.
|
|
11
|
+
|
|
12
|
+
## 0.1.1 — Released
|
|
13
|
+
|
|
14
|
+
- Add contextual phrases, compound nouns, and potential and causative verb forms.
|
|
15
|
+
- Add requested abstract nouns, degree adjectives, and vague predicates.
|
|
16
|
+
- Use MeCab with UTF-8 IPADIC by default to match inflected predicates and adjectives.
|
|
17
|
+
- Preserve original surfaces, dictionary forms, and negative auxiliary information.
|
|
18
|
+
- Record analyzer and dictionary fingerprints in review sessions.
|
|
19
|
+
- Add an explicit literal mode for environments without MeCab.
|
|
20
|
+
- Reject incompatible dictionaries and unavailable analyzers with setup guidance.
|
|
21
|
+
- Require a new review session when upgrading from 0.1.0.
|
|
22
|
+
|
|
23
|
+
## 0.1.0 — Released
|
|
4
24
|
|
|
5
25
|
- Add AsciiDoc running-prose extraction with checked source locations.
|
|
6
26
|
- Add Japanese review candidates and configurable terminology checks.
|
data/README.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+
[](https://rubygems.org/gems/asciidoc-pubkit)
|
|
4
|
+
[](https://github.com/cybergarage/asciidoc-pubkit/actions/workflows/test.yml)
|
|
5
|
+
|
|
1
6
|
# asciidoc-pubkit
|
|
2
7
|
|
|
3
8
|
A toolkit for authoring, reviewing, and publishing AsciiDoc books.
|
|
@@ -14,12 +19,15 @@ rewrite manuscripts, or publish books. EPUB, image, and book scaffolding command
|
|
|
14
19
|
are planned extensions, not available features.
|
|
15
20
|
|
|
16
21
|
Ruby 3.2 or later is required. Asciidoctor is installed as a gem dependency.
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
Starting with version 0.1.1, the default review backend requires the external MeCab
|
|
23
|
+
command and a UTF-8 IPADIC dictionary. Node.js and textlint are not required.
|
|
24
|
+
Explicit `--tokenizer literal` mode provides limited phrase matching without MeCab.
|
|
25
|
+
|
|
26
|
+
Version 0.1.1 includes morphological analysis. Version 0.1.0 uses literal matching.
|
|
19
27
|
|
|
20
28
|
## Install from RubyGems
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
Install the CLI from [RubyGems](https://rubygems.org/gems/asciidoc-pubkit):
|
|
23
31
|
|
|
24
32
|
```sh
|
|
25
33
|
gem install asciidoc-pubkit
|
|
@@ -28,8 +36,8 @@ asciidoc-pubkit --help
|
|
|
28
36
|
```
|
|
29
37
|
|
|
30
38
|
Ruby 3.2 or later is required. RubyGems installs the required Ruby dependencies;
|
|
31
|
-
no repository clone or Node.js installation is needed.
|
|
32
|
-
|
|
39
|
+
no repository clone or Node.js installation is needed. MeCab and IPADIC must be
|
|
40
|
+
installed separately when using version 0.1.1 or later in the default mode.
|
|
33
41
|
|
|
34
42
|
Run the review workflow from your manuscript directory:
|
|
35
43
|
|
|
@@ -52,7 +60,7 @@ Add the gem to your project's `Gemfile` to manage its version with Bundler:
|
|
|
52
60
|
|
|
53
61
|
```ruby
|
|
54
62
|
source 'https://rubygems.org'
|
|
55
|
-
gem 'asciidoc-pubkit', '~> 0.1.
|
|
63
|
+
gem 'asciidoc-pubkit', '~> 0.1.2'
|
|
56
64
|
```
|
|
57
65
|
|
|
58
66
|
Then install dependencies and run the CLI through Bundler:
|
|
@@ -74,7 +82,7 @@ git clone https://github.com/cybergarage/asciidoc-pubkit.git
|
|
|
74
82
|
cd asciidoc-pubkit
|
|
75
83
|
bundle install
|
|
76
84
|
gem build asciidoc-pubkit.gemspec
|
|
77
|
-
gem install ./asciidoc-pubkit-0.1.
|
|
85
|
+
gem install ./asciidoc-pubkit-0.1.2.gem
|
|
78
86
|
asciidoc-pubkit --version
|
|
79
87
|
```
|
|
80
88
|
|
|
@@ -85,6 +93,64 @@ For a small trial, use `examples/book.adoc` as the scan input. Its Japanese
|
|
|
85
93
|
paragraphs deliberately contain review candidates; its code block must remain
|
|
86
94
|
unchanged.
|
|
87
95
|
|
|
96
|
+
## Run from a local checkout
|
|
97
|
+
|
|
98
|
+
Use the `run` Make target to execute the checkout without installing the
|
|
99
|
+
asciidoc-pubkit gem. Ruby dependencies and, for the default tokenizer, MeCab and
|
|
100
|
+
UTF-8 IPADIC must already be installed.
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
make run ARGS="--version"
|
|
104
|
+
make run ARGS="review scan examples/book.adoc"
|
|
105
|
+
make run ARGS='review scan "manuscripts/my book.adoc"'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
With no `ARGS`, `make run` displays CLI help. Set `RUBY` to choose another Ruby
|
|
109
|
+
executable. `ARGS` is shell command-line text; quote paths containing spaces and
|
|
110
|
+
use only trusted arguments.
|
|
111
|
+
|
|
112
|
+
To run from a manuscript directory, select the checkout's Makefile with `-f`.
|
|
113
|
+
Relative manuscript paths and output paths remain relative to your current
|
|
114
|
+
working directory:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
make -f "$HOME/Src/asciidoc-pubkit/Makefile" run ARGS="review scan book.adoc"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Use `ARGS` instead of `make review scan book.adoc`: Make interprets positional
|
|
121
|
+
words as build targets, not CLI arguments. Avoid `make -C` when manuscript paths
|
|
122
|
+
should remain relative to the current directory, because it changes directories.
|
|
123
|
+
|
|
124
|
+
## Install the morphological analyzer (0.1.1 and later)
|
|
125
|
+
|
|
126
|
+
On macOS with Homebrew:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
brew install mecab mecab-ipadic
|
|
130
|
+
mecab -D
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
On Ubuntu or Debian:
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
sudo apt-get update
|
|
137
|
+
sudo apt-get install mecab mecab-ipadic-utf8
|
|
138
|
+
mecab -D
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Use a UTF-8 IPADIC dictionary. If the default dictionary is different, configure
|
|
142
|
+
`review.mecab_dictionary` with the IPADIC directory reported by your package
|
|
143
|
+
manager. UniDic and other feature layouts are rejected explicitly. MeCab and
|
|
144
|
+
IPADIC are separately installed dependencies, not bundled inside this gem.
|
|
145
|
+
|
|
146
|
+
The tool checks MeCab availability, dictionary encoding, and feature layout.
|
|
147
|
+
It never silently falls back to literal matching. To deliberately run without
|
|
148
|
+
morphological analysis:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
asciidoc-pubkit review scan book.adoc --tokenizer literal --output .pubkit/literal-review
|
|
152
|
+
```
|
|
153
|
+
|
|
88
154
|
## Review workflow
|
|
89
155
|
|
|
90
156
|
Run the following commands from the manuscript project directory:
|
|
@@ -111,7 +177,7 @@ asciidoc-pubkit review scan book.adoc --attribute edition=print --base-dir .
|
|
|
111
177
|
|
|
112
178
|
An entrypoint or a standalone chapter can be scanned. Includes and conditionals
|
|
113
179
|
are processed by Asciidoctor. Match the publishing build's attributes and base
|
|
114
|
-
directory to review the intended edition. `--only` selects one included source
|
|
180
|
+
directory to review the intended edition. `--only` selects one included source
|
|
115
181
|
file while retaining the book's attributes and heading hierarchy. Its path is
|
|
116
182
|
relative to the current working directory, as are other CLI path arguments.
|
|
117
183
|
|
|
@@ -190,6 +256,10 @@ The initial release loads one configuration file, not merged book/repository fil
|
|
|
190
256
|
review:
|
|
191
257
|
language: ja
|
|
192
258
|
style: desu-masu
|
|
259
|
+
tokenizer: mecab
|
|
260
|
+
# Optional overrides (dictionary paths are relative to this file):
|
|
261
|
+
# mecab_command: /opt/homebrew/bin/mecab
|
|
262
|
+
# mecab_dictionary: /opt/homebrew/lib/mecab/dic/ipadic
|
|
193
263
|
base_dir: .
|
|
194
264
|
glossary: glossary.yml
|
|
195
265
|
exclude:
|
|
@@ -213,10 +283,13 @@ JavaScript:
|
|
|
213
283
|
- Java Script
|
|
214
284
|
```
|
|
215
285
|
|
|
216
|
-
Variants are review candidates, not automatic replacement instructions.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
286
|
+
Variants are review candidates, not automatic replacement instructions. In MeCab
|
|
287
|
+
mode, `allows` can suppress a canonical dictionary form (all its inflections) or
|
|
288
|
+
an exact matched surface (that occurrence's form only). In literal mode it
|
|
289
|
+
suppresses exact dictionary entries. It does not disable glossary checks.
|
|
290
|
+
Regex patterns are not interpreted in glossary or allow-list entries. Unknown
|
|
291
|
+
configuration keys are rejected. A bare `mecab_command` is resolved through PATH;
|
|
292
|
+
use an absolute path for an explicit executable override.
|
|
220
293
|
|
|
221
294
|
## Rules and coverage
|
|
222
295
|
|
|
@@ -224,11 +297,44 @@ Unknown configuration keys are rejected.
|
|
|
224
297
|
| --- | --- | --- |
|
|
225
298
|
| `abstract-reference` | hint | Ask what an abstract noun refers to |
|
|
226
299
|
| `weak-predicate` | hint | Ask whether an operation's purpose or result is clear |
|
|
300
|
+
| `contextual-phrase` | hint | Review referents, assumptions, and qualifications while preserving negation |
|
|
227
301
|
| `generic-framing` | hint | Review generic introductions and emphasis |
|
|
302
|
+
| `vague-degree` | hint | Ask what depth, level, scope, or comparison is intended |
|
|
228
303
|
| `repeated-ending` | info | Identify three consecutive sentences with the same detected ending |
|
|
229
304
|
| `glossary-variant` | warning | Identify project-specific terminology variants |
|
|
230
305
|
| `style-candidate` | hint | Check selected polite/plain endings against an explicit style |
|
|
231
306
|
|
|
307
|
+
MeCab mode matches noun and adjective tokens and dictionary forms of verbs.
|
|
308
|
+
Sahen predicates are matched as a noun followed by the verb for "do"; standalone
|
|
309
|
+
sahen nouns are not treated as verbal predicates. Auxiliary sequences retain
|
|
310
|
+
negation, past tense, passive forms, and progressive forms in the reported surface.
|
|
311
|
+
The added reach predicate is negative-only; the existing handling predicate is
|
|
312
|
+
reviewed in both affirmative and negative forms. Glossary variants and generic
|
|
313
|
+
framing phrases continue to use literal matching. Contextual phrases also use
|
|
314
|
+
literal matching and suppress overlapping morphological candidates. Compound
|
|
315
|
+
nouns are matched across adjacent noun tokens. Selection and narrowing verbs
|
|
316
|
+
include potential forms; predicate surfaces also preserve causative auxiliaries.
|
|
317
|
+
|
|
318
|
+
Morphological candidates include `lemma`, `part_of_speech`, `negative`, and
|
|
319
|
+
`detector` alongside the original `match`, line, and column. Negation detection
|
|
320
|
+
covers common IPADIC negative auxiliaries; it is not full semantic analysis of
|
|
321
|
+
negation scope or double negatives. Unknown tokens are not guessed. Kana/kanji
|
|
322
|
+
variants of the alignment and gathering verbs have explicit canonical mappings;
|
|
323
|
+
other spelling variants are not automatically normalized.
|
|
324
|
+
|
|
325
|
+
The session records the MeCab version and dictionary file hashes. Verification
|
|
326
|
+
reports analyzer changes instead of treating results from different dictionaries
|
|
327
|
+
as directly comparable. Prompt generation uses saved evidence and does not need
|
|
328
|
+
MeCab. Changed rules or dictionary settings require a new scan.
|
|
329
|
+
|
|
330
|
+
Sessions from earlier tool versions are not compatible with 0.1.2. Keep the original baseline for
|
|
331
|
+
an ongoing review and finish it with the original version, or start a new review
|
|
332
|
+
pass in a different directory:
|
|
333
|
+
|
|
334
|
+
```sh
|
|
335
|
+
asciidoc-pubkit review scan book.adoc --output .pubkit/review-0.1.2
|
|
336
|
+
```
|
|
337
|
+
|
|
232
338
|
Severity describes review priority, not proof of an error. There is no AI-authorship
|
|
233
339
|
score and no requirement to eliminate every match.
|
|
234
340
|
|
|
@@ -254,7 +360,9 @@ gem build asciidoc-pubkit.gemspec
|
|
|
254
360
|
```
|
|
255
361
|
|
|
256
362
|
The tests exercise include-boundary mapping, inline masking, configuration,
|
|
257
|
-
contextual prompts, stale inputs,
|
|
363
|
+
contextual prompts, stale inputs, protected-content verification, and real MeCab
|
|
364
|
+
analysis of inflections, negative predicates, Unicode positions, and long lines.
|
|
365
|
+
Install MeCab and UTF-8 IPADIC before running the complete test suite.
|
|
258
366
|
GitHub Actions is configured for Ruby 3.2, 3.3, 3.4, and 4.0 on Linux.
|
|
259
367
|
|
|
260
368
|
## License
|
|
@@ -262,3 +370,69 @@ GitHub Actions is configured for Ruby 3.2, 3.3, 3.4, and 4.0 on Linux.
|
|
|
262
370
|
Copyright 2026 CyberGarage.
|
|
263
371
|
|
|
264
372
|
Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE).
|
|
373
|
+
|
|
374
|
+
## Customize review rules
|
|
375
|
+
|
|
376
|
+
The UTF-8 YAML file [`data/review-rules.ja.yml`](data/review-rules.ja.yml) is
|
|
377
|
+
included in the gem and loaded by default. Edit that file when running a local
|
|
378
|
+
checkout, or copy it to a project-owned file for custom rules. No Ruby changes
|
|
379
|
+
are required to update candidate terms.
|
|
380
|
+
|
|
381
|
+
```sh
|
|
382
|
+
asciidoc-pubkit review scan book.adoc --rules ./review-rules.yml
|
|
383
|
+
make run ARGS="review scan book.adoc --rules ./review-rules.yml"
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
For an installed gem, copy the default file with:
|
|
387
|
+
|
|
388
|
+
```sh
|
|
389
|
+
ruby -rasciidoc_pubkit -e 'puts File.read(AsciidocPubkit::RuleSet::DEFAULT_PATH)' > review-rules.yml
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Alternatively, configure a path in `.asciidoc-pubkit.yml`:
|
|
393
|
+
|
|
394
|
+
```yaml
|
|
395
|
+
review:
|
|
396
|
+
rules: review-rules.yml
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Precedence is `--rules`, then `review.rules`, then the packaged default.
|
|
400
|
+
CLI paths are relative to the current working directory; configuration paths
|
|
401
|
+
are relative to the configuration file. A custom file replaces the entire rule
|
|
402
|
+
set; it is not merged with defaults. Start by copying the standard file.
|
|
403
|
+
|
|
404
|
+
### Rule file format (schema version 1)
|
|
405
|
+
|
|
406
|
+
All top-level fields below are required. Unknown keys and invalid types are
|
|
407
|
+
rejected. YAML aliases and object tags are not supported.
|
|
408
|
+
|
|
409
|
+
| Field | Format and behavior |
|
|
410
|
+
| --- | --- |
|
|
411
|
+
| `schema_version` | Integer `1` |
|
|
412
|
+
| `terms` | Mapping containing all five categories listed below |
|
|
413
|
+
| `verbs` | Mapping from canonical verb forms to nonempty arrays of MeCab/IPADIC dictionary forms; each dictionary form belongs to only one canonical form |
|
|
414
|
+
| `sahen` | Array of nouns matched with a following `する` verb |
|
|
415
|
+
| `negative_only` | Array of canonical predicates restricted to negative forms; use the noun plus `する` for sahen predicates |
|
|
416
|
+
| `compound_nouns` | Array of terms matched across contiguous noun tokens; each must also appear in `abstract-reference.terms` |
|
|
417
|
+
|
|
418
|
+
Each `terms` category must contain `terms` (an array of unique nonempty strings)
|
|
419
|
+
and `question` (a nonempty review instruction string). The required categories
|
|
420
|
+
are `abstract-reference`, `weak-predicate`, `vague-degree`, `contextual-phrase`,
|
|
421
|
+
and `generic-framing`. Empty term arrays disable that category's literal
|
|
422
|
+
candidates. Empty `verbs`, `sahen`, and `compound_nouns` collections disable their
|
|
423
|
+
respective morphological matchers. Keep `negative_only` consistent with the
|
|
424
|
+
configured predicates.
|
|
425
|
+
|
|
426
|
+
In literal mode, all category term lists use exact phrase matching. In MeCab
|
|
427
|
+
mode, abstract nouns and degree adjectives use dictionary forms, while predicates
|
|
428
|
+
use `verbs` and `sahen`; add a predicate's desired literal surface to
|
|
429
|
+
`weak-predicate.terms` as well if literal mode should detect it. Contextual and
|
|
430
|
+
generic framing phrases use literal matching in both modes. Questions apply to
|
|
431
|
+
both detectors. Glossary, style, repeated-ending checks, inline exclusions, and
|
|
432
|
+
morphological suffix handling remain implemented in Ruby.
|
|
433
|
+
|
|
434
|
+
A scan saves the resolved rule contents and source path in `manifest.json`.
|
|
435
|
+
Prompt generation and verification use the saved contents, even if the original
|
|
436
|
+
YAML file is subsequently edited or removed. Start a new session to apply rule
|
|
437
|
+
changes. Older sessions without a rule snapshot fall back to the currently
|
|
438
|
+
installed default file; start a new session for reproducible custom-rule reviews.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 1
|
|
3
|
+
terms:
|
|
4
|
+
abstract-reference:
|
|
5
|
+
terms:
|
|
6
|
+
- コスト
|
|
7
|
+
- 境界
|
|
8
|
+
- 契約
|
|
9
|
+
- 観点
|
|
10
|
+
- 土台
|
|
11
|
+
- 橋渡し
|
|
12
|
+
- 入口
|
|
13
|
+
- 記述
|
|
14
|
+
- 場所
|
|
15
|
+
- 意図
|
|
16
|
+
- 役割
|
|
17
|
+
- 一続き
|
|
18
|
+
- 根拠
|
|
19
|
+
- 部品
|
|
20
|
+
- 開発者
|
|
21
|
+
- 概念
|
|
22
|
+
question: Identify the concrete referent, components, or measurable work. Keep
|
|
23
|
+
established technical meanings.
|
|
24
|
+
weak-predicate:
|
|
25
|
+
terms:
|
|
26
|
+
- 利用します
|
|
27
|
+
- 整理します
|
|
28
|
+
- 扱います
|
|
29
|
+
- 示します
|
|
30
|
+
- 変わります
|
|
31
|
+
- 把握します
|
|
32
|
+
- 分けられます
|
|
33
|
+
- そろえます
|
|
34
|
+
- まとまっています
|
|
35
|
+
- 加えます
|
|
36
|
+
- 探します
|
|
37
|
+
- 到達しません
|
|
38
|
+
- 扱いません
|
|
39
|
+
- そろいます
|
|
40
|
+
- 選べます
|
|
41
|
+
- 成り立たせています
|
|
42
|
+
- 確かめます
|
|
43
|
+
- 書き換える
|
|
44
|
+
- 絞れます
|
|
45
|
+
- 渡します
|
|
46
|
+
- あります
|
|
47
|
+
- 意味しません
|
|
48
|
+
question: Check whether the purpose, operation, or result is clear from the surrounding
|
|
49
|
+
paragraph. Preserve negation and conditions.
|
|
50
|
+
vague-degree:
|
|
51
|
+
terms:
|
|
52
|
+
- 浅い
|
|
53
|
+
- 深い
|
|
54
|
+
question: Identify the concrete depth, level, scope, or comparison. Keep literal
|
|
55
|
+
measurements and established technical meanings.
|
|
56
|
+
contextual-phrase:
|
|
57
|
+
terms:
|
|
58
|
+
- これらを
|
|
59
|
+
- であることだけでは
|
|
60
|
+
- あるものとします
|
|
61
|
+
- わけではありません
|
|
62
|
+
- 別です
|
|
63
|
+
question: Check the referent, assumption, or qualification against the surrounding
|
|
64
|
+
explanation. Preserve conditions and negation.
|
|
65
|
+
generic-framing:
|
|
66
|
+
terms:
|
|
67
|
+
- 重要なのは
|
|
68
|
+
- ポイントは
|
|
69
|
+
- 本章では
|
|
70
|
+
- ここでは
|
|
71
|
+
- まとめると
|
|
72
|
+
question: Check whether this framing adds useful scope or information instead
|
|
73
|
+
of repeating the explanation.
|
|
74
|
+
verbs:
|
|
75
|
+
扱う:
|
|
76
|
+
- 扱う
|
|
77
|
+
示す:
|
|
78
|
+
- 示す
|
|
79
|
+
変わる:
|
|
80
|
+
- 変わる
|
|
81
|
+
分ける:
|
|
82
|
+
- 分ける
|
|
83
|
+
そろえる:
|
|
84
|
+
- そろえる
|
|
85
|
+
- 揃える
|
|
86
|
+
まとまる:
|
|
87
|
+
- まとまる
|
|
88
|
+
- 纏まる
|
|
89
|
+
加える:
|
|
90
|
+
- 加える
|
|
91
|
+
探す:
|
|
92
|
+
- 探す
|
|
93
|
+
そろう:
|
|
94
|
+
- そろう
|
|
95
|
+
- 揃う
|
|
96
|
+
選ぶ:
|
|
97
|
+
- 選ぶ
|
|
98
|
+
- 選べる
|
|
99
|
+
成り立つ:
|
|
100
|
+
- 成り立つ
|
|
101
|
+
確かめる:
|
|
102
|
+
- 確かめる
|
|
103
|
+
書き換える:
|
|
104
|
+
- 書き換える
|
|
105
|
+
絞る:
|
|
106
|
+
- 絞る
|
|
107
|
+
- 絞れる
|
|
108
|
+
渡す:
|
|
109
|
+
- 渡す
|
|
110
|
+
ある:
|
|
111
|
+
- ある
|
|
112
|
+
sahen:
|
|
113
|
+
- 利用
|
|
114
|
+
- 整理
|
|
115
|
+
- 把握
|
|
116
|
+
- 到達
|
|
117
|
+
- 意味
|
|
118
|
+
negative_only:
|
|
119
|
+
- 到達する
|
|
120
|
+
- 意味する
|
|
121
|
+
compound_nouns:
|
|
122
|
+
- 一続き
|
|
123
|
+
- 開発者
|
data/lib/asciidoc_pubkit/cli.rb
CHANGED
|
@@ -36,8 +36,10 @@ module AsciidocPubkit
|
|
|
36
36
|
if command == 'scan'
|
|
37
37
|
opts.on('--only FILE', 'Review one included file in the book context') { |v| options[:only] = v }
|
|
38
38
|
opts.on('--config FILE', 'Use an explicit YAML configuration') { |v| options[:config] = v }
|
|
39
|
+
opts.on('--rules FILE', 'Replace default review rules with a YAML rule set') { |v| options[:rules] = v }
|
|
39
40
|
opts.on('--base-dir DIR', 'Set the Asciidoctor base directory') { |v| options[:base_dir] = v }
|
|
40
41
|
opts.on('--lang LANG', 'Prose language (ja only)') { |v| options[:language] = v }
|
|
42
|
+
opts.on('--tokenizer NAME', 'mecab (default) or literal (limited phrase matching)') { |v| options[:tokenizer] = v }
|
|
41
43
|
opts.on('--style STYLE', 'preserve (default), desu-masu, or dearu') { |v| options[:style] = v }
|
|
42
44
|
opts.on('-a', '--attribute NAME=VALUE', 'Set an Asciidoctor attribute; repeat as needed') do |v|
|
|
43
45
|
key, value = v.split('=', 2)
|
|
@@ -58,6 +60,7 @@ module AsciidocPubkit
|
|
|
58
60
|
if command == 'scan'
|
|
59
61
|
result = Session.scan(args.first, options)
|
|
60
62
|
out.puts "Scanned #{result['paragraphs']} paragraphs; found #{result['findings']} review candidates."
|
|
63
|
+
out.puts "Tokenizer: #{result['tokenizer']}#{result['tokenizer'] == 'literal' ? ' (limited phrase matching; no morphological analysis)' : ' (UTF-8 IPADIC)'}"
|
|
61
64
|
out.puts "Coverage notices: #{result['coverage_notices']}. See document.json for limitations."
|
|
62
65
|
out.puts "Review session: #{result['session']}"
|
|
63
66
|
return 0
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module AsciidocPubkit
|
|
6
|
+
# An adapter for the MeCab CLI with UTF-8 IPADIC features. No shell is used.
|
|
7
|
+
class Morphology
|
|
8
|
+
attr_reader :identity
|
|
9
|
+
|
|
10
|
+
def initialize(settings)
|
|
11
|
+
@command = settings.fetch('mecab_command', 'mecab')
|
|
12
|
+
@args = []
|
|
13
|
+
dictionary = settings['mecab_dictionary']
|
|
14
|
+
@args += ['--dicdir', dictionary] if dictionary
|
|
15
|
+
version = execute(['--version']).strip
|
|
16
|
+
info = execute(@args + ['--dictionary-info'])
|
|
17
|
+
charsets = info.scan(/^charset:\s*(\S+)/).flatten
|
|
18
|
+
unless !charsets.empty? && charsets.all? { |charset| %w[utf8 utf-8].include?(charset.downcase) }
|
|
19
|
+
raise Error, 'MeCab requires a UTF-8 IPADIC dictionary. Set review.mecab_dictionary to its directory.'
|
|
20
|
+
end
|
|
21
|
+
files = info.scan(/^filename:\s*(.+)$/).flatten.map(&:strip)
|
|
22
|
+
raise Error, 'MeCab did not report a dictionary filename.' if files.empty?
|
|
23
|
+
fingerprints = files.flat_map do |file|
|
|
24
|
+
[file, *%w[unk.dic matrix.bin char.bin dicrc].map { |name| File.join(File.dirname(file), name) }]
|
|
25
|
+
end.uniq.to_h do |file|
|
|
26
|
+
raise Error, "MeCab dictionary component is missing: #{file}" unless File.file?(file)
|
|
27
|
+
[File.realpath(file), Digest::SHA256.file(file).hexdigest]
|
|
28
|
+
end
|
|
29
|
+
@identity = { 'engine' => 'mecab', 'version' => version, 'feature_schema' => 'ipadic', 'dictionary_files' => fingerprints }
|
|
30
|
+
probe = tokenize('食べました。浅かった。')
|
|
31
|
+
unless probe.any? { |t| t['lemma'] == '食べる' && t['pos'] == '動詞' } &&
|
|
32
|
+
probe.any? { |t| t['lemma'] == '浅い' && t['pos'] == '形容詞' }
|
|
33
|
+
raise Error, 'Unsupported MeCab dictionary. Use UTF-8 IPADIC; UniDic and other feature schemas are not supported.'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def tokenize(text)
|
|
38
|
+
return [] if text.empty?
|
|
39
|
+
raise Error, 'MeCab input contains a NUL character.' if text.include?("\0")
|
|
40
|
+
format = '%m\t%H\t%s\n'
|
|
41
|
+
buffer = [8192, text.lines.map(&:bytesize).max.to_i + 1].max
|
|
42
|
+
output = execute(@args + ['--node-format', format, '--unk-format', format,
|
|
43
|
+
'--bos-format', '', '--eos-format', '', '--input-buffer-size', buffer.to_s], text)
|
|
44
|
+
cursor = 0
|
|
45
|
+
tokens = output.lines.filter_map do |line|
|
|
46
|
+
surface, feature, kind = line.chomp.split("\t", 3)
|
|
47
|
+
raise Error, 'MeCab returned an invalid token record.' unless surface && feature && kind && !surface.empty?
|
|
48
|
+
offset = text.index(surface, cursor)
|
|
49
|
+
unless offset && text[cursor...offset].match?(/\A[[:space:]]*\z/)
|
|
50
|
+
raise Error, 'MeCab token positions could not be aligned with the source text.'
|
|
51
|
+
end
|
|
52
|
+
cursor = offset + surface.length
|
|
53
|
+
fields = feature.split(',', -1)
|
|
54
|
+
if kind == '0' && fields.length != 9
|
|
55
|
+
raise Error, 'Unsupported MeCab feature schema. A UTF-8 IPADIC dictionary is required.'
|
|
56
|
+
end
|
|
57
|
+
{
|
|
58
|
+
'surface' => surface, 'offset' => offset, 'end_offset' => cursor,
|
|
59
|
+
'pos' => fields[0], 'pos_detail' => fields[1],
|
|
60
|
+
'lemma' => fields[6] == '*' ? surface : fields[6], 'unknown' => kind != '0'
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
unless text[cursor..].match?(/\A[[:space:]]*\z/)
|
|
64
|
+
raise Error, 'MeCab did not analyze the entire source text.'
|
|
65
|
+
end
|
|
66
|
+
tokens
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def execute(args, input = '')
|
|
72
|
+
stdout, stderr, status = Open3.capture3(@command, *args, stdin_data: input)
|
|
73
|
+
# MeCab 0.996 returns 1 after successfully printing dictionary information.
|
|
74
|
+
dictionary_info = args.include?('--dictionary-info') && status.exitstatus == 1 && stderr.empty? && stdout.start_with?('filename:')
|
|
75
|
+
raise Error, "MeCab failed (exit #{status.exitstatus}): #{(stderr.empty? ? stdout : stderr).strip[0, 1000]}" unless status.success? || dictionary_info
|
|
76
|
+
stdout.force_encoding(Encoding::UTF_8)
|
|
77
|
+
raise Error, 'MeCab output is not UTF-8.' unless stdout.valid_encoding?
|
|
78
|
+
stdout
|
|
79
|
+
rescue Errno::ENOENT
|
|
80
|
+
raise Error, 'MeCab is not installed. Install MeCab and UTF-8 IPADIC (macOS: brew install mecab mecab-ipadic), or explicitly use --tokenizer literal for limited phrase matching.'
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AsciidocPubkit
|
|
4
|
+
class RuleSet
|
|
5
|
+
DEFAULT_PATH = File.expand_path('../../data/review-rules.ja.yml', __dir__)
|
|
6
|
+
CATEGORIES = %w[abstract-reference weak-predicate vague-degree contextual-phrase generic-framing].freeze
|
|
7
|
+
KEYS = %w[schema_version terms verbs sahen negative_only compound_nouns].freeze
|
|
8
|
+
|
|
9
|
+
def self.load(path = DEFAULT_PATH)
|
|
10
|
+
validate(YAML.safe_load(AsciidocPubkit.read_text(path), permitted_classes: [], aliases: false))
|
|
11
|
+
rescue Psych::Exception => e
|
|
12
|
+
raise Error, "Invalid rule YAML in #{path}: #{e.message}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.mapping(value, keys, label)
|
|
16
|
+
unless value.is_a?(Hash) && (value.keys - keys).empty? && (keys - value.keys).empty?
|
|
17
|
+
raise Error, "#{label} must contain exactly these keys: #{keys.join(', ')}."
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.strings(value, label)
|
|
22
|
+
unless value.is_a?(Array) && value.all? { |s| s.is_a?(String) && !s.strip.empty? } && value.uniq == value
|
|
23
|
+
raise Error, "#{label} must be an array of unique nonempty strings."
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.validate(data)
|
|
28
|
+
mapping(data, KEYS, 'Rule set')
|
|
29
|
+
raise Error, 'Rule schema_version must be integer 1.' unless data['schema_version'].is_a?(Integer) && data['schema_version'] == 1
|
|
30
|
+
mapping(data['terms'], CATEGORIES, 'Rule terms')
|
|
31
|
+
data['terms'].each do |category, entry|
|
|
32
|
+
mapping(entry, %w[terms question], category)
|
|
33
|
+
strings(entry['terms'], "#{category}.terms")
|
|
34
|
+
raise Error, "#{category}.question must be a nonempty string." unless entry['question'].is_a?(String) && !entry['question'].strip.empty?
|
|
35
|
+
end
|
|
36
|
+
verbs = data['verbs']
|
|
37
|
+
raise Error, 'verbs must be a mapping of canonical forms to lemma arrays.' unless verbs.is_a?(Hash)
|
|
38
|
+
verbs.each do |canonical, forms|
|
|
39
|
+
raise Error, 'Verb canonical forms must be nonempty strings.' unless canonical.is_a?(String) && !canonical.strip.empty?
|
|
40
|
+
strings(forms, "verbs.#{canonical}")
|
|
41
|
+
raise Error, "verbs.#{canonical} must contain at least one lemma." if forms.empty?
|
|
42
|
+
end
|
|
43
|
+
aliases = verbs.values.flatten
|
|
44
|
+
raise Error, 'Verb lemmas must have only one canonical form.' unless aliases.uniq == aliases
|
|
45
|
+
%w[sahen negative_only compound_nouns].each { |key| strings(data[key], key) }
|
|
46
|
+
canonical_forms = verbs.keys + data['sahen'].map { |noun| noun + 'する' }
|
|
47
|
+
raise Error, 'negative_only must refer to configured canonical predicates.' unless (data['negative_only'] - canonical_forms).empty?
|
|
48
|
+
raise Error, 'compound_nouns must also appear in abstract-reference terms.' unless (data['compound_nouns'] - data['terms']['abstract-reference']['terms']).empty?
|
|
49
|
+
data
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -3,22 +3,24 @@
|
|
|
3
3
|
module AsciidocPubkit
|
|
4
4
|
class Rules
|
|
5
5
|
INLINE = /`[^`\n]*`|\+\+\+.*?\+\+\+|\+\+[^\n]*?\+\+|(?<!\w)\+[^+\n]+\+|「[^」\n]*」|『[^』\n]*』|\{[^}\n]+\}|<<[^>\n]+>>|\[\[[^\]\n]+\]\]|(?:link|xref|image|footnote|pass):[^\s\[]*\[[^\]\n]*\]|https?:\/\/[^\s\[\]<>]+(?:\[[^\]\n]*\])?/m
|
|
6
|
-
TERMS = {
|
|
7
|
-
'abstract-reference' => [%w[コスト 境界 契約 観点 土台 橋渡し], 'Identify the concrete referent, components, or measurable work. Keep established technical meanings.'],
|
|
8
|
-
'weak-predicate' => [%w[利用します 整理します 扱います 示します], 'Check whether the purpose, operation, or result is clear from the surrounding paragraph.'],
|
|
9
|
-
'generic-framing' => [%w[重要なのは ポイントは 本章では ここでは まとめると], 'Check whether this framing adds useful scope or information instead of repeating the explanation.']
|
|
10
|
-
}.freeze
|
|
11
|
-
|
|
12
6
|
def self.mask(text)
|
|
13
7
|
# Keep character offsets stable while excluding common inline constructs.
|
|
14
8
|
text.gsub(INLINE) { |match| match.gsub(/[^\n]/, ' ') }
|
|
15
9
|
end
|
|
16
10
|
|
|
17
|
-
def self.scan(paragraphs, settings)
|
|
11
|
+
def self.scan(paragraphs, settings, tokenizer: nil)
|
|
12
|
+
rules = RuleSet.validate(settings.fetch('rules') { RuleSet.load })
|
|
13
|
+
terms = rules.fetch('terms').transform_values { |entry| entry.values_at('terms', 'question') }
|
|
18
14
|
findings = []
|
|
19
|
-
|
|
15
|
+
if settings.fetch('tokenizer', 'mecab') == 'mecab'
|
|
16
|
+
tokenizer ||= Morphology.new(settings)
|
|
17
|
+
end
|
|
18
|
+
token_groups = tokenizer ? tokenize_paragraphs(paragraphs, tokenizer) : []
|
|
19
|
+
paragraphs.each_with_index do |paragraph, index|
|
|
20
20
|
text = mask(paragraph.fetch('text'))
|
|
21
|
-
|
|
21
|
+
scan_morphemes(findings, paragraph, text, token_groups[index], settings, rules, terms) if tokenizer
|
|
22
|
+
terms.each do |rule, (terms, question)|
|
|
23
|
+
next if tokenizer && !%w[generic-framing contextual-phrase].include?(rule)
|
|
22
24
|
terms.each do |term|
|
|
23
25
|
next if settings.fetch('allows').include?(term)
|
|
24
26
|
find_term(findings, paragraph, text, term, rule, 'hint', question)
|
|
@@ -49,6 +51,104 @@ module AsciidocPubkit
|
|
|
49
51
|
findings
|
|
50
52
|
end
|
|
51
53
|
|
|
54
|
+
def self.tokenize_paragraphs(paragraphs, tokenizer)
|
|
55
|
+
text = +''
|
|
56
|
+
spans = paragraphs.map do |paragraph|
|
|
57
|
+
start = text.length
|
|
58
|
+
text << mask(paragraph['text'])
|
|
59
|
+
finish = text.length
|
|
60
|
+
text << "\n"
|
|
61
|
+
[start, finish]
|
|
62
|
+
end
|
|
63
|
+
tokens = tokenizer.tokenize(text)
|
|
64
|
+
cursor = 0
|
|
65
|
+
spans.map do |start, finish|
|
|
66
|
+
group = []
|
|
67
|
+
while cursor < tokens.length && tokens[cursor]['offset'] < finish
|
|
68
|
+
token = tokens[cursor]
|
|
69
|
+
raise Error, 'A morphological token crossed a paragraph boundary.' if token['offset'] < start || token['end_offset'] > finish
|
|
70
|
+
group << token.merge('offset' => token['offset'] - start, 'end_offset' => token['end_offset'] - start)
|
|
71
|
+
cursor += 1
|
|
72
|
+
end
|
|
73
|
+
group
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.scan_morphemes(findings, paragraph, text, tokens, settings, rules, terms)
|
|
78
|
+
phrase_ranges = terms['contextual-phrase'][0].flat_map do |phrase|
|
|
79
|
+
text.to_enum(:scan, Regexp.new(Regexp.escape(phrase))).map do
|
|
80
|
+
match = Regexp.last_match
|
|
81
|
+
match.begin(0)...match.end(0)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
tokens.each_with_index do |token, index|
|
|
85
|
+
next if token['unknown'] || phrase_ranges.any? { |range| range.cover?(token['offset']) }
|
|
86
|
+
lemma = token['lemma']
|
|
87
|
+
rule = nil
|
|
88
|
+
finish_index = index
|
|
89
|
+
compound = rules.fetch('compound_nouns').find do |term|
|
|
90
|
+
surface = +''
|
|
91
|
+
cursor = index
|
|
92
|
+
while (member = tokens[cursor]) && member['pos'] == '名詞' && !member['unknown']
|
|
93
|
+
break if cursor > index && tokens[cursor - 1]['end_offset'] != member['offset']
|
|
94
|
+
surface << member['surface']
|
|
95
|
+
break unless term.start_with?(surface)
|
|
96
|
+
if surface == term
|
|
97
|
+
finish_index = cursor
|
|
98
|
+
break
|
|
99
|
+
end
|
|
100
|
+
cursor += 1
|
|
101
|
+
end
|
|
102
|
+
surface == term
|
|
103
|
+
end
|
|
104
|
+
if compound
|
|
105
|
+
lemma = compound
|
|
106
|
+
rule = 'abstract-reference'
|
|
107
|
+
elsif token['pos'] == '名詞' && terms['abstract-reference'][0].include?(lemma)
|
|
108
|
+
rule = 'abstract-reference'
|
|
109
|
+
elsif token['pos'] == '形容詞' && terms['vague-degree'][0].include?(lemma)
|
|
110
|
+
rule = 'vague-degree'
|
|
111
|
+
finish_index = predicate_end(tokens, index, text)
|
|
112
|
+
elsif token['pos'] == '動詞' && (entry = rules.fetch('verbs').find { |_canonical, forms| forms.include?(lemma) })
|
|
113
|
+
lemma = entry[0]
|
|
114
|
+
rule = 'weak-predicate'
|
|
115
|
+
finish_index = predicate_end(tokens, index, text)
|
|
116
|
+
elsif token['pos'] == '名詞' && token['pos_detail'] == 'サ変接続' && rules.fetch('sahen').include?(lemma)
|
|
117
|
+
following = tokens[index + 1]
|
|
118
|
+
next unless following && following['pos'] == '動詞' && following['lemma'] == 'する' && adjacent?(token, following, text)
|
|
119
|
+
lemma += 'する'
|
|
120
|
+
rule = 'weak-predicate'
|
|
121
|
+
finish_index = predicate_end(tokens, index + 1, text)
|
|
122
|
+
end
|
|
123
|
+
next unless rule
|
|
124
|
+
members = tokens[index..finish_index]
|
|
125
|
+
negative = members.any? { |member| member['pos'] == '助動詞' && %w[ない ぬ ん].include?(member['lemma']) }
|
|
126
|
+
next if rules.fetch('negative_only').include?(lemma) && !negative
|
|
127
|
+
surface = text[token['offset']...tokens[finish_index]['end_offset']]
|
|
128
|
+
allows = settings.fetch('allows')
|
|
129
|
+
next if [lemma, token['lemma'], surface, surface + '。'].any? { |form| allows.include?(form) }
|
|
130
|
+
add(findings, paragraph, text, token['offset'], surface, rule, 'hint', terms.fetch(rule)[1])
|
|
131
|
+
findings.last.merge!('lemma' => lemma, 'part_of_speech' => token['pos'], 'negative' => negative,
|
|
132
|
+
'detector' => 'mecab-ipadic')
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.adjacent?(left, right, text)
|
|
137
|
+
text[left['end_offset']...right['offset']].match?(/\A\n?\z/)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.predicate_end(tokens, index, text)
|
|
141
|
+
finish = index
|
|
142
|
+
while (following = tokens[finish + 1]) && adjacent?(tokens[finish], following, text)
|
|
143
|
+
auxiliary = following['pos'] == '助動詞'
|
|
144
|
+
dependent_verb = following['pos'] == '動詞' && %w[非自立 接尾].include?(following['pos_detail'])
|
|
145
|
+
connector = following['pos'] == '助詞' && following['pos_detail'] == '接続助詞' && %w[て で].include?(following['lemma'])
|
|
146
|
+
break unless auxiliary || dependent_verb || connector
|
|
147
|
+
finish += 1
|
|
148
|
+
end
|
|
149
|
+
finish
|
|
150
|
+
end
|
|
151
|
+
|
|
52
152
|
def self.find_term(findings, paragraph, text, term, rule, severity, question)
|
|
53
153
|
text.to_enum(:scan, Regexp.new(Regexp.escape(term))).each do
|
|
54
154
|
match = Regexp.last_match
|
|
@@ -14,7 +14,9 @@ module AsciidocPubkit
|
|
|
14
14
|
end
|
|
15
15
|
destination = File.expand_path(options.fetch(:output, '.pubkit/review'))
|
|
16
16
|
raise Error, "Output already exists: #{destination}" if File.exist?(destination)
|
|
17
|
-
|
|
17
|
+
tokenizer = settings.data['tokenizer'] == 'mecab' ? Morphology.new(settings.data) : nil
|
|
18
|
+
analysis = tokenizer ? tokenizer.identity : { 'engine' => 'literal' }
|
|
19
|
+
findings = Rules.scan(document.paragraphs, settings.data, tokenizer: tokenizer)
|
|
18
20
|
parent = File.dirname(destination)
|
|
19
21
|
FileUtils.mkdir_p(parent)
|
|
20
22
|
staging = Dir.mktmpdir('.pubkit-', parent)
|
|
@@ -29,6 +31,7 @@ module AsciidocPubkit
|
|
|
29
31
|
'schema_version' => SCHEMA, 'tool_version' => VERSION,
|
|
30
32
|
'entry' => File.realpath(entry), 'only' => options[:only] && File.realpath(options[:only]),
|
|
31
33
|
'settings' => settings.data, 'sources' => sources,
|
|
34
|
+
'analysis' => analysis,
|
|
32
35
|
'protected' => protected_content(document),
|
|
33
36
|
'numeric_tokens' => numeric_tokens(document)
|
|
34
37
|
}
|
|
@@ -43,7 +46,8 @@ module AsciidocPubkit
|
|
|
43
46
|
FileUtils.remove_entry(staging) if File.exist?(staging)
|
|
44
47
|
end
|
|
45
48
|
{ 'session' => destination, 'paragraphs' => document.paragraphs.length,
|
|
46
|
-
'findings' => findings.length, 'coverage_notices' => document.coverage.length
|
|
49
|
+
'findings' => findings.length, 'coverage_notices' => document.coverage.length,
|
|
50
|
+
'tokenizer' => settings.data['tokenizer'] }
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
def self.write_json(path, value)
|
|
@@ -126,6 +130,13 @@ module AsciidocPubkit
|
|
|
126
130
|
|
|
127
131
|
#{JSON.pretty_generate(@manifest['settings'])}
|
|
128
132
|
|
|
133
|
+
## Analysis backend
|
|
134
|
+
|
|
135
|
+
#{JSON.pretty_generate(@manifest['analysis'])}
|
|
136
|
+
|
|
137
|
+
Morphological findings include a dictionary form and the original inflected surface.
|
|
138
|
+
A negative form must not be rewritten as an affirmative assertion. Keep the original polarity and uncertainty.
|
|
139
|
+
|
|
129
140
|
## Coverage
|
|
130
141
|
|
|
131
142
|
Only source-mapped running-prose paragraphs are reviewed. Inline macros and literal spans are masked by a conservative heuristic.
|
|
@@ -176,10 +187,15 @@ module AsciidocPubkit
|
|
|
176
187
|
changed = @manifest['sources'].filter_map do |source|
|
|
177
188
|
source['path'] if document.sources[source['path']] && AsciidocPubkit.hash_text(document.sources[source['path']]) != source['sha256']
|
|
178
189
|
end
|
|
190
|
+
tokenizer = @manifest['settings']['tokenizer'] == 'mecab' ? Morphology.new(@manifest['settings']) : nil
|
|
191
|
+
analysis = tokenizer ? tokenizer.identity : { 'engine' => 'literal' }
|
|
192
|
+
if analysis != @manifest['analysis']
|
|
193
|
+
issues << { 'kind' => 'analyzer-changed', 'message' => 'The MeCab version or dictionary changed. Finish verification with the original analyzer or start a new review pass.' }
|
|
194
|
+
end
|
|
179
195
|
{
|
|
180
196
|
'passed' => issues.empty?, 'meaning_verified' => false, 'changed_files' => changed,
|
|
181
197
|
'issues' => issues, 'notices' => notices, 'coverage' => document.coverage,
|
|
182
|
-
'findings' => Rules.scan(document.paragraphs, @manifest['settings'])
|
|
198
|
+
'analysis' => analysis, 'findings' => Rules.scan(document.paragraphs, @manifest['settings'], tokenizer: tokenizer)
|
|
183
199
|
}
|
|
184
200
|
rescue Error, Errno::ENOENT => e
|
|
185
201
|
{ 'passed' => false, 'meaning_verified' => false, 'issues' => [{ 'kind' => 'verification-error', 'message' => e.message }],
|
|
@@ -23,22 +23,29 @@ module AsciidocPubkit
|
|
|
23
23
|
reject_keys(config, ['review'], 'configuration')
|
|
24
24
|
review = config.fetch('review', {})
|
|
25
25
|
raise Error, 'review must be a mapping.' unless review.is_a?(Hash)
|
|
26
|
-
reject_keys(review, %w[language style glossary exclude allows attributes base_dir], 'review')
|
|
26
|
+
reject_keys(review, %w[language style glossary exclude allows attributes base_dir tokenizer mecab_command mecab_dictionary rules], 'review')
|
|
27
27
|
raise Error, 'attributes must be a mapping.' unless review.fetch('attributes', {}).is_a?(Hash)
|
|
28
|
-
%w[base_dir glossary].each do |key|
|
|
28
|
+
%w[base_dir glossary mecab_command mecab_dictionary rules].each do |key|
|
|
29
29
|
raise Error, "#{key} must be a nonempty path string." if review.key?(key) && (!review[key].is_a?(String) || review[key].empty?)
|
|
30
30
|
end
|
|
31
31
|
base = @path ? File.dirname(@path) : File.dirname(File.expand_path(entry))
|
|
32
|
+
rules_path = options[:rules] ? File.expand_path(options[:rules]) : (review['rules'] ? File.expand_path(review['rules'], base) : RuleSet::DEFAULT_PATH)
|
|
32
33
|
@data = {
|
|
34
|
+
'rules' => RuleSet.load(rules_path),
|
|
35
|
+
'rules_path' => rules_path,
|
|
33
36
|
'language' => options[:language] || review.fetch('language', 'ja'),
|
|
34
37
|
'style' => options[:style] || review.fetch('style', 'preserve'),
|
|
35
38
|
'exclude' => review.fetch('exclude', []),
|
|
36
39
|
'allows' => review.fetch('allows', []),
|
|
40
|
+
'tokenizer' => options[:tokenizer] || review.fetch('tokenizer', 'mecab'),
|
|
41
|
+
'mecab_command' => review.fetch('mecab_command', 'mecab'),
|
|
42
|
+
'mecab_dictionary' => review['mecab_dictionary'] && File.expand_path(review['mecab_dictionary'], base),
|
|
37
43
|
'attributes' => review.fetch('attributes', {}).merge(options.fetch(:attributes, {})),
|
|
38
44
|
'base_dir' => File.expand_path(options[:base_dir] || review.fetch('base_dir', base), options[:base_dir] ? Dir.pwd : base),
|
|
39
45
|
'glossary' => {}
|
|
40
46
|
}
|
|
41
47
|
raise Error, 'Only Japanese (ja) is supported in this release.' unless @data['language'] == 'ja'
|
|
48
|
+
raise Error, 'tokenizer must be mecab or literal.' unless %w[mecab literal].include?(@data['tokenizer'])
|
|
42
49
|
raise Error, 'style must be preserve, desu-masu, or dearu.' unless %w[preserve desu-masu dearu].include?(@data['style'])
|
|
43
50
|
%w[exclude allows].each do |key|
|
|
44
51
|
raise Error, "#{key} must be an array of strings." unless @data[key].is_a?(Array) && @data[key].all? { |v| v.is_a?(String) }
|
data/lib/asciidoc_pubkit.rb
CHANGED
|
@@ -9,7 +9,7 @@ require 'pathname'
|
|
|
9
9
|
require 'optparse'
|
|
10
10
|
|
|
11
11
|
module AsciidocPubkit
|
|
12
|
-
VERSION = '0.1.
|
|
12
|
+
VERSION = '0.1.2'
|
|
13
13
|
class Error < StandardError; end
|
|
14
14
|
|
|
15
15
|
def self.hash_text(text)
|
|
@@ -23,8 +23,10 @@ module AsciidocPubkit
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
require_relative 'asciidoc_pubkit/rule_set'
|
|
26
27
|
require_relative 'asciidoc_pubkit/settings'
|
|
27
28
|
require_relative 'asciidoc_pubkit/document'
|
|
29
|
+
require_relative 'asciidoc_pubkit/morphology'
|
|
28
30
|
require_relative 'asciidoc_pubkit/rules'
|
|
29
31
|
require_relative 'asciidoc_pubkit/session'
|
|
30
32
|
require_relative 'asciidoc_pubkit/cli'
|
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.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- CyberGarage
|
|
@@ -47,11 +47,14 @@ files:
|
|
|
47
47
|
- CHANGELOG.md
|
|
48
48
|
- LICENSE
|
|
49
49
|
- README.md
|
|
50
|
+
- data/review-rules.ja.yml
|
|
50
51
|
- examples/book.adoc
|
|
51
52
|
- exe/asciidoc-pubkit
|
|
52
53
|
- lib/asciidoc_pubkit.rb
|
|
53
54
|
- lib/asciidoc_pubkit/cli.rb
|
|
54
55
|
- lib/asciidoc_pubkit/document.rb
|
|
56
|
+
- lib/asciidoc_pubkit/morphology.rb
|
|
57
|
+
- lib/asciidoc_pubkit/rule_set.rb
|
|
55
58
|
- lib/asciidoc_pubkit/rules.rb
|
|
56
59
|
- lib/asciidoc_pubkit/session.rb
|
|
57
60
|
- lib/asciidoc_pubkit/settings.rb
|