chaffjs 0.23.0 → 0.24.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.
Files changed (111) hide show
  1. package/dist/detectors/bare-comparative.d.ts +26 -0
  2. package/dist/detectors/bare-comparative.d.ts.map +1 -0
  3. package/dist/detectors/bare-comparative.js +117 -0
  4. package/dist/detectors/bare-comparative.js.map +1 -0
  5. package/dist/detectors/chat-citation.d.ts +6 -0
  6. package/dist/detectors/chat-citation.d.ts.map +1 -1
  7. package/dist/detectors/chat-citation.js +26 -1
  8. package/dist/detectors/chat-citation.js.map +1 -1
  9. package/dist/detectors/clause-shape.d.ts.map +1 -1
  10. package/dist/detectors/clause-shape.js +19 -5
  11. package/dist/detectors/clause-shape.js.map +1 -1
  12. package/dist/detectors/definition-use.d.ts.map +1 -1
  13. package/dist/detectors/definition-use.js +4 -2
  14. package/dist/detectors/definition-use.js.map +1 -1
  15. package/dist/detectors/generic-heading.d.ts +8 -0
  16. package/dist/detectors/generic-heading.d.ts.map +1 -0
  17. package/dist/detectors/generic-heading.js +26 -0
  18. package/dist/detectors/generic-heading.js.map +1 -0
  19. package/dist/detectors/ime-residue.d.ts +13 -0
  20. package/dist/detectors/ime-residue.d.ts.map +1 -0
  21. package/dist/detectors/ime-residue.js +27 -0
  22. package/dist/detectors/ime-residue.js.map +1 -0
  23. package/dist/detectors/lexicon.d.ts.map +1 -1
  24. package/dist/detectors/lexicon.js +9 -2
  25. package/dist/detectors/lexicon.js.map +1 -1
  26. package/dist/detectors/lookalike-character.d.ts +18 -0
  27. package/dist/detectors/lookalike-character.d.ts.map +1 -0
  28. package/dist/detectors/lookalike-character.js +50 -0
  29. package/dist/detectors/lookalike-character.js.map +1 -0
  30. package/dist/detectors/lowercase-name.d.ts +9 -0
  31. package/dist/detectors/lowercase-name.d.ts.map +1 -0
  32. package/dist/detectors/lowercase-name.js +40 -0
  33. package/dist/detectors/lowercase-name.js.map +1 -0
  34. package/dist/detectors/registry/bare-comparative.d.ts +3 -0
  35. package/dist/detectors/registry/bare-comparative.d.ts.map +1 -0
  36. package/dist/detectors/registry/bare-comparative.js +3 -0
  37. package/dist/detectors/registry/bare-comparative.js.map +1 -0
  38. package/dist/detectors/registry/calendar-name-case.d.ts +3 -0
  39. package/dist/detectors/registry/calendar-name-case.d.ts.map +1 -0
  40. package/dist/detectors/registry/calendar-name-case.js +3 -0
  41. package/dist/detectors/registry/calendar-name-case.js.map +1 -0
  42. package/dist/detectors/registry/generic-heading.d.ts +3 -0
  43. package/dist/detectors/registry/generic-heading.d.ts.map +1 -0
  44. package/dist/detectors/registry/generic-heading.js +3 -0
  45. package/dist/detectors/registry/generic-heading.js.map +1 -0
  46. package/dist/detectors/registry/ime-residue.d.ts +3 -0
  47. package/dist/detectors/registry/ime-residue.d.ts.map +1 -0
  48. package/dist/detectors/registry/ime-residue.js +3 -0
  49. package/dist/detectors/registry/ime-residue.js.map +1 -0
  50. package/dist/detectors/registry/lookalike-character.d.ts +3 -0
  51. package/dist/detectors/registry/lookalike-character.d.ts.map +1 -0
  52. package/dist/detectors/registry/lookalike-character.js +3 -0
  53. package/dist/detectors/registry/lookalike-character.js.map +1 -0
  54. package/dist/detectors/registry/unit-spacing-consistency.d.ts +3 -0
  55. package/dist/detectors/registry/unit-spacing-consistency.d.ts.map +1 -0
  56. package/dist/detectors/registry/unit-spacing-consistency.js +3 -0
  57. package/dist/detectors/registry/unit-spacing-consistency.js.map +1 -0
  58. package/dist/detectors/signals.d.ts.map +1 -1
  59. package/dist/detectors/signals.js +8 -4
  60. package/dist/detectors/signals.js.map +1 -1
  61. package/dist/detectors/structure-tree.d.ts.map +1 -1
  62. package/dist/detectors/structure-tree.js +32 -7
  63. package/dist/detectors/structure-tree.js.map +1 -1
  64. package/dist/detectors/technical-docs.d.ts.map +1 -1
  65. package/dist/detectors/technical-docs.js +14 -0
  66. package/dist/detectors/technical-docs.js.map +1 -1
  67. package/dist/detectors/unit-spacing.d.ts +25 -0
  68. package/dist/detectors/unit-spacing.d.ts.map +1 -0
  69. package/dist/detectors/unit-spacing.js +50 -0
  70. package/dist/detectors/unit-spacing.js.map +1 -0
  71. package/dist/quoted-span.d.ts +7 -0
  72. package/dist/quoted-span.d.ts.map +1 -1
  73. package/dist/quoted-span.js +20 -0
  74. package/dist/quoted-span.js.map +1 -1
  75. package/dist/structure/definition-use.d.ts +14 -4
  76. package/dist/structure/definition-use.d.ts.map +1 -1
  77. package/dist/structure/definition-use.js +29 -9
  78. package/dist/structure/definition-use.js.map +1 -1
  79. package/package.json +3 -3
  80. package/rules/calendar-name-case.yaml +55 -0
  81. package/rules/comparative-without-baseline.yaml +71 -0
  82. package/rules/cross-doc-broken-link.yaml +1 -1
  83. package/rules/cross-doc-duplicate-definition.yaml +1 -1
  84. package/rules/cross-doc-fact-conflict.yaml +1 -1
  85. package/rules/cross-doc-term-variant.yaml +5 -3
  86. package/rules/ime-residue.yaml +55 -0
  87. package/rules/lookalike-character.yaml +82 -0
  88. package/rules/numbered-generic-heading.yaml +111 -0
  89. package/rules/undefined-acronym.yaml +1 -0
  90. package/rules/unit-spacing-consistency.yaml +74 -0
  91. package/src/detectors/bare-comparative.ts +152 -0
  92. package/src/detectors/chat-citation.ts +28 -1
  93. package/src/detectors/clause-shape.ts +20 -6
  94. package/src/detectors/definition-use.ts +13 -3
  95. package/src/detectors/generic-heading.ts +30 -0
  96. package/src/detectors/ime-residue.ts +37 -0
  97. package/src/detectors/lexicon.ts +11 -2
  98. package/src/detectors/lookalike-character.ts +75 -0
  99. package/src/detectors/lowercase-name.ts +58 -0
  100. package/src/detectors/registry/bare-comparative.ts +4 -0
  101. package/src/detectors/registry/calendar-name-case.ts +4 -0
  102. package/src/detectors/registry/generic-heading.ts +4 -0
  103. package/src/detectors/registry/ime-residue.ts +4 -0
  104. package/src/detectors/registry/lookalike-character.ts +4 -0
  105. package/src/detectors/registry/unit-spacing-consistency.ts +4 -0
  106. package/src/detectors/signals.ts +9 -5
  107. package/src/detectors/structure-tree.ts +55 -30
  108. package/src/detectors/technical-docs.ts +13 -0
  109. package/src/detectors/unit-spacing.ts +71 -0
  110. package/src/quoted-span.ts +25 -0
  111. package/src/structure/definition-use.ts +42 -10
