chaffjs 0.21.0 → 0.23.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 (360) hide show
  1. package/README.md +9 -2
  2. package/dist/api.d.ts +14 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js.map +1 -1
  5. package/dist/cli-args.d.ts +2 -0
  6. package/dist/cli-args.d.ts.map +1 -1
  7. package/dist/cli-args.js +2 -0
  8. package/dist/cli-args.js.map +1 -1
  9. package/dist/cli-text.d.ts.map +1 -1
  10. package/dist/cli-text.js +6 -0
  11. package/dist/cli-text.js.map +1 -1
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +17 -30
  14. package/dist/cli.js.map +1 -1
  15. package/dist/commands/feedback.d.ts +3 -0
  16. package/dist/commands/feedback.d.ts.map +1 -1
  17. package/dist/commands/feedback.js.map +1 -1
  18. package/dist/commands/fix-plan.d.ts.map +1 -1
  19. package/dist/commands/fix-plan.js +5 -0
  20. package/dist/commands/fix-plan.js.map +1 -1
  21. package/dist/commands/init-command.d.ts +4 -0
  22. package/dist/commands/init-command.d.ts.map +1 -0
  23. package/dist/commands/init-command.js +15 -0
  24. package/dist/commands/init-command.js.map +1 -0
  25. package/dist/commands/plugin-init.d.ts +20 -0
  26. package/dist/commands/plugin-init.d.ts.map +1 -0
  27. package/dist/commands/plugin-init.js +129 -0
  28. package/dist/commands/plugin-init.js.map +1 -0
  29. package/dist/commands/plugin-test.d.ts +11 -0
  30. package/dist/commands/plugin-test.d.ts.map +1 -0
  31. package/dist/commands/plugin-test.js +81 -0
  32. package/dist/commands/plugin-test.js.map +1 -0
  33. package/dist/commands/rules.d.ts +9 -0
  34. package/dist/commands/rules.d.ts.map +1 -0
  35. package/dist/commands/rules.js +26 -0
  36. package/dist/commands/rules.js.map +1 -0
  37. package/dist/config/load.d.ts +6 -1
  38. package/dist/config/load.d.ts.map +1 -1
  39. package/dist/config/load.js +1 -0
  40. package/dist/config/load.js.map +1 -1
  41. package/dist/config/style.js +1 -1
  42. package/dist/config/style.js.map +1 -1
  43. package/dist/config/warnings.d.ts +1 -1
  44. package/dist/config/warnings.d.ts.map +1 -1
  45. package/dist/config/warnings.js +3 -1
  46. package/dist/config/warnings.js.map +1 -1
  47. package/dist/custom/problems.d.ts.map +1 -1
  48. package/dist/custom/problems.js +1 -2
  49. package/dist/custom/problems.js.map +1 -1
  50. package/dist/detectors/amount-for-count.d.ts +21 -0
  51. package/dist/detectors/amount-for-count.d.ts.map +1 -0
  52. package/dist/detectors/amount-for-count.js +55 -0
  53. package/dist/detectors/amount-for-count.js.map +1 -0
  54. package/dist/detectors/currency-notation.d.ts +29 -0
  55. package/dist/detectors/currency-notation.d.ts.map +1 -0
  56. package/dist/detectors/currency-notation.js +58 -0
  57. package/dist/detectors/currency-notation.js.map +1 -0
  58. package/dist/detectors/dangling-opener.d.ts +14 -0
  59. package/dist/detectors/dangling-opener.d.ts.map +1 -0
  60. package/dist/detectors/dangling-opener.js +55 -0
  61. package/dist/detectors/dangling-opener.js.map +1 -0
  62. package/dist/detectors/ellipsis.d.ts +10 -0
  63. package/dist/detectors/ellipsis.d.ts.map +1 -0
  64. package/dist/detectors/ellipsis.js +56 -0
  65. package/dist/detectors/ellipsis.js.map +1 -0
  66. package/dist/detectors/footnote-mismatch.d.ts +11 -0
  67. package/dist/detectors/footnote-mismatch.d.ts.map +1 -0
  68. package/dist/detectors/footnote-mismatch.js +55 -0
  69. package/dist/detectors/footnote-mismatch.js.map +1 -0
  70. package/dist/detectors/form-minority.d.ts +13 -0
  71. package/dist/detectors/form-minority.d.ts.map +1 -0
  72. package/dist/detectors/form-minority.js +19 -0
  73. package/dist/detectors/form-minority.js.map +1 -0
  74. package/dist/detectors/heading-end-mark.d.ts +10 -0
  75. package/dist/detectors/heading-end-mark.d.ts.map +1 -0
  76. package/dist/detectors/heading-end-mark.js +59 -0
  77. package/dist/detectors/heading-end-mark.js.map +1 -0
  78. package/dist/detectors/list-marker.d.ts +7 -0
  79. package/dist/detectors/list-marker.d.ts.map +1 -0
  80. package/dist/detectors/list-marker.js +50 -0
  81. package/dist/detectors/list-marker.js.map +1 -0
  82. package/dist/detectors/markdown-slip.d.ts +19 -0
  83. package/dist/detectors/markdown-slip.d.ts.map +1 -0
  84. package/dist/detectors/markdown-slip.js +122 -0
  85. package/dist/detectors/markdown-slip.js.map +1 -0
  86. package/dist/detectors/numeral-opener.d.ts +10 -0
  87. package/dist/detectors/numeral-opener.d.ts.map +1 -0
  88. package/dist/detectors/numeral-opener.js +65 -0
  89. package/dist/detectors/numeral-opener.js.map +1 -0
  90. package/dist/detectors/ordinal-suffix.d.ts +12 -0
  91. package/dist/detectors/ordinal-suffix.d.ts.map +1 -0
  92. package/dist/detectors/ordinal-suffix.js +33 -0
  93. package/dist/detectors/ordinal-suffix.js.map +1 -0
  94. package/dist/detectors/range-notation.d.ts +15 -0
  95. package/dist/detectors/range-notation.d.ts.map +1 -0
  96. package/dist/detectors/range-notation.js +58 -0
  97. package/dist/detectors/range-notation.js.map +1 -0
  98. package/dist/detectors/registry/amount-for-count.d.ts +3 -0
  99. package/dist/detectors/registry/amount-for-count.d.ts.map +1 -0
  100. package/dist/detectors/registry/amount-for-count.js +3 -0
  101. package/dist/detectors/registry/amount-for-count.js.map +1 -0
  102. package/dist/detectors/registry/currency-notation.d.ts +3 -0
  103. package/dist/detectors/registry/currency-notation.d.ts.map +1 -0
  104. package/dist/detectors/registry/currency-notation.js +3 -0
  105. package/dist/detectors/registry/currency-notation.js.map +1 -0
  106. package/dist/detectors/registry/dangling-opener.d.ts +3 -0
  107. package/dist/detectors/registry/dangling-opener.d.ts.map +1 -0
  108. package/dist/detectors/registry/dangling-opener.js +3 -0
  109. package/dist/detectors/registry/dangling-opener.js.map +1 -0
  110. package/dist/detectors/registry/ellipsis-consistency.d.ts +3 -0
  111. package/dist/detectors/registry/ellipsis-consistency.d.ts.map +1 -0
  112. package/dist/detectors/registry/ellipsis-consistency.js +3 -0
  113. package/dist/detectors/registry/ellipsis-consistency.js.map +1 -0
  114. package/dist/detectors/registry/footnote-mismatch.d.ts +3 -0
  115. package/dist/detectors/registry/footnote-mismatch.d.ts.map +1 -0
  116. package/dist/detectors/registry/footnote-mismatch.js +3 -0
  117. package/dist/detectors/registry/footnote-mismatch.js.map +1 -0
  118. package/dist/detectors/registry/heading-end-punctuation.d.ts +3 -0
  119. package/dist/detectors/registry/heading-end-punctuation.d.ts.map +1 -0
  120. package/dist/detectors/registry/heading-end-punctuation.js +3 -0
  121. package/dist/detectors/registry/heading-end-punctuation.js.map +1 -0
  122. package/dist/detectors/registry/list-marker.d.ts +3 -0
  123. package/dist/detectors/registry/list-marker.d.ts.map +1 -0
  124. package/dist/detectors/registry/list-marker.js +3 -0
  125. package/dist/detectors/registry/list-marker.js.map +1 -0
  126. package/dist/detectors/registry/numeral-opener.d.ts +3 -0
  127. package/dist/detectors/registry/numeral-opener.d.ts.map +1 -0
  128. package/dist/detectors/registry/numeral-opener.js +3 -0
  129. package/dist/detectors/registry/numeral-opener.js.map +1 -0
  130. package/dist/detectors/registry/ordinal-suffix.d.ts +3 -0
  131. package/dist/detectors/registry/ordinal-suffix.d.ts.map +1 -0
  132. package/dist/detectors/registry/ordinal-suffix.js +3 -0
  133. package/dist/detectors/registry/ordinal-suffix.js.map +1 -0
  134. package/dist/detectors/registry/range-notation.d.ts +3 -0
  135. package/dist/detectors/registry/range-notation.d.ts.map +1 -0
  136. package/dist/detectors/registry/range-notation.js +3 -0
  137. package/dist/detectors/registry/range-notation.js.map +1 -0
  138. package/dist/detectors/registry/relative-pronoun-mix.d.ts +3 -0
  139. package/dist/detectors/registry/relative-pronoun-mix.d.ts.map +1 -0
  140. package/dist/detectors/registry/relative-pronoun-mix.js +3 -0
  141. package/dist/detectors/registry/relative-pronoun-mix.js.map +1 -0
  142. package/dist/detectors/registry/share-over-hundred.d.ts +3 -0
  143. package/dist/detectors/registry/share-over-hundred.d.ts.map +1 -0
  144. package/dist/detectors/registry/share-over-hundred.js +3 -0
  145. package/dist/detectors/registry/share-over-hundred.js.map +1 -0
  146. package/dist/detectors/registry/step-reference.d.ts +3 -0
  147. package/dist/detectors/registry/step-reference.d.ts.map +1 -0
  148. package/dist/detectors/registry/step-reference.js +3 -0
  149. package/dist/detectors/registry/step-reference.js.map +1 -0
  150. package/dist/detectors/registry/table-row-overflow.d.ts +3 -0
  151. package/dist/detectors/registry/table-row-overflow.d.ts.map +1 -0
  152. package/dist/detectors/registry/table-row-overflow.js +3 -0
  153. package/dist/detectors/registry/table-row-overflow.js.map +1 -0
  154. package/dist/detectors/registry/toc-heading.d.ts +3 -0
  155. package/dist/detectors/registry/toc-heading.d.ts.map +1 -0
  156. package/dist/detectors/registry/toc-heading.js +3 -0
  157. package/dist/detectors/registry/toc-heading.js.map +1 -0
  158. package/dist/detectors/registry/unclosed-code-fence.d.ts +3 -0
  159. package/dist/detectors/registry/unclosed-code-fence.d.ts.map +1 -0
  160. package/dist/detectors/registry/unclosed-code-fence.js +3 -0
  161. package/dist/detectors/registry/unclosed-code-fence.js.map +1 -0
  162. package/dist/detectors/registry/unrendered-emphasis.d.ts +3 -0
  163. package/dist/detectors/registry/unrendered-emphasis.d.ts.map +1 -0
  164. package/dist/detectors/registry/unrendered-emphasis.js +3 -0
  165. package/dist/detectors/registry/unrendered-emphasis.js.map +1 -0
  166. package/dist/detectors/relative-pronoun-mix.d.ts +19 -0
  167. package/dist/detectors/relative-pronoun-mix.d.ts.map +1 -0
  168. package/dist/detectors/relative-pronoun-mix.js +60 -0
  169. package/dist/detectors/relative-pronoun-mix.js.map +1 -0
  170. package/dist/detectors/share-over-hundred.d.ts +20 -0
  171. package/dist/detectors/share-over-hundred.d.ts.map +1 -0
  172. package/dist/detectors/share-over-hundred.js +55 -0
  173. package/dist/detectors/share-over-hundred.js.map +1 -0
  174. package/dist/detectors/step-reference.d.ts +20 -0
  175. package/dist/detectors/step-reference.d.ts.map +1 -0
  176. package/dist/detectors/step-reference.js +57 -0
  177. package/dist/detectors/step-reference.js.map +1 -0
  178. package/dist/detectors/toc-heading.d.ts +10 -0
  179. package/dist/detectors/toc-heading.d.ts.map +1 -0
  180. package/dist/detectors/toc-heading.js +99 -0
  181. package/dist/detectors/toc-heading.js.map +1 -0
  182. package/dist/extension/load.d.ts +3 -0
  183. package/dist/extension/load.d.ts.map +1 -1
  184. package/dist/extension/load.js +1 -0
  185. package/dist/extension/load.js.map +1 -1
  186. package/dist/extension/pack-read.d.ts.map +1 -1
  187. package/dist/extension/pack-read.js +1 -0
  188. package/dist/extension/pack-read.js.map +1 -1
  189. package/dist/extension/plugin-load.js.map +1 -1
  190. package/dist/extension/plugin-parse.d.ts +7 -0
  191. package/dist/extension/plugin-parse.d.ts.map +1 -1
  192. package/dist/extension/plugin-parse.js +18 -2
  193. package/dist/extension/plugin-parse.js.map +1 -1
  194. package/dist/extension/problem-text.d.ts.map +1 -1
  195. package/dist/extension/problem-text.js +5 -0
  196. package/dist/extension/problem-text.js.map +1 -1
  197. package/dist/fix-plan/plan.d.ts +4 -0
  198. package/dist/fix-plan/plan.d.ts.map +1 -1
  199. package/dist/fix-plan/plan.js +1 -0
  200. package/dist/fix-plan/plan.js.map +1 -1
  201. package/dist/fix-plan/render.d.ts.map +1 -1
  202. package/dist/fix-plan/render.js +9 -0
  203. package/dist/fix-plan/render.js.map +1 -1
  204. package/dist/fix-plan/text.d.ts +4 -0
  205. package/dist/fix-plan/text.d.ts.map +1 -1
  206. package/dist/fix-plan/text.js +8 -2
  207. package/dist/fix-plan/text.js.map +1 -1
  208. package/dist/genre-guide/key-rules.d.ts +10 -0
  209. package/dist/genre-guide/key-rules.d.ts.map +1 -0
  210. package/dist/genre-guide/key-rules.js +23 -0
  211. package/dist/genre-guide/key-rules.js.map +1 -0
  212. package/dist/genre-guide/layer.d.ts +44 -0
  213. package/dist/genre-guide/layer.d.ts.map +1 -0
  214. package/dist/genre-guide/layer.js +43 -0
  215. package/dist/genre-guide/layer.js.map +1 -0
  216. package/dist/genre-guide/lines.d.ts +15 -0
  217. package/dist/genre-guide/lines.d.ts.map +1 -0
  218. package/dist/genre-guide/lines.js +39 -0
  219. package/dist/genre-guide/lines.js.map +1 -0
  220. package/dist/genre-guide/of-config.d.ts +13 -0
  221. package/dist/genre-guide/of-config.d.ts.map +1 -0
  222. package/dist/genre-guide/of-config.js +18 -0
  223. package/dist/genre-guide/of-config.js.map +1 -0
  224. package/dist/genre-guide/problem-text.d.ts +5 -0
  225. package/dist/genre-guide/problem-text.d.ts.map +1 -0
  226. package/dist/genre-guide/problem-text.js +29 -0
  227. package/dist/genre-guide/problem-text.js.map +1 -0
  228. package/dist/genre-guide/render.d.ts +5 -0
  229. package/dist/genre-guide/render.d.ts.map +1 -0
  230. package/dist/genre-guide/render.js +38 -0
  231. package/dist/genre-guide/render.js.map +1 -0
  232. package/dist/genre-guide/report.d.ts +32 -0
  233. package/dist/genre-guide/report.d.ts.map +1 -0
  234. package/dist/genre-guide/report.js +42 -0
  235. package/dist/genre-guide/report.js.map +1 -0
  236. package/dist/genre-guide/resolve.d.ts +16 -0
  237. package/dist/genre-guide/resolve.d.ts.map +1 -0
  238. package/dist/genre-guide/resolve.js +31 -0
  239. package/dist/genre-guide/resolve.js.map +1 -0
  240. package/dist/genre-parse.d.ts +5 -1
  241. package/dist/genre-parse.d.ts.map +1 -1
  242. package/dist/genre-parse.js +8 -1
  243. package/dist/genre-parse.js.map +1 -1
  244. package/dist/init.d.ts.map +1 -1
  245. package/dist/init.js +14 -0
  246. package/dist/init.js.map +1 -1
  247. package/dist/list-marks.d.ts +21 -0
  248. package/dist/list-marks.d.ts.map +1 -0
  249. package/dist/list-marks.js +82 -0
  250. package/dist/list-marks.js.map +1 -0
  251. package/dist/render/rules-json.d.ts +2 -1
  252. package/dist/render/rules-json.d.ts.map +1 -1
  253. package/dist/render/rules-json.js +3 -1
  254. package/dist/render/rules-json.js.map +1 -1
  255. package/dist/render/sarif.d.ts +3 -1
  256. package/dist/render/sarif.d.ts.map +1 -1
  257. package/dist/render/sarif.js +3 -1
  258. package/dist/render/sarif.js.map +1 -1
  259. package/dist/rule-fields.d.ts +5 -0
  260. package/dist/rule-fields.d.ts.map +1 -1
  261. package/dist/rule-fields.js +21 -0
  262. package/dist/rule-fields.js.map +1 -1
  263. package/dist/style-parse.d.ts +3 -0
  264. package/dist/style-parse.d.ts.map +1 -1
  265. package/dist/style-parse.js +18 -2
  266. package/dist/style-parse.js.map +1 -1
  267. package/genres.yaml +329 -0
  268. package/package.json +3 -3
  269. package/rules/cliche.yaml +67 -0
  270. package/rules/currency-notation-consistency.yaml +77 -0
  271. package/rules/dangling-participle.yaml +58 -0
  272. package/rules/ellipsis-consistency.yaml +73 -0
  273. package/rules/figurative-density.yaml +84 -0
  274. package/rules/footnote-mismatch.yaml +91 -0
  275. package/rules/heading-end-punctuation-mix.yaml +158 -0
  276. package/rules/less-for-fewer.yaml +59 -0
  277. package/rules/list-marker-mix.yaml +111 -0
  278. package/rules/nonstandard-word.yaml +80 -0
  279. package/rules/numeral-sentence-start.yaml +56 -0
  280. package/rules/ordinal-suffix-mismatch.yaml +65 -0
  281. package/rules/range-notation-consistency.yaml +73 -0
  282. package/rules/share-over-hundred.yaml +65 -0
  283. package/rules/step-reference-missing.yaml +85 -0
  284. package/rules/table-row-overflow.yaml +90 -0
  285. package/rules/toc-heading-mismatch.yaml +136 -0
  286. package/rules/unclosed-code-fence.yaml +107 -0
  287. package/rules/uncomparable-graded.yaml +65 -0
  288. package/rules/unrendered-emphasis.yaml +73 -0
  289. package/rules/which-that-consistency.yaml +64 -0
  290. package/skills/chaff/SKILL.md +13 -7
  291. package/src/api.ts +13 -0
  292. package/src/cli-args.ts +3 -0
  293. package/src/cli-text.ts +6 -0
  294. package/src/cli.ts +21 -29
  295. package/src/commands/feedback.ts +3 -0
  296. package/src/commands/fix-plan.ts +8 -0
  297. package/src/commands/init-command.ts +13 -0
  298. package/src/commands/plugin-init.ts +150 -0
  299. package/src/commands/plugin-test.ts +111 -0
  300. package/src/commands/rules.ts +34 -0
  301. package/src/config/load.ts +7 -1
  302. package/src/config/style.ts +1 -1
  303. package/src/config/warnings.ts +3 -1
  304. package/src/custom/problems.ts +1 -3
  305. package/src/detectors/amount-for-count.ts +90 -0
  306. package/src/detectors/currency-notation.ts +90 -0
  307. package/src/detectors/dangling-opener.ts +74 -0
  308. package/src/detectors/ellipsis.ts +74 -0
  309. package/src/detectors/footnote-mismatch.ts +72 -0
  310. package/src/detectors/form-minority.ts +21 -0
  311. package/src/detectors/heading-end-mark.ts +82 -0
  312. package/src/detectors/list-marker.ts +60 -0
  313. package/src/detectors/markdown-slip.ts +140 -0
  314. package/src/detectors/numeral-opener.ts +72 -0
  315. package/src/detectors/ordinal-suffix.ts +40 -0
  316. package/src/detectors/range-notation.ts +76 -0
  317. package/src/detectors/registry/amount-for-count.ts +4 -0
  318. package/src/detectors/registry/currency-notation.ts +4 -0
  319. package/src/detectors/registry/dangling-opener.ts +4 -0
  320. package/src/detectors/registry/ellipsis-consistency.ts +4 -0
  321. package/src/detectors/registry/footnote-mismatch.ts +4 -0
  322. package/src/detectors/registry/heading-end-punctuation.ts +4 -0
  323. package/src/detectors/registry/list-marker.ts +4 -0
  324. package/src/detectors/registry/numeral-opener.ts +4 -0
  325. package/src/detectors/registry/ordinal-suffix.ts +4 -0
  326. package/src/detectors/registry/range-notation.ts +4 -0
  327. package/src/detectors/registry/relative-pronoun-mix.ts +4 -0
  328. package/src/detectors/registry/share-over-hundred.ts +4 -0
  329. package/src/detectors/registry/step-reference.ts +4 -0
  330. package/src/detectors/registry/table-row-overflow.ts +4 -0
  331. package/src/detectors/registry/toc-heading.ts +4 -0
  332. package/src/detectors/registry/unclosed-code-fence.ts +4 -0
  333. package/src/detectors/registry/unrendered-emphasis.ts +4 -0
  334. package/src/detectors/relative-pronoun-mix.ts +79 -0
  335. package/src/detectors/share-over-hundred.ts +75 -0
  336. package/src/detectors/step-reference.ts +83 -0
  337. package/src/detectors/toc-heading.ts +125 -0
  338. package/src/extension/load.ts +4 -0
  339. package/src/extension/pack-read.ts +1 -0
  340. package/src/extension/plugin-load.ts +1 -1
  341. package/src/extension/plugin-parse.ts +27 -4
  342. package/src/extension/problem-text.ts +7 -1
  343. package/src/fix-plan/plan.ts +5 -0
  344. package/src/fix-plan/render.ts +9 -0
  345. package/src/fix-plan/text.ts +12 -2
  346. package/src/genre-guide/key-rules.ts +37 -0
  347. package/src/genre-guide/layer.ts +77 -0
  348. package/src/genre-guide/lines.ts +41 -0
  349. package/src/genre-guide/of-config.ts +26 -0
  350. package/src/genre-guide/problem-text.ts +34 -0
  351. package/src/genre-guide/render.ts +50 -0
  352. package/src/genre-guide/report.ts +63 -0
  353. package/src/genre-guide/resolve.ts +36 -0
  354. package/src/genre-parse.ts +13 -3
  355. package/src/init.ts +14 -0
  356. package/src/list-marks.ts +106 -0
  357. package/src/render/rules-json.ts +4 -0
  358. package/src/render/sarif.ts +4 -1
  359. package/src/rule-fields.ts +28 -0
  360. package/src/style-parse.ts +19 -1
