chaffjs 0.14.0 → 0.15.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 (273) hide show
  1. package/dist/address-chain.d.ts +4 -1
  2. package/dist/address-chain.d.ts.map +1 -1
  3. package/dist/address-chain.js +5 -2
  4. package/dist/address-chain.js.map +1 -1
  5. package/dist/cli-text.d.ts +4 -1
  6. package/dist/cli-text.d.ts.map +1 -1
  7. package/dist/cli-text.js +14 -2
  8. package/dist/cli-text.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +21 -21
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/eval.d.ts +4 -0
  13. package/dist/commands/eval.d.ts.map +1 -1
  14. package/dist/commands/eval.js +20 -6
  15. package/dist/commands/eval.js.map +1 -1
  16. package/dist/commands/feedback.d.ts.map +1 -1
  17. package/dist/commands/feedback.js +2 -1
  18. package/dist/commands/feedback.js.map +1 -1
  19. package/dist/commands/test.d.ts.map +1 -1
  20. package/dist/commands/test.js +3 -2
  21. package/dist/commands/test.js.map +1 -1
  22. package/dist/commands/tree.d.ts.map +1 -1
  23. package/dist/commands/tree.js +2 -1
  24. package/dist/commands/tree.js.map +1 -1
  25. package/dist/detectors/acronym-compound.d.ts +8 -0
  26. package/dist/detectors/acronym-compound.d.ts.map +1 -0
  27. package/dist/detectors/acronym-compound.js +8 -0
  28. package/dist/detectors/acronym-compound.js.map +1 -0
  29. package/dist/detectors/acronym-context.d.ts +4 -0
  30. package/dist/detectors/acronym-context.d.ts.map +1 -1
  31. package/dist/detectors/acronym-context.js +17 -1
  32. package/dist/detectors/acronym-context.js.map +1 -1
  33. package/dist/detectors/acronym-expansion.d.ts +10 -0
  34. package/dist/detectors/acronym-expansion.d.ts.map +1 -1
  35. package/dist/detectors/acronym-expansion.js +63 -5
  36. package/dist/detectors/acronym-expansion.js.map +1 -1
  37. package/dist/detectors/char-shape.d.ts.map +1 -1
  38. package/dist/detectors/char-shape.js +36 -14
  39. package/dist/detectors/char-shape.js.map +1 -1
  40. package/dist/detectors/citation-key.d.ts +13 -0
  41. package/dist/detectors/citation-key.d.ts.map +1 -0
  42. package/dist/detectors/citation-key.js +9 -0
  43. package/dist/detectors/citation-key.js.map +1 -0
  44. package/dist/detectors/cited-name.d.ts +11 -0
  45. package/dist/detectors/cited-name.d.ts.map +1 -0
  46. package/dist/detectors/cited-name.js +15 -0
  47. package/dist/detectors/cited-name.js.map +1 -0
  48. package/dist/detectors/concrete-evidence.d.ts +7 -0
  49. package/dist/detectors/concrete-evidence.d.ts.map +1 -0
  50. package/dist/detectors/concrete-evidence.js +21 -0
  51. package/dist/detectors/concrete-evidence.js.map +1 -0
  52. package/dist/detectors/counter-edge.d.ts +8 -0
  53. package/dist/detectors/counter-edge.d.ts.map +1 -0
  54. package/dist/detectors/counter-edge.js +27 -0
  55. package/dist/detectors/counter-edge.js.map +1 -0
  56. package/dist/detectors/dotted-name.d.ts +13 -0
  57. package/dist/detectors/dotted-name.d.ts.map +1 -0
  58. package/dist/detectors/dotted-name.js +10 -0
  59. package/dist/detectors/dotted-name.js.map +1 -0
  60. package/dist/detectors/doubled-word.d.ts +6 -1
  61. package/dist/detectors/doubled-word.d.ts.map +1 -1
  62. package/dist/detectors/doubled-word.js +22 -2
  63. package/dist/detectors/doubled-word.js.map +1 -1
  64. package/dist/detectors/en-shape.d.ts +1 -1
  65. package/dist/detectors/en-shape.d.ts.map +1 -1
  66. package/dist/detectors/en-shape.js +11 -22
  67. package/dist/detectors/en-shape.js.map +1 -1
  68. package/dist/detectors/gram-place.d.ts +0 -4
  69. package/dist/detectors/gram-place.d.ts.map +1 -1
  70. package/dist/detectors/gram-place.js +6 -1
  71. package/dist/detectors/gram-place.js.map +1 -1
  72. package/dist/detectors/gram-predicate.d.ts +7 -0
  73. package/dist/detectors/gram-predicate.d.ts.map +1 -0
  74. package/dist/detectors/gram-predicate.js +44 -0
  75. package/dist/detectors/gram-predicate.js.map +1 -0
  76. package/dist/detectors/heading-case.d.ts +21 -0
  77. package/dist/detectors/heading-case.d.ts.map +1 -0
  78. package/dist/detectors/heading-case.js +38 -0
  79. package/dist/detectors/heading-case.js.map +1 -0
  80. package/dist/detectors/heading-echo.d.ts.map +1 -1
  81. package/dist/detectors/heading-echo.js +4 -1
  82. package/dist/detectors/heading-echo.js.map +1 -1
  83. package/dist/detectors/lexicon.d.ts.map +1 -1
  84. package/dist/detectors/lexicon.js +7 -4
  85. package/dist/detectors/lexicon.js.map +1 -1
  86. package/dist/detectors/name-numeral.d.ts +16 -0
  87. package/dist/detectors/name-numeral.d.ts.map +1 -0
  88. package/dist/detectors/name-numeral.js +30 -0
  89. package/dist/detectors/name-numeral.js.map +1 -0
  90. package/dist/detectors/name-part.d.ts +8 -0
  91. package/dist/detectors/name-part.d.ts.map +1 -0
  92. package/dist/detectors/name-part.js +20 -0
  93. package/dist/detectors/name-part.js.map +1 -0
  94. package/dist/detectors/name-run.d.ts +8 -0
  95. package/dist/detectors/name-run.d.ts.map +1 -0
  96. package/dist/detectors/name-run.js +13 -0
  97. package/dist/detectors/name-run.js.map +1 -0
  98. package/dist/detectors/orthography.d.ts.map +1 -1
  99. package/dist/detectors/orthography.js +15 -5
  100. package/dist/detectors/orthography.js.map +1 -1
  101. package/dist/detectors/placeholder.d.ts +14 -0
  102. package/dist/detectors/placeholder.d.ts.map +1 -0
  103. package/dist/detectors/placeholder.js +24 -0
  104. package/dist/detectors/placeholder.js.map +1 -0
  105. package/dist/detectors/quoted-variant.d.ts +7 -0
  106. package/dist/detectors/quoted-variant.d.ts.map +1 -0
  107. package/dist/detectors/quoted-variant.js +18 -0
  108. package/dist/detectors/quoted-variant.js.map +1 -0
  109. package/dist/detectors/series-label.d.ts +13 -0
  110. package/dist/detectors/series-label.d.ts.map +1 -0
  111. package/dist/detectors/series-label.js +26 -0
  112. package/dist/detectors/series-label.js.map +1 -0
  113. package/dist/detectors/signals.d.ts.map +1 -1
  114. package/dist/detectors/signals.js +29 -24
  115. package/dist/detectors/signals.js.map +1 -1
  116. package/dist/detectors/structure-tree.js +1 -1
  117. package/dist/detectors/structure-tree.js.map +1 -1
  118. package/dist/detectors/superlative-name.d.ts +9 -0
  119. package/dist/detectors/superlative-name.d.ts.map +1 -0
  120. package/dist/detectors/superlative-name.js +24 -0
  121. package/dist/detectors/superlative-name.js.map +1 -0
  122. package/dist/detectors/token-shape.d.ts.map +1 -1
  123. package/dist/detectors/token-shape.js +10 -2
  124. package/dist/detectors/token-shape.js.map +1 -1
  125. package/dist/document.d.ts.map +1 -1
  126. package/dist/document.js +10 -4
  127. package/dist/document.js.map +1 -1
  128. package/dist/genre-check.d.ts +34 -0
  129. package/dist/genre-check.d.ts.map +1 -0
  130. package/dist/genre-check.js +21 -0
  131. package/dist/genre-check.js.map +1 -0
  132. package/dist/genre.d.ts +8 -1
  133. package/dist/genre.d.ts.map +1 -1
  134. package/dist/genre.js +29 -5
  135. package/dist/genre.js.map +1 -1
  136. package/dist/init.d.ts +0 -1
  137. package/dist/init.d.ts.map +1 -1
  138. package/dist/init.js +0 -12
  139. package/dist/init.js.map +1 -1
  140. package/dist/joined-view.d.ts +25 -0
  141. package/dist/joined-view.d.ts.map +1 -0
  142. package/dist/joined-view.js +76 -0
  143. package/dist/joined-view.js.map +1 -0
  144. package/dist/line-continues.d.ts +6 -0
  145. package/dist/line-continues.d.ts.map +1 -0
  146. package/dist/line-continues.js +33 -0
  147. package/dist/line-continues.js.map +1 -0
  148. package/dist/measure.d.ts +1 -0
  149. package/dist/measure.d.ts.map +1 -1
  150. package/dist/measure.js +5 -1
  151. package/dist/measure.js.map +1 -1
  152. package/dist/number-name.d.ts +6 -3
  153. package/dist/number-name.d.ts.map +1 -1
  154. package/dist/number-name.js +31 -14
  155. package/dist/number-name.js.map +1 -1
  156. package/dist/orthography.d.ts +4 -0
  157. package/dist/orthography.d.ts.map +1 -1
  158. package/dist/orthography.js +11 -9
  159. package/dist/orthography.js.map +1 -1
  160. package/dist/plain-source.d.ts +6 -0
  161. package/dist/plain-source.d.ts.map +1 -0
  162. package/dist/plain-source.js +6 -0
  163. package/dist/plain-source.js.map +1 -0
  164. package/dist/plugin.d.ts +16 -1
  165. package/dist/plugin.d.ts.map +1 -1
  166. package/dist/quoted-span.d.ts +10 -0
  167. package/dist/quoted-span.d.ts.map +1 -0
  168. package/dist/quoted-span.js +20 -0
  169. package/dist/quoted-span.js.map +1 -0
  170. package/dist/render/eval.d.ts +2 -1
  171. package/dist/render/eval.d.ts.map +1 -1
  172. package/dist/render/eval.js +84 -43
  173. package/dist/render/eval.js.map +1 -1
  174. package/dist/resolve-genre.d.ts +11 -0
  175. package/dist/resolve-genre.d.ts.map +1 -0
  176. package/dist/resolve-genre.js +18 -0
  177. package/dist/resolve-genre.js.map +1 -0
  178. package/dist/soft-break.d.ts +19 -0
  179. package/dist/soft-break.d.ts.map +1 -0
  180. package/dist/soft-break.js +78 -0
  181. package/dist/soft-break.js.map +1 -0
  182. package/dist/structure/build.d.ts.map +1 -1
  183. package/dist/structure/build.js +19 -9
  184. package/dist/structure/build.js.map +1 -1
  185. package/dist/structure/date-order.d.ts +3 -0
  186. package/dist/structure/date-order.d.ts.map +1 -1
  187. package/dist/structure/date-order.js +9 -1
  188. package/dist/structure/date-order.js.map +1 -1
  189. package/dist/structure/issues.d.ts +1 -1
  190. package/dist/structure/issues.d.ts.map +1 -1
  191. package/dist/structure/issues.js +14 -3
  192. package/dist/structure/issues.js.map +1 -1
  193. package/dist/structure/listed-tags.d.ts +3 -0
  194. package/dist/structure/listed-tags.d.ts.map +1 -0
  195. package/dist/structure/listed-tags.js +6 -0
  196. package/dist/structure/listed-tags.js.map +1 -0
  197. package/dist/structure/number-in-sentence.d.ts +15 -0
  198. package/dist/structure/number-in-sentence.d.ts.map +1 -0
  199. package/dist/structure/number-in-sentence.js +13 -0
  200. package/dist/structure/number-in-sentence.js.map +1 -0
  201. package/dist/structure/unit-word.d.ts +8 -0
  202. package/dist/structure/unit-word.d.ts.map +1 -0
  203. package/dist/structure/unit-word.js +10 -0
  204. package/dist/structure/unit-word.js.map +1 -0
  205. package/dist/structure/wrapped-tail.d.ts +18 -0
  206. package/dist/structure/wrapped-tail.d.ts.map +1 -0
  207. package/dist/structure/wrapped-tail.js +11 -0
  208. package/dist/structure/wrapped-tail.js.map +1 -0
  209. package/package.json +3 -3
  210. package/rules/dangling-reference.yaml +2 -2
  211. package/rules/date-order.yaml +2 -2
  212. package/rules/latin-spacing.yaml +4 -2
  213. package/rules/max-kanji-continuous.yaml +1 -1
  214. package/rules/no-doubled-joshi.yaml +2 -2
  215. package/rules/title-case-consistency.yaml +2 -2
  216. package/rules/undefined-acronym.yaml +14 -1
  217. package/rules/unqualified-superlative.yaml +1 -1
  218. package/src/address-chain.ts +5 -2
  219. package/src/cli-text.ts +19 -3
  220. package/src/cli.ts +22 -19
  221. package/src/commands/eval.ts +26 -5
  222. package/src/commands/feedback.ts +2 -1
  223. package/src/commands/test.ts +3 -2
  224. package/src/commands/tree.ts +2 -1
  225. package/src/detectors/acronym-compound.ts +9 -0
  226. package/src/detectors/acronym-context.ts +22 -1
  227. package/src/detectors/acronym-expansion.ts +72 -5
  228. package/src/detectors/char-shape.ts +42 -14
  229. package/src/detectors/citation-key.ts +13 -0
  230. package/src/detectors/cited-name.ts +23 -0
  231. package/src/detectors/concrete-evidence.ts +31 -0
  232. package/src/detectors/counter-edge.ts +29 -0
  233. package/src/detectors/dotted-name.ts +14 -0
  234. package/src/detectors/doubled-word.ts +25 -2
  235. package/src/detectors/en-shape.ts +17 -30
  236. package/src/detectors/gram-place.ts +8 -1
  237. package/src/detectors/gram-predicate.ts +55 -0
  238. package/src/detectors/heading-case.ts +41 -0
  239. package/src/detectors/heading-echo.ts +5 -1
  240. package/src/detectors/lexicon.ts +8 -5
  241. package/src/detectors/name-numeral.ts +37 -0
  242. package/src/detectors/name-part.ts +24 -0
  243. package/src/detectors/name-run.ts +16 -0
  244. package/src/detectors/orthography.ts +17 -8
  245. package/src/detectors/placeholder.ts +38 -0
  246. package/src/detectors/quoted-variant.ts +20 -0
  247. package/src/detectors/series-label.ts +33 -0
  248. package/src/detectors/signals.ts +35 -30
  249. package/src/detectors/structure-tree.ts +2 -2
  250. package/src/detectors/superlative-name.ts +30 -0
  251. package/src/detectors/token-shape.ts +12 -3
  252. package/src/document.ts +12 -5
  253. package/src/genre-check.ts +46 -0
  254. package/src/genre.ts +34 -5
  255. package/src/init.ts +0 -13
  256. package/src/joined-view.ts +96 -0
  257. package/src/line-continues.ts +43 -0
  258. package/src/measure.ts +9 -1
  259. package/src/number-name.ts +35 -13
  260. package/src/orthography.ts +15 -9
  261. package/src/plain-source.ts +5 -0
  262. package/src/plugin.ts +21 -2
  263. package/src/quoted-span.ts +25 -0
  264. package/src/render/eval.ts +101 -38
  265. package/src/resolve-genre.ts +20 -0
  266. package/src/soft-break.ts +94 -0
  267. package/src/structure/build.ts +19 -10
  268. package/src/structure/date-order.ts +16 -1
  269. package/src/structure/issues.ts +18 -3
  270. package/src/structure/listed-tags.ts +7 -0
  271. package/src/structure/number-in-sentence.ts +20 -0
  272. package/src/structure/unit-word.ts +11 -0
  273. package/src/structure/wrapped-tail.ts +22 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unit-word.js","sourceRoot":"","sources":["../../src/structure/unit-word.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,SAAwB,EAAE,aAAgC,EAAW,EAAE;IACrG,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAC7C,OAAO,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,EAAE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AACrH,CAAC,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { Mention } from "../plugin.ts";