@@ -0,0 +1,55 @@
1
+ id: ime-residue
2
+ layer: L1
3
+ status: stable
4
+ how_to_find: ime-residue
5
+ languages: [ja]
6
+ severity: warning
7
+
8
+ name:
9
+ ja: 変換し損ねた英字
10
+ en: A Latin letter left by the input method
11
+
12
+ why:
13
+ ja: 「おもしrおい」「対応sた」のように、日本語の語の中に英字が一字だけ残るのは、ローマ字入力で押したキーが仮名にならなかった跡です。読み手はその語で止まり、検索にも当たりません。打ち間違いなので、どちらの書き方が正しいかの話ではありません。
14
+ en: "A single Latin letter inside a Japanese word (おもしrおい for おもしろい) is a key pressed in romaji input that never became kana. The reader stops at the word, and search does not find it. It is a typing slip, not a style."
15
+
16
+ message:
17
+ ja: 日本語の語の中に英字「{letter}」が一字だけあります(変換し損ねた跡)
18
+ en: 'A lone Latin letter "{letter}" inside a Japanese word, left by the input method'
19
+
20
+ how_to_fix:
21
+ ja: 英字を消して、語を打ち直してください(おもしrおい → おもしろい)。
22
+ en: Delete the letter and retype the word (おもしrおい → おもしろい).
23
+
24
+ levels: { strict: error, normal: warning, relaxed: info }
25
+
26
+ use_for: [business, technical, blog, legal, docs, academic, literature, speech]
27
+
28
+ group: slips
29
+ summary:
30
+ ja: 「おもしrおい」「対応sた」のような、日本語の語の中に一字だけ残った英字
31
+ en: A single Latin letter left inside a Japanese word by the input method, as in おもしrおい
32
+ example:
33
+ ja:
34
+ before: |
35
+ この資料はとてもおもしrおいです。
36
+ after: |
37
+ この資料はとてもおもしろいです。
38
+
39
+ not_flagged:
40
+ ja: 大文字(A 社、B さん)、二字以上の英字(while 文、note)、コードの中は指しません。後ろに助詞が続く一字(変数 x が、まず x を、を x で)は変数の名と読み、一字の後ろが漢字のもの(の x 座標)も指しません。片仮名の後ろの一字(サイズ m、プラン b)は記号と読み、パスの中(/docs/…)も見ません。
41
+ en: Capitals (A社, Bさん), runs of two or more letters (while文, note) and code are not reported. A letter followed by a particle (変数xが, まずxを, をxで) is read as the name of a variable, and a letter followed by a kanji (のx座標) is not reported either. A letter after katakana (サイズm, プランb) is read as a label, and paths (/docs/…) are not read.
42
+ sources: [textlint-ja-presets]
43
+ rewrite:
44
+ depth: light
45
+ ja:
46
+ direction: 残った英字を消して、前後の仮名から語を打ち直してください。語が決められないときは書き手に尋ねます。
47
+ pairs:
48
+ - before: この資料はとてもおもしrおいです。
49
+ after: この資料はとてもおもしろいです。
50
+ - before: 申請に対応sたことを報告します。
51
+ after: 申請に対応したことを報告します。
52
+ keep:
53
+ - 文のほかの語
54
+ avoid:
55
+ - 前後の語まで書き換える
@@ -0,0 +1,82 @@
1
+ id: lookalike-character
2
+ layer: L1
3
+ status: stable
4
+ how_to_find: lookalike-character
5
+ severity: warning
6
+
7
+ name:
8
+ ja: 見た目が同じで別の字
9
+ en: Look-alike character
10
+
11
+ why:
12
+ ja: 部首の字(⼈ U+2F08)や互換漢字は、ふつうの「人」と同じに見えて別の字です。濁点を別の字にした「か+゛」(macOS のファイル名や、そこからコピーした文に多い)も、「が」と同じに見えて別の並びです。どれも検索、並べ替え、辞書引き、表記の照合から漏れ、読み手には直す所が見えません。PDF からのコピーや、入力の変換で入ります。コードの中も見ます。
13
+ en: "A Kangxi radical (⼈ U+2F08) looks like the ordinary 人 and is another character; so is a CJK compatibility ideograph. A letter written as a base and a separate combining mark (e + U+0301, common in macOS file names and text copied from them) looks like é and is another sequence. Search, sorting, dictionaries and spelling checks all miss them, and the reader cannot see what to fix. They come from PDF copies and input methods. Code is checked too."
14
+
15
+ message:
16
+ ja: 「{written}」({code})は「{usual}」と同じに見える別の字です
17
+ en: '"{written}" ({code}) looks like "{usual}" but is another character'
18
+
19
+ messages:
20
+ radical:
21
+ ja: 「{written}」({code})は部首の字です。ふつうの「{usual}」と書きます
22
+ en: '"{written}" ({code}) is a radical, not the ordinary "{usual}"'
23
+ compatibility:
24
+ ja: 「{written}」({code})は互換漢字です。ふつうの「{usual}」と書きます
25
+ en: '"{written}" ({code}) is a compatibility ideograph, not the ordinary "{usual}"'
26
+ decomposed:
27
+ ja: 「{written}」は字と記号が分かれています({code})。一字の「{usual}」と書きます
28
+ en: '"{written}" is a letter and a separate mark ({code}), not the single letter "{usual}"'
29
+ spacing-mark:
30
+ ja: 「{written}」は濁点・半濁点を別の字で書いています({code})。一字の「{usual}」と書きます
31
+ en: '"{written}" writes the voicing mark as a separate character ({code}), not the single letter "{usual}"'
32
+
33
+ how_to_fix:
34
+ ja: その字を消して、ふつうの字を打ち直してください。文書全体を NFC に正規化しても直ります(部首の字は NFKC)。
35
+ en: Delete the character and type the ordinary one. Normalising the document to NFC also fixes it (NFKC for radicals).
36
+
37
+ levels: { strict: error, normal: warning, relaxed: info }
38
+
39
+ use_for: [business, technical, blog, legal, docs, academic, literature, speech]
40
+
41
+ group: slips
42
+ summary:
43
+ ja: 部首の字(⼈)、互換漢字、字と分かれた濁点(か+゛)のような、ふつうの字と同じに見える別の字
44
+ en: A character that looks like an ordinary one but is another, such as a Kangxi radical (⼈ for 人) or a letter and a separate combining mark
45
+ example:
46
+ ja:
47
+ before: "申込書は総務課の⼭田さんへ送ってください。\n"
48
+ after: |
49
+ 申込書は総務課の山田さんへ送ってください。
50
+ en:
51
+ before: "Send the form to the café office.\n"
52
+ after: |
53
+ Send the form to the café office.
54
+
55
+ not_flagged:
56
+ ja: 一字にまとまる形の無い組み合わせ(字に付いた異体字の指定、合成済みの形の無いアクセント)と、Unicode が別の字として持つ互換漢字(﨑 など)は指しません。
57
+ en: Combinations with no single precomposed form (a variation selector after a character, an accent with no precomposed letter) and compatibility ideographs that Unicode keeps as their own characters (﨑) are not reported.
58
+ sources: [textlint-ja-presets]
59
+ rewrite:
60
+ depth: light
61
+ ja:
62
+ direction: 指した字だけを、同じに見えるふつうの字に打ち直してください。ほかの字は変えません。
63
+ pairs:
64
+ - before: "総務課の⼭田さんへ送ってください。"
65
+ after: 総務課の山田さんへ送ってください。
66
+ - before: "がいしゃに送ります。"
67
+ after: がいしゃに送ります。
68
+ keep:
69
+ - 字の並びと文の意味
70
+ avoid:
71
+ - 同じに見えるからと、指した字をそのまま残す
72
+ en:
73
+ direction: Retype only the marked character as the ordinary one it looks like. Change nothing else.
74
+ pairs:
75
+ - before: "Send it to the café office."
76
+ after: Send it to the café office.
77
+ - before: "The résumé is attached."
78
+ after: The résumé is attached.
79
+ keep:
80
+ - the words and their order
81
+ avoid:
82
+ - leaving the marked character because it looks right
@@ -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
@@ -18,6 +18,7 @@ extra_word_lists:
18
18
  name-title,
