chaffjs 0.23.0 → 0.25.0
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.
- package/dist/detectors/bare-comparative.d.ts +26 -0
- package/dist/detectors/bare-comparative.d.ts.map +1 -0
- package/dist/detectors/bare-comparative.js +117 -0
- package/dist/detectors/bare-comparative.js.map +1 -0
- package/dist/detectors/chat-citation.d.ts +6 -0
- package/dist/detectors/chat-citation.d.ts.map +1 -1
- package/dist/detectors/chat-citation.js +26 -1
- package/dist/detectors/chat-citation.js.map +1 -1
- package/dist/detectors/clause-shape.d.ts.map +1 -1
- package/dist/detectors/clause-shape.js +19 -5
- package/dist/detectors/clause-shape.js.map +1 -1
- package/dist/detectors/connective-hierarchy.d.ts +16 -0
- package/dist/detectors/connective-hierarchy.d.ts.map +1 -0
- package/dist/detectors/connective-hierarchy.js +68 -0
- package/dist/detectors/connective-hierarchy.js.map +1 -0
- package/dist/detectors/definition-use.d.ts.map +1 -1
- package/dist/detectors/definition-use.js +4 -2
- package/dist/detectors/definition-use.js.map +1 -1
- package/dist/detectors/generic-heading.d.ts +8 -0
- package/dist/detectors/generic-heading.d.ts.map +1 -0
- package/dist/detectors/generic-heading.js +26 -0
- package/dist/detectors/generic-heading.js.map +1 -0
- package/dist/detectors/ime-residue.d.ts +13 -0
- package/dist/detectors/ime-residue.d.ts.map +1 -0
- package/dist/detectors/ime-residue.js +27 -0
- package/dist/detectors/ime-residue.js.map +1 -0
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +9 -2
- package/dist/detectors/lexicon.js.map +1 -1
- package/dist/detectors/lookalike-character.d.ts +18 -0
- package/dist/detectors/lookalike-character.d.ts.map +1 -0
- package/dist/detectors/lookalike-character.js +50 -0
- package/dist/detectors/lookalike-character.js.map +1 -0
- package/dist/detectors/lowercase-name.d.ts +9 -0
- package/dist/detectors/lowercase-name.d.ts.map +1 -0
- package/dist/detectors/lowercase-name.js +40 -0
- package/dist/detectors/lowercase-name.js.map +1 -0
- package/dist/detectors/missing-final-period.d.ts +17 -0
- package/dist/detectors/missing-final-period.d.ts.map +1 -0
- package/dist/detectors/missing-final-period.js +62 -0
- package/dist/detectors/missing-final-period.js.map +1 -0
- package/dist/detectors/missing-space-after-comma.d.ts +10 -0
- package/dist/detectors/missing-space-after-comma.d.ts.map +1 -0
- package/dist/detectors/missing-space-after-comma.js +42 -0
- package/dist/detectors/missing-space-after-comma.js.map +1 -0
- package/dist/detectors/registry/bare-comparative.d.ts +3 -0
- package/dist/detectors/registry/bare-comparative.d.ts.map +1 -0
- package/dist/detectors/registry/bare-comparative.js +3 -0
- package/dist/detectors/registry/bare-comparative.js.map +1 -0
- package/dist/detectors/registry/calendar-name-case.d.ts +3 -0
- package/dist/detectors/registry/calendar-name-case.d.ts.map +1 -0
- package/dist/detectors/registry/calendar-name-case.js +3 -0
- package/dist/detectors/registry/calendar-name-case.js.map +1 -0
- package/dist/detectors/registry/connective-hierarchy.d.ts +3 -0
- package/dist/detectors/registry/connective-hierarchy.d.ts.map +1 -0
- package/dist/detectors/registry/connective-hierarchy.js +3 -0
- package/dist/detectors/registry/connective-hierarchy.js.map +1 -0
- package/dist/detectors/registry/generic-heading.d.ts +3 -0
- package/dist/detectors/registry/generic-heading.d.ts.map +1 -0
- package/dist/detectors/registry/generic-heading.js +3 -0
- package/dist/detectors/registry/generic-heading.js.map +1 -0
- package/dist/detectors/registry/ime-residue.d.ts +3 -0
- package/dist/detectors/registry/ime-residue.d.ts.map +1 -0
- package/dist/detectors/registry/ime-residue.js +3 -0
- package/dist/detectors/registry/ime-residue.js.map +1 -0
- package/dist/detectors/registry/lookalike-character.d.ts +3 -0
- package/dist/detectors/registry/lookalike-character.d.ts.map +1 -0
- package/dist/detectors/registry/lookalike-character.js +3 -0
- package/dist/detectors/registry/lookalike-character.js.map +1 -0
- package/dist/detectors/registry/missing-final-period.d.ts +3 -0
- package/dist/detectors/registry/missing-final-period.d.ts.map +1 -0
- package/dist/detectors/registry/missing-final-period.js +3 -0
- package/dist/detectors/registry/missing-final-period.js.map +1 -0
- package/dist/detectors/registry/missing-space-after-comma.d.ts +3 -0
- package/dist/detectors/registry/missing-space-after-comma.d.ts.map +1 -0
- package/dist/detectors/registry/missing-space-after-comma.js +3 -0
- package/dist/detectors/registry/missing-space-after-comma.js.map +1 -0
- package/dist/detectors/registry/unit-spacing-consistency.d.ts +3 -0
- package/dist/detectors/registry/unit-spacing-consistency.d.ts.map +1 -0
- package/dist/detectors/registry/unit-spacing-consistency.js +3 -0
- package/dist/detectors/registry/unit-spacing-consistency.js.map +1 -0
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +8 -4
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +32 -7
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/technical-docs.d.ts.map +1 -1
- package/dist/detectors/technical-docs.js +14 -0
- package/dist/detectors/technical-docs.js.map +1 -1
- package/dist/detectors/unit-spacing.d.ts +25 -0
- package/dist/detectors/unit-spacing.d.ts.map +1 -0
- package/dist/detectors/unit-spacing.js +50 -0
- package/dist/detectors/unit-spacing.js.map +1 -0
- package/dist/quoted-span.d.ts +7 -0
- package/dist/quoted-span.d.ts.map +1 -1
- package/dist/quoted-span.js +20 -0
- package/dist/quoted-span.js.map +1 -1
- package/dist/structure/definition-use.d.ts +14 -4
- package/dist/structure/definition-use.d.ts.map +1 -1
- package/dist/structure/definition-use.js +29 -9
- package/dist/structure/definition-use.js.map +1 -1
- package/package.json +3 -3
- package/rules/calendar-name-case.yaml +55 -0
- package/rules/comparative-without-baseline.yaml +71 -0
- package/rules/connective-hierarchy.yaml +62 -0
- package/rules/cross-doc-broken-link.yaml +1 -1
- package/rules/cross-doc-duplicate-definition.yaml +1 -1
- package/rules/cross-doc-fact-conflict.yaml +1 -1
- package/rules/cross-doc-term-variant.yaml +5 -3
- package/rules/ime-residue.yaml +55 -0
- package/rules/lookalike-character.yaml +82 -0
- package/rules/missing-final-period.yaml +76 -0
- package/rules/missing-space-after-comma.yaml +57 -0
- package/rules/numbered-generic-heading.yaml +111 -0
- package/rules/undefined-acronym.yaml +1 -0
- package/rules/unit-spacing-consistency.yaml +74 -0
- package/skills/chaff/SKILL.md +20 -3
- package/src/detectors/bare-comparative.ts +152 -0
- package/src/detectors/chat-citation.ts +28 -1
- package/src/detectors/clause-shape.ts +20 -6
- package/src/detectors/connective-hierarchy.ts +99 -0
- package/src/detectors/definition-use.ts +13 -3
- package/src/detectors/generic-heading.ts +30 -0
- package/src/detectors/ime-residue.ts +37 -0
- package/src/detectors/lexicon.ts +11 -2
- package/src/detectors/lookalike-character.ts +75 -0
- package/src/detectors/lowercase-name.ts +58 -0
- package/src/detectors/missing-final-period.ts +79 -0
- package/src/detectors/missing-space-after-comma.ts +54 -0
- package/src/detectors/registry/bare-comparative.ts +4 -0
- package/src/detectors/registry/calendar-name-case.ts +4 -0
- package/src/detectors/registry/connective-hierarchy.ts +4 -0
- package/src/detectors/registry/generic-heading.ts +4 -0
- package/src/detectors/registry/ime-residue.ts +4 -0
- package/src/detectors/registry/lookalike-character.ts +4 -0
- package/src/detectors/registry/missing-final-period.ts +4 -0
- package/src/detectors/registry/missing-space-after-comma.ts +4 -0
- package/src/detectors/registry/unit-spacing-consistency.ts +4 -0
- package/src/detectors/signals.ts +9 -5
- package/src/detectors/structure-tree.ts +55 -30
- package/src/detectors/technical-docs.ts +13 -0
- package/src/detectors/unit-spacing.ts +71 -0
- package/src/quoted-span.ts +25 -0
- package/src/structure/definition-use.ts +42 -10
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
id: missing-space-after-comma
|
|
2
|
+
layer: L1
|
|
3
|
+
status: stable
|
|
4
|
+
how_to_find: missing-space-after-comma
|
|
5
|
+
languages: [en]
|
|
6
|
+
severity: warning
|
|
7
|
+
|
|
8
|
+
name:
|
|
9
|
+
ja: カンマの後ろに空白が無い
|
|
10
|
+
en: No space after a comma
|
|
11
|
+
|
|
12
|
+
why:
|
|
13
|
+
ja: 英文では、語をつなぐカンマの後ろに空白を一つ置きます。「address,and」のように空白が無いと、二つの語が一語に見え、検索でも別の語として当たりません。急いで打ったときや、書き換えの途中で空白が落ちた跡です。どちらの書き方を選ぶかの話ではなく、英語の句読点の決まりです。
|
|
14
|
+
en: 'English puts one space after a comma between words. Without it ("address,and") the two words read as one, and a search does not find either. It is a space dropped while typing or editing, not a matter of style.'
|
|
15
|
+
|
|
16
|
+
message:
|
|
17
|
+
ja: 「{written}」のカンマの後ろに空白がありません
|
|
18
|
+
en: 'No space after the comma in "{written}"'
|
|
19
|
+
|
|
20
|
+
how_to_fix:
|
|
21
|
+
ja: カンマの後ろに空白を一つ入れてください。
|
|
22
|
+
en: Put one space after the comma.
|
|
23
|
+
|
|
24
|
+
levels: { strict: error, normal: warning, relaxed: info }
|
|
25
|
+
|
|
26
|
+
use_for: [business, technical, blog, legal, docs, academic, literature, speech]
|
|
27
|
+
off_for:
|
|
28
|
+
literature: Older printed texts and poetry set commas tight on purpose, and a transcribed edition keeps the printer's spacing.
|
|
29
|
+
|
|
30
|
+
group: slips
|
|
31
|
+
summary:
|
|
32
|
+
ja: 英文の「address,and」のように、語をつなぐカンマの後ろに空白が無い所
|
|
33
|
+
en: A comma between two words with no space after it, as in "address,and"
|
|
34
|
+
example:
|
|
35
|
+
en:
|
|
36
|
+
before: |
|
|
37
|
+
Send the form to the office address,and keep a copy.
|
|
38
|
+
after: |
|
|
39
|
+
Send the form to the office address, and keep a copy.
|
|
40
|
+
|
|
41
|
+
not_flagged:
|
|
42
|
+
ja: 英語の文書だけで動きます。値を並べたもの(blue,black,brown のようにカンマが二つ以上)、数字や記号を含むもの(3,000、a,b=c、path/a,b、user@x,y、file_name,size)、引用符で囲んだ値("true,false")、二字以下の語(id,x)、コードと URL は見ません。
|
|
43
|
+
en: 'Only in English documents. A run of values (two or more commas, as in blue,black,brown), a run holding a digit or a mark (3,000, a,b=c, path/a,b, user@x,y, file_name,size), a quoted value ("true,false"), words of one or two letters (id,x), code and URLs are not read.'
|
|
44
|
+
sources: [vale-styles]
|
|
45
|
+
rewrite:
|
|
46
|
+
depth: light
|
|
47
|
+
en:
|
|
48
|
+
direction: Put one space after the marked comma. Change nothing else.
|
|
49
|
+
pairs:
|
|
50
|
+
- before: Send it to the office address,and keep a copy.
|
|
51
|
+
after: Send it to the office address, and keep a copy.
|
|
52
|
+
- before: If the box is damaged,call us first.
|
|
53
|
+
after: If the box is damaged, call us first.
|
|
54
|
+
keep:
|
|
55
|
+
- the words and their order
|
|
56
|
+
avoid:
|
|
57
|
+
- adding spaces inside lists of values or code
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
id: numbered-generic-heading
|
|
2
|
+
layer: L1
|
|
3
|
+
status: stable
|
|
4
|
+
how_to_find: generic-heading
|
|
5
|
+
word_list: generic-heading-label
|
|
6
|
+
requires: [markdown]
|
|
7
|
+
severity: info
|
|
8
|
+
|
|
9
|
+
name:
|
|
10
|
+
ja: 札と番号だけの見出し
|
|
11
|
+
en: Headings that are only a label and a number
|
|
12
|
+
|
|
13
|
+
why:
|
|
14
|
+
ja: 「メリット1」「メリット2」「ポイント3」のように札と番号だけの見出しは、目次や見出しの一覧で読んでも、その節に何が書いてあるかが分かりません。読み手は節を開くまで中身を知れず、あとで探すこともできません。型をなぞって書いた文書に多い形です。札は語彙表 generic-heading-label にあり、手順の札(ステップ1)は順番そのものが中身なので入れていません。
|
|
15
|
+
en: 'Headings such as "Benefit 1", "Benefit 2" and "Tip 3" say nothing about their sections. A reader scanning the table of contents learns nothing until they open each one, and cannot find a section again later. Documents written to a template are full of them. The labels are in the lexicon generic-heading-label; step labels ("Step 1") are left out, since in a procedure the order is the content.'
|
|
16
|
+
|
|
17
|
+
message:
|
|
18
|
+
ja: 見出し「{heading}」は札と番号だけで、節の中身が分かりません(そういう見出しが {count} 個)
|
|
19
|
+
en: 'The heading "{heading}" is only a label and a number and says nothing about the section ({count} such headings)'
|
|
20
|
+
|
|
21
|
+
how_to_fix:
|
|
22
|
+
ja: 札の後ろに、その節で言うことを書いてください(メリット1 → メリット1:設定が要らない、または 設定が要らない)。
|
|
23
|
+
en: 'Say what the section says ("Benefit 1" becomes "Benefit 1: no setup" or simply "No setup needed").'
|
|
24
|
+
|
|
25
|
+
levels: { strict: 1, normal: 2, relaxed: 3 }
|
|
26
|
+
|
|
27
|
+
use_for: [blog, business, technical, docs, academic, legal, literature, speech]
|
|
28
|
+
off_for:
|
|
29
|
+
literature: "A literary work names its parts as it likes."
|
|
30
|
+
speech: "A transcript has no headings of its own."
|
|
31
|
+
|
|
32
|
+
group: structure
|
|
33
|
+
summary:
|
|
34
|
+
ja: 「メリット1」「ポイント2」「Benefit 3」のように、札と番号だけで中身を言わない見出しが並ぶ所
|
|
35
|
+
en: Headings such as "Benefit 1" and "Tip 2" that are only a label and a number
|
|
36
|
+
|
|
37
|
+
example:
|
|
38
|
+
ja:
|
|
39
|
+
before: |
|
|
40
|
+
# 新しい会議室予約の仕組み
|
|
41
|
+
|
|
42
|
+
## メリット1
|
|
43
|
+
|
|
44
|
+
空き状況がすぐ分かります。
|
|
45
|
+
|
|
46
|
+
## メリット2
|
|
47
|
+
|
|
48
|
+
予約の取り消しが一度で済みます。
|
|
49
|
+
after: |
|
|
50
|
+
# 新しい会議室予約の仕組み
|
|
51
|
+
|
|
52
|
+
## 空き状況がすぐ分かる
|
|
53
|
+
|
|
54
|
+
空き状況がすぐ分かります。
|
|
55
|
+
|
|
56
|
+
## 取り消しが一度で済む
|
|
57
|
+
|
|
58
|
+
予約の取り消しが一度で済みます。
|
|
59
|
+
en:
|
|
60
|
+
before: |
|
|
61
|
+
# The new room booking system
|
|
62
|
+
|
|
63
|
+
## Benefit 1
|
|
64
|
+
|
|
65
|
+
You can see free rooms at once.
|
|
66
|
+
|
|
67
|
+
## Benefit 2
|
|
68
|
+
|
|
69
|
+
You can cancel a booking in one step.
|
|
70
|
+
after: |
|
|
71
|
+
# The new room booking system
|
|
72
|
+
|
|
73
|
+
## Free rooms at a glance
|
|
74
|
+
|
|
75
|
+
You can see free rooms at once.
|
|
76
|
+
|
|
77
|
+
## One-step cancelling
|
|
78
|
+
|
|
79
|
+
You can cancel a booking in one step.
|
|
80
|
+
|
|
81
|
+
not_flagged:
|
|
82
|
+
ja: 札の後ろに題を足した見出し(メリット1:設定が要らない)、手順・理由・課題の見出し(ステップ1、理由2、課題3)、章や節の番号(第1章)は、番号で引かれる単位なので指しません。Markdown の見出しだけを読みます。
|
|
83
|
+
en: 'A heading with a title after the label ("Benefit 1: no setup"), step, lesson, reason and feature headings ("Step 1", "Lesson 2", "Reason 3"), and chapter or section numbers ("Chapter 1") are not reported: each is a unit cited by its number. Only Markdown headings are read.'
|
|
84
|
+
level_meaning:
|
|
85
|
+
ja: 札と番号だけの見出しが {limit} 個から指摘
|
|
86
|
+
en: reported from {limit} such headings
|
|
87
|
+
|
|
88
|
+
rewrite:
|
|
89
|
+
depth: structure
|
|
90
|
+
ja:
|
|
91
|
+
direction: 札と番号だけの見出しに、その節で言うことを一言で書いてください。節の本文から言葉を取り、本文に無いことは足しません。
|
|
92
|
+
pairs:
|
|
93
|
+
- before: "## メリット1"
|
|
94
|
+
after: "## 空き状況がすぐ分かる"
|
|
95
|
+
- before: "## ポイント2"
|
|
96
|
+
after: "## 取り消しは前日まで"
|
|
97
|
+
keep:
|
|
98
|
+
- 節の順番と本文
|
|
99
|
+
avoid:
|
|
100
|
+
- 本文に書かれていない中身を見出しに書く
|
|
101
|
+
en:
|
|
102
|
+
direction: Give each label-and-number heading a few words that say what its section says, taken from the section itself.
|
|
103
|
+
pairs:
|
|
104
|
+
- before: "## Benefit 1"
|
|
105
|
+
after: "## Free rooms at a glance"
|
|
106
|
+
- before: "## Tip 2"
|
|
107
|
+
after: "## Cancel by the day before"
|
|
108
|
+
keep:
|
|
109
|
+
- the order of the sections and their text
|
|
110
|
+
avoid:
|
|
111
|
+
- putting a claim in a heading that the section does not make
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
id: unit-spacing-consistency
|
|
2
|
+
layer: L2
|
|
3
|
+
status: stable
|
|
4
|
+
how_to_find: unit-spacing-consistency
|
|
5
|
+
severity: info
|
|
6
|
+
|
|
7
|
+
name:
|
|
8
|
+
ja: 数と単位の間の空白が混ざっている
|
|
9
|
+
en: Units spaced two ways
|
|
10
|
+
|
|
11
|
+
why:
|
|
12
|
+
ja: 一つの文書で「5 GB」と「5GB」のように、数と単位記号の間を空ける所と詰める所が混ざると、読み手は違いに意味があるのかと迷い、別々の所から貼り合わせた文書に見えます。検索でも片方を取りこぼします。どちらが正しいかは決めず、単位ごとに、文書の中で少ないほうを指します。
|
|
13
|
+
en: 'A document writing "5 GB" in one place and "5GB" in another makes the reader wonder whether the difference means something, looks pasted together, and splits a search. chaff does not pick a side; unit by unit, it points at the way the document uses less.'
|
|
14
|
+
|
|
15
|
+
message:
|
|
16
|
+
ja: 「{written}」と書いています(この文書はこの単位をふつう「{usual}」のように書きます。{of} 箇所のうち {count} 箇所が違う)
|
|
17
|
+
en: '"{written}" here, where the document usually writes this unit like "{usual}" ({count} of {of})'
|
|
18
|
+
|
|
19
|
+
how_to_fix:
|
|
20
|
+
ja: 文書の多いほうの書き方に合わせてください。
|
|
21
|
+
en: Match the way the rest of the document writes units.
|
|
22
|
+
|
|
23
|
+
# 少ないほうが、数と単位の組の何パーセントまでなら「混ざっている」と見るか。これを超えたら、文書が使い分けている。
|
|
24
|
+
levels: { strict: 49, normal: 34, relaxed: 20 }
|
|
25
|
+
|
|
26
|
+
use_for: [business, technical, blog, legal, docs, academic, literature, speech]
|
|
27
|
+
|
|
28
|
+
group: consistency
|
|
29
|
+
summary:
|
|
30
|
+
ja: 「5 GB」と「5GB」のように、一つの文書で数と単位記号の間を空けたり詰めたりしている所
|
|
31
|
+
en: A number and a unit symbol spaced in some places and not in others, such as "5 GB" and "5GB"
|
|
32
|
+
example:
|
|
33
|
+
ja:
|
|
34
|
+
before: |
|
|
35
|
+
容量は 5 GB、転送量は 10 GB、上限は 20GB です。
|
|
36
|
+
after: |
|
|
37
|
+
容量は 5 GB、転送量は 10 GB、上限は 20 GB です。
|
|
38
|
+
en:
|
|
39
|
+
before: |
|
|
40
|
+
Storage is 5 GB, transfer is 10 GB and the cap is 20GB.
|
|
41
|
+
after: |
|
|
42
|
+
Storage is 5 GB, transfer is 10 GB and the cap is 20 GB.
|
|
43
|
+
|
|
44
|
+
not_flagged:
|
|
45
|
+
ja: 単位記号の一覧にあるものだけを見ます。一字の記号(m、s、g)、パーセント、温度(°C)は、空けない書き方が別に定着しているので数えません。語で書いた単位(5 時間、5 hours)、コードの中、版や番地の中の数(1.2.3GB)も見ません。単位ごとに比べるので、「5 GB」と「500ms」が並んでも混在とは読みません。ノーブレークスペースも空白と数えます。少ないほうが 3 分の 1 を超える文書や、二通りが同じ数の文書は、使い分けていると読んで指しません。
|
|
46
|
+
en: Only the unit symbols on chaff's list are read. One-letter symbols (m, s, g), the percent sign and temperatures (°C) are left out, since many styles write them unspaced on purpose. Units written as words (5 hours), code, and numbers inside versions or addresses (1.2.3GB) are not read. Each unit is compared with itself, so "5 GB" next to "500ms" is not a mix. A no-break space counts as a space. When the less common way is over a third, or the two are equally common, the document is read as using both on purpose.
|
|
47
|
+
level_meaning:
|
|
48
|
+
ja: 少ないほうが {limit}% までなら指摘
|
|
49
|
+
en: reported while the less common way is at most {limit}% of the units
|
|
50
|
+
sources: [vale-styles]
|
|
51
|
+
rewrite:
|
|
52
|
+
depth: light
|
|
53
|
+
ja:
|
|
54
|
+
direction: 指した所だけ、数と単位の間を文書の多いほうに合わせてください。ほかは変えません。
|
|
55
|
+
pairs:
|
|
56
|
+
- before: 上限は 20GB です。
|
|
57
|
+
after: 上限は 20 GB です。
|
|
58
|
+
- before: 間隔は 500 ms です。
|
|
59
|
+
after: 間隔は 500ms です。
|
|
60
|
+
keep:
|
|
61
|
+
- 数と単位そのもの
|
|
62
|
+
avoid:
|
|
63
|
+
- 単位を別の単位に換える
|
|
64
|
+
en:
|
|
65
|
+
direction: Add or remove the space between the number and the unit at the marked place only, to match the rest of the document.
|
|
66
|
+
pairs:
|
|
67
|
+
- before: The cap is 20GB.
|
|
68
|
+
after: The cap is 20 GB.
|
|
69
|
+
- before: The interval is 500 ms.
|
|
70
|
+
after: The interval is 500ms.
|
|
71
|
+
keep:
|
|
72
|
+
- the number and the unit
|
|
73
|
+
avoid:
|
|
74
|
+
- converting the unit
|
package/skills/chaff/SKILL.md
CHANGED
|
@@ -96,9 +96,26 @@ Pick the easiest way that can say the requirement:
|
|
|
96
96
|
| Requirement | Use |
|
|
97
97
|
| --- | --- |
|
|
98
98
|
| An existing rule covers it | A level, a number, `prefer`, `jargon` or `required_sections` in `chaff.yaml` |
|
|
99
|
-
| A fixed phrase or pattern, with the team's own message | `custom_rules` of type `words` or `pattern`
|
|
100
|
-
| Decided by part of speech or inflection | `custom_rules` of type `tokens`
|
|
101
|
-
| Counting or comparing that the above cannot express | a Node function, `type: module`
|
|
99
|
+
| A fixed phrase or pattern, with the team's own message | `custom_rules` of type `words` or `pattern` |
|
|
100
|
+
| Decided by part of speech or inflection | `custom_rules` of type `tokens` |
|
|
101
|
+
| Counting or comparing that the above cannot express | a Node function, `type: module` |
|
|
102
|
+
| The same rules shared across repositories | a rule pack of YAML files: `npx chaffjs init --plugin <name>`, then `plugins:` in `chaff.yaml` |
|
|
103
|
+
|
|
104
|
+
A `words` rule names the words, the team's text for each field `explain` shows, and one example:
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
custom_rules:
|
|
108
|
+
- id: banned-word
|
|
109
|
+
type: words
|
|
110
|
+
words: [禁止ワード]
|
|
111
|
+
severity: warning
|
|
112
|
+
name: { ja: 禁止ワード }
|
|
113
|
+
why: { ja: この文書で避ける語です }
|
|
114
|
+
message: { ja: "「{matched}」は使いません" }
|
|
115
|
+
how_to_fix: { ja: 別の言い方にします }
|
|
116
|
+
example:
|
|
117
|
+
ja: { before: 禁止ワードを使った文, after: 別の言い方の文 }
|
|
118
|
+
```
|
|
102
119
|
|
|
103
120
|
Test a new team rule the same way: `explain`, then a sample it must report and a sample it must not, before
|
|
104
121
|
committing `chaff.yaml`. The guide page "Adding a rule" covers each way and how to add a rule to chaff itself.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { Detector, Finding, Lexicon, Sentence, Span, Token } from "../plugin.ts";
|
|
2
|
+
import { quotedIn } from "../quoted-span.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 比べる相手の無い比較(より良い結果、さらに高速に、The new engine is faster.)。何より良いのか、何より速いのかが書かれていない。
|
|
6
|
+
* 語は語彙表 comparative-baseline が言う。group marker は日本語の比べる副詞(より、さらに)、more は英語の比べる語、comparative は比べた形の語
|
|
7
|
+
* (faster、better)、baseline は相手を言う語(従来、以前、than、previous)、exempt は比較でない決まった言い方(より多くの、より詳しく)、
|
|
8
|
+
* condition は比較を条件や仮定の下に置く語(使うと、if、would)。条件を書いた比較は、その条件の無い場合と比べている。
|
|
9
|
+
* 同じ文か前の二文に相手を言う語があれば、相手は書いてある。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type ComparativeLists = {
|
|
13
|
+
readonly markers: ReadonlySet<string>;
|
|
14
|
+
readonly more: ReadonlySet<string>;
|
|
15
|
+
readonly comparatives: ReadonlySet<string>;
|
|
16
|
+
readonly baselines: readonly string[];
|
|
17
|
+
readonly exempt: ReadonlySet<string>;
|
|
18
|
+
readonly conditions: ReadonlySet<string>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type BareComparative = { readonly first: Token; readonly last: Token };
|
|
22
|
+
|
|
23
|
+
/** 前に立てば、比べた形が述語として読める語の品詞(is faster、runs faster)。 */
|
|
24
|
+
const PREDICATE_HEAD = new Set(["VERB", "AUX"]);
|
|
25
|
+
/** 比べた形の後ろで節が終わる印。 */
|
|
26
|
+
const CLAUSE_END = new Set([".", "!", "?", ",", ";", ":"]);
|
|
27
|
+
/** 述語と比べた形のあいだに挟まってよい副詞の数(is now much faster)。 */
|
|
28
|
+
const MAX_ADVERBS = 2;
|
|
29
|
+
/** 日本語の形容動詞の語幹の後ろに来る語(高速な、高速に、高速です)。 */
|
|
30
|
+
const ADJECTIVAL_TAIL = new Set(["な", "に", "だ", "です", "で"]);
|
|
31
|
+
|
|
32
|
+
const lower = (token: Token | undefined): string => token?.surface.toLowerCase() ?? "";
|
|
33
|
+
|
|
34
|
+
const isThirdPersonVerb = (token: Token): boolean => token.pos === "NOUN" && token.features?.["AlsoVerb"] === "Yes" && token.features["Number"] === "Plur";
|
|
35
|
+
|
|
36
|
+
/** 比べる語の後ろの、程度を言える語(良い、高速な)。形容詞か、形容動詞の語幹。 */
|
|
37
|
+
const isGradable = (tokens: readonly Token[], at: number): boolean => {
|
|
38
|
+
const word = tokens[at];
|
|
39
|
+
if (word?.pos === "ADJ") return true;
|
|
40
|
+
return word?.pos === "NOUN" && word.features?.["Bound"] !== "Yes" && ADJECTIVAL_TAIL.has(tokens[at + 1]?.surface ?? "");
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** 比べる副詞が語の頭に立つか。前が助詞・記号・接続の語か文の頭のときだけ(「学年だより等」の「より」は語の一部)。 */
|
|
44
|
+
const OPENS_WORD = new Set(["ADP", "PUNCT", "SCONJ", "CCONJ", "ADV"]);
|
|
45
|
+
|
|
46
|
+
/** 日本語: 副詞の「より」「さらに」の直後の、程度を言える語。比較の決まった言い方(より多くの)は除く。 */
|
|
47
|
+
const markedAt = (tokens: readonly Token[], at: number, lists: ComparativeLists): BareComparative | undefined => {
|
|
48
|
+
const [marker, word] = [tokens[at], tokens[at + 1]];
|
|
49
|
+
if (marker?.pos !== "ADV" || !lists.markers.has(marker.surface) || word === undefined || lists.exempt.has(word.surface)) return undefined;
|
|
50
|
+
const previous = tokens[at - 1];
|
|
51
|
+
if (previous !== undefined && !OPENS_WORD.has(previous.pos)) return undefined;
|
|
52
|
+
return isGradable(tokens, at + 1) ? { first: marker, last: word } : undefined;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** 比べた形の後ろで節が終わるか。読点の後ろに形容詞が続けば、名詞の前に並べた修飾語(better, more predictable results)。 */
|
|
56
|
+
const closesClause = (tokens: readonly Token[], at: number): boolean => {
|
|
57
|
+
const next = tokens[at + 1];
|
|
58
|
+
if (next === undefined) return true;
|
|
59
|
+
if (!CLAUSE_END.has(next.surface)) return false;
|
|
60
|
+
const after = tokens[at + 2];
|
|
61
|
+
return next.surface !== "," || (after?.pos !== "ADJ" && after?.pos !== "ADV");
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** more と形容詞のあいだの副詞(more computationally efficient)を飛ばした、more の位置。無ければ -1。 */
|
|
65
|
+
const moreBefore = (tokens: readonly Token[], at: number, more: ReadonlySet<string>): number => {
|
|
66
|
+
const before = tokens.slice(Math.max(0, at - 1 - MAX_ADVERBS), at).reverse();
|
|
67
|
+
const found = before.findIndex((token) => token.pos !== "ADV" || more.has(lower(token)));
|
|
68
|
+
return found !== -1 && more.has(lower(before[found])) ? at - 1 - found : -1;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** 英語: 述語の後ろで節を閉じる比べた形(is faster.、runs faster,)か、more と形容詞。 */
|
|
72
|
+
const predicateAt = (tokens: readonly Token[], at: number, lists: ComparativeLists): BareComparative | undefined => {
|
|
73
|
+
const word = tokens[at];
|
|
74
|
+
if (word === undefined || !closesClause(tokens, at)) return undefined;
|
|
75
|
+
const moreAt = word.pos === "ADJ" ? moreBefore(tokens, at, lists.more) : -1;
|
|
76
|
+
if (moreAt === -1 && !lists.comparatives.has(lower(word))) return undefined;
|
|
77
|
+
const start = moreAt === -1 ? at : moreAt;
|
|
78
|
+
const first = tokens[start] ?? word;
|
|
79
|
+
const adverbs = tokens.slice(Math.max(0, start - MAX_ADVERBS), start).reverse();
|
|
80
|
+
const skipped = adverbs.findIndex((token) => token.pos !== "ADV");
|
|
81
|
+
const head = tokens[start - 1 - (skipped === -1 ? adverbs.length : skipped)];
|
|
82
|
+
return head !== undefined && (PREDICATE_HEAD.has(head.pos) || isThirdPersonVerb(head)) ? { first, last: word } : undefined;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** 比べた形より前に、条件か仮定を言う語があるか。並べる「と」(A と B)は条件でない。 */
|
|
86
|
+
const isConditional = (tokens: readonly Token[], found: BareComparative, conditions: ReadonlySet<string>): boolean =>
|
|
87
|
+
tokens.some((token) => token.span.start < found.first.span.start && token.pos !== "CCONJ" && conditions.has(lower(token)));
|
|
88
|
+
|
|
89
|
+
export const bareComparativesIn = (tokens: readonly Token[], lists: ComparativeLists): BareComparative[] =>
|
|
90
|
+
tokens
|
|
91
|
+
.flatMap((_, at) => markedAt(tokens, at, lists) ?? predicateAt(tokens, at, lists) ?? [])
|
|
92
|
+
.filter((found) => !isConditional(tokens, found, lists.conditions));
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 語が比べる相手を言う語か。語の形か原形で照らし、一字の語(旧)は語の頭だけ(旧版は相手、復旧は相手でない)。
|
|
96
|
+
*/
|
|
97
|
+
const isBaselineWord = (token: Token, baselines: readonly string[]): boolean => {
|
|
98
|
+
const [surface, lemma] = [token.surface.toLowerCase(), token.lemma?.toLowerCase()];
|
|
99
|
+
return baselines.some((baseline) => surface === baseline || lemma === baseline || ([...baseline].length === 1 && surface.startsWith(baseline)));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** 相手を言う語が、続く語に分かれて書かれているか(これ・まで、で・は・なく)。 */
|
|
103
|
+
const MAX_BASELINE_TOKENS = 3;
|
|
104
|
+
const isSplitBaseline = (tokens: readonly Token[], at: number, baselines: readonly string[]): boolean => {
|
|
105
|
+
const joined = tokens
|
|
106
|
+
.slice(at, at + MAX_BASELINE_TOKENS)
|
|
107
|
+
.map((token) => token.surface.toLowerCase())
|
|
108
|
+
.reduce<string[]>((runs, surface) => [...runs, `${runs.at(-1) ?? ""}${surface}`], []);
|
|
109
|
+
return joined.slice(1).some((run) => baselines.includes(run));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** 文の中に、比べる相手を言う語か、相手を受ける助詞の「より」(従来より)があるか。 */
|
|
113
|
+
export const namesBaseline = (sentence: Sentence | undefined, baselines: readonly string[]): boolean =>
|
|
114
|
+
(sentence?.tokens ?? []).some(
|
|
115
|
+
(token, at, tokens) => (token.surface === "より" && token.pos === "ADP") || isBaselineWord(token, baselines) || isSplitBaseline(tokens, at, baselines),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
/** 比べる相手を探す、前の文の数。段落の中で相手を言ってから、一つ文を挟んで比べることがある。 */
|
|
119
|
+
const BASELINE_REACH = 2;
|
|
120
|
+
|
|
121
|
+
const groupOf = (lexicon: Lexicon, group: string): string[] => lexicon.filter((entry) => entry.group === group).map((entry) => entry.pattern.toLowerCase());
|
|
122
|
+
|
|
123
|
+
export const listsOf = (lexicon: Lexicon): ComparativeLists => ({
|
|
124
|
+
markers: new Set(groupOf(lexicon, "marker")),
|
|
125
|
+
more: new Set(groupOf(lexicon, "more")),
|
|
126
|
+
comparatives: new Set(groupOf(lexicon, "comparative")),
|
|
127
|
+
baselines: groupOf(lexicon, "baseline"),
|
|
128
|
+
exempt: new Set(groupOf(lexicon, "exempt")),
|
|
129
|
+
conditions: new Set(groupOf(lexicon, "condition")),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/** 鉤括弧の中の比較(「給与所得の確定申告がさらに簡単に!」ページ)は題や語を挙げたもので、書き手の比較ではない。 */
|
|
133
|
+
const insideSpan = (found: BareComparative, span: Span): boolean => found.first.span.start >= span.start && found.last.span.end <= span.end;
|
|
134
|
+
|
|
135
|
+
export const bareComparative: Detector = (doc): Finding[] => {
|
|
136
|
+
const lists = listsOf(doc.lexicons["comparative-baseline"] ?? []);
|
|
137
|
+
return doc.sentences.flatMap((sentence, at) => {
|
|
138
|
+
const nearby = doc.sentences.slice(Math.max(0, at - BASELINE_REACH), at + 1);
|
|
139
|
+
if (sentence.embeddedLanguage !== undefined || nearby.some((near) => namesBaseline(near, lists.baselines))) return [];
|
|
140
|
+
const quoted = quotedIn(sentence);
|
|
141
|
+
return bareComparativesIn(sentence.tokens ?? [], lists)
|
|
142
|
+
.filter((found) => !quoted.some((span) => insideSpan(found, span)))
|
|
143
|
+
.map((found) => ({
|
|
144
|
+
rule: "",
|
|
145
|
+
severity: "info",
|
|
146
|
+
line: 0,
|
|
147
|
+
column: 0,
|
|
148
|
+
quote: sentence.text.trim(),
|
|
149
|
+
values: { matched: doc.source.slice(found.first.span.start, found.last.span.end), offset: found.first.span.start },
|
|
150
|
+
}));
|
|
151
|
+
});
|
|
152
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Detector, Finding, Lexicon, ProseDocument, Span } from "../plugin.ts";
|
|
2
2
|
import { escapeRegExp } from "../orthography.ts";
|
|
3
|
+
import { isQuotedAlone } from "../quoted-span.ts";
|
|
3
4
|
import { findingAt, markupOf } from "./markup-finding.ts";
|
|
4
5
|
|
|
5
6
|
/** One marker a chat interface leaves when its answer is pasted: where it is and what was written. */
|
|
@@ -42,9 +43,35 @@ const reachesReader = (doc: ProseDocument, span: Span): boolean => {
|
|
|
42
43
|
return showsInProse(doc.prose, span) || within([...markup.texts, ...destinations], span);
|
|
43
44
|
};
|
|
44
45
|
|
|
46
|
+
/** Quotation marks, parentheses and sentence punctuation at the ends of a word, which are not part of it. */
|
|
47
|
+
const SURROUNDING = new Set(['"', "'", "“", "”", "‘", "’", "(", ")", ".", ",", ";", ":", "!", "?"]);
|
|
48
|
+
const SPACE = /\s/u;
|
|
49
|
+
|
|
50
|
+
/** What is left of a string after the surrounding marks at both of its ends. */
|
|
51
|
+
const withoutSurrounding = (text: string): string => {
|
|
52
|
+
const chars = [...text];
|
|
53
|
+
const first = chars.findIndex((char) => !SURROUNDING.has(char));
|
|
54
|
+
return first === -1 ? "" : chars.slice(first, chars.findLastIndex((char) => !SURROUNDING.has(char)) + 1).join("");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether the marker stands as a word of its own (links ending in "?utm_source=chatgpt.com", marks such as oaicite.), or
|
|
59
|
+
* alone in quotation marks, as Japanese writes it with no spaces (「?utm_source=chatgpt.com」の付いたリンク): prose
|
|
60
|
+
* about the marks names them. What a chat answer leaves is always joined to a URL or a citation (oaicite:0).
|
|
61
|
+
*/
|
|
62
|
+
export const standsAlone = (source: string, span: Span): boolean => {
|
|
63
|
+
if (isQuotedAlone(source, span)) return true;
|
|
64
|
+
const lineStart = source.lastIndexOf("\n", span.start - 1) + 1;
|
|
65
|
+
const lineEnd = source.indexOf("\n", span.end);
|
|
66
|
+
const before = source.slice(lineStart, span.start).split(SPACE).at(-1) ?? "";
|
|
67
|
+
const after = source.slice(span.end, lineEnd === -1 ? source.length : lineEnd).split(SPACE)[0] ?? "";
|
|
68
|
+
const marker = source.slice(span.start, span.end);
|
|
69
|
+
return withoutSurrounding(`${before}${marker}${after}`) === marker;
|
|
70
|
+
};
|
|
71
|
+
|
|
45
72
|
/** Each marker is a leftover in its own right, so each is reported, from the first. */
|
|
46
73
|
export const chatCitationResidue: Detector = (doc, options): Finding[] => {
|
|
47
|
-
const markers = markersIn(doc.source, options.lexicon ?? []).filter((marker) => reachesReader(doc, marker.span));
|
|
74
|
+
const markers = markersIn(doc.source, options.lexicon ?? []).filter((marker) => reachesReader(doc, marker.span) && !standsAlone(doc.source, marker.span));
|
|
48
75
|
if (markers.length === 0 || markers.length < options.limit) return [];
|
|
49
76
|
return markers.map((marker) => findingAt(doc, marker.span, { matched: marker.matched, count: markers.length, limit: options.limit }));
|
|
50
77
|
};
|
|
@@ -159,16 +159,30 @@ const NOMINAL_POS: ReadonlySet<string> = new Set(["NOUN", "PROPN"]);
|
|
|
159
159
|
const headAfter = (tokens: readonly Token[], at: number): number =>
|
|
160
160
|
tokens.findIndex((token, index) => index > at && !isSpace(token) && !SKIPPED_POS.has(token.pos) && token.features?.["Bound"] !== "Yes");
|
|
161
161
|
|
|
162
|
+
/** The last noun of the compound that starts at head (新規受付, 受付開始): the compound is named by its last word. */
|
|
163
|
+
const compoundEnd = (tokens: readonly Token[], head: number): number => {
|
|
164
|
+
const after = tokens.findIndex((token, index) => index > head && !isSpace(token) && !NOMINAL_POS.has(token.pos));
|
|
165
|
+
const end = after === -1 ? tokens.length : after;
|
|
166
|
+
return tokens.slice(head, end).reduce((last, token, offset) => (NOMINAL_POS.has(token.pos) ? head + offset : last), head);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const SENTENCE_STOP = "。";
|
|
170
|
+
|
|
162
171
|
/**
|
|
163
|
-
* The head names a start or a sending (開始, 参る, 連絡). A noun head must be the action itself:
|
|
164
|
-
*
|
|
172
|
+
* The head names a start or a sending (開始, 参る, 連絡). A noun head must be the action itself: a compound whose last
|
|
173
|
+
* noun is listed (新規受付を開始, 受付開始します), followed by a light verb, the end of the sentence, or an object marker
|
|
174
|
+
* whose verb is listed too; not a noun being compared (昨年より販売が増えた, 昨年より新規受付を増やした) or a compound
|
|
175
|
+
* named by another noun (昨年より受付件数を増やした).
|
|
165
176
|
*/
|
|
166
177
|
const isOriginHead = (tokens: readonly Token[], head: number, heads: ReadonlySet<string>, objectMarkers: ReadonlySet<string>): boolean => {
|
|
167
178
|
const token = tokens[head];
|
|
168
|
-
if (token === undefined
|
|
169
|
-
if (!NOMINAL_POS.has(token.pos)) return
|
|
170
|
-
const
|
|
171
|
-
|
|
179
|
+
if (token === undefined) return false;
|
|
180
|
+
if (!NOMINAL_POS.has(token.pos)) return isListed(heads, token);
|
|
181
|
+
const last = compoundEnd(tokens, head);
|
|
182
|
+
if (!isListed(heads, tokens[last])) return false;
|
|
183
|
+
const next = nextWord(tokens, last);
|
|
184
|
+
if (next === undefined || next.surface === SENTENCE_STOP || next.features?.["VerbType"] === "Light") return true;
|
|
185
|
+
return objectMarkers.has(next.surface) && isListed(heads, nextWord(tokens, tokens.indexOf(next)));
|
|
172
186
|
};
|
|
173
187
|
|
|
174
188
|
type ParticleWords = { readonly particles: ReadonlySet<string>; readonly heads: ReadonlySet<string>; readonly objectMarkers: ReadonlySet<string> };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Detector, Finding, LexiconEntry, Sentence, Span, Token } from "../plugin.ts";
|
|
2
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
3
|
+
import { quotedSpans } from "../quoted-span.ts";
|
|
4
|
+
|
|
5
|
+
// A connective that joins only the outer level of a two-level list, in a sentence with no inner level under it. The
|
|
6
|
+
// lexicon holds both levels: an entry with a rewrite is an outer-level word (rewrite is the word a one-level list uses),
|
|
7
|
+
// and an entry with instead_of is an inner-level word that stands under that outer-level word.
|
|
8
|
+
|
|
9
|
+
/** One outer-level connective with nothing to stand above, and where it is in the document. */
|
|
10
|
+
export type LoneConnective = { readonly offset: number; readonly written: string; readonly usual: string };
|
|
11
|
+
|
|
12
|
+
const CONJUNCTION = "CCONJ";
|
|
13
|
+
|
|
14
|
+
/** The word lists the detector reads: the connectives, and the phrases that hold one's letters without being one (歯並びに). */
|
|
15
|
+
export type ConnectiveLists = { readonly connectives: readonly LexiconEntry[]; readonly lookalikes: readonly LexiconEntry[] };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The tagger does not always tell a connective from a noun and a particle: it reads 申請書並びに as 並び + に and
|
|
19
|
+
* 文字の並びに as the conjunction. After の, a determiner or an adjective (の並びに, この並びに, 正しい並びに) the word is
|
|
20
|
+
* a noun of its own. Where the tagger splits it, it is a connective only between two nouns: a noun, a number or a mark
|
|
21
|
+
* before it, and no verb right after it (一列並びに座る).
|
|
22
|
+
*/
|
|
23
|
+
const MODIFIES_NOUN: ReadonlySet<string> = new Set(["DET", "ADJ"]);
|
|
24
|
+
const GENITIVE = "の";
|
|
25
|
+
const JOINS_AFTER: ReadonlySet<string> = new Set(["NOUN", "PROPN", "NUM", "PUNCT", "SYM"]);
|
|
26
|
+
const ENDS_CLAUSE: ReadonlySet<string> = new Set(["VERB", "AUX"]);
|
|
27
|
+
|
|
28
|
+
const modifiesNoun = (token: Token | undefined): boolean =>
|
|
29
|
+
token !== undefined && (MODIFIES_NOUN.has(token.pos) || (token.pos === "ADP" && token.surface === GENITIVE));
|
|
30
|
+
|
|
31
|
+
const joinsNouns = (previous: Token | undefined, next: Token | undefined): boolean =>
|
|
32
|
+
previous !== undefined && JOINS_AFTER.has(previous.pos) && next !== undefined && !ENDS_CLAUSE.has(next.pos);
|
|
33
|
+
|
|
34
|
+
/** A sentence's tokens by where they start and end, and the letters a connective there would not be read in. */
|
|
35
|
+
type Reading = { readonly byStart: ReadonlyMap<number, Token>; readonly byEnd: ReadonlyMap<number, Token>; readonly hidden: readonly boolean[] };
|
|
36
|
+
|
|
37
|
+
const placesOf = (text: string, word: string): number[] => [...text.matchAll(new RegExp(escapeRegExp(word), "gu"))].map((match) => match.index);
|
|
38
|
+
|
|
39
|
+
/** Inside 「」 or 『』 (a quoted phrase holds half a list) or inside a lookalike (歯並びに), by the sentence's own offsets. */
|
|
40
|
+
const hiddenLetters = (text: string, lookalikes: readonly LexiconEntry[]): boolean[] => {
|
|
41
|
+
const hidden = Array.from({ length: text.length }, () => false);
|
|
42
|
+
const hide = (start: number, end: number): boolean[] => hidden.fill(true, start, end);
|
|
43
|
+
quotedSpans(text).forEach((span) => hide(span.start, span.end));
|
|
44
|
+
lookalikes.forEach(({ pattern }) => placesOf(text, pattern).forEach((at) => hide(at, at + pattern.length)));
|
|
45
|
+
return hidden;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const readingOf = (sentence: Sentence, lookalikes: readonly LexiconEntry[]): Reading => {
|
|
49
|
+
const tokens = sentence.tokens ?? [];
|
|
50
|
+
return {
|
|
51
|
+
byStart: new Map(tokens.map((token) => [token.span.start, token])),
|
|
52
|
+
byEnd: new Map(tokens.map((token) => [token.span.end, token])),
|
|
53
|
+
hidden: hiddenLetters(sentence.text, lookalikes),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const standsAsConnective = (reading: Reading, span: Span): boolean => {
|
|
58
|
+
const previous = reading.byEnd.get(span.start);
|
|
59
|
+
if (modifiesNoun(previous)) return false;
|
|
60
|
+
const own = reading.byStart.get(span.start);
|
|
61
|
+
const tagged = own !== undefined && own.pos === CONJUNCTION && own.span.end === span.end;
|
|
62
|
+
return tagged || joinsNouns(previous, reading.byStart.get(span.end));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const hasInnerLevel = (sentence: Sentence, outer: LexiconEntry, connectives: readonly LexiconEntry[]): boolean =>
|
|
66
|
+
connectives.some((entry) => entry.instead_of === outer.pattern && sentence.text.includes(entry.pattern));
|
|
67
|
+
|
|
68
|
+
/** Where one outer-level word stands as a connective in the sentence. */
|
|
69
|
+
const connectiveSpans = (sentence: Sentence, outer: LexiconEntry, reading: Reading): Span[] =>
|
|
70
|
+
placesOf(sentence.text, outer.pattern)
|
|
71
|
+
.filter((at) => reading.hidden[at] !== true)
|
|
72
|
+
.map((at) => ({ start: sentence.span.start + at, end: sentence.span.start + at + outer.pattern.length }))
|
|
73
|
+
.filter((span) => standsAsConnective(reading, span));
|
|
74
|
+
|
|
75
|
+
/** The outer-level connectives of a sentence that stand over no inner level. */
|
|
76
|
+
export const loneConnectivesIn = (sentence: Sentence, lists: ConnectiveLists): LoneConnective[] => {
|
|
77
|
+
const lone = lists.connectives.filter(
|
|
78
|
+
(entry) => entry.rewrite !== undefined && sentence.text.includes(entry.pattern) && !hasInnerLevel(sentence, entry, lists.connectives),
|
|
79
|
+
);
|
|
80
|
+
if (lone.length === 0) return [];
|
|
81
|
+
const reading = readingOf(sentence, lists.lookalikes);
|
|
82
|
+
return lone
|
|
83
|
+
.flatMap((entry) => connectiveSpans(sentence, entry, reading).map((span) => ({ offset: span.start, written: entry.pattern, usual: entry.rewrite ?? "" })))
|
|
84
|
+
.sort((a, b) => a.offset - b.offset);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const connectiveHierarchy: Detector = (doc, options): Finding[] => {
|
|
88
|
+
const lists = { connectives: options.lexicon ?? [], lookalikes: doc.lexicons["connective-lookalike"] ?? [] };
|
|
89
|
+
return doc.sentences.flatMap((sentence) =>
|
|
90
|
+
loneConnectivesIn(sentence, lists).map((lone) => ({
|
|
91
|
+
rule: "",
|
|
92
|
+
severity: "info",
|
|
93
|
+
line: 0,
|
|
94
|
+
column: 0,
|
|
95
|
+
quote: sentence.text.trim(),
|
|
96
|
+
values: { written: lone.written, usual: lone.usual, offset: lone.offset },
|
|
97
|
+
})),
|
|
98
|
+
);
|
|
99
|
+
};
|