2
+ /**
3
+ * 参照を読む行と、その後ろにつないだ次の行。
4
+ *
5
+ * 72 桁で折り返す RFC は「Section 16.3.2\n of [HTTP]」のように、参照と「どの文書の」を別の行に書く。行だけを読むと
6
+ * 自分の文書の 16.3.2 に見える。改行と字下げは読み手には空白一つなので、そうつないだ文字列を言語の読み手に渡す。
7
+ * 拾った参照は行の中にあるものだけを残す。次の行の参照は、次の行を読むときに拾う。位置は行の上のまま。
8
+ */
9
+ export type WrappedLine = {
10
+ readonly line: string;
11
+ readonly text: string;
12
+ readonly lineLength: number;
13
+ };
14
+ /** 見出しは次の行へ続かない。見出しの行か、次の行が無い・空なら、行だけ。 */
15
+ export declare const wrappedLine: (text: string, onHeading: boolean, next: string | undefined) => WrappedLine;
16
+ /** 行の中で終わる参照だけ。つないだ次の行に入り込んだもの(「Sections 1,\n 2」の 2)は捨てる。 */
17
+ export declare const onLine: (mentions: readonly Mention[], wrapped: WrappedLine) => readonly Mention[];
18
+ //# sourceMappingURL=wrapped-tail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wrapped-tail.d.ts","sourceRoot":"","sources":["../../src/structure/wrapped-tail.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAExG,0CAA0C;AAC1C,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,EAAE,WAAW,OAAO,EAAE,MAAM,MAAM,GAAG,SAAS,KAAG,WAKxF,CAAC;AAEF,6DAA6D;AAC7D,eAAO,MAAM,MAAM,GAAI,UAAU,SAAS,OAAO,EAAE,EAAE,SAAS,WAAW,KAAG,SAAS,OAAO,EAC3B,CAAC"}
@@ -0,0 +1,11 @@
1
+ /** 見出しは次の行へ続かない。見出しの行か、次の行が無い・空なら、行だけ。 */
2
+ export const wrappedLine = (text, onHeading, next) => {
3
+ const tail = onHeading ? "" : (next?.trim() ?? "");
4
+ if (tail === "")
5
+ return { line: text, text, lineLength: text.length };
6
+ const kept = text.trimEnd();
7
+ return { line: text, text: `${kept} ${tail}`, lineLength: kept.length };
8
+ };
9
+ /** 行の中で終わる参照だけ。つないだ次の行に入り込んだもの(「Sections 1,\n 2」の 2)は捨てる。 */
10
+ export const onLine = (mentions, wrapped) => mentions.filter((mention) => mention.end <= wrapped.lineLength);
11
+ //# sourceMappingURL=wrapped-tail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wrapped-tail.js","sourceRoot":"","sources":["../../src/structure/wrapped-tail.ts"],"names":[],"mappings":"AAWA,0CAA0C;AAC1C,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAY,EAAE,SAAkB,EAAE,IAAwB,EAAe,EAAE;IACrG,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACnD,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IACtE,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;IAC5B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,IAAI,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AAC1E,CAAC,CAAC;AAEF,6DAA6D;AAC7D,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,QAA4B,EAAE,OAAoB,EAAsB,EAAE,CAC/F,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaffjs",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Prose validation harness. Finds what is hard to read, and never rewrites it for you.",
5
5
  "keywords": [
6
6
  "chaff",
@@ -54,8 +54,8 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@anthropic-ai/sdk": "^0.125.0",
57
- "@chaffjs/lang-en": "0.12.0",
58
- "@chaffjs/lang-ja": "0.13.0",
57
+ "@chaffjs/lang-en": "0.13.0",
58
+ "@chaffjs/lang-ja": "0.14.0",
59
59
  "mdast-util-from-markdown": "^2.0.3",
60
60
  "mdast-util-frontmatter": "^2.0.1",
61
61
  "mdast-util-gfm-table": "^2.0.0",
@@ -10,8 +10,8 @@ name:
10
10
  en: Reference to a missing provision
11
11
 
12
12
  why:
13
- ja: 「第12条に定める」のような参照は、その条が文書に無ければ読み手を行き止まりにします。条を消したり番号を振り直したりしたときに、参照だけが古い番号のまま残ることがよくあります。chaff は文書を番地の付いた木にして(chaff tree)、参照の番地が木にあるかを見ます。
14
- en: A reference such as "as set out in Section 12" leaves the reader at a dead end when that section is not in the document. It usually happens when a provision is deleted or renumbered and the reference keeps the old number. chaff builds the document's tree of addresses (chaff tree) and checks that each reference's address is in it.
13
+ ja: 「第12条に定める」のような参照は、その条が文書に無ければ読み手を行き止まりにします。条を消したり番号を振り直したりしたときに、参照だけが古い番号のまま残ることがよくあります。chaff は文書を番地の付いた木にして(chaff tree)、参照の番地が木にあるかを見ます。見出しに「1 目的」と番号を振っただけで「第N条」と書いた条の無い文書(ガイドラインや通達)の「法第2条」は、別の文書の条なので探しません。
14
+ en: A reference such as "as set out in Section 12" leaves the reader at a dead end when that section is not in the document. It usually happens when a provision is deleted or renumbered and the reference keeps the old number. chaff builds the document's tree of addresses (chaff tree) and checks that each reference's address is in it. A Japanese reference counted in 条 (法第2条) is not looked up in a document that numbers none of its provisions with 条, such as a guideline whose headings read "1 目的", because it points into another document.
15
15
 
16
16
  message:
17
17
  ja: 「{label}」(番地 {target})はこの文書にありません
@@ -10,8 +10,8 @@ name:
10
10
  en: A date out of order in a schedule
11
11
 
12
12
  why:
13
- ja: 旅程やマイルストーンのように日付を並べた箇条書きや表で、ひとつだけ前の日付より早い日付があると、書き間違いか順番の入れ替わりかを読み手が確かめることになります。並びが古い順か新しい順かは多いほうで決め、それに逆らう日付だけを指します。
14
- en: In a list or a table of dates, such as an itinerary or milestones, one date earlier than the one before it makes the reader check whether it is a typo or a swapped row. The direction of the list (oldest or newest first) is taken from most of its steps, and only a date against it is pointed out.
13
+ ja: 旅程やマイルストーンのように日付を並べた箇条書きや表で、ひとつだけ前の日付より早い日付があると、書き間違いか順番の入れ替わりかを読み手が確かめることになります。並びが古い順か新しい順かは多いほうで決め、それに逆らう日付だけを指します。その向きに沿う日付が半分以下の並びは、名前や版の順に並べた一覧として扱い、何も言いません。
14
+ en: In a list or a table of dates, such as an itinerary or milestones, one date earlier than the one before it makes the reader check whether it is a typo or a swapped row. The direction of the list (oldest or newest first) is taken from most of its steps, and only a date against it is pointed out. A list where no more than half of the dates follow that direction is taken as sorted by something else, such as a name or a version, and nothing is said.
15
15
 
16
16
  message:
17
17
  ja: 「{date}」が、前後の日付の並びから外れています(前は「{previous}」)
@@ -6,14 +6,16 @@ languages: [ja]
6
6
  severity: warning
7
7
  # 番号・識別子として書かれた数を見分けるのに、数の後ろの語の品詞を使う。無ければ全部数える。
8
8
  uses: [pos]
9
+ # 番地(千代田区紀尾井町1-3)を、都道府県までの地名の後ろの範囲(東京都2-3 営業日)と分ける。
10
+ extra_word_lists: [prefecture-unit]
9
11
 
10
12
  name:
11
13
  ja: 英数字の前後の空白がそろっていない
12
14
  en: Spacing around Latin text is inconsistent
13
15
 
14
16
  why:
15
- ja: 日本語と英字・数字のあいだを空けるか詰めるかは、どちらでも読めます。困るのは一つの文書の中で混ざることで、読み手には違いに意味があるように見えます。chaff はどちらが正しいかを決めず、文書の中で少ないほうを指摘します。英字と、数字の前、数字の後ろは別々に数えます。「を 3回」のように、数字の前は空けて助数詞とは詰める書き方がふつうにあるからです。「3GBの」のように数字に続く単位は、英字ではなく数字の側として数えます。逆に「H30 等」「EC2 で」「Phase 1 は」のように英字で始まって数字で終わる名前は、英字の側として数えます。「073-489-5909」「2026-06-02」のような番号は数量ではないので数えません。題や注の番号として書かれた数(「3.1 リサーチの原則」の節番号、※1 の注番号、行の頭に続き番号で並べた注の番号)と、0 で始まる組を含む番号(〒102-0094)も、後ろの空白が番号と題・項目の区切りなので数えません。
16
- en: Whether to put a space between Japanese and Latin letters or digits is a choice either way. What reads badly is mixing both in one document, which suggests the difference means something. chaff does not decide which is right; it points at whichever the document uses less. Letters, the space before a digit and the space after one are counted separately, since spacing before a number but not before its counter is common. A unit after a number (3GB) counts with the number; a name that starts with a letter and ends in a digit (H30, EC2, Phase 1) counts with the letters. A code such as a phone number or a date (073-489-5909, 2026-06-02) is not a quantity and is not counted. Neither is a number written as the label of a title or a note (a section number such as 3.1 before its title, a note number after ※, the numbers of notes listed one per line in sequence) or a code with a part starting with 0 (〒102-0094), since the space after it separates the label from its title or the next item.
17
+ ja: 日本語と英字・数字のあいだを空けるか詰めるかは、どちらでも読めます。困るのは一つの文書の中で混ざることで、読み手には違いに意味があるように見えます。chaff はどちらが正しいかを決めず、文書の中で少ないほうを指摘します。英字と、数字の前、数字の後ろは別々に数えます。「を 3回」のように、数字の前は空けて助数詞とは詰める書き方がふつうにあるからです。「3GBの」のように数字に続く単位は、英字ではなく数字の側として数えます。逆に「H30 等」「EC2 で」「Phase 1 は」のように英字で始まって数字で終わる名前は、英字の側として数えます。「073-489-5909」「2026-06-02」のような番号は数量ではないので数えません。題や注の番号として書かれた数(「3.1 リサーチの原則」の節番号、※1 の注番号、行の頭に続き番号で並べた注の番号)、0 で始まる組を含む番号(102-0094)、〒の後ろの郵便番号(〒100-8916)、市・区・町まで下りた地名のすぐ後ろの番地(千代田区紀尾井町1-3)も、後ろの空白が番号と題・項目の区切りなので数えません。鉤括弧(「」『』)で引いた題名や発言の中も、空け方は引いた元のものなので数えません。
18
+ en: Whether to put a space between Japanese and Latin letters or digits is a choice either way. What reads badly is mixing both in one document, which suggests the difference means something. chaff does not decide which is right; it points at whichever the document uses less. Letters, the space before a digit and the space after one are counted separately, since spacing before a number but not before its counter is common. A unit after a number (3GB) counts with the number; a name that starts with a letter and ends in a digit (H30, EC2, Phase 1) counts with the letters. A code such as a phone number or a date (073-489-5909, 2026-06-02) is not a quantity and is not counted. Neither is a number written as the label of a title or a note (a section number such as 3.1 before its title, a note number after ※, the numbers of notes listed one per line in sequence), a code with a part starting with 0 (102-0094), a postal code after 〒 (〒100-8916), or a hyphenated address number right after a city, ward or town (千代田区紀尾井町1-3), since the space after it separates the label from its title or the next item. Nor is the spacing inside a title or a quotation in 「」 or 『』, which is the source's, not the writer's.
17
19
 
18
20
  message:
19
21
  ja: "日本語と{kind}のあいだを{style}(この文書はふつう{usual}。{of} 箇所のうち {count} 箇所が違う)"
@@ -2,7 +2,7 @@ id: max-kanji-continuous
2
2
  layer: L3
3
3
  status: experimental
4
4
  how_to_find: kanji-run
5
- # 品詞があれば、住所(地名と数だけでできた連なり)を数えない。無くても文字だけで動く。
5
+ # 品詞があれば、住所(地名と数だけでできた連なり)と 1 つの名前(辞書の固有名詞 1 語、人の姓と名)を数えない。無くても文字だけで動く。
6
6
  uses: [pos]
7
7
  # 住所の先頭にしか来ない単位(都道府県)。地名の後ろに付けば、割られた町名ではなく地名の並び。
8
8
  extra_word_lists: [prefecture-unit]
@@ -12,8 +12,8 @@ name:
12
12
  en: Nested repeated particle
13
13
 
14
14
  why:
15
- ja: 「弊社の新製品の販売の計画」のように同じ助詞が読点を挟まずに続くと、どれがどれに係るのか読み手が追えなくなります。読点で区切られた並列は数えていません。
16
- en: When the same particle repeats without a break, the reader cannot tell which phrase attaches to which. Items separated by punctuation are not counted.
15
+ ja: 「弊社の新製品の販売の計画」のように同じ助詞が読点を挟まずに続くと、どれがどれに係るのか読み手が追えなくなります。読点で区切られた並列は数えていません。鉤括弧で引いた題名や発言(「食品中のウイルスの制御のための…」)の中は書き手が言い換えられないので数えず、引いたもの全体を一つの名詞として読みます。
16
+ en: When the same particle repeats without a break, the reader cannot tell which phrase attaches to which. Items separated by punctuation are not counted. Nor are particles inside a title or a quotation in 「」 or 『』, which the writer cannot rephrase; the quotation as a whole is read as one noun.
17
17
 
18
18
  message:
19
19
  ja: 「{word}」が読点を挟まずに {count} 回続いています({limit} 回まで)
@@ -10,8 +10,8 @@ name:
10
10
  en: Heading capitalisation is inconsistent
11
11
 
12
12
  why:
13
- ja: Title Case と sentence case のどちらが正しいかは決めません。1 つの文書で混ざっていると、見出しの階層が変わったように見えます。
14
- en: Neither Title Case nor sentence case is wrong. Mixed within one document, the headings look like they change level.
13
+ ja: Title Case と sentence case のどちらが正しいかは決めません。1 つの文書で混ざっていると、見出しの階層が変わったように見えます。文書の題名(先頭にある深さ 1 の見出しが 1 つだけのとき)は名前なので比べません。節の見出しが同数に割れたときだけ、題名の流儀に合わせます。
14
+ en: Neither Title Case nor sentence case is wrong. Mixed within one document, the headings look like they change level. The document's title (a single top-level heading that opens it) is a name and is not compared; when the section headings split evenly, the title's style decides.
15
15
 
16
16
  message:
17
17
  ja: この見出しだけ他と大文字化の流儀が違います(文書の中で {count} 個)
@@ -3,7 +3,20 @@ layer: L1
3
3
  status: experimental
4
4
  how_to_find: undefined-acronym
5
5
  word_list: common-acronym
6
- extra_word_lists: [definition-marker, definition-verb, meridiem, time-zone, currency-code, us-state-code, emphasis-word, numbered-division]
6
+ extra_word_lists:
7
+ [
8
+ definition-marker,
9
+ definition-verb,
10
+ meridiem,
11
+ time-zone,
12
+ currency-code,
13
+ us-state-code,
14
+ emphasis-word,
15
+ numbered-division,
16
+ honorific,
17
+ date-time-unit,
18
+ http-method,
19
+ ]
7
20
  uses: [pos]
8
21
  severity: info
9
22
 
@@ -3,7 +3,7 @@ layer: L2
3
3
  status: experimental
4
4
  how_to_find: unqualified-superlative
5
5
  word_list: superlative
6
- extra_word_lists: [comparison-marker, superlative-scope]
6
+ extra_word_lists: [comparison-marker, superlative-scope, quantity-noun]
7
7
  uses: [pos]
8
8
  severity: warning
9
9
 
@@ -102,12 +102,15 @@ export const addressSpans = (text: string, profile: DocumentProfile | undefined)
102
102
  return spans;
103
103
  };