19
19
  date-time-unit,
20
20
  http-method,
21
+ reference-list-heading,
21
22
  ]
22
23
  uses: [pos]
23
24
  severity: info
@@ -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
@@ -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: followed by an object
164
- * marker or a light verb (受付を開始, 開始します), not a noun being compared (昨年より販売が増えた).
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 || !isListed(heads, token)) return false;
169
- if (!NOMINAL_POS.has(token.pos)) return true;
170
- const next = nextWord(tokens, head);
171
- return next !== undefined && (objectMarkers.has(next.surface) || next.features?.["VerbType"] === "Light");
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> };
@@ -1,11 +1,17 @@
1
1
  import type { Detector, Finding, ProseDocument } from "../plugin.ts";
2
- import { definedTerms, unusedDefinitions, usesBeforeDefinition, type BodyText } from "../structure/definition-use.ts";
2
+ import { definedTerms, unusedDefinitions, usesBeforeDefinition, type BodyText, type Mentioned } from "../structure/definition-use.ts";
3
3
  import { expansionConflicts, expansionsIn } from "../acronym-expansions.ts";
4
4
  import { linesOf, lineNumberAt } from "../structure/lines.ts";
5
+ import { isQuotedAlone } from "../quoted-span.ts";
5
6
  import { quoteAt } from "./structure-tree.ts";
