chaffjs 0.1.0 → 0.3.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 (217) hide show
  1. package/dist/backends/anthropic.d.ts +24 -0
  2. package/dist/backends/anthropic.d.ts.map +1 -0
  3. package/dist/backends/anthropic.js +44 -0
  4. package/dist/backends/anthropic.js.map +1 -0
  5. package/dist/backends/openai.d.ts +29 -0
  6. package/dist/backends/openai.d.ts.map +1 -0
  7. package/dist/backends/openai.js +33 -0
  8. package/dist/backends/openai.js.map +1 -0
  9. package/dist/backends/types.d.ts +32 -0
  10. package/dist/backends/types.d.ts.map +1 -0
  11. package/dist/backends/types.js +18 -0
  12. package/dist/backends/types.js.map +1 -0
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +21 -67
  15. package/dist/cli.js.map +1 -1
  16. package/dist/commands/eval.d.ts +15 -0
  17. package/dist/commands/eval.d.ts.map +1 -0
  18. package/dist/commands/eval.js +47 -0
  19. package/dist/commands/eval.js.map +1 -0
  20. package/dist/commands/test.d.ts +21 -0
  21. package/dist/commands/test.d.ts.map +1 -0
  22. package/dist/commands/test.js +116 -0
  23. package/dist/commands/test.js.map +1 -0
  24. package/dist/config/by-path.d.ts +15 -0
  25. package/dist/config/by-path.d.ts.map +1 -0
  26. package/dist/config/by-path.js +41 -0
  27. package/dist/config/by-path.js.map +1 -0
  28. package/dist/config/load.d.ts +8 -1
  29. package/dist/config/load.d.ts.map +1 -1
  30. package/dist/config/load.js +31 -3
  31. package/dist/config/load.js.map +1 -1
  32. package/dist/detectors/agentless-passive.d.ts +3 -0
  33. package/dist/detectors/agentless-passive.d.ts.map +1 -0
  34. package/dist/detectors/agentless-passive.js +31 -0
  35. package/dist/detectors/agentless-passive.js.map +1 -0
  36. package/dist/detectors/char-shape.d.ts +12 -0
  37. package/dist/detectors/char-shape.d.ts.map +1 -0
  38. package/dist/detectors/char-shape.js +39 -0
  39. package/dist/detectors/char-shape.js.map +1 -0
  40. package/dist/detectors/count-per-section.d.ts +8 -1
  41. package/dist/detectors/count-per-section.d.ts.map +1 -1
  42. package/dist/detectors/count-per-section.js +26 -5
  43. package/dist/detectors/count-per-section.js.map +1 -1
  44. package/dist/detectors/en-shape.d.ts +19 -0
  45. package/dist/detectors/en-shape.d.ts.map +1 -0
  46. package/dist/detectors/en-shape.js +176 -0
  47. package/dist/detectors/en-shape.js.map +1 -0
  48. package/dist/detectors/index.d.ts.map +1 -1
  49. package/dist/detectors/index.js +33 -0
  50. package/dist/detectors/index.js.map +1 -1
  51. package/dist/detectors/lexicon.d.ts +15 -0
  52. package/dist/detectors/lexicon.d.ts.map +1 -0
  53. package/dist/detectors/lexicon.js +109 -0
  54. package/dist/detectors/lexicon.js.map +1 -0
  55. package/dist/detectors/repeated-head.d.ts.map +1 -1
  56. package/dist/detectors/repeated-head.js +17 -5
  57. package/dist/detectors/repeated-head.js.map +1 -1
  58. package/dist/detectors/sentence-ending.d.ts +3 -0
  59. package/dist/detectors/sentence-ending.d.ts.map +1 -0
  60. package/dist/detectors/sentence-ending.js +34 -0
  61. package/dist/detectors/sentence-ending.js.map +1 -0
  62. package/dist/detectors/sentence-length.d.ts.map +1 -1
  63. package/dist/detectors/sentence-length.js +3 -9
  64. package/dist/detectors/sentence-length.js.map +1 -1
  65. package/dist/detectors/sentence-rhythm.d.ts.map +1 -1
  66. package/dist/detectors/sentence-rhythm.js +3 -2
  67. package/dist/detectors/sentence-rhythm.js.map +1 -1
  68. package/dist/detectors/signals.d.ts +6 -0
  69. package/dist/detectors/signals.d.ts.map +1 -0
  70. package/dist/detectors/signals.js +183 -0
  71. package/dist/detectors/signals.js.map +1 -0
  72. package/dist/detectors/structure.d.ts +24 -0
  73. package/dist/detectors/structure.d.ts.map +1 -0
  74. package/dist/detectors/structure.js +111 -0
  75. package/dist/detectors/structure.js.map +1 -0
  76. package/dist/detectors/token-shape.d.ts +4 -0
  77. package/dist/detectors/token-shape.d.ts.map +1 -0
  78. package/dist/detectors/token-shape.js +62 -0
  79. package/dist/detectors/token-shape.js.map +1 -0
  80. package/dist/document.d.ts.map +1 -1
  81. package/dist/document.js +95 -12
  82. package/dist/document.js.map +1 -1
  83. package/dist/env.d.ts +12 -0
  84. package/dist/env.d.ts.map +1 -0
  85. package/dist/env.js +20 -0
  86. package/dist/env.js.map +1 -0
  87. package/dist/eval.d.ts +31 -0
  88. package/dist/eval.d.ts.map +1 -0
  89. package/dist/eval.js +62 -0
  90. package/dist/eval.js.map +1 -0
  91. package/dist/genre.d.ts.map +1 -1
  92. package/dist/genre.js +8 -1
  93. package/dist/genre.js.map +1 -1
  94. package/dist/init.d.ts.map +1 -1
  95. package/dist/init.js +11 -6
  96. package/dist/init.js.map +1 -1
  97. package/dist/judge.d.ts +14 -16
  98. package/dist/judge.d.ts.map +1 -1
  99. package/dist/judge.js +0 -0
  100. package/dist/judge.js.map +1 -1
  101. package/dist/levels.d.ts +1 -9
  102. package/dist/levels.d.ts.map +1 -1
  103. package/dist/levels.js +15 -3
  104. package/dist/levels.js.map +1 -1
  105. package/dist/look-at.d.ts +18 -0
  106. package/dist/look-at.d.ts.map +1 -0
  107. package/dist/look-at.js +43 -0
  108. package/dist/look-at.js.map +1 -0
  109. package/dist/measure.d.ts +15 -0
  110. package/dist/measure.d.ts.map +1 -0
  111. package/dist/measure.js +16 -0
  112. package/dist/measure.js.map +1 -0
  113. package/dist/plugin.d.ts +44 -1
  114. package/dist/plugin.d.ts.map +1 -1
  115. package/dist/render/eval.d.ts +7 -0
  116. package/dist/render/eval.d.ts.map +1 -0
  117. package/dist/render/eval.js +59 -0
  118. package/dist/render/eval.js.map +1 -0
  119. package/dist/render/explain.d.ts +1 -1
  120. package/dist/render/explain.d.ts.map +1 -1
  121. package/dist/render/explain.js +12 -4
  122. package/dist/render/explain.js.map +1 -1
  123. package/dist/render/rules-json.d.ts.map +1 -1
  124. package/dist/render/rules-json.js +13 -5
  125. package/dist/render/rules-json.js.map +1 -1
  126. package/dist/render/semantic.d.ts +2 -1
  127. package/dist/render/semantic.d.ts.map +1 -1
  128. package/dist/render/semantic.js +35 -0
  129. package/dist/render/semantic.js.map +1 -1
  130. package/dist/rule-load.d.ts.map +1 -1
  131. package/dist/rule-load.js +27 -1
  132. package/dist/rule-load.js.map +1 -1
  133. package/dist/run-semantic.d.ts +22 -1
  134. package/dist/run-semantic.d.ts.map +1 -1
  135. package/dist/run-semantic.js +21 -10
  136. package/dist/run-semantic.js.map +1 -1
  137. package/dist/run.d.ts +6 -1
  138. package/dist/run.d.ts.map +1 -1
  139. package/dist/run.js +34 -2
  140. package/dist/run.js.map +1 -1
  141. package/dist/sentence-shape.d.ts +6 -0
  142. package/dist/sentence-shape.d.ts.map +1 -0
  143. package/dist/sentence-shape.js +15 -0
  144. package/dist/sentence-shape.js.map +1 -0
  145. package/package.json +4 -3
  146. package/rules/adverb-overuse.yaml +28 -0
  147. package/rules/agentless-passive.yaml +28 -0
  148. package/rules/ai-tell.yaml +27 -0
  149. package/rules/bold-density.yaml +8 -6
  150. package/rules/concrete-evidence-density.yaml +25 -0
  151. package/rules/cushion-phrase-density.yaml +26 -0
  152. package/rules/double-keigo.yaml +27 -0
  153. package/rules/emoji-density.yaml +26 -0
  154. package/rules/empty-intensifier.yaml +1 -1
  155. package/rules/excessive-hedging.yaml +26 -0
  156. package/rules/expletive-construction.yaml +27 -0
  157. package/rules/heading-echo.yaml +1 -1
  158. package/rules/hiragana-fukushi.yaml +27 -0
  159. package/rules/max-kanji-continuous.yaml +26 -0
  160. package/rules/max-paragraph-length.yaml +25 -0
  161. package/rules/max-sentence-length.yaml +23 -1
  162. package/rules/ngram-repetition.yaml +25 -0
  163. package/rules/no-doubled-joshi.yaml +28 -0
  164. package/rules/no-mixed-desumasu.yaml +29 -0
  165. package/rules/no-nakaguro-parallel.yaml +27 -0
  166. package/rules/oxford-comma-consistency.yaml +26 -0
  167. package/rules/paragraph-length-variance.yaml +25 -0
  168. package/rules/preamble-length.yaml +25 -0
  169. package/rules/repeated-conjunction.yaml +26 -0
  170. package/rules/repeated-sentence-head.yaml +1 -1
  171. package/rules/rule-of-three.yaml +25 -0
  172. package/rules/sasete-itadaku.yaml +27 -0
  173. package/rules/section-length-uniformity.yaml +25 -0
  174. package/rules/sentence-initial-conjunction-run.yaml +27 -0
  175. package/rules/taigen-dome-in-prose.yaml +27 -0
  176. package/rules/title-case-consistency.yaml +26 -0
  177. package/rules/undefined-acronym.yaml +25 -0
  178. package/rules/unqualified-superlative.yaml +26 -0
  179. package/src/backends/anthropic.ts +61 -0
  180. package/src/backends/openai.ts +46 -0
  181. package/src/backends/types.ts +38 -0
  182. package/src/cli.ts +19 -78
  183. package/src/commands/eval.ts +67 -0
  184. package/src/commands/test.ts +147 -0
  185. package/src/config/by-path.ts +47 -0
  186. package/src/config/load.ts +36 -3
  187. package/src/detectors/agentless-passive.ts +34 -0
  188. package/src/detectors/char-shape.ts +47 -0
  189. package/src/detectors/count-per-section.ts +30 -6
  190. package/src/detectors/en-shape.ts +187 -0
  191. package/src/detectors/index.ts +33 -0
  192. package/src/detectors/lexicon.ts +124 -0
  193. package/src/detectors/repeated-head.ts +17 -6
  194. package/src/detectors/sentence-ending.ts +37 -0
  195. package/src/detectors/sentence-length.ts +4 -10
  196. package/src/detectors/sentence-rhythm.ts +3 -2
  197. package/src/detectors/signals.ts +199 -0
  198. package/src/detectors/structure.ts +119 -0
  199. package/src/detectors/token-shape.ts +81 -0
  200. package/src/document.ts +101 -13
  201. package/src/env.ts +20 -0
  202. package/src/eval.ts +80 -0
  203. package/src/genre.ts +8 -1
  204. package/src/init.ts +11 -6
  205. package/src/judge.ts +0 -0
  206. package/src/levels.ts +16 -4
  207. package/src/look-at.ts +53 -0
  208. package/src/measure.ts +21 -0
  209. package/src/plugin.ts +45 -3
  210. package/src/render/eval.ts +62 -0
  211. package/src/render/explain.ts +12 -4
  212. package/src/render/rules-json.ts +16 -5
  213. package/src/render/semantic.ts +38 -1
  214. package/src/rule-load.ts +28 -1
  215. package/src/run-semantic.ts +32 -11
  216. package/src/run.ts +36 -3
  217. package/src/sentence-shape.ts +22 -0