104
104
 
105
- /** 番地を空白にする。空白が連なりを切るので、番地は数えられず、前後は別の連なりになる。 */
105
+ /**
106
+ * 番地を同じ長さの空白にする。空白が連なりを切るので、番地は数えられず、前後は別の連なりになる。
107
+ * 長さを保つので、覆った後の位置は元の文字列の位置のまま。
108
+ */
106
109
  export const maskAddresses = (text: string, profile: DocumentProfile | undefined): string => {
107
110
  const spans = addressSpans(text, profile);
108
111
  if (spans.length === 0) return text;
109
112
  const pieces: string[] = [];
110
- spans.forEach((span, index) => pieces.push(text.slice(spans[index - 1]?.end ?? 0, span.start), " "));
113
+ spans.forEach((span, index) => pieces.push(text.slice(spans[index - 1]?.end ?? 0, span.start), " ".repeat(span.end - span.start)));
111
114
  pieces.push(text.slice(spans.at(-1)?.end ?? 0));
112
115
  return pieces.join("");
113
116
  };
package/src/cli-text.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // What the command line says, in Japanese and English. Per-file text follows the file's language; the rest
2
2
  // follows hostLanguage (chaff.yaml's language, else the terminal's locale).
3
3
  import type { Texts } from "./ui.ts";