6
7
 
7
8
  const bodyOf = (doc: ProseDocument): BodyText[] => doc.sentences.map((sentence) => ({ start: sentence.span.start, text: sentence.text }));
8
9
 
10
+ const mentionedIn =
11
+ (doc: ProseDocument): Mentioned =>
12
+ (start, end) =>
13
+ isQuotedAlone(doc.source, { start, end });
14
+
9
15
  const findingAt = (doc: ProseDocument, offset: number, values: Readonly<Record<string, string | number>>): Finding => ({
10
16
  rule: "",
11
17
  severity: "warning",
@@ -19,13 +25,17 @@ const findingAt = (doc: ProseDocument, offset: number, values: Readonly<Record<s
19
25
  export const unusedDefinition: Detector = (doc): Finding[] =>
20
26
  doc.structure === undefined
21
27
  ? []
22
- : unusedDefinitions(definedTerms(doc.structure), bodyOf(doc)).map((defined) => findingAt(doc, defined.span.start, { term: defined.term }));
28
+ : unusedDefinitions(definedTerms(doc.structure), bodyOf(doc), mentionedIn(doc)).map((defined) =>
29
+ findingAt(doc, defined.span.start, { term: defined.term }),
30
+ );
23
31
 
24
32
  /** 文の途中で括弧に入れて定義した語を、定義より前で使っている所。 */
25
33
  export const useBeforeDefinition: Detector = (doc): Finding[] =>
26
34
  doc.structure === undefined
27
35
  ? []
28
- : usesBeforeDefinition(definedTerms(doc.structure), bodyOf(doc)).map((use) => findingAt(doc, use.offset, { term: use.term.term, defined: use.term.line }));
36
+ : usesBeforeDefinition(definedTerms(doc.structure), bodyOf(doc), mentionedIn(doc)).map((use) =>
37
+ findingAt(doc, use.offset, { term: use.term.term, defined: use.term.line }),
38
+ );
29
39
 
30
40
  /** 同じ略語を、文書の中で二通りに展開している所。 */
31
41
  export const acronymExpansionConflict: Detector = (doc): Finding[] => {
@@ -0,0 +1,30 @@
1
+ import type { Detector, Finding, Lexicon, MarkupHeading, ProseDocument } from "../plugin.ts";
2
+ import { escapeRegExp } from "../orthography.ts";
3
+ import { findingAt, writtenHeadings } from "./markup-finding.ts";
4
+
5
+ /**
6
+ * 札と番号だけの見出し(メリット1、ポイント2、Benefit 3)。目次を見ても、節に何が書いてあるかが分からない。
7
+ * 札は語彙表 generic-heading-label が言う。題を足した見出し(メリット1:設定が要らない)は読まない。
8
+ */
9
+
10
+ /** 番号の書き方。数字、丸数字、漢数字。前に「その」や「#」、後ろに区切りの記号が付いてもよい。 */
11
+ const NUMBER = String.raw`(?:その)?\s*#?\s*[((]?\s*(?:[0-90-9]+|[①-⑳]|[一二三四五六七八九十]+)\s*(?:つ目)?\s*[::..。))]?`;
12
+
13
+ /** 札のどれかと番号だけで書いた見出しの形。札が無ければ undefined。 */
14
+ export const bareLabelPattern = (labels: Lexicon): RegExp | undefined => {
15
+ const words = labels.map((entry) => escapeRegExp(entry.pattern)).filter((word) => word !== "");
16
+ return words.length === 0 ? undefined : new RegExp(`^(?:${words.join("|")})\\s*${NUMBER}$`, "iu");
17
+ };
18
+
19
+ /** 札と番号だけの見出し。 */
20
+ export const bareLabelHeadings = (doc: ProseDocument): MarkupHeading[] => {
21
+ const pattern = bareLabelPattern(doc.lexicons["generic-heading-label"] ?? []);
22
+ return pattern === undefined ? [] : writtenHeadings(doc).filter((heading) => pattern.test(heading.text.trim()));
23
+ };
24
+
25
+ /** 数で見る。一つなら書き手の選び方、並べば型をなぞった見出し。 */
26
+ export const genericHeading: Detector = (doc, options): Finding[] => {
27
+ const bare = bareLabelHeadings(doc);
28
+ if (bare.length === 0 || bare.length < options.limit) return [];
29
+ return bare.map((heading) => findingAt(doc, heading, { heading: heading.text.trim(), count: bare.length, limit: options.limit }));
30
+ };