@@ -0,0 +1,72 @@
1
+ import type { Detector, Finding, Sentence, Span } from "../plugin.ts";
2
+
3
+ /**
4
+ * 数字で始まる文(20 people came.)。読み手は文の頭を大文字で探すので、数字の頭は文の切れ目を見えにくくする。
5
+ * 数字の後ろが小文字の語のときだけ読む: 「1 Introduction」のような番号の付いた題や「9:00 Opening」のような時刻の行は文ではない。
6
+ * 年(2026 was a good year.)は綴れないので読まない。
7
+ */
8
+
9
+ const OPENING_NUMERAL = /^(?<numeral>\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?%?(?=\s+\p{Ll})/u;
10
+ const SENTENCE_END = /[.!?]["'”’)]*$/u;
11
+ const YEAR = /^[12]\d{3}$/u;
12
+ const HEADING_LINE = /(?:^|\n)#{1,6}\s[^\n]*$/u;
13
+ const CLOSERS = new Set(['"', "'", "”", "’", ")"]);
14
+ const STOP = /[.!?]$/u;
15
+ const WORD_CHAR = /[\p{L}\p{N}.]/u;
16
+ const DIGITS = /^\d+$/u;
17
+ /** 前の文の終わりを探す字数。見出しの行と、終わりの語が入れば足りる。 */
18
+ const LOOKBACK = 300;
19
+ const PLAIN_WORD = /^\p{L}{2,}$/u;
20
+
21
+ /** 文の頭の数字。年、数字の後ろが小文字の語でないもの、言い終えていない行(見出しのような)は無い。 */
22
+ export const openingNumeral = (text: string): string | undefined => {
23
+ const trimmed = text.trim();
24
+ const match = OPENING_NUMERAL.exec(trimmed);
25
+ const numeral = match?.groups?.["numeral"];
26
+ if (match === null || numeral === undefined || YEAR.test(numeral) || !SENTENCE_END.test(trimmed)) return undefined;
27
+ return match[0];
28
+ };
29
+
30
+ /** text の終わりに続く、belongs を満たす字の並びの始まり(UTF-16 の位置)。 */
31
+ const tailStart = (text: string, belongs: (char: string) => boolean): number =>
32
+ text.split("").reduceRight((at, char, index) => (at === index + 1 && belongs(char) ? index : at), text.length);
33
+
34
+ /** 終わりの印(. ! ?)の前の語。点を含めて読む(H.R. は「H.R」)。印で終わっていなければ undefined。 */
35
+ const lastWordBeforeStop = (text: string): string | undefined => {
36
+ const closed = text.slice(
37
+ 0,
38
+ tailStart(text, (char) => CLOSERS.has(char)),
39
+ );
40
+ if (!STOP.test(closed)) return undefined;
41
+ const body = closed.slice(0, -1);
42
+ const start = tailStart(body, (char) => WORD_CHAR.test(char));
43
+ return start === body.length ? undefined : body.slice(start);
44
+ };
45
+
46
+ /**
47
+ * offset の前で、文がほんとうに終わっているか。文書か見出しの直後か、前の文が二字以上の語か数で言い終えているときだけ。
48
+ * 番号に付ける略した語(vol. 28、H.R. 5376)や、句点の無い行(Lane\n\n2 contains、HTML から移したリンクの切れ目)の後ろの数は文の頭ではない。
49
+ */
50
+ export const followsSentenceEnd = (source: string, offset: number, labels: ReadonlySet<string>): boolean => {
51
+ const from = Math.max(0, offset - LOOKBACK);
52
+ const ended = source.slice(from, offset).trimEnd();
53
+ if ((ended === "" && from === 0) || HEADING_LINE.test(ended)) return true;
54
+ const word = lastWordBeforeStop(ended);
55
+ if (word === undefined) return false;
56
+ return DIGITS.test(word) || (PLAIN_WORD.test(word) && !labels.has(word.toLowerCase()));
57
+ };
58
+
59
+ const inside = (spans: readonly Span[], offset: number): boolean => spans.some((span) => span.start <= offset && offset < span.end);
60
+
61
+ const leadingSpace = (sentence: Sentence): number => sentence.text.length - sentence.text.trimStart().length;
62
+
63
+ export const numeralOpener: Detector = (doc): Finding[] => {
64
+ const labels = new Set((doc.lexicons["numeral-reference-label"] ?? []).map((entry) => entry.pattern.toLowerCase()));
65
+ return doc.sentences.flatMap((sentence) => {
66
+ const offset = sentence.span.start + leadingSpace(sentence);
67
+ if (sentence.embeddedLanguage !== undefined || inside(doc.listSpans, offset)) return [];
68
+ const numeral = openingNumeral(sentence.text);
69
+ if (numeral === undefined || !followsSentenceEnd(doc.source, offset, labels)) return [];
70
+ return [{ rule: "", severity: "warning", line: 0, column: 0, quote: sentence.text.trim(), values: { numeral, offset } }];
71
+ });
72
+ };
@@ -0,0 +1,40 @@
1
+ import type { Detector, Finding, Lexicon } from "../plugin.ts";
2
+ import { escapeRegExp } from "../orthography.ts";
3
+ import { quoteAround } from "./quote-around.ts";
4
+
5
+ // 数と序数の字が合わない所(3th、22th、11st)。どの数の終わりにどの字が付くかは語彙表 ordinal-suffix が言う。
6
+
7
+ /** 合わない序数 1 つ。expected はその数に付く字(一つとは限らない)。 */
8
+ export type OrdinalSlip = { readonly offset: number; readonly written: string; readonly number: string; readonly expected: readonly string[] };
9
+
10
+ /** 数の終わりのうち、語彙表の組にある最も長いものに付く字。 */
11
+ const suffixesFor = (digits: string, suffixes: Lexicon): string[] => {
12
+ const ending = suffixes
13
+ .map((entry) => entry.group ?? "")
14
+ .filter((group) => digits.endsWith(group))
15
+ .reduce((longest, group) => (group.length > longest.length ? group : longest), "");
16
+ return suffixes.filter((entry) => (entry.group ?? "") === ending).map((entry) => entry.pattern);
17
+ };
18
+
19
+ /** 文字列の中の、数と字の合わない序数。語や番号の一部(v1th、0x2nd、1-2th)は読まない。 */
20
+ export const ordinalSlips = (text: string, suffixes: Lexicon): OrdinalSlip[] => {
21
+ const written = [...new Set(suffixes.map((entry) => entry.pattern))];
22
+ if (written.length === 0) return [];
23
+ const pattern = new RegExp(`(?<![\\p{L}\\p{N}_.,\\-])(?<n>\\d+(?:,\\d{3})*)(?<suffix>${written.map(escapeRegExp).join("|")})(?![\\p{L}\\p{N}_])`, "gu");
24
+ return [...text.matchAll(pattern)].flatMap((match) => {
25
+ const expected = suffixesFor((match.groups?.["n"] ?? "").replaceAll(",", ""), suffixes);
26
+ return expected.includes(match.groups?.["suffix"] ?? "") ? [] : [{ offset: match.index, written: match[0], number: match.groups?.["n"] ?? "", expected }];
27
+ });
28
+ };
29
+
30
+ export const ordinalSuffix: Detector = (doc): Finding[] => {
31
+ const text = doc.prose ?? doc.source;
32
+ return ordinalSlips(text, doc.lexicons["ordinal-suffix"] ?? []).map((slip) => ({
33
+ rule: "",
34
+ severity: "warning",
35
+ line: 0,
36
+ column: 0,
37
+ quote: quoteAround(text, slip.offset, slip.offset + slip.written.length),
38
+ values: { written: slip.written, expected: `${slip.number}${slip.expected[0] ?? ""}`, offset: slip.offset },
39
+ }));
40
+ };
@@ -0,0 +1,76 @@
1
+ import type { Detector, Finding } from "../plugin.ts";
2
+ import { escapeRegExp } from "../orthography.ts";
3
+ import { formMinority } from "./form-minority.ts";
4
+ import { quoteAround } from "./quote-around.ts";
5
+
6
+ // 一つの文書で、数の範囲を二通りの記号で書いた所(10〜20 と 10-20、10–20 と 10-20)。どちらが正しいかは決めず、少ないほうを指す。
7
+ // 範囲の記号は語彙表 range-notation が言う。
8
+
9
+ /** 数の範囲 1 つ。form は二つの数のあいだの記号。 */
10
+ export type WrittenRange = { readonly offset: number; readonly written: string; readonly form: string };
11
+
12
+ const FULLWIDTH_OFFSET = 0xfee0;
13
+ const NUMBER = "[0-90-9]+(?:[..][0-90-9]+)?";
14
+
15
+ /** 範囲のすぐ後ろ(空白一つまで)の単位や語。番号にも使う記号(ハイフン)は、これが続くときだけ範囲と読む。 */
16
+ const UNIT_AFTER = /^\s?[\p{L}%%]/u;
17
+
18
+ /** 行の頭(見出しや箇条書きの印の後ろ)の「1-2」は節の番号、閉じ括弧のすぐ後ろの「(703) 292-7827」は電話番号。 */
19
+ const LABEL_BEFORE = /(?:^\s*[#>*+|-][\s#>*+|-]*|\)\s?)$/u;
20
+
21
+ /** 番号の前の札。語彙表の札の語(図、Section)か、頭が大文字の英語の語(NSF)。 */
22
+ const isNumberLabel = (lineBefore: string, labels: readonly string[]): boolean => {
23
+ const trimmed = lineBefore.trimEnd();
24
+ if (labels.some((label) => trimmed.endsWith(label))) return true;
25
+ return /^\p{Lu}/u.test(trimmed.split(/\s/u).at(-1) ?? "");
26
+ };
27
+
28
+ const halfWidth = (text: string): string => text.replace(/[0-9.]/gu, (char) => String.fromCharCode(char.charCodeAt(0) - FULLWIDTH_OFFSET));
29
+
30
+ /**
31
+ * 文字列の中の数の範囲。前後にさらに数が続く並び(日付 2026-10-02、電話番号、時刻 10:00、版 1.2-3)は範囲ではない。
32
+ * 終わりが始まりより小さいもの(郵便番号 100-0001、試合の 3-2)も範囲ではない。ambiguous の記号は、後ろに単位や語が続き、
33
+ * 前が番号の札(図 2-1、Section 1-2、NSF 13-542)でないときだけ。
34
+ */
35
+ export const rangesIn = (text: string, marks: readonly string[], ambiguous: readonly string[] = [], labels: readonly string[] = []): WrittenRange[] => {
36
+ if (marks.length === 0) return [];
37
+ const joiner = marks
38
+ .toSorted((left, right) => right.length - left.length)
39
+ .map(escapeRegExp)
40
+ .join("|");
41
+ const pattern = new RegExp(
42
+ `(?<![\\w0-9..,::/\\--–〜~~])(?<from>${NUMBER})\\s?(?<mark>${joiner})\\s?(?<to>${NUMBER})(?![\\w0-9]|[..,::/\\--–〜~~][0-90-9])`,
43
+ "gu",
44
+ );
45
+ return [...text.matchAll(pattern)].flatMap((match) => {
46
+ const from = Number(halfWidth(match.groups?.["from"] ?? ""));
47
+ const to = Number(halfWidth(match.groups?.["to"] ?? ""));
48
+ const mark = match.groups?.["mark"] ?? "";
49
+ const unitFollows = UNIT_AFTER.test(text.slice(match.index + match[0].length));
50
+ const lineBefore = text.slice(text.lastIndexOf("\n", match.index - 1) + 1, match.index);
51
+ return to > from && !LABEL_BEFORE.test(lineBefore) && (!ambiguous.includes(mark) || (unitFollows && !isNumberLabel(lineBefore, labels)))
52
+ ? [{ offset: match.index, written: match[0], form: mark }]
53
+ : [];
54
+ });
55
+ };
56
+
57
+ export const rangeNotation: Detector = (doc, options): Finding[] => {
58
+ const text = doc.prose ?? doc.source;
59
+ const entries = doc.lexicons["range-notation"] ?? [];
60
+ const ranges = rangesIn(
61
+ text,
62
+ entries.map((entry) => entry.pattern),
63
+ entries.filter((entry) => entry.group === "ambiguous").map((entry) => entry.pattern),
64
+ ["figure-label", "numbered-division", "numbered-label"].flatMap((id) => (doc.lexicons[id] ?? []).map((entry) => entry.pattern)),
65
+ );
66
+ const minority = formMinority(ranges, options.limit);
67
+ if (minority === undefined) return [];
68
+ return minority.odd.map((range) => ({
69
+ rule: "",
70
+ severity: "info",
71
+ line: 0,
72
+ column: 0,
73
+ quote: quoteAround(text, range.offset, range.offset + range.written.length),
74
+ values: { written: range.written, mark: range.form, usual: minority.usual.written, count: minority.odd.length, of: ranges.length, offset: range.offset },
75
+ }));
76
+ };
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { amountForCount } from "../amount-for-count.ts";
3
+
4
+ export const detector: Detector = amountForCount;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { currencyNotation } from "../currency-notation.ts";
3
+
4
+ export const detector: Detector = currencyNotation;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { danglingOpener } from "../dangling-opener.ts";
3
+
4
+ export const detector: Detector = danglingOpener;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { ellipsisConsistency } from "../ellipsis.ts";
3
+
4
+ export const detector: Detector = ellipsisConsistency;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { footnoteMismatch } from "../footnote-mismatch.ts";
3
+
4
+ export const detector: Detector = footnoteMismatch;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { headingEndMark } from "../heading-end-mark.ts";
3
+
4
+ export const detector: Detector = headingEndMark;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { listMarkerMix } from "../list-marker.ts";
3
+
4
+ export const detector: Detector = listMarkerMix;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { numeralOpener } from "../numeral-opener.ts";
3
+
4
+ export const detector: Detector = numeralOpener;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { ordinalSuffix } from "../ordinal-suffix.ts";
3
+
4
+ export const detector: Detector = ordinalSuffix;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { rangeNotation } from "../range-notation.ts";
3
+
4
+ export const detector: Detector = rangeNotation;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { relativePronounMix } from "../relative-pronoun-mix.ts";
3
+
4
+ export const detector: Detector = relativePronounMix;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { shareOverHundred } from "../share-over-hundred.ts";
3
+
4
+ export const detector: Detector = shareOverHundred;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { stepReference } from "../step-reference.ts";
3
+
4
+ export const detector: Detector = stepReference;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { tableRowOverflow } from "../markdown-slip.ts";
3
+
4
+ export const detector: Detector = tableRowOverflow;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { tocHeading } from "../toc-heading.ts";
3
+
4
+ export const detector: Detector = tocHeading;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { unclosedCodeFence } from "../markdown-slip.ts";
3
+
4
+ export const detector: Detector = unclosedCodeFence;
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { unrenderedEmphasis } from "../markdown-slip.ts";
3
+
4
+ export const detector: Detector = unrenderedEmphasis;
@@ -0,0 +1,79 @@
1
+ import type { Detector, Finding, Lexicon, Token } from "../plugin.ts";
2
+ import { minorityOf } from "./technical-docs.ts";
3
+ import { isWithinAny, quotedIn, QUOTATION_MARKS } from "../quoted-span.ts";
4
+
5
+ /**
6
+ * 限定の関係節を which で書く所と that で書く所が、一つの文書に混ざっている所(the file which contains / the file that contains)。
7
+ * どちらが正しいかは決めず、少ないほうを指す。読むのは「名詞 + which|that + 動詞」の、読点を挟まない形だけ。
8
+ * that の後ろに主語が来る形(the fact that it works)は、関係節か同格の節か語の並びで決まらないので数えない。
9
+ */
10
+
11
+ export type RelativeUse = { readonly word: Token; readonly kind: string };
12
+
13
+ /** 多いほうの書き方が、文書の書き方と言えるだけの数。 */
14
+ const MIN_USUAL = 3;
15
+ const VERBAL = new Set(["VERB", "AUX"]);
16
+
17
+ /** 解析器は三人称の動詞を複数の名詞と読むことがある(the script that checks links の checks)。 */
18
+ const isThirdPersonVerb = (token: Token): boolean => token.pos === "NOUN" && token.features?.["AlsoVerb"] === "Yes" && token.features["Number"] === "Plur";
19
+
20
+ const isTaggedFiniteVerb = (token: Token | undefined): boolean =>
21
+ token !== undefined && VERBAL.has(token.pos) && token.features?.["VerbForm"] !== "Part" && token.features?.["VerbForm"] !== "Ger";
22
+
23
+ /**
24
+ * 動詞と読める語。名詞と読まれた三人称の動詞は、後ろに動詞が無いときだけ(the idea that results matter の results は主語)。
25
+ */
26
+ const isFiniteVerb = (token: Token | undefined, next: Token | undefined): boolean =>
27
+ isTaggedFiniteVerb(token) || (token !== undefined && isThirdPersonVerb(token) && !isTaggedFiniteVerb(next));
28
+
29
+ const isBaseForm = (verb: Token): boolean => verb.pos === "VERB" && verb.lemma?.toLowerCase() === verb.surface.toLowerCase();
30
+
31
+ /**
32
+ * 名詞の後ろの関係節の動詞。原形の動詞は複数の名詞にだけ続く(resources which set)。単数の名詞の後ろの原形は、
33
+ * 関係節ではなく「どの」の which(tells the command which file to use)。
34
+ */
35
+ const opensRelative = (noun: Token | undefined, verb: Token | undefined, next: Token | undefined): boolean =>
36
+ noun?.pos === "NOUN" && verb !== undefined && isFiniteVerb(verb, next) && (!isBaseForm(verb) || noun.features?.["Number"] === "Plur");
37
+
38
+ /** which / that の後ろの、副詞を飛ばした最初の語(the file which usually stores の stores)。 */
39
+ const verbAfter = (tokens: readonly Token[], at: number): number => {
40
+ const found = tokens.findIndex((token, index) => index > at && token.pos !== "ADV");
41
+ return found === -1 ? tokens.length : found;
42
+ };
43
+
44
+ /** 語彙表の組(which / that)で、tokens の中の限定の関係節の頭。 */
45
+ export const relativeUses = (tokens: readonly Token[], kinds: Lexicon): RelativeUse[] =>
46
+ tokens.flatMap((word, at) => {
47
+ const entry = kinds.find((candidate) => candidate.pattern.toLowerCase() === word.surface.toLowerCase());
48
+ const verb = verbAfter(tokens, at);
49
+ if (entry?.group === undefined || !opensRelative(tokens[at - 1], tokens[verb], tokens[verb + 1])) return [];
50
+ return [{ word, kind: entry.group }];
51
+ });
52
+
53
+ /** 二通りの書き方のうち少ないほう。多いほうが MIN_USUAL に満たなければ、文書の書き方と言えないので空。 */
54
+ export const oddRelatives = (uses: readonly RelativeUse[], limit: number): { readonly odd: readonly RelativeUse[]; readonly usual: string } => {
55
+ const kinds = [...new Set(uses.map((use) => use.kind))];
56
+ const [first, second] = kinds.map((kind) => uses.filter((use) => use.kind === kind));
57
+ if (kinds.length !== 2 || first === undefined || second === undefined) return { odd: [], usual: "" };
58
+ const odd = minorityOf(first, second, limit);
59
+ const usual = odd === first ? second : first;
60
+ return usual.length < MIN_USUAL ? { odd: [], usual: "" } : { odd, usual: usual[0]?.word.surface.toLowerCase() ?? "" };
61
+ };
62
+
63
+ export const relativePronounMix: Detector = (doc, options): Finding[] => {
64
+ const kinds = doc.lexicons["restrictive-relative"] ?? [];
65
+ const uses = doc.sentences.flatMap((sentence) => {
66
+ if (sentence.embeddedLanguage !== undefined) return [];
67
+ const quoted = quotedIn(sentence, QUOTATION_MARKS);
68
+ return relativeUses(sentence.tokens ?? [], kinds).filter((use) => !isWithinAny(quoted, use.word.span));
69
+ });
70
+ const { odd, usual } = oddRelatives(uses, options.limit);
71
+ return odd.map((use) => ({
72
+ rule: "",
73
+ severity: "info",
74
+ line: 0,
75
+ column: 0,
76
+ quote: doc.sentences.find((sentence) => sentence.span.start <= use.word.span.start && use.word.span.start < sentence.span.end)?.text.trim() ?? "",
77
+ values: { word: use.word.surface, usual, count: odd.length, limit: options.limit, offset: use.word.span.start },
78
+ }));
79
+ };
@@ -0,0 +1,75 @@
1
+ import type { Detector, Finding, ProseDocument } from "../plugin.ts";
2
+ import { escapeRegExp } from "../orthography.ts";
3
+ import { quoteAround } from "./quote-around.ts";
4
+
5
+ // 集まりの一部の割合なのに 100% を超える百分率(回答者の120%、150% of respondents)。一部は全体を超えない。
6
+ // 集まりの語は語彙表 share-whole、百分率とのあいだの語とその側は share-whole-link、百分率の単位は percent-unit が言う。
7
+
8
+ export type ShareWords = {
9
+ readonly wholes: readonly string[];
10
+ readonly links: readonly { readonly pattern: string; readonly position: "before" | "after" }[];
11
+ readonly units: readonly string[];
12
+ };
13
+
14
+ /** 100% を超える一部の割合 1 つ。written は百分率、whole は何の一部か。 */
15
+ export type ShareOverHundred = { readonly offset: number; readonly end: number; readonly written: string; readonly whole: string };
16
+
17
+ const WHOLE_PERCENT = 100;
18
+ /** 集まりの語が、より長い語の頭でないこと(users_2025、回答者数)。漢字やカタカナが続けば別の語(回答者数は人数で、割合の元ではない)。 */
19
+ const NOT_INSIDE_WORD = "(?![A-Za-z0-9_\\p{Script=Han}\\p{Script=Katakana}])";
20
+ const FULLWIDTH_OFFSET = 0xfee0;
21
+
22
+ const halfWidth = (text: string): string => text.replace(/[0-9.]/gu, (char) => String.fromCharCode(char.charCodeAt(0) - FULLWIDTH_OFFSET));
23
+
24
+ const alternation = (words: readonly string[]): string =>
25
+ words
26
+ .toSorted((left, right) => right.length - left.length)
27
+ .map(escapeRegExp)
28
+ .join("|");
29
+
30
+ /** 集まりの語の前後の英字だけを語の切れ目に見る。日本語は語を続けて書く(アンケートでは回答者の、120%の回答者が)。 */
31
+ const wholeNear = (text: string, start: number, end: number, words: ShareWords): string | undefined => {
32
+ const wholes = alternation(words.wholes);
33
+ return words.links
34
+ .map((link) => {
35
+ const near =
36
+ link.position === "after"
37
+ ? new RegExp(`^\\s*${escapeRegExp(link.pattern)}\\s*(?<whole>${wholes})${NOT_INSIDE_WORD}`, "u").exec(text.slice(end))
38
+ : new RegExp(`(?<![A-Za-z])(?<whole>${wholes})${escapeRegExp(link.pattern)}\\s*$`, "u").exec(text.slice(0, start));
39
+ return near?.groups?.["whole"];
40
+ })
41
+ .find((whole) => whole !== undefined);
42
+ };
43
+
44
+ /** 文字列の中の、集まりの一部の割合なのに 100% を超える百分率。 */
45
+ export const sharesOverHundred = (text: string, words: ShareWords): ShareOverHundred[] => {
46
+ if (words.wholes.length === 0 || words.units.length === 0) return [];
47
+ const percent = new RegExp(`(?<![\\d.,,0-9])(?<value>[0-90-9]+(?:[,,][0-90-9]{3})*(?:[..][0-90-9]+)?)\\s?(?:${alternation(words.units)})`, "gu");
48
+ return [...text.matchAll(percent)].flatMap((match) => {
49
+ const value = Number(halfWidth(match.groups?.["value"] ?? "").replace(/[,,]/gu, ""));
50
+ if (value <= WHOLE_PERCENT) return [];
51
+ const end = match.index + match[0].length;
52
+ const whole = wholeNear(text, match.index, end, words);
53
+ return whole === undefined ? [] : [{ offset: match.index, end, written: match[0], whole }];
54
+ });
55
+ };
56
+
57
+ const positioned = (doc: ProseDocument): ShareWords["links"] =>
58
+ (doc.lexicons["share-whole-link"] ?? []).flatMap((entry) => (entry.position === undefined ? [] : [{ pattern: entry.pattern, position: entry.position }]));
59
+
60
+ export const shareOverHundred: Detector = (doc): Finding[] => {
61
+ const text = doc.prose ?? doc.source;
62
+ const words: ShareWords = {
63
+ wholes: (doc.lexicons["share-whole"] ?? []).map((entry) => entry.pattern),
64
+ links: positioned(doc),
65
+ units: (doc.lexicons["percent-unit"] ?? []).map((entry) => entry.pattern),
66
+ };
67
+ return sharesOverHundred(text, words).map((found) => ({
68
+ rule: "",
69
+ severity: "warning",
70
+ line: 0,
71
+ column: 0,
72
+ quote: quoteAround(text, found.offset, found.end),
73
+ values: { written: found.written, whole: found.whole, offset: found.offset },
74
+ }));
75
+ };
@@ -0,0 +1,83 @@
1
+ import type { Detector, Finding, ProseDocument } from "../plugin.ts";
2
+ import { escapeRegExp } from "../orthography.ts";
3
+ import { quoteAround } from "./quote-around.ts";
4
+
5
+ // 本文が指す手順の番号(「手順5を参照」「see step 5」)が、文書のどの手順にも無い所。
6
+ // 手順は、手順の語(語彙表 step-word)と番号で始まる見出しと、番号付きの箇条書きの項目。手順が一つも無い文書では言わない。
7
+ // 番号のすぐ後ろの語(語彙表 figure-elsewhere の of・in)や前の文書の名前が、ほかの文書の手順だと言えば数えない。
8
+
9
+ /** 無い手順を指す参照 1 つ。label は書いたままの参照、last は文書の手順の最後の番号。 */
10
+ export type MissingStep = { readonly offset: number; readonly label: string; readonly last: number };
11
+
12
+ /**
13
+ * 手順を読むのに要るもの。source は見出しの印(#)を残した文書、prose はコードを覆った本文(位置は source と同じ)。
14
+ * elsewhere は参照がほかの文書の手順を指すか(参照の始まりと終わりの位置で)。
15
+ */
16
+ export type StepText = { readonly source: string; readonly prose: string; readonly elsewhere: (start: number, end: number) => boolean };
17
+
18
+ const FULLWIDTH_OFFSET = 0xfee0;
19
+ const LIST_ITEM = /^[ \t]*(?<number>\d{1,3})[.)][ \t]/u;
20
+ const HEADING = /^#{1,6}[ \t]+/u;
21
+
22
+ const halfWidth = (text: string): string => text.replace(/[0-9]/gu, (char) => String.fromCharCode(char.charCodeAt(0) - FULLWIDTH_OFFSET));
23
+
24
+ type Mention = { readonly offset: number; readonly label: string; readonly number: number; readonly heading: boolean };
25
+
26
+ const mentionPattern = (words: readonly string[]): RegExp =>
27
+ new RegExp(`(?<![A-Za-z])(?:${words.map(escapeRegExp).join("|")})[ \\t\\u3000]?(?<number>[0-90-9]{1,3})(?![0-90-9A-Za-z])`, "gu");
28
+
29
+ const isHeadingAt = (source: string, offset: number): boolean => HEADING.test(source.slice(source.lastIndexOf("\n", offset - 1) + 1, offset));
30
+
31
+ /** 本文の参照(コードを覆った prose から)と、見出しの手順(prose は見出しも覆うので source から)。 */
32
+ const mentionsOf = ({ source, prose }: StepText, words: readonly string[]): Mention[] => {
33
+ const toMention = (match: RegExpExecArray | RegExpMatchArray, heading: boolean): Mention => ({
34
+ offset: match.index ?? 0,
35
+ label: match[0],
36
+ number: Number(halfWidth(match.groups?.["number"] ?? "")),
37
+ heading,
38
+ });
39
+ const headings = [...source.matchAll(mentionPattern(words))].filter((match) => isHeadingAt(source, match.index)).map((match) => toMention(match, true));
40
+ const references = [...prose.matchAll(mentionPattern(words))].filter((match) => !isHeadingAt(source, match.index)).map((match) => toMention(match, false));
41
+ return [...headings, ...references];
42
+ };
43
+
44
+ const listNumbers = (prose: string): number[] =>
45
+ prose.split("\n").flatMap((line) => {
46
+ const number = LIST_ITEM.exec(line)?.groups?.["number"];
47
+ return number === undefined ? [] : [Number(number)];
48
+ });
49
+
50
+ /** 文書のどの手順にも無い番号を指す参照。手順の最後の番号を超えるものだけ(箇条書きが二つあれば、番号はどちらかにある)。 */
51
+ export const missingSteps = (text: StepText, words: readonly string[]): MissingStep[] => {
52
+ if (words.length === 0) return [];
53
+ const mentions = mentionsOf(text, words);
54
+ const steps = [...mentions.filter((mention) => mention.heading).map((mention) => mention.number), ...listNumbers(text.prose)];
55
+ if (steps.length === 0) return [];
56
+ const last = Math.max(...steps);
57
+ return mentions
58
+ .filter((mention) => !mention.heading && mention.number > last && !text.elsewhere(mention.offset, mention.offset + mention.label.length))
59
+ .map((mention) => ({ offset: mention.offset, label: mention.label, last }));
60
+ };
61
+
62
+ /** 番号のすぐ後ろが figure-elsewhere の語か、すぐ前がこの文書でない文書の名前なら、ほかの文書の手順。 */
63
+ const elsewhereOf =
64
+ (doc: ProseDocument, text: string) =>
65
+ (start: number, end: number): boolean => {
66
+ const after = text.slice(end).trimStart();
67
+ const placed = (doc.lexicons["figure-elsewhere"] ?? []).some((entry) => after.startsWith(`${entry.pattern} `) && /^\s/u.test(text.slice(end)));
68
+ const named = doc.namedDocument?.(text, start);
69
+ return placed || (named !== undefined && !named.self);
70
+ };
71
+
72
+ export const stepReference: Detector = (doc): Finding[] => {
73
+ const prose = doc.prose ?? doc.source;
74
+ const words = (doc.lexicons["step-word"] ?? []).map((entry) => entry.pattern);
75
+ return missingSteps({ source: doc.source, prose, elsewhere: elsewhereOf(doc, prose) }, words).map((missing) => ({
76
+ rule: "",
77
+ severity: "warning",
78
+ line: 0,
79
+ column: 0,
80
+ quote: quoteAround(prose, missing.offset, missing.offset + missing.label.length),
81
+ values: { label: missing.label, last: missing.last, offset: missing.offset },
82
+ }));
83
+ };