4
+ import type { GenreSetting } from "./genre-check.ts";
4
5
 
5
6
  /** Where a file's genre came from, as the header names it. */
6
7
  export type GenreSource = "--genre" | "by_path" | "config" | "default" | "front-matter" | "path" | "content";
@@ -81,7 +82,9 @@ export type CliText = {
81
82
  readonly noMarkdown: (targets: string) => string;
82
83
  readonly noMarkdownHere: string;
83
84
  readonly noAdapter: (language: string) => string;
84
- readonly unknownGenre: (genre: string) => string;
85
+ readonly unknownGenre: (genre: string, where: string, known: readonly string[]) => string;
86
+ readonly genreWhere: (where: GenreSetting, files: readonly string[]) => string;
87
+ readonly unreadFrontMatterGenre: (path: string, genre: string, known: readonly string[]) => string;
85
88
  readonly unknownRule: (id: string) => string;
86
89
  readonly unknownRuleWithList: (id: string, list: string) => string;
87
90
  readonly unnamed: string;
@@ -115,7 +118,14 @@ export const CLI_TEXT: Texts<CliText> = {
115
118
  noMarkdown: (targets) => `Markdown が 1 つも見つかりませんでした: ${targets}`,
116
119
  noMarkdownHere: "Markdown が 1 つも見つかりませんでした。",
117
120
  noAdapter: (language) => `言語 "${language}" のアダプタがありません。`,
118
- unknownGenre: (genre) => `ジャンル "${genre}" はありません。npx chaff genres で一覧が出ます。`,
121
+ unknownGenre: (genre, where, known) =>
122
+ `ジャンル "${genre}" はありません(${where})。使えるのは ${known.join("、")} です。npx chaff genres で一覧が出ます。`,
123
+ genreWhere: (where, files) => {
124
+ if (where === "config") return "chaff.yaml の genre";
125
+ return where === "by_path" ? `chaff.yaml の by_path、files: ${files.join(", ")}` : "--genre";
126
+ },
127
+ unreadFrontMatterGenre: (path, genre, known) =>
128
+ `${path}: front matter の genre "${genre}" はジャンルではないので読みませんでした。使えるのは ${known.join("、")} です。`,
119
129
  unknownRule: (id) => `${id} というルールはありません。npx chaff rules --json で一覧が出ます。`,
120
130
  unknownRuleWithList: (id, list) => `${id} というルールはありません。\n一覧:\n${list}`,
121
131
  unnamed: "(名前なし)",
@@ -159,7 +169,13 @@ export const CLI_TEXT: Texts<CliText> = {
159
169
  noMarkdown: (targets) => `No Markdown files found: ${targets}`,
160
170
  noMarkdownHere: "No Markdown files found.",
161
171
  noAdapter: (language) => `No language package for "${language}".`,
162
- unknownGenre: (genre) => `There is no genre "${genre}". npx chaff genres lists them.`,
172
+ unknownGenre: (genre, where, known) => `There is no genre "${genre}" (${where}). The genres are ${known.join(", ")}. npx chaff genres lists them.`,
173
+ genreWhere: (where, files) => {
174
+ if (where === "config") return "genre in chaff.yaml";
175
+ return where === "by_path" ? `by_path in chaff.yaml, files: ${files.join(", ")}` : "--genre";
176
+ },
177
+ unreadFrontMatterGenre: (path, genre, known) =>
178
+ `${path}: the front matter's genre "${genre}" is not a genre, so it was not used. The genres are ${known.join(", ")}.`,
163
179
  unknownRule: (id) => `There is no rule named ${id}. npx chaff rules --json lists them.`,
164
180
  unknownRuleWithList: (id, list) => `There is no rule named ${id}.\nRules:\n${list}`,
165
181
  unnamed: "(no name)",
package/src/cli.ts CHANGED
@@ -7,6 +7,7 @@ import { applyByPath } from "./config/by-path.ts";
7
7
  import { applyLevel } from "./config/write.ts";
8
8
  import { buildDocument, teamRules } from "./document.ts";
9
9
  import { guessLanguage } from "./detect.ts";
10
+ import { plainSource } from "./plain-source.ts";
10
11
  import { collectTargets } from "./files.ts";
11
12
  import { BASELINE_FILE, fingerprint, readBaseline, splitByBaseline, writeBaseline } from "./baseline.ts";
12
13
  import { applySuppressions } from "./stet.ts";
@@ -14,8 +15,10 @@ import { renderSuppressions, type PerFile } from "./render/suppressions.ts";
14
15
  import { clock, describeChange, snapshotOf, watchPaths, type Snapshot } from "./watch.ts";
15
16
  import { runEval } from "./commands/eval.ts";
16
17
  import { runTest } from "./commands/test.ts";
17
- import { frontMatterGenre, guessGenre } from "./genre.ts";
18
- import { GENRES, runInit } from "./init.ts";
18
+ import { GENRES } from "./genre.ts";
19
+ import { unknownGenres, writtenGenres } from "./genre-check.ts";
20
+ import { resolveGenre } from "./resolve-genre.ts";
21
+ import { runInit } from "./init.ts";
19
22
  import { targetsOf } from "./cli-args.ts";
20
23
  import { loadRules } from "./rule-load.ts";
21
24
  import { renderCompact } from "./render/compact.ts";
@@ -42,15 +45,12 @@ const hostText = (config: Config): CliText => CLI_TEXT[hostLanguage(config.langu
42
45
 
43
46
  const readConfig = (): Config => (existsSync(join(process.cwd(), CONFIG_FILE)) ? loadConfig(join(process.cwd(), CONFIG_FILE)) : EMPTY);
44
47
 
45
- const resolveGenre = (path: string, source: string, config: Config, cliGenre?: string): { genre: string; from: GenreSource } => {
46
- // コマンドで指定したものが最優先。その実行だけの指定だから、設定より強い。
47
- if (cliGenre !== undefined) return { genre: cliGenre, from: "--genre" };
48
- // パスごとの上書きが次。「全体はこう、ここだけは違う」を書けるようにする。
49
- const override = applyByPath(config.byPath, config.baseDir, path);
50
- if (override.genre !== undefined) return { genre: override.genre, from: "by_path" };
51
- if (config.genre !== undefined) return { genre: config.genre, from: "config" };
52
- const guess = guessGenre(path, source, frontMatterGenre(source));
53
- return guess === undefined ? { genre: "blog/tech", from: "default" } : { genre: guess.genre, from: guess.from };
48
+ const genreProblems = (command: string, argv: readonly string[]): string[] => {
49
+ const config = readConfig();
50
+ const text = hostText(config);
51
+ return unknownGenres(writtenGenres(command, flag(argv, "--genre"), config), GENRES).map((entry) =>
52
+ text.unknownGenre(entry.genre, text.genreWhere(entry.where, entry.files), GENRES),
53
+ );
54
54
  };
55
55
 
56
56
  /** resolveGenre with this run's --genre, for the commands that take it as a dependency. */
@@ -97,10 +97,11 @@ const headerFor = (path: string, genre: string, from: GenreSource, language: str
97
97
  };
98
98
 
99
99
  const inspect = async (path: string, config: Config, argv: readonly string[]): Promise<Inspected> => {
100
- const source = await readFile(path, "utf8");
100
+ const source = plainSource(await readFile(path, "utf8"));
101
101
  const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
102
102
  const adapter = await loadAdapter(language);
103
- const { genre, from } = resolveGenre(path, source, config, flag(argv, "--genre"));
103
+ const { genre, from, unread } = resolveGenre(path, source, config, flag(argv, "--genre"));
104
+ if (unread !== undefined) console.error(`chaff: ${CLI_TEXT[uiLanguageOf(language)].unreadFrontMatterGenre(path, unread, GENRES)}`);
104
105
  const rules = loadRules(language);
105
106
  const experimental = config.experimental || argv.includes("--experimental");
106
107
  await adapter.prepare?.(neededBy(rules, config.rules, experimental, genre, language));
@@ -292,7 +293,10 @@ const HANDLERS: Readonly<Record<string, Handler>> = {
292
293
  genres: showGenres,
293
294
  rules: showRules,
294
295
  explain: (argv) => explain(argv[1]),
295
- eval: (argv) => runEval(positional(argv), argv, { config: readConfig(), resolveGenre: genreFrom(argv), flag }),
296
+ eval: (argv) => {
297
+ const config = readConfig();
298
+ return runEval(positional(argv), argv, { config, resolveGenre: genreFrom(argv), flag, ui: hostLanguage(config.language, process.env) });
299
+ },
296
300
  tree: (argv) => runTree(treeTargets(argv), argv, treeContext()),
297
301
  cite: (argv) => runCite(citeTargets(argv), argv, treeContext()),
298
302
  test: (argv) => runTest(positional(argv), argv, { config: readConfig(), resolveGenre: genreFrom(argv), inspect }),
@@ -332,11 +336,10 @@ export const main = async (argv: readonly string[]): Promise<number> => {
332
336
  console.log(VERSION_LINES.join("\n"));
333
337
  return 0;
334
338
  }
335
- const cliGenre = flag(argv, "--genre");
336
- if (cliGenre !== undefined && !GENRES.includes(cliGenre)) {
337
- console.error(hostText(readConfig()).unknownGenre(cliGenre));
338
- return 1;
339
- }
339
+ // 知らないジャンルでは、どの rule も当たらず、動かなかった rule も並ばない。「指摘なし」が素通りに見えるので、何かする前に止める。
340
+ const problems = genreProblems(first, argv);
341
+ problems.forEach((problem) => console.error(problem));
342
+ if (problems.length > 0) return 1;
340
343
  const handler = HANDLERS[first];
341
344
  if (handler !== undefined) return handler(argv);
342
345
  const targets = targetsOf(first === "lint" ? argv.slice(1) : argv);
@@ -1,3 +1,4 @@
1
+ import { plainSource } from "../plain-source.ts";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { loadAdapter } from "../adapter-load.ts";
3
4
  import { applyByPath } from "../config/by-path.ts";
@@ -10,28 +11,47 @@ import { renderEval } from "../render/eval.ts";
10
11
  import { neededBy } from "../run.ts";
11
12
  import type { Config } from "../config/load.ts";
12
13
  import { profileFor } from "../profile/for-file.ts";
14
+ import { CLI_TEXT } from "../cli-text.ts";
15
+ import { uiLanguageOf, type Texts, type UiLanguage } from "../ui.ts";
16
+
17
+ const TEXT: Texts<{
18
+ readonly mixed: (mixes: string) => string;
19
+ readonly unknownRule: (id: string) => string;
20
+ }> = {
21
+ ja: {
22
+ mixed: (mixes) => `言語かジャンルが混ざっています: ${mixes}\n1 つに絞って測ってください(例: npx chaff eval examples/blog-ja/)。`,
23
+ unknownRule: (id) => `${id} という rule はありません。`,
24
+ },
25
+ en: {
26
+ mixed: (mixes) => `Languages or genres are mixed: ${mixes}\nMeasure one at a time (for example npx chaff eval examples/blog-en/).`,
27
+ unknownRule: (id) => `There is no rule named ${id}.`,
28
+ },
29
+ };
13
30
 
14
31
  export type Context = {
15
32
  readonly config: Config;
16
33
  readonly resolveGenre: (path: string, source: string, config: Config) => { genre: string; from: string };
17
34
  readonly flag: (argv: readonly string[], name: string) => string | undefined;
35
+ /** The language of what is not about the documents measured: chaff.yaml's language, else the locale. */
36
+ readonly ui: UiLanguage;
18
37
  };
19
38
 
20
39
  /**
21
40
  * 閾値の較正。手元の文書を「人間の良い文書」として扱い、どの閾値なら
22
41
  * 誤検知が目標を下回るかを測る。書き換えはしない。
42
+ * 測った結果は文書の言語で出す(混ざっていれば測らないので、言語は 1 つ)。断りは ui の言語。
23
43
  */
24
44
  export const runEval = async (targets: readonly string[], argv: readonly string[], context: Context): Promise<number> => {
25
45
  const paths = collectTargets(targets.length > 0 ? targets : ["."]);
26
46
  if (paths.length === 0) {
27
- console.error(`Markdown が 1 つも見つかりませんでした: ${targets.join(", ")}`);
47
+ console.error(CLI_TEXT[context.ui].noMarkdown(targets.join(", ")));
28
48
  return 1;
29
49
  }
30
- const { config, resolveGenre, flag } = context;
50
+ const { config, resolveGenre, flag, ui } = context;
31
51
  const only = flag(argv, "--rule");
32
52
  const docs = await Promise.all(
33
53
  paths.map(async (path) => {
34
- const source = await readFile(path, "utf8");
54
+ const source = plainSource(await readFile(path, "utf8"));
35
55
  const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
36
56
  const adapter = await loadAdapter(language);
37
57
  const { genre } = resolveGenre(path, source, config);
@@ -44,12 +64,12 @@ export const runEval = async (targets: readonly string[], argv: readonly string[
44
64
  // 言語とジャンルが混ざった corpus は測れない。閾値の単位が違うため。
45
65
  const mixed = new Set(docs.map((entry) => `${entry.language}/${entry.genre}`));
46
66
  if (mixed.size > 1) {
47
- console.error(`言語かジャンルが混ざっています: ${[...mixed].join(", ")}\n1 つに絞って測ってください(例: npx chaff eval examples/blog-ja/)。`);
67
+ console.error(TEXT[ui].mixed([...mixed].join(", ")));
48
68
  return 1;
49
69
  }
50
70
  const rules = loadRules(language).filter((rule) => only === undefined || rule.id === only);
51
71
  if (rules.length === 0) {
52
- console.error(`${only ?? "(名前なし)"} という rule はありません。`);
72
+ console.error(TEXT[ui].unknownRule(only ?? CLI_TEXT[ui].unnamed));
53
73
  return 1;
54
74
  }
55
75
  console.log(
@@ -63,6 +83,7 @@ export const runEval = async (targets: readonly string[], argv: readonly string[
63
83
  ),
64
84
  docs.length,
65
85
  paths.length,
86
+ uiLanguageOf(language),
66
87
  ),
67
88
  );
68
89
  return 0;
@@ -1,3 +1,4 @@
1
+ import { plainSource } from "../plain-source.ts";
1
2
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
3
  import { basename, join } from "node:path";
3
4
  import type { Finding, RuleDefinition } from "../plugin.ts";
@@ -178,7 +179,7 @@ export const runFeedback = async (targets: readonly string[], argv: readonly str
178
179
  console.error(text.notFound(request.path));
179
180
  return 1;
180
181
  }
181
- const source = readFileSync(request.path, "utf8");
182
+ const source = plainSource(readFileSync(request.path, "utf8"));
182
183
  const checked = await context.check(request.path);
183
184
  const picked = pick(request, checked, source.split("\n").length, context.ui);
184
185
  if ("error" in picked) {
@@ -1,3 +1,4 @@
1
+ import { plainSource } from "../plain-source.ts";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { join } from "node:path";
3
4
  import { loadAdapter } from "../adapter-load.ts";
@@ -33,7 +34,7 @@ const judgeAll = async (
33
34
  ): Promise<Judged[]> =>
34
35
  Promise.all(
35
36
  paths.map(async (path) => {
36
- const source = await readFile(path, "utf8");
37
+ const source = plainSource(await readFile(path, "utf8"));
37
38
  const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
38
39
  const adapter = await loadAdapter(language);
39
40
  const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language));
@@ -53,7 +54,7 @@ const dryRun = async (
53
54
  ): Promise<void> => {
54
55
  const plans = await Promise.all(
55
56
  paths.map(async (path) => {
56
- const source = await readFile(path, "utf8");
57
+ const source = plainSource(await readFile(path, "utf8"));
57
58
  const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
58
59
  const adapter = await loadAdapter(language);
59
60
  const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language));
@@ -1,3 +1,4 @@
1
+ import { plainSource } from "../plain-source.ts";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { loadAdapter } from "../adapter-load.ts";
3
4
  import { applyByPath } from "../config/by-path.ts";
@@ -46,7 +47,7 @@ export const treeTargets = (argv: readonly string[]): string[] =>
46
47
 
47
48
  export const readSource = async (path: string, context: TreeContext): Promise<string | undefined> => {
48
49
  try {
49
- return await readFile(path, "utf8");
50
+ return plainSource(await readFile(path, "utf8"));
50
51
  } catch (err) {
51
52
  console.error(treeText(context).unreadable(path, err instanceof Error ? err.message : String(err)));
52
53
  return undefined;
@@ -0,0 +1,9 @@
1
+ /** - で繋いだ略語(RT-PCR)の部品。繋いでいなければ、その語だけ。 */
2
+ export const partsOf = (acronym: string): string[] => acronym.split("-");
3
+
4
+ /**
5
+ * 略語が説明済みか。- で繋いだ語は、繋いだ形のまま説明されているか、部品がどれも説明済みなら説明済み。
6
+ * US-EU は US も EU も通じるので指摘しない。RT-PCR は RT がどこにも説明されていないので、繋いだ形で指摘する。
7
+ */
8
+ export const isExplained = (acronym: string, explainedAlone: (word: string) => boolean): boolean =>
9
+ explainedAlone(acronym) || partsOf(acronym).every(explainedAlone);
@@ -1,5 +1,10 @@
1
1
  import { escapeRegExp } from "../orthography.ts";
2
2
  import { ROMAN_NUMERAL } from "./roman-numeral.ts";
3
+ import { citationKeySpans } from "./citation-key.ts";
4
+ import { dottedNameSpans } from "./dotted-name.ts";
5
+ import { loneNumeralSpans, nameNumeralSpans } from "./name-numeral.ts";
6
+ import { placeholderSpans } from "./placeholder.ts";
7
+ import { seriesLabelSpans } from "./series-label.ts";
3
8
 
4
9
  /**
5
10
  * 大文字の語が略語ではなく、決まった書き方の一部として読める所(3:30 PM、1pm ET、USD 1,000、Kansas City, MO 64108)。
@@ -18,6 +23,10 @@ export type NotationWords = {
18
23
  readonly emphasis: readonly string[];
19
24
  /** 番号を後ろに書く、文書の区切りの名前(Part、Section、Title)。 */
20
25
  readonly divisions: readonly string[];
26
+ /** 速記録が大文字で書く発言者の姓の前に置く敬称(Mr.、Mrs.、Madam)。 */
27
+ readonly honorifics: readonly string[];
28
+ /** 日付・時刻の数字の後ろに書く単位(年、月、日、時、分)。その前の同じ大文字の繰り返し(YY年)は書式の置き場所。 */
29
+ readonly dateTimeUnits: readonly string[];
21
30
  };
22
31
 
23
32
  // 空の語彙表は「その書き方が無い」。空の選択肢 (?:) は至る所で空文字に当たるので、何にも当たらない形にする。
@@ -37,6 +46,9 @@ const AMOUNT = String.raw`(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?`;
37
46
  */
38
47
  const divisionOf = (words: readonly string[]): string => oneOf(words.flatMap((word) => [word, word.toUpperCase()]));
39
48
 
49
+ /** 敬称のすぐ後ろの大文字の姓(Mr. HAWLEY、Dr. O'NEIL)。速記録は発言者をこう書き、略語ではない。 */
50
+ const SURNAME_IN_CAPITALS = String.raw`[A-Z]+(?:['’-][A-Z]+)*(?![\p{L}\p{N}_])`;
51
+
40
52
  // 強調の記号は空白に置き換えてある(**3:30** PM)ので、部品の間の空白は数を問わない。
41
53
  const patternsOf = (words: NotationWords): readonly RegExp[] => [
42
54
  new RegExp(String.raw`${CLOCK_12}\s*${oneOf(words.meridiem)}`, "gu"),
@@ -46,6 +58,7 @@ const patternsOf = (words: NotationWords): readonly RegExp[] => [
46
58
  new RegExp(String.raw`,\s+${oneOf(words.usStates)}\s+\d{5}(?:-\d{4})?(?!\d)`, "gu"),
47
59
  new RegExp(oneOf(words.emphasis), "gu"),
48
60
  new RegExp(String.raw`(?<![\p{L}\p{N}_])${divisionOf(words.divisions)}\s+${ROMAN_NUMERAL}(?![\p{L}\p{N}_&])`, "gu"),
61
+ new RegExp(String.raw`(?<![\p{L}\p{N}_])${oneOf(words.honorifics)}\s+${SURNAME_IN_CAPITALS}`, "gu"),
49
62
  ];
50
63
 
51
64
  export type NotAcronymSpans = (text: string) => Span[];
@@ -56,5 +69,13 @@ export type NotAcronymSpans = (text: string) => Span[];
56
69
  */
57
70
  export const notAcronymSpansOf = (words: NotationWords): NotAcronymSpans => {
58
71
  const patterns = patternsOf(words);
59
- return (text) => patterns.flatMap((pattern) => [...text.matchAll(pattern)].map((match) => ({ start: match.index, end: match.index + match[0].length })));
72
+ return (text) => [
73
+ ...patterns.flatMap((pattern) => [...text.matchAll(pattern)].map((match) => ({ start: match.index, end: match.index + match[0].length }))),
74
+ ...citationKeySpans(text),
75
+ ...dottedNameSpans(text),
76
+ ...nameNumeralSpans(text),
77
+ ...loneNumeralSpans(text),
78
+ ...seriesLabelSpans(text),
79
+ ...placeholderSpans(text, words.dateTimeUnits),
80
+ ];
60
81
  };