asciidoc-pubkit 0.1.0 → 0.1.1
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 +12 -1
- data/README.md +92 -13
- data/lib/asciidoc_pubkit/cli.rb +2 -0
- data/lib/asciidoc_pubkit/morphology.rb +83 -0
- data/lib/asciidoc_pubkit/rules.rb +112 -4
- data/lib/asciidoc_pubkit/session.rb +19 -3
- data/lib/asciidoc_pubkit/settings.rb +6 -2
- data/lib/asciidoc_pubkit.rb +2 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5e89872635e44464421539669ea3fb4a76c886a3adae9f5ca1df98fd864d05c
|
|
4
|
+
data.tar.gz: bf081702caf7eca34423eea571fd23ad9dfffc89ed11c9b1d013dd84fb25d57c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 93bae7f5ef964afa60b0d6eee95b332270907df107dedae820c09fbbc1829d0d88077df96c20eaff8d9654f31ff6e47390517d1922b6486186dfa04ff17d107a
|
|
7
|
+
data.tar.gz: 5ab456dedf2c322836c827e00a0e2443b3bb9709202d88a0e82203561af98c6953debc601c799355f4df45b744d645f7c4cb670b812c0d7240c374973de09f7c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1 — Released
|
|
4
|
+
|
|
5
|
+
- Add contextual phrases, compound nouns, and potential and causative verb forms.
|
|
6
|
+
- Add requested abstract nouns, degree adjectives, and vague predicates.
|
|
7
|
+
- Use MeCab with UTF-8 IPADIC by default to match inflected predicates and adjectives.
|
|
8
|
+
- Preserve original surfaces, dictionary forms, and negative auxiliary information.
|
|
9
|
+
- Record analyzer and dictionary fingerprints in review sessions.
|
|
10
|
+
- Add an explicit literal mode for environments without MeCab.
|
|
11
|
+
- Reject incompatible dictionaries and unavailable analyzers with setup guidance.
|
|
12
|
+
- Require a new review session when upgrading from 0.1.0.
|
|
13
|
+
|
|
14
|
+
## 0.1.0 — Released
|
|
4
15
|
|
|
5
16
|
- Add AsciiDoc running-prose extraction with checked source locations.
|
|
6
17
|
- Add Japanese review candidates and configurable terminology checks.
|
data/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+
[](https://rubygems.org/gems/asciidoc-pubkit)
|
|
4
|
+
|
|
1
5
|
# asciidoc-pubkit
|
|
2
6
|
|
|
3
7
|
A toolkit for authoring, reviewing, and publishing AsciiDoc books.
|
|
@@ -14,12 +18,15 @@ rewrite manuscripts, or publish books. EPUB, image, and book scaffolding command
|
|
|
14
18
|
are planned extensions, not available features.
|
|
15
19
|
|
|
16
20
|
Ruby 3.2 or later is required. Asciidoctor is installed as a gem dependency.
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
Starting with version 0.1.1, the default review backend requires the external MeCab
|
|
22
|
+
command and a UTF-8 IPADIC dictionary. Node.js and textlint are not required.
|
|
23
|
+
Explicit `--tokenizer literal` mode provides limited phrase matching without MeCab.
|
|
24
|
+
|
|
25
|
+
Version 0.1.1 includes morphological analysis. Version 0.1.0 uses literal matching.
|
|
19
26
|
|
|
20
27
|
## Install from RubyGems
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
Install the CLI from [RubyGems](https://rubygems.org/gems/asciidoc-pubkit):
|
|
23
30
|
|
|
24
31
|
```sh
|
|
25
32
|
gem install asciidoc-pubkit
|
|
@@ -28,8 +35,8 @@ asciidoc-pubkit --help
|
|
|
28
35
|
```
|
|
29
36
|
|
|
30
37
|
Ruby 3.2 or later is required. RubyGems installs the required Ruby dependencies;
|
|
31
|
-
no repository clone or Node.js installation is needed.
|
|
32
|
-
|
|
38
|
+
no repository clone or Node.js installation is needed. MeCab and IPADIC must be
|
|
39
|
+
installed separately when using version 0.1.1 or later in the default mode.
|
|
33
40
|
|
|
34
41
|
Run the review workflow from your manuscript directory:
|
|
35
42
|
|
|
@@ -52,7 +59,7 @@ Add the gem to your project's `Gemfile` to manage its version with Bundler:
|
|
|
52
59
|
|
|
53
60
|
```ruby
|
|
54
61
|
source 'https://rubygems.org'
|
|
55
|
-
gem 'asciidoc-pubkit', '~> 0.1.
|
|
62
|
+
gem 'asciidoc-pubkit', '~> 0.1.1'
|
|
56
63
|
```
|
|
57
64
|
|
|
58
65
|
Then install dependencies and run the CLI through Bundler:
|
|
@@ -74,7 +81,7 @@ git clone https://github.com/cybergarage/asciidoc-pubkit.git
|
|
|
74
81
|
cd asciidoc-pubkit
|
|
75
82
|
bundle install
|
|
76
83
|
gem build asciidoc-pubkit.gemspec
|
|
77
|
-
gem install ./asciidoc-pubkit-0.1.
|
|
84
|
+
gem install ./asciidoc-pubkit-0.1.1.gem
|
|
78
85
|
asciidoc-pubkit --version
|
|
79
86
|
```
|
|
80
87
|
|
|
@@ -85,6 +92,36 @@ For a small trial, use `examples/book.adoc` as the scan input. Its Japanese
|
|
|
85
92
|
paragraphs deliberately contain review candidates; its code block must remain
|
|
86
93
|
unchanged.
|
|
87
94
|
|
|
95
|
+
## Install the morphological analyzer (0.1.1 and later)
|
|
96
|
+
|
|
97
|
+
On macOS with Homebrew:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
brew install mecab mecab-ipadic
|
|
101
|
+
mecab -D
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
On Ubuntu or Debian:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
sudo apt-get update
|
|
108
|
+
sudo apt-get install mecab mecab-ipadic-utf8
|
|
109
|
+
mecab -D
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Use a UTF-8 IPADIC dictionary. If the default dictionary is different, configure
|
|
113
|
+
`review.mecab_dictionary` with the IPADIC directory reported by your package
|
|
114
|
+
manager. UniDic and other feature layouts are rejected explicitly. MeCab and
|
|
115
|
+
IPADIC are separately installed dependencies, not bundled inside this gem.
|
|
116
|
+
|
|
117
|
+
The tool checks MeCab availability, dictionary encoding, and feature layout.
|
|
118
|
+
It never silently falls back to literal matching. To deliberately run without
|
|
119
|
+
morphological analysis:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
asciidoc-pubkit review scan book.adoc --tokenizer literal --output .pubkit/literal-review
|
|
123
|
+
```
|
|
124
|
+
|
|
88
125
|
## Review workflow
|
|
89
126
|
|
|
90
127
|
Run the following commands from the manuscript project directory:
|
|
@@ -111,7 +148,7 @@ asciidoc-pubkit review scan book.adoc --attribute edition=print --base-dir .
|
|
|
111
148
|
|
|
112
149
|
An entrypoint or a standalone chapter can be scanned. Includes and conditionals
|
|
113
150
|
are processed by Asciidoctor. Match the publishing build's attributes and base
|
|
114
|
-
directory to review the intended edition. `--only` selects one included source
|
|
151
|
+
directory to review the intended edition. `--only` selects one included source
|
|
115
152
|
file while retaining the book's attributes and heading hierarchy. Its path is
|
|
116
153
|
relative to the current working directory, as are other CLI path arguments.
|
|
117
154
|
|
|
@@ -190,6 +227,10 @@ The initial release loads one configuration file, not merged book/repository fil
|
|
|
190
227
|
review:
|
|
191
228
|
language: ja
|
|
192
229
|
style: desu-masu
|
|
230
|
+
tokenizer: mecab
|
|
231
|
+
# Optional overrides (dictionary paths are relative to this file):
|
|
232
|
+
# mecab_command: /opt/homebrew/bin/mecab
|
|
233
|
+
# mecab_dictionary: /opt/homebrew/lib/mecab/dic/ipadic
|
|
193
234
|
base_dir: .
|
|
194
235
|
glossary: glossary.yml
|
|
195
236
|
exclude:
|
|
@@ -213,10 +254,13 @@ JavaScript:
|
|
|
213
254
|
- Java Script
|
|
214
255
|
```
|
|
215
256
|
|
|
216
|
-
Variants are review candidates, not automatic replacement instructions.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
257
|
+
Variants are review candidates, not automatic replacement instructions. In MeCab
|
|
258
|
+
mode, `allows` can suppress a canonical dictionary form (all its inflections) or
|
|
259
|
+
an exact matched surface (that occurrence's form only). In literal mode it
|
|
260
|
+
suppresses exact dictionary entries. It does not disable glossary checks.
|
|
261
|
+
Regex patterns are not interpreted in glossary or allow-list entries. Unknown
|
|
262
|
+
configuration keys are rejected. A bare `mecab_command` is resolved through PATH;
|
|
263
|
+
use an absolute path for an explicit executable override.
|
|
220
264
|
|
|
221
265
|
## Rules and coverage
|
|
222
266
|
|
|
@@ -224,11 +268,44 @@ Unknown configuration keys are rejected.
|
|
|
224
268
|
| --- | --- | --- |
|
|
225
269
|
| `abstract-reference` | hint | Ask what an abstract noun refers to |
|
|
226
270
|
| `weak-predicate` | hint | Ask whether an operation's purpose or result is clear |
|
|
271
|
+
| `contextual-phrase` | hint | Review referents, assumptions, and qualifications while preserving negation |
|
|
227
272
|
| `generic-framing` | hint | Review generic introductions and emphasis |
|
|
273
|
+
| `vague-degree` | hint | Ask what depth, level, scope, or comparison is intended |
|
|
228
274
|
| `repeated-ending` | info | Identify three consecutive sentences with the same detected ending |
|
|
229
275
|
| `glossary-variant` | warning | Identify project-specific terminology variants |
|
|
230
276
|
| `style-candidate` | hint | Check selected polite/plain endings against an explicit style |
|
|
231
277
|
|
|
278
|
+
MeCab mode matches noun and adjective tokens and dictionary forms of verbs.
|
|
279
|
+
Sahen predicates are matched as a noun followed by the verb for "do"; standalone
|
|
280
|
+
sahen nouns are not treated as verbal predicates. Auxiliary sequences retain
|
|
281
|
+
negation, past tense, passive forms, and progressive forms in the reported surface.
|
|
282
|
+
The added reach predicate is negative-only; the existing handling predicate is
|
|
283
|
+
reviewed in both affirmative and negative forms. Glossary variants and generic
|
|
284
|
+
framing phrases continue to use literal matching. Contextual phrases also use
|
|
285
|
+
literal matching and suppress overlapping morphological candidates. Compound
|
|
286
|
+
nouns are matched across adjacent noun tokens. Selection and narrowing verbs
|
|
287
|
+
include potential forms; predicate surfaces also preserve causative auxiliaries.
|
|
288
|
+
|
|
289
|
+
Morphological candidates include `lemma`, `part_of_speech`, `negative`, and
|
|
290
|
+
`detector` alongside the original `match`, line, and column. Negation detection
|
|
291
|
+
covers common IPADIC negative auxiliaries; it is not full semantic analysis of
|
|
292
|
+
negation scope or double negatives. Unknown tokens are not guessed. Kana/kanji
|
|
293
|
+
variants of the alignment and gathering verbs have explicit canonical mappings;
|
|
294
|
+
other spelling variants are not automatically normalized.
|
|
295
|
+
|
|
296
|
+
The session records the MeCab version and dictionary file hashes. Verification
|
|
297
|
+
reports analyzer changes instead of treating results from different dictionaries
|
|
298
|
+
as directly comparable. Prompt generation uses saved evidence and does not need
|
|
299
|
+
MeCab. Changed rules or dictionary settings require a new scan.
|
|
300
|
+
|
|
301
|
+
Sessions from 0.1.0 are not compatible with 0.1.1. Keep the original baseline for
|
|
302
|
+
an ongoing review and finish it with the original version, or start a new review
|
|
303
|
+
pass in a different directory:
|
|
304
|
+
|
|
305
|
+
```sh
|
|
306
|
+
asciidoc-pubkit review scan book.adoc --output .pubkit/review-0.1.1
|
|
307
|
+
```
|
|
308
|
+
|
|
232
309
|
Severity describes review priority, not proof of an error. There is no AI-authorship
|
|
233
310
|
score and no requirement to eliminate every match.
|
|
234
311
|
|
|
@@ -254,7 +331,9 @@ gem build asciidoc-pubkit.gemspec
|
|
|
254
331
|
```
|
|
255
332
|
|
|
256
333
|
The tests exercise include-boundary mapping, inline masking, configuration,
|
|
257
|
-
contextual prompts, stale inputs,
|
|
334
|
+
contextual prompts, stale inputs, protected-content verification, and real MeCab
|
|
335
|
+
analysis of inflections, negative predicates, Unicode positions, and long lines.
|
|
336
|
+
Install MeCab and UTF-8 IPADIC before running the complete test suite.
|
|
258
337
|
GitHub Actions is configured for Ruby 3.2, 3.3, 3.4, and 4.0 on Linux.
|
|
259
338
|
|
|
260
339
|
## License
|
data/lib/asciidoc_pubkit/cli.rb
CHANGED
|
@@ -38,6 +38,7 @@ module AsciidocPubkit
|
|
|
38
38
|
opts.on('--config FILE', 'Use an explicit YAML configuration') { |v| options[:config] = v }
|
|
39
39
|
opts.on('--base-dir DIR', 'Set the Asciidoctor base directory') { |v| options[:base_dir] = v }
|
|
40
40
|
opts.on('--lang LANG', 'Prose language (ja only)') { |v| options[:language] = v }
|
|
41
|
+
opts.on('--tokenizer NAME', 'mecab (default) or literal (limited phrase matching)') { |v| options[:tokenizer] = v }
|
|
41
42
|
opts.on('--style STYLE', 'preserve (default), desu-masu, or dearu') { |v| options[:style] = v }
|
|
42
43
|
opts.on('-a', '--attribute NAME=VALUE', 'Set an Asciidoctor attribute; repeat as needed') do |v|
|
|
43
44
|
key, value = v.split('=', 2)
|
|
@@ -58,6 +59,7 @@ module AsciidocPubkit
|
|
|
58
59
|
if command == 'scan'
|
|
59
60
|
result = Session.scan(args.first, options)
|
|
60
61
|
out.puts "Scanned #{result['paragraphs']} paragraphs; found #{result['findings']} review candidates."
|
|
62
|
+
out.puts "Tokenizer: #{result['tokenizer']}#{result['tokenizer'] == 'literal' ? ' (limited phrase matching; no morphological analysis)' : ' (UTF-8 IPADIC)'}"
|
|
61
63
|
out.puts "Coverage notices: #{result['coverage_notices']}. See document.json for limitations."
|
|
62
64
|
out.puts "Review session: #{result['session']}"
|
|
63
65
|
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
|
|
@@ -4,21 +4,39 @@ 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
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.'],
|
|
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. Preserve negation and conditions.'],
|
|
9
|
+
'vague-degree' => [%w[浅い 深い], 'Identify the concrete depth, level, scope, or comparison. Keep literal measurements and established technical meanings.'],
|
|
10
|
+
'contextual-phrase' => [%w[これらを であることだけでは あるものとします わけではありません], 'Check the referent, assumption, or qualification against the surrounding explanation. Preserve conditions and negation.'],
|
|
9
11
|
'generic-framing' => [%w[重要なのは ポイントは 本章では ここでは まとめると], 'Check whether this framing adds useful scope or information instead of repeating the explanation.']
|
|
10
12
|
}.freeze
|
|
13
|
+
VERBS = {
|
|
14
|
+
'扱う' => %w[扱う], '示す' => %w[示す], '変わる' => %w[変わる],
|
|
15
|
+
'分ける' => %w[分ける], 'そろえる' => %w[そろえる 揃える],
|
|
16
|
+
'まとまる' => %w[まとまる 纏まる], '加える' => %w[加える],
|
|
17
|
+
'探す' => %w[探す], 'そろう' => %w[そろう 揃う],
|
|
18
|
+
'選ぶ' => %w[選ぶ 選べる], '成り立つ' => %w[成り立つ],
|
|
19
|
+
'確かめる' => %w[確かめる], '書き換える' => %w[書き換える],
|
|
20
|
+
'絞る' => %w[絞る 絞れる], '渡す' => %w[渡す], 'ある' => %w[ある]
|
|
21
|
+
}.freeze
|
|
22
|
+
SAHEN = %w[利用 整理 把握 到達].freeze
|
|
11
23
|
|
|
12
24
|
def self.mask(text)
|
|
13
25
|
# Keep character offsets stable while excluding common inline constructs.
|
|
14
26
|
text.gsub(INLINE) { |match| match.gsub(/[^\n]/, ' ') }
|
|
15
27
|
end
|
|
16
28
|
|
|
17
|
-
def self.scan(paragraphs, settings)
|
|
29
|
+
def self.scan(paragraphs, settings, tokenizer: nil)
|
|
18
30
|
findings = []
|
|
19
|
-
|
|
31
|
+
if settings.fetch('tokenizer', 'mecab') == 'mecab'
|
|
32
|
+
tokenizer ||= Morphology.new(settings)
|
|
33
|
+
end
|
|
34
|
+
token_groups = tokenizer ? tokenize_paragraphs(paragraphs, tokenizer) : []
|
|
35
|
+
paragraphs.each_with_index do |paragraph, index|
|
|
20
36
|
text = mask(paragraph.fetch('text'))
|
|
37
|
+
scan_morphemes(findings, paragraph, text, token_groups[index], settings) if tokenizer
|
|
21
38
|
TERMS.each do |rule, (terms, question)|
|
|
39
|
+
next if tokenizer && !%w[generic-framing contextual-phrase].include?(rule)
|
|
22
40
|
terms.each do |term|
|
|
23
41
|
next if settings.fetch('allows').include?(term)
|
|
24
42
|
find_term(findings, paragraph, text, term, rule, 'hint', question)
|
|
@@ -49,6 +67,96 @@ module AsciidocPubkit
|
|
|
49
67
|
findings
|
|
50
68
|
end
|
|
51
69
|
|
|
70
|
+
def self.tokenize_paragraphs(paragraphs, tokenizer)
|
|
71
|
+
text = +''
|
|
72
|
+
spans = paragraphs.map do |paragraph|
|
|
73
|
+
start = text.length
|
|
74
|
+
text << mask(paragraph['text'])
|
|
75
|
+
finish = text.length
|
|
76
|
+
text << "\n"
|
|
77
|
+
[start, finish]
|
|
78
|
+
end
|
|
79
|
+
tokens = tokenizer.tokenize(text)
|
|
80
|
+
cursor = 0
|
|
81
|
+
spans.map do |start, finish|
|
|
82
|
+
group = []
|
|
83
|
+
while cursor < tokens.length && tokens[cursor]['offset'] < finish
|
|
84
|
+
token = tokens[cursor]
|
|
85
|
+
raise Error, 'A morphological token crossed a paragraph boundary.' if token['offset'] < start || token['end_offset'] > finish
|
|
86
|
+
group << token.merge('offset' => token['offset'] - start, 'end_offset' => token['end_offset'] - start)
|
|
87
|
+
cursor += 1
|
|
88
|
+
end
|
|
89
|
+
group
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.scan_morphemes(findings, paragraph, text, tokens, settings)
|
|
94
|
+
phrase_ranges = TERMS['contextual-phrase'][0].flat_map do |phrase|
|
|
95
|
+
text.to_enum(:scan, Regexp.new(Regexp.escape(phrase))).map do
|
|
96
|
+
match = Regexp.last_match
|
|
97
|
+
match.begin(0)...match.end(0)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
tokens.each_with_index do |token, index|
|
|
101
|
+
next if token['unknown'] || phrase_ranges.any? { |range| range.cover?(token['offset']) }
|
|
102
|
+
lemma = token['lemma']
|
|
103
|
+
rule = nil
|
|
104
|
+
finish_index = index
|
|
105
|
+
compound = %w[一続き 開発者].find do |term|
|
|
106
|
+
following = tokens[index + 1]
|
|
107
|
+
following && token['pos'] == '名詞' && following['pos'] == '名詞' &&
|
|
108
|
+
!following['unknown'] && token['end_offset'] == following['offset'] &&
|
|
109
|
+
token['surface'] + following['surface'] == term
|
|
110
|
+
end
|
|
111
|
+
if compound
|
|
112
|
+
lemma = compound
|
|
113
|
+
finish_index = index + 1
|
|
114
|
+
rule = 'abstract-reference'
|
|
115
|
+
elsif token['pos'] == '名詞' && TERMS['abstract-reference'][0].include?(lemma)
|
|
116
|
+
rule = 'abstract-reference'
|
|
117
|
+
elsif token['pos'] == '形容詞' && TERMS['vague-degree'][0].include?(lemma)
|
|
118
|
+
rule = 'vague-degree'
|
|
119
|
+
finish_index = predicate_end(tokens, index, text)
|
|
120
|
+
elsif token['pos'] == '動詞' && (entry = VERBS.find { |_canonical, forms| forms.include?(lemma) })
|
|
121
|
+
lemma = entry[0]
|
|
122
|
+
rule = 'weak-predicate'
|
|
123
|
+
finish_index = predicate_end(tokens, index, text)
|
|
124
|
+
elsif token['pos'] == '名詞' && token['pos_detail'] == 'サ変接続' && SAHEN.include?(lemma)
|
|
125
|
+
following = tokens[index + 1]
|
|
126
|
+
next unless following && following['pos'] == '動詞' && following['lemma'] == 'する' && adjacent?(token, following, text)
|
|
127
|
+
lemma += 'する'
|
|
128
|
+
rule = 'weak-predicate'
|
|
129
|
+
finish_index = predicate_end(tokens, index + 1, text)
|
|
130
|
+
end
|
|
131
|
+
next unless rule
|
|
132
|
+
members = tokens[index..finish_index]
|
|
133
|
+
negative = members.any? { |member| member['pos'] == '助動詞' && %w[ない ぬ ん].include?(member['lemma']) }
|
|
134
|
+
next if lemma == '到達する' && !negative
|
|
135
|
+
surface = text[token['offset']...tokens[finish_index]['end_offset']]
|
|
136
|
+
allows = settings.fetch('allows')
|
|
137
|
+
next if [lemma, token['lemma'], surface, surface + '。'].any? { |form| allows.include?(form) }
|
|
138
|
+
add(findings, paragraph, text, token['offset'], surface, rule, 'hint', TERMS.fetch(rule)[1])
|
|
139
|
+
findings.last.merge!('lemma' => lemma, 'part_of_speech' => token['pos'], 'negative' => negative,
|
|
140
|
+
'detector' => 'mecab-ipadic')
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.adjacent?(left, right, text)
|
|
145
|
+
text[left['end_offset']...right['offset']].match?(/\A\n?\z/)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def self.predicate_end(tokens, index, text)
|
|
149
|
+
finish = index
|
|
150
|
+
while (following = tokens[finish + 1]) && adjacent?(tokens[finish], following, text)
|
|
151
|
+
auxiliary = following['pos'] == '助動詞'
|
|
152
|
+
dependent_verb = following['pos'] == '動詞' && %w[非自立 接尾].include?(following['pos_detail'])
|
|
153
|
+
connector = following['pos'] == '助詞' && following['pos_detail'] == '接続助詞' && %w[て で].include?(following['lemma'])
|
|
154
|
+
break unless auxiliary || dependent_verb || connector
|
|
155
|
+
finish += 1
|
|
156
|
+
end
|
|
157
|
+
finish
|
|
158
|
+
end
|
|
159
|
+
|
|
52
160
|
def self.find_term(findings, paragraph, text, term, rule, severity, question)
|
|
53
161
|
text.to_enum(:scan, Regexp.new(Regexp.escape(term))).each do
|
|
54
162
|
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,9 +23,9 @@ 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], '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].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))
|
|
@@ -34,11 +34,15 @@ module AsciidocPubkit
|
|
|
34
34
|
'style' => options[:style] || review.fetch('style', 'preserve'),
|
|
35
35
|
'exclude' => review.fetch('exclude', []),
|
|
36
36
|
'allows' => review.fetch('allows', []),
|
|
37
|
+
'tokenizer' => options[:tokenizer] || review.fetch('tokenizer', 'mecab'),
|
|
38
|
+
'mecab_command' => review.fetch('mecab_command', 'mecab'),
|
|
39
|
+
'mecab_dictionary' => review['mecab_dictionary'] && File.expand_path(review['mecab_dictionary'], base),
|
|
37
40
|
'attributes' => review.fetch('attributes', {}).merge(options.fetch(:attributes, {})),
|
|
38
41
|
'base_dir' => File.expand_path(options[:base_dir] || review.fetch('base_dir', base), options[:base_dir] ? Dir.pwd : base),
|
|
39
42
|
'glossary' => {}
|
|
40
43
|
}
|
|
41
44
|
raise Error, 'Only Japanese (ja) is supported in this release.' unless @data['language'] == 'ja'
|
|
45
|
+
raise Error, 'tokenizer must be mecab or literal.' unless %w[mecab literal].include?(@data['tokenizer'])
|
|
42
46
|
raise Error, 'style must be preserve, desu-masu, or dearu.' unless %w[preserve desu-masu dearu].include?(@data['style'])
|
|
43
47
|
%w[exclude allows].each do |key|
|
|
44
48
|
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.1'
|
|
13
13
|
class Error < StandardError; end
|
|
14
14
|
|
|
15
15
|
def self.hash_text(text)
|
|
@@ -25,6 +25,7 @@ end
|
|
|
25
25
|
|
|
26
26
|
require_relative 'asciidoc_pubkit/settings'
|
|
27
27
|
require_relative 'asciidoc_pubkit/document'
|
|
28
|
+
require_relative 'asciidoc_pubkit/morphology'
|
|
28
29
|
require_relative 'asciidoc_pubkit/rules'
|
|
29
30
|
require_relative 'asciidoc_pubkit/session'
|
|
30
31
|
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.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- CyberGarage
|
|
@@ -52,6 +52,7 @@ files:
|
|
|
52
52
|
- lib/asciidoc_pubkit.rb
|
|
53
53
|
- lib/asciidoc_pubkit/cli.rb
|
|
54
54
|
- lib/asciidoc_pubkit/document.rb
|
|
55
|
+
- lib/asciidoc_pubkit/morphology.rb
|
|
55
56
|
- lib/asciidoc_pubkit/rules.rb
|
|
56
57
|
- lib/asciidoc_pubkit/session.rb
|
|
57
58
|
- lib/asciidoc_pubkit/settings.rb
|