@@ -0,0 +1,183 @@
1
+ import { proseText } from "../measure.js";
2
+ import { wordsOf } from "./structure.js";
3
+ const PER = 1000;
4
+ /**
5
+ * これより短い文書では密度が暴れる。1 個で「1000 あたり 100」になる。
6
+ * 単位は言語で違うので、床も分ける。英語の 200 語と日本語の 200 文字では長さが桁で違う。
7
+ */
8
+ const FLOOR = { word: 200, char: 500 };
9
+ const bodyOf = (doc) => doc.sentences.map(proseText).join(" ");
10
+ const findIn = (doc, pattern) => doc.sentences.flatMap((sentence) => [...sentence.text.matchAll(pattern)].map((match) => ({ sentence, offset: sentence.span.start + (match.index ?? 0) })));
11
+ const density = (doc, count) => {
12
+ const length = wordsOf(doc);
13
+ return length === 0 ? 0 : Math.round((count / length) * PER);
14
+ };
15
+ /**
16
+ * 絵文字と装飾記号。1 つなら親しみだが、並ぶと文字そのものが読めなくなる。
17
+ * 件数ではなく密度で見る。長い文書ほど当たるのを避けるため(bold-density と同じ)。
18
+ */
19
+ const EMOJI = /\p{Extended_Pictographic}|[←-⇿☀-➿]/gu;
20
+ export const emojiDensity = (doc, options) => {
21
+ const hits = findIn(doc, EMOJI);
22
+ const first = hits[0];
23
+ const rate = density(doc, hits.length);
24
+ if (wordsOf(doc) < FLOOR[doc.lengthUnit] || first === undefined || rate <= options.limit)
25
+ return [];
26
+ return [
27
+ {
28
+ rule: "emoji-density",
29
+ severity: "info",
30
+ line: 0,
31
+ column: 0,
32
+ quote: first.sentence.text.trim(),
33
+ values: { count: hits.length, density: rate, limit: options.limit, offset: first.offset },
34
+ },
35
+ ];
36
+ };
37
+ /**
38
+ * 同じ言い回しの繰り返し。character n-gram で見るので、語の区切りが要らない。
39
+ * spec §10 が n-gram を選んだのはこのためで、新しい言語でそのまま動く。
40
+ */
41
+ const GRAM = 8;
42
+ /** 短い n-gram は偶然一致する。8 文字は「〜することができます」のような言い回しの長さ。 */
43
+ const countGrams = (text) => {
44
+ const counts = new Map();
45
+ Array.from({ length: Math.max(0, text.length - GRAM + 1) }).forEach((_, index) => {
46
+ const gram = text.slice(index, index + GRAM);
47
+ counts.set(gram, (counts.get(gram) ?? 0) + 1);
48
+ });
49
+ return counts;
50
+ };
51
+ /**
52
+ * 名前ではなく言い回しだけを数える。実文書で測ったら、上位は
53
+ * 「AGENTS.m」「シンギュラリティ」のような固有名詞と識別子だった。名前は繰り返して当たり前。
54
+ *
55
+ * 言い回しは、その言語の「つなぎの материал」を含む。日本語ならひらがな、英語なら語の切れ目。
56
+ * どちらを見るかは adapter が宣言する単位で決める。ひらがなを持たない char 単位の言語が
57
+ * 来たら、この見分けは効かなくなる(そのときは adapter 側に判定を移す)。
58
+ */
59
+ const CONNECTIVE = { char: /[ぁ-ゖ]/u, word: / /u };
60
+ const isPhrasing = (gram, unit) => CONNECTIVE[unit].test(gram);
61
+ /** 文をまたいで数えない。文の終わりと次の文の始まりが繋がると、固有名詞が言い回しに見える。 */
62
+ const gramsOf = (doc) => {
63
+ const counts = new Map();
64
+ doc.sentences.forEach((sentence) => {
65
+ const text = doc.lengthUnit === "char" ? proseText(sentence).replace(/\s+/gu, "") : proseText(sentence);
66
+ countGrams(text).forEach((count, gram) => counts.set(gram, (counts.get(gram) ?? 0) + count));
67
+ });
68
+ return counts;
69
+ };
70
+ export const ngramRepetition = (doc, options) => {
71
+ if (wordsOf(doc) < FLOOR[doc.lengthUnit])
72
+ return [];
73
+ const worst = [...gramsOf(doc).entries()]
74
+ .filter(([gram]) => isPhrasing(gram, doc.lengthUnit))
75
+ .reduce((best, [gram, count]) => (count > best.count ? { gram, count } : best), { gram: "", count: 0 });
76
+ if (worst.count <= options.limit)
77
+ return [];
78
+ const at = doc.sentences.find((sentence) => sentence.text.includes(worst.gram));
79
+ return [
80
+ {
81
+ rule: "ngram-repetition",
82
+ severity: "info",
83
+ line: 0,
84
+ column: 0,
85
+ quote: at?.text.trim() ?? worst.gram,
86
+ values: { word: worst.gram, count: worst.count, limit: options.limit, offset: at?.span.start ?? 0 },
87
+ },
88
+ ];
89
+ };
90
+ /**
91
+ * 略語が初出で展開されているか。「CI」だけでは、読む人によって指すものが違う。
92
+ * 展開は「略語の直前か直後の括弧」か「括弧の中の略語」で書かれる。
93
+ */
94
+ const ACRONYM = /\b[A-Z]{2,6}\b/gu;
95
+ /** 読み手が説明なしで通じると見なしてよい語。展開すると逆に読みにくい。 */
96
+ const COMMON = new Set([
97
+ "OK",
98
+ "NG",
99
+ "URL",
100
+ "API",
101
+ "CSS",
102
+ "HTML",
103
+ "JSON",
104
+ "YAML",
105
+ "HTTP",
106
+ "HTTPS",
107
+ "PDF",
108
+ "CPU",
109
+ "GPU",
110
+ "RAM",
111
+ "USB",
112
+ "AI",
113
+ "ID",
114
+ "FAQ",
115
+ "PR",
116
+ "OS",
117
+ "CLI",
118
+ "UI",
119
+ "UX",
120
+ "SQL",
121
+ "CSV",
122
+ "XML",
123
+ "TODO",
124
+ "NOTE",
125
+ ]);
126
+ /**
127
+ * 展開は略語の**すぐ隣**にあるときだけ認める。
128
+ * 60 文字も見ると、同じ文のどこかに括弧があるだけで「説明済み」になり、1 件も出なくなる。
129
+ */
130
+ const AFTER = /^\s*[((]/u;
131
+ const BEFORE = /[))]\s*$/u;
132
+ const isExpanded = (body, acronym) => {
133
+ const at = body.indexOf(acronym);
134
+ if (at === -1)
135
+ return false;
136
+ return AFTER.test(body.slice(at + acronym.length, at + acronym.length + 3)) || BEFORE.test(body.slice(Math.max(0, at - 3), at));
137
+ };
138
+ export const undefinedAcronym = (doc, options) => {
139
+ const body = bodyOf(doc);
140
+ const seen = new Map();
141
+ findIn(doc, ACRONYM).forEach((hit) => {
142
+ const text = /^[A-Z]{2,6}/u.exec(doc.source.slice(hit.offset, hit.offset + 6))?.[0] ?? "";
143
+ if (text.length > 0 && !COMMON.has(text) && !seen.has(text))
144
+ seen.set(text, hit);
145
+ });
146
+ const bare = [...seen.entries()].filter(([acronym]) => !isExpanded(body, acronym));
147
+ if (bare.length < options.limit)
148
+ return [];
149
+ return bare.map(([acronym, hit]) => ({
150
+ rule: "undefined-acronym",
151
+ severity: "info",
152
+ line: 0,
153
+ column: 0,
154
+ quote: hit.sentence.text.trim(),
155
+ values: { word: acronym, count: bare.length, limit: options.limit, offset: hit.offset },
156
+ }));
157
+ };
158
+ /**
159
+ * 具体物の密度。数値・コード・リンク・引用が 1 つも無い節は、
160
+ * 読み終えても何も持ち帰れない。**少ないほうを指摘する**ので、他の密度 rule と向きが逆。
161
+ */
162
+ const CONCRETE = /\d|`|https?:\/\//u;
163
+ export const concreteEvidence = (doc, options) => {
164
+ const sections = doc.sections.filter((section) => section.sentences.length >= 3);
165
+ const empty = sections.filter((section) => !CONCRETE.test(doc.source.slice(section.span.start, section.span.end)));
166
+ if (empty.length < options.limit)
167
+ return [];
168
+ return empty.map((section) => ({
169
+ rule: "concrete-evidence-density",
170
+ severity: "info",
171
+ line: 0,
172
+ column: 0,
173
+ quote: section.heading.length > 0 ? section.heading : (section.sentences[0]?.text.trim() ?? ""),
174
+ values: {
175
+ word: section.heading.length > 0 ? section.heading : "この節",
176
+ count: empty.length,
177
+ total: sections.length,
178
+ limit: options.limit,
179
+ offset: section.span.start,
180
+ },
181
+ }));
182
+ };
183
+ //# sourceMappingURL=signals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signals.js","sourceRoot":"","sources":["../../src/detectors/signals.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { Detector, ProseDocument } from "../plugin.ts";
2
+ /**
3
+ * ばらつきは変動係数(標準偏差 ÷ 平均)で測る。
4
+ *
5
+ * 標準偏差そのものを見ると、長い文書ほど大きくなって閾値が効かない。
6
+ * 平均で割ると長さに依らなくなり、「揃いすぎ」と「ばらつきすぎ」を同じ尺度で言える。
7
+ */
8
+ export declare const coefficientOfVariation: (values: readonly number[]) => number | undefined;
9
+ /** 1 段落に文を詰めすぎると、読み手はどこで息継ぎしていいか分からなくなる。 */
10
+ export declare const paragraphLength: Detector;
11
+ /** 段落の長さが揃いすぎている。人が書くと、言いたいことの重さで段落の長さは揺れる。 */
12
+ export declare const paragraphVariance: Detector;
13
+ /** 節の長さが揃いすぎている。見出しごとに機械的に埋めた文書で起きる。 */
14
+ export declare const sectionUniformity: Detector;
15
+ export declare const ruleOfThree: Detector;
16
+ /**
17
+ * 本題に入るまでが長い。業務文書では、読み手は結論を探しに来ている。
18
+ *
19
+ * 「本題」は最初の中見出し(深さ 2 以上)とする。表題(深さ 1)の直後から数えると、
20
+ * 表題しか無い文書で全文が前置きになる。中見出しが無い文書では何も言わない。
21
+ */
22
+ export declare const preambleLength: Detector;
23
+ export declare const wordsOf: (doc: ProseDocument) => number;
24
+ //# sourceMappingURL=structure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structure.d.ts","sourceRoot":"","sources":["../../src/detectors/structure.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAc,QAAQ,EAAsB,aAAa,EAAW,MAAM,cAAc,CAAC;AAErG;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GAAI,QAAQ,SAAS,MAAM,EAAE,KAAG,MAAM,GAAG,SAM3E,CAAC;AAsBF,4CAA4C;AAC5C,eAAO,MAAM,eAAe,EAAE,QAUvB,CAAC;AAIR,+CAA+C;AAC/C,eAAO,MAAM,iBAAiB,EAAE,QAI/B,CAAC;AAIF,wCAAwC;AACxC,eAAO,MAAM,iBAAiB,EAAE,QAI/B,CAAC;AAQF,eAAO,MAAM,WAAW,EAAE,QAgBzB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,QAkB5B,CAAC;AAEF,eAAO,MAAM,OAAO,GAAI,KAAK,aAAa,KAAG,MAA8F,CAAC"}
@@ -0,0 +1,111 @@
1
+ import { charLength, lengthOf } from "../measure.js";
2
+ /**
3
+ * ばらつきは変動係数(標準偏差 ÷ 平均)で測る。
4
+ *
5
+ * 標準偏差そのものを見ると、長い文書ほど大きくなって閾値が効かない。
6
+ * 平均で割ると長さに依らなくなり、「揃いすぎ」と「ばらつきすぎ」を同じ尺度で言える。
7
+ */
8
+ export const coefficientOfVariation = (values) => {
9
+ if (values.length < 2)
10
+ return undefined;
11
+ const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
12
+ if (mean === 0)
13
+ return undefined;
14
+ const variance = values.reduce((sum, value) => sum + (value - mean) ** 2, 0) / values.length;
15
+ return Math.sqrt(variance) / mean;
16
+ };
17
+ /** 変動係数は 0..1 では読めないので、パーセントの整数にして閾値と突き合わせる。 */
18
+ const PERCENT = 100;
19
+ const asPercent = (value) => Math.round(value * PERCENT);
20
+ const uniformity = (doc, values, span, limit, rule) => {
21
+ const cv = coefficientOfVariation(values);
22
+ if (cv === undefined || asPercent(cv) >= limit)
23
+ return [];
24
+ return [
25
+ {
26
+ rule,
27
+ severity: "info",
28
+ line: 0,
29
+ column: 0,
30
+ quote: doc.source.slice(span.start, span.start + 80).trim(),
31
+ values: { count: values.length, variance: asPercent(cv), limit, offset: span.start },
32
+ },
33
+ ];
34
+ };
35
+ /** 1 段落に文を詰めすぎると、読み手はどこで息継ぎしていいか分からなくなる。 */
36
+ export const paragraphLength = (doc, options) => doc.paragraphs
37
+ .filter((paragraph) => paragraph.sentences.length > options.limit)
38
+ .map((paragraph) => ({
39
+ rule: "max-paragraph-length",
40
+ severity: "warning",
41
+ line: 0,
42
+ column: 0,
43
+ quote: doc.source.slice(paragraph.span.start, paragraph.span.start + 80).trim(),
44
+ values: { count: paragraph.sentences.length, limit: options.limit, offset: paragraph.span.start },
45
+ }));
46
+ const charsOf = (paragraph) => paragraph.sentences.reduce((sum, sentence) => sum + charLength(sentence), 0);
47
+ /** 段落の長さが揃いすぎている。人が書くと、言いたいことの重さで段落の長さは揺れる。 */
48
+ export const paragraphVariance = (doc, options) => {
49
+ const lengths = doc.paragraphs.map(charsOf).filter((length) => length > 0);
50
+ const first = doc.paragraphs[0];
51
+ return first === undefined ? [] : uniformity(doc, lengths, first.span, options.limit, "paragraph-length-variance");
52
+ };
53
+ const sectionChars = (section) => section.sentences.reduce((sum, sentence) => sum + charLength(sentence), 0);
54
+ /** 節の長さが揃いすぎている。見出しごとに機械的に埋めた文書で起きる。 */
55
+ export const sectionUniformity = (doc, options) => {
56
+ const lengths = doc.sections.map(sectionChars).filter((length) => length > 0);
57
+ const first = doc.sections[0];
58
+ return first === undefined ? [] : uniformity(doc, lengths, first.span, options.limit, "section-length-uniformity");
59
+ };
60
+ /**
61
+ * 3 項目の箇条書きばかりになっている。
62
+ * 3 つ並べると収まりがよいので、言いたいことが 2 つでも 4 つでも 3 つにされる。
63
+ */
64
+ const isThree = (list) => list.items.length === 3;
65
+ export const ruleOfThree = (doc, options) => {
66
+ const lists = doc.lists;
67
+ const threes = lists.filter(isThree);
68
+ const share = lists.length === 0 ? 0 : Math.round((threes.length / lists.length) * PERCENT);
69
+ const first = threes[0];
70
+ if (lists.length < 3 || first === undefined || share <= options.limit)
71
+ return [];
72
+ return [
73
+ {
74
+ rule: "rule-of-three",
75
+ severity: "info",
76
+ line: 0,
77
+ column: 0,
78
+ quote: doc.source.slice(first.span.start, first.span.start + 80).trim(),
79
+ values: { count: threes.length, total: lists.length, share, limit: options.limit, offset: first.span.start },
80
+ },
81
+ ];
82
+ };
83
+ /**
84
+ * 本題に入るまでが長い。業務文書では、読み手は結論を探しに来ている。
85
+ *
86
+ * 「本題」は最初の中見出し(深さ 2 以上)とする。表題(深さ 1)の直後から数えると、
87
+ * 表題しか無い文書で全文が前置きになる。中見出しが無い文書では何も言わない。
88
+ */
89
+ export const preambleLength = (doc, options) => {
90
+ const body = doc.sections.find((section) => section.depth >= 2);
91
+ if (body === undefined)
92
+ return [];
93
+ const before = doc.paragraphs.filter((paragraph) => paragraph.span.start < body.span.start);
94
+ if (before.length <= options.limit)
95
+ return [];
96
+ const first = before[0];
97
+ return first === undefined
98
+ ? []
99
+ : [
100
+ {
101
+ rule: "preamble-length",
102
+ severity: "info",
103
+ line: 0,
104
+ column: 0,
105
+ quote: doc.source.slice(first.span.start, first.span.start + 80).trim(),
106
+ values: { count: before.length, limit: options.limit, offset: first.span.start },
107
+ },
108
+ ];
109
+ };
110
+ export const wordsOf = (doc) => doc.sentences.reduce((sum, sentence) => sum + lengthOf(sentence, doc.lengthUnit), 0);
111
+ //# sourceMappingURL=structure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structure.js","sourceRoot":"","sources":["../../src/detectors/structure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../plugin.ts";
2
+ export declare const nounEnding: Detector;
3
+ export declare const doubledParticle: Detector;
4
+ //# sourceMappingURL=token-shape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-shape.d.ts","sourceRoot":"","sources":["../../src/detectors/token-shape.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAwB,MAAM,cAAc,CAAC;AAanE,eAAO,MAAM,UAAU,EAAE,QAmBxB,CAAC;AAiCF,eAAO,MAAM,eAAe,EAAE,QAc7B,CAAC"}
@@ -0,0 +1,62 @@
1
+ import { hasParticle, isClosed, lastContent } from "../sentence-shape.js";
2
+ /**
3
+ * 文末が名詞で終わる(体言止め)。箇条書きでは普通だが、本文では文が途中で切れて読める。
4
+ * 実文書で測ったら、指摘の 9 割が箇条書きの項目だった。箇条書きは除く。
5
+ */
6
+ const NOMINAL = new Set(["NOUN", "PROPN"]);
7
+ /** 非自立名詞は単独では何も指さない。「回るのか。」の「の」を体言止めと読まない。 */
8
+ const isDependent = (token) => token.features?.["NounType"] === "Dependent";
9
+ const inside = (span, spans) => spans.some((other) => span.start >= other.start && span.start < other.end);
10
+ export const nounEnding = (doc, options) => {
11
+ const ended = doc.sentences.filter((sentence) => {
12
+ if (inside(sentence.span, doc.listSpans) || !isClosed(sentence) || !hasParticle(sentence))
13
+ return false;
14
+ const last = lastContent(sentence);
15
+ return last !== undefined && NOMINAL.has(last.pos) && !isDependent(last);
16
+ });
17
+ const first = ended[0];
18
+ // 文書全体の数についての指摘なので、1 文ずつ出さない。13 件並べても直す順番が分からない。
19
+ if (first === undefined || ended.length <= options.limit)
20
+ return [];
21
+ return [
22
+ {
23
+ rule: "taigen-dome-in-prose",
24
+ severity: "info",
25
+ line: 0,
26
+ column: 0,
27
+ quote: first.text.trim(),
28
+ values: { count: ended.length, limit: options.limit, offset: first.span.start },
29
+ },
30
+ ];
31
+ };
32
+ const extend = (runs, token) => {
33
+ const last = runs.at(-1);
34
+ if (last !== undefined && last.surface === token.surface)
35
+ return [...runs.slice(0, -1), { surface: last.surface, tokens: [...last.tokens, token] }];
36
+ return [...runs, { surface: token.surface, tokens: [token] }];
37
+ };
38
+ const PARTICLE = new Set(["ADP", "SCONJ", "PART", "CCONJ"]);
39
+ const BREAK = { surface: "", tokens: [] };
40
+ /**
41
+ * 名詞は連なりの中身なので飛ばす。読点と、語彙表に無い助詞は連なりを切る。
42
+ * 「弊社のAはBの」を 2 回続いたと数えないため。「は」でいちど句が閉じている。
43
+ */
44
+ const runsOf = (tokens, nesting) => tokens.reduce((acc, token) => {
45
+ if (nesting.includes(token.surface))
46
+ return extend(acc, token);
47
+ return token.pos === "PUNCT" || PARTICLE.has(token.pos) ? [...acc, BREAK] : acc;
48
+ }, []);
49
+ export const doubledParticle = (doc, options) => {
50
+ const nesting = (options.lexicon ?? []).map((entry) => entry.pattern);
51
+ return doc.sentences.flatMap((sentence) => runsOf(sentence.tokens ?? [], nesting)
52
+ .filter((run) => run.tokens.length > options.limit)
53
+ .map((run) => ({
54
+ rule: "no-doubled-joshi",
55
+ severity: "warning",
56
+ line: 0,
57
+ column: 0,
58
+ quote: sentence.text.trim(),
59
+ values: { word: run.surface, count: run.tokens.length, limit: options.limit, offset: run.tokens[0]?.span.start ?? sentence.span.start },
60
+ })));
61
+ };
62
+ //# sourceMappingURL=token-shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-shape.js","sourceRoot":"","sources":["../../src/detectors/token-shape.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAG1E;;;GAGG;AACH,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE3C,8CAA8C;AAC9C,MAAM,WAAW,GAAG,CAAC,KAAY,EAAW,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,KAAK,WAAW,CAAC;AAE5F,MAAM,MAAM,GAAG,CAAC,IAAU,EAAE,KAAsB,EAAW,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;AAE3I,MAAM,CAAC,MAAM,UAAU,GAAa,CAAC,GAAG,EAAE,OAAO,EAAa,EAAE;IAC9D,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE;QAC9C,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAC;QACxG,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;QACnC,OAAO,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACvB,iDAAiD;IACjD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,IAAI,OAAO,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACpE,OAAO;QACL;YACE,IAAI,EAAE,sBAAsB;YAC5B,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,CAAC;YACP,MAAM,EAAE,CAAC;YACT,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE;YACxB,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE;SAChF;KACF,CAAC;AACJ,CAAC,CAAC;AAaF,MAAM,MAAM,GAAG,CAAC,IAAoB,EAAE,KAAY,EAAS,EAAE;IAC3D,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACzB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK,CAAC,OAAO;QAAE,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;IACpJ,OAAO,CAAC,GAAG,IAAI,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AAChE,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAE5D,MAAM,KAAK,GAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AAE/C;;;GAGG;AACH,MAAM,MAAM,GAAG,CAAC,MAAwB,EAAE,OAA0B,EAAS,EAAE,CAC7E,MAAM,CAAC,MAAM,CAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;IAClC,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC;QAAE,OAAO,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC/D,OAAO,KAAK,CAAC,GAAG,KAAK,OAAO,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AAClF,CAAC,EAAE,EAAE,CAAC,CAAC;AAET,MAAM,CAAC,MAAM,eAAe,GAAa,CAAC,GAAG,EAAE,OAAO,EAAa,EAAE;IACnE,MAAM,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACtE,OAAO,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CACxC,MAAM,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,EAAE,OAAO,CAAC;SACnC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;SAClD,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACb,IAAI,EAAE,kBAAkB;QACxB,QAAQ,EAAE,SAAkB;QAC5B,IAAI,EAAE,CAAC;QACP,MAAM,EAAE,CAAC;QACT,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE;QAC3B,MAAM,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE;KACxI,CAAC,CAAC,CACN,CAAC;AACJ,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAA2B,MAAM,aAAa,CAAC;AAsI3F,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,EAAE,QAAQ,MAAM,EAAE,SAAS,eAAe,KAAG,aAatF,CAAC"}
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAc,eAAe,EAAa,aAAa,EAA2B,MAAM,aAAa,CAAC;AAoNlH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,EAAE,QAAQ,MAAM,EAAE,SAAS,eAAe,KAAG,aAuBtF,CAAC"}
package/dist/document.js CHANGED
@@ -4,8 +4,14 @@ import { gfmTableFromMarkdown } from "mdast-util-gfm-table";
4
4
  import { frontmatter } from "micromark-extension-frontmatter";
5
5
  import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
6
6
  import { maskSpans } from "./mask.js";
7
- /** 本文として数えないもの。コードは文章ではなく、表は文章の形をしていない。 */
8
- const NOT_PROSE = new Set(["code", "inlineCode", "html", "yaml", "toml", "table", "thematicBreak", "definition", "image", "imageReference"]);
7
+ /**
8
+ * 本文として数えないもの。
9
+ *
10
+ * コードは文章ではなく、表は文章の形をしていない。
11
+ * **引用は自分の文章ではない。** chaff が言えるのは「2 文に割ってください」までで、
12
+ * 引用文にそれはできない。直せないものを指摘しても、書いた人は動けない。
13
+ */
14
+ const NOT_PROSE = new Set(["code", "inlineCode", "html", "yaml", "toml", "table", "blockquote", "thematicBreak", "definition", "image", "imageReference"]);
9
15
  const spanOf = (node) => {
10
16
  const start = node.position?.start.offset;
11
17
  const end = node.position?.end.offset;
@@ -29,8 +35,34 @@ const linkChrome = (node) => {
29
35
  { start: inner.end, end: whole.end },
30
36
  ];
31
37
  };
32
- const collectMasks = (root) => {
33
- const spans = [];
38
+ /**
39
+ * 強調の記号(`**` `*` `_` `~~`)だけを覆う。囲まれた文字は本文なので残す。
40
+ *
41
+ * 残すと解析器が `**。` を 1 語の名詞として拾い、文の終わりが消える。
42
+ * 実文書で「文末が名詞」の誤検知を追って見つけた。文長にも同じだけ効いている。
43
+ */
44
+ const MARKER = /^[*_~]+/u;
45
+ const emphasisChrome = (node, source) => {
46
+ const whole = spanOf(node);
47
+ if (whole === undefined)
48
+ return [];
49
+ const width = MARKER.exec(source.slice(whole.start, whole.end))?.[0].length ?? 0;
50
+ if (width === 0)
51
+ return [];
52
+ return [
53
+ { start: whole.start, end: whole.start + width },
54
+ { start: whole.end - width, end: whole.end },
55
+ ];
56
+ };
57
+ const EMPHASIS = new Set(["strong", "emphasis", "delete"]);
58
+ /**
59
+ * `:::note` `:::` のようなディレクティブ。Zenn・Docusaurus・VitePress が使う記法で、
60
+ * 標準の Markdown には無いため段落として解析される。囲みの指定であって文章ではない。
61
+ */
62
+ const DIRECTIVE = /^:::[^\n]*/gmu;
63
+ const directiveSpans = (source) => [...source.matchAll(DIRECTIVE)].map((match) => ({ start: match.index, end: match.index + match[0].length }));
64
+ const collectMasks = (root, source) => {
65
+ const spans = [...directiveSpans(source)];
34
66
  walk(root, (node) => {
35
67
  if (node.type === "link" || node.type === "linkReference") {
36
68
  spans.push(...linkChrome(node));
@@ -44,6 +76,14 @@ const collectMasks = (root) => {
44
76
  });
45
77
  return spans;
46
78
  };
79
+ const emphasisSpans = (root, source) => {
80
+ const spans = [];
81
+ walk(root, (node) => {
82
+ if (EMPHASIS.has(node.type))
83
+ spans.push(...emphasisChrome(node, source));
84
+ });
85
+ return spans;
86
+ };
47
87
  const textOf = (node, source) => {
48
88
  const parts = [];
49
89
  walk(node, (child) => {
@@ -67,22 +107,30 @@ const headingsOf = (root, source) => {
67
107
  });
68
108
  return found;
69
109
  };
70
- const strongSpans = (root) => {
110
+ const startsInside = (span, regions) => regions.some((region) => span.start >= region.start && span.start < region.end);
111
+ /**
112
+ * 本文の強調だけを数える。
113
+ *
114
+ * 表のセルや見出しの中の太字はラベルであって強調ではない。
115
+ * 「太字は読者の目を止める道具」という bold-density の理屈が当てはまらない。
116
+ * 「本文でないもの」の定義は覆う範囲(collectMasks)に 1 つだけ置き、ここはそれを使う。
117
+ */
118
+ const strongSpans = (root, masked) => {
71
119
  const found = [];
72
120
  walk(root, (node) => {
73
121
  if (node.type !== "strong")
74
122
  return;
75
123
  const span = spanOf(node);
76
- if (span !== undefined)
124
+ if (span !== undefined && !startsInside(span, masked))
77
125
  found.push(span);
78
126
  });
79
127
  return found;
80
128
  };
81
129
  const within = (span, from, to) => span.start >= from && span.start < to;
82
- const paragraphSpans = (root) => {
130
+ const spansOfType = (root, type) => {
83
131
  const found = [];
84
132
  walk(root, (node) => {
85
- if (node.type !== "paragraph")
133
+ if (node.type !== type)
86
134
  return;
87
135
  const span = spanOf(node);
88
136
  if (span !== undefined)
@@ -97,12 +145,14 @@ const paragraphSpans = (root) => {
97
145
  * 覆った表やコードブロックを越えて次の句点まで飲み込む。段落は文が跨がない境界なので、
98
146
  * ここで切れば構造的に起きない。
99
147
  */
148
+ const shift = (span, by) => ({ start: by + span.start, end: by + span.end });
100
149
  const sentencesOf = (prose, paragraphs, adapter) => paragraphs.flatMap((paragraph) => adapter
101
150
  .segment(prose.slice(paragraph.start, paragraph.end))
102
151
  .sentences.filter((sentence) => sentence.text.trim().length > 0)
103
152
  .map((sentence) => ({
104
- span: { start: paragraph.start + sentence.span.start, end: paragraph.start + sentence.span.end },
153
+ span: shift(sentence.span, paragraph.start),
105
154
  text: sentence.text,
155
+ ...(sentence.tokens === undefined ? {} : { tokens: sentence.tokens.map((token) => ({ ...token, span: shift(token.span, paragraph.start) })) }),
106
156
  })));
107
157
  const sectionsOf = (headings, sentences, strongs, length) => {
108
158
  const bounds = headings.map((heading, index) => ({ heading, from: heading.end, to: headings[index + 1]?.start ?? length }));
@@ -121,17 +171,50 @@ const sectionsOf = (headings, sentences, strongs, length) => {
121
171
  };
122
172
  });
123
173
  };
174
+ const paragraphsOf = (spans, sentences, listSpans) => spans
175
+ // 箇条書きの中の段落は「段落」として数えない。項目 1 つを 1 段落と読むと、
176
+ // 段落あたりの文数も長さのばらつきも、箇条書きの多い文書で壊れる。
177
+ .filter((span) => !listSpans.some((list) => span.start >= list.start && span.start < list.end))
178
+ .map((span) => ({ span, sentences: sentences.filter((sentence) => within(sentence.span, span.start, span.end)) }));
179
+ /** 箇条書きは list ノードの直下の項目を数える。入れ子の項目は内側の list のものとして数える。 */
180
+ const listsOf = (root, source) => {
181
+ const found = [];
182
+ walk(root, (node) => {
183
+ if (node.type !== "list")
184
+ return;
185
+ const span = spanOf(node);
186
+ if (span === undefined)
187
+ return;
188
+ const items = (node.children ?? []).flatMap((child) => {
189
+ const item = spanOf(child);
190
+ return child.type === "listItem" && item !== undefined ? [source.slice(item.start, item.end).replace(/\s+/gu, "").length] : [];
191
+ });
192
+ if (items.length > 0)
193
+ found.push({ span, items });
194
+ });
195
+ return found;
196
+ };
124
197
  export const buildDocument = (path, source, adapter) => {
125
198
  const root = parse(source);
126
- const prose = maskSpans(source, collectMasks(root));
127
- const sentences = sentencesOf(prose, paragraphSpans(root), adapter);
199
+ // 強調の記号は「本文でないもの」だが、太字の数を数えるときの「覆われた場所」ではない。
200
+ // 同じ集合にすると、太字が自分の記号のせいで覆われた場所にあることになり、1 つも数えられなくなる。
201
+ const blocks = collectMasks(root, source);
202
+ const masked = [...blocks, ...emphasisSpans(root, source)];
203
+ const prose = maskSpans(source, masked);
204
+ const paragraphSpans = spansOfType(root, "paragraph");
205
+ const listItems = spansOfType(root, "listItem");
206
+ const sentences = sentencesOf(prose, paragraphSpans, adapter);
128
207
  return {
129
208
  path,
130
209
  source,
131
210
  language: adapter.id,
132
211
  lengthUnit: adapter.capabilities.lengthUnit,
133
- sections: sectionsOf(headingsOf(root, source), sentences, strongSpans(root), source.length),
212
+ capabilities: adapter.capabilities,
213
+ sections: sectionsOf(headingsOf(root, source), sentences, strongSpans(root, blocks), source.length),
134
214
  sentences,
215
+ listSpans: listItems,
216
+ paragraphs: paragraphsOf(paragraphSpans, sentences, listItems),
217
+ lists: listsOf(root, source),
135
218
  lexicons: adapter.lexicons,
136
219
  };
137
220
  };