chaffjs 0.20.0 → 0.22.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 (762) hide show
  1. package/README.md +7 -5
  2. package/dist/api.d.ts +16 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js.map +1 -1
  5. package/dist/cli-text.d.ts +2 -0
  6. package/dist/cli-text.d.ts.map +1 -1
  7. package/dist/cli-text.js +18 -5
  8. package/dist/cli-text.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +8 -11
  11. package/dist/cli.js.map +1 -1
  12. package/dist/command-help.d.ts +7 -0
  13. package/dist/command-help.d.ts.map +1 -0
  14. package/dist/command-help.js +18 -0
  15. package/dist/command-help.js.map +1 -0
  16. package/dist/commands/eval.js +2 -2
  17. package/dist/commands/eval.js.map +1 -1
  18. package/dist/commands/fix-plan.d.ts.map +1 -1
  19. package/dist/commands/fix-plan.js +11 -1
  20. package/dist/commands/fix-plan.js.map +1 -1
  21. package/dist/commands/help.d.ts +4 -0
  22. package/dist/commands/help.d.ts.map +1 -0
  23. package/dist/commands/help.js +7 -0
  24. package/dist/commands/help.js.map +1 -0
  25. package/dist/commands/init-command.d.ts +4 -0
  26. package/dist/commands/init-command.d.ts.map +1 -0
  27. package/dist/commands/init-command.js +15 -0
  28. package/dist/commands/init-command.js.map +1 -0
  29. package/dist/commands/plugin-init.d.ts +20 -0
  30. package/dist/commands/plugin-init.d.ts.map +1 -0
  31. package/dist/commands/plugin-init.js +129 -0
  32. package/dist/commands/plugin-init.js.map +1 -0
  33. package/dist/commands/plugin-test.d.ts +11 -0
  34. package/dist/commands/plugin-test.d.ts.map +1 -0
  35. package/dist/commands/plugin-test.js +81 -0
  36. package/dist/commands/plugin-test.js.map +1 -0
  37. package/dist/config/load.d.ts +2 -0
  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/option-problems.js +2 -2
  42. package/dist/config/option-problems.js.map +1 -1
  43. package/dist/config/rule-problems.js +2 -2
  44. package/dist/config/rule-problems.js.map +1 -1
  45. package/dist/custom/checked.d.ts +9 -0
  46. package/dist/custom/checked.d.ts.map +1 -0
  47. package/dist/custom/checked.js +3 -0
  48. package/dist/custom/checked.js.map +1 -0
  49. package/dist/custom/dsl-fields.d.ts +27 -0
  50. package/dist/custom/dsl-fields.d.ts.map +1 -0
  51. package/dist/custom/dsl-fields.js +86 -0
  52. package/dist/custom/dsl-fields.js.map +1 -0
  53. package/dist/custom/load.d.ts.map +1 -1
  54. package/dist/custom/load.js +2 -0
  55. package/dist/custom/load.js.map +1 -1
  56. package/dist/custom/parse.d.ts +14 -3
  57. package/dist/custom/parse.d.ts.map +1 -1
  58. package/dist/custom/parse.js +44 -40
  59. package/dist/custom/parse.js.map +1 -1
  60. package/dist/custom/problems.d.ts.map +1 -1
  61. package/dist/custom/problems.js +10 -1
  62. package/dist/custom/problems.js.map +1 -1
  63. package/dist/detectors/amount-for-count.d.ts +21 -0
  64. package/dist/detectors/amount-for-count.d.ts.map +1 -0
  65. package/dist/detectors/amount-for-count.js +55 -0
  66. package/dist/detectors/amount-for-count.js.map +1 -0
  67. package/dist/detectors/blank-structure.d.ts.map +1 -1
  68. package/dist/detectors/blank-structure.js +6 -4
  69. package/dist/detectors/blank-structure.js.map +1 -1
  70. package/dist/detectors/clause-shape.d.ts +22 -0
  71. package/dist/detectors/clause-shape.d.ts.map +1 -0
  72. package/dist/detectors/clause-shape.js +149 -0
  73. package/dist/detectors/clause-shape.js.map +1 -0
  74. package/dist/detectors/code-fences.d.ts +34 -0
  75. package/dist/detectors/code-fences.d.ts.map +1 -0
  76. package/dist/detectors/code-fences.js +100 -0
  77. package/dist/detectors/code-fences.js.map +1 -0
  78. package/dist/detectors/cross-definitions.d.ts +4 -0
  79. package/dist/detectors/cross-definitions.d.ts.map +1 -0
  80. package/dist/detectors/cross-definitions.js +43 -0
  81. package/dist/detectors/cross-definitions.js.map +1 -0
  82. package/dist/detectors/cross-facts.d.ts +4 -0
  83. package/dist/detectors/cross-facts.d.ts.map +1 -0
  84. package/dist/detectors/cross-facts.js +53 -0
  85. package/dist/detectors/cross-facts.js.map +1 -0
  86. package/dist/detectors/cross-registry/cross-doc-duplicate-definition.d.ts +3 -0
  87. package/dist/detectors/cross-registry/cross-doc-duplicate-definition.d.ts.map +1 -0
  88. package/dist/detectors/cross-registry/cross-doc-duplicate-definition.js +3 -0
  89. package/dist/detectors/cross-registry/cross-doc-duplicate-definition.js.map +1 -0
  90. package/dist/detectors/cross-registry/cross-doc-fact-conflict.d.ts +3 -0
  91. package/dist/detectors/cross-registry/cross-doc-fact-conflict.d.ts.map +1 -0
  92. package/dist/detectors/cross-registry/cross-doc-fact-conflict.js +3 -0
  93. package/dist/detectors/cross-registry/cross-doc-fact-conflict.js.map +1 -0
  94. package/dist/detectors/currency-notation.d.ts +29 -0
  95. package/dist/detectors/currency-notation.d.ts.map +1 -0
  96. package/dist/detectors/currency-notation.js +58 -0
  97. package/dist/detectors/currency-notation.js.map +1 -0
  98. package/dist/detectors/custom.d.ts.map +1 -1
  99. package/dist/detectors/custom.js +4 -1
  100. package/dist/detectors/custom.js.map +1 -1
  101. package/dist/detectors/dangling-opener.d.ts +14 -0
  102. package/dist/detectors/dangling-opener.d.ts.map +1 -0
  103. package/dist/detectors/dangling-opener.js +55 -0
  104. package/dist/detectors/dangling-opener.js.map +1 -0
  105. package/dist/detectors/defined-term-form.d.ts +25 -0
  106. package/dist/detectors/defined-term-form.d.ts.map +1 -0
  107. package/dist/detectors/defined-term-form.js +170 -0
  108. package/dist/detectors/defined-term-form.js.map +1 -0
  109. package/dist/detectors/email-letter.d.ts +46 -0
  110. package/dist/detectors/email-letter.d.ts.map +1 -0
  111. package/dist/detectors/email-letter.js +234 -0
  112. package/dist/detectors/email-letter.js.map +1 -0
  113. package/dist/detectors/footnote-mismatch.d.ts +11 -0
  114. package/dist/detectors/footnote-mismatch.d.ts.map +1 -0
  115. package/dist/detectors/footnote-mismatch.js +55 -0
  116. package/dist/detectors/footnote-mismatch.js.map +1 -0
  117. package/dist/detectors/form-minority.d.ts +13 -0
  118. package/dist/detectors/form-minority.d.ts.map +1 -0
  119. package/dist/detectors/form-minority.js +19 -0
  120. package/dist/detectors/form-minority.js.map +1 -0
  121. package/dist/detectors/heading-end-mark.d.ts +10 -0
  122. package/dist/detectors/heading-end-mark.d.ts.map +1 -0
  123. package/dist/detectors/heading-end-mark.js +59 -0
  124. package/dist/detectors/heading-end-mark.js.map +1 -0
  125. package/dist/detectors/impossible-date.d.ts +20 -0
  126. package/dist/detectors/impossible-date.d.ts.map +1 -0
  127. package/dist/detectors/impossible-date.js +105 -0
  128. package/dist/detectors/impossible-date.js.map +1 -0
  129. package/dist/detectors/impossible-time.d.ts +18 -0
  130. package/dist/detectors/impossible-time.d.ts.map +1 -0
  131. package/dist/detectors/impossible-time.js +116 -0
  132. package/dist/detectors/impossible-time.js.map +1 -0
  133. package/dist/detectors/link-shape.d.ts +11 -0
  134. package/dist/detectors/link-shape.d.ts.map +1 -0
  135. package/dist/detectors/link-shape.js +56 -0
  136. package/dist/detectors/link-shape.js.map +1 -0
  137. package/dist/detectors/list-marker.d.ts +7 -0
  138. package/dist/detectors/list-marker.d.ts.map +1 -0
  139. package/dist/detectors/list-marker.js +50 -0
  140. package/dist/detectors/list-marker.js.map +1 -0
  141. package/dist/detectors/mark-shape.d.ts +53 -0
  142. package/dist/detectors/mark-shape.d.ts.map +1 -0
  143. package/dist/detectors/mark-shape.js +160 -0
  144. package/dist/detectors/mark-shape.js.map +1 -0
  145. package/dist/detectors/markdown-slip.d.ts +19 -0
  146. package/dist/detectors/markdown-slip.d.ts.map +1 -0
  147. package/dist/detectors/markdown-slip.js +122 -0
  148. package/dist/detectors/markdown-slip.js.map +1 -0
  149. package/dist/detectors/numeral-opener.d.ts +10 -0
  150. package/dist/detectors/numeral-opener.d.ts.map +1 -0
  151. package/dist/detectors/numeral-opener.js +65 -0
  152. package/dist/detectors/numeral-opener.js.map +1 -0
  153. package/dist/detectors/ordinal-suffix.d.ts +12 -0
  154. package/dist/detectors/ordinal-suffix.d.ts.map +1 -0
  155. package/dist/detectors/ordinal-suffix.js +33 -0
  156. package/dist/detectors/ordinal-suffix.js.map +1 -0
  157. package/dist/detectors/range-notation.d.ts +15 -0
  158. package/dist/detectors/range-notation.d.ts.map +1 -0
  159. package/dist/detectors/range-notation.js +58 -0
  160. package/dist/detectors/range-notation.js.map +1 -0
  161. package/dist/detectors/registry/adverb-polarity.d.ts +3 -0
  162. package/dist/detectors/registry/adverb-polarity.d.ts.map +1 -0
  163. package/dist/detectors/registry/adverb-polarity.js +3 -0
  164. package/dist/detectors/registry/adverb-polarity.js.map +1 -0
  165. package/dist/detectors/registry/amount-for-count.d.ts +3 -0
  166. package/dist/detectors/registry/amount-for-count.d.ts.map +1 -0
  167. package/dist/detectors/registry/amount-for-count.js +3 -0
  168. package/dist/detectors/registry/amount-for-count.js.map +1 -0
  169. package/dist/detectors/registry/attachment-missing.d.ts +3 -0
  170. package/dist/detectors/registry/attachment-missing.d.ts.map +1 -0
  171. package/dist/detectors/registry/attachment-missing.js +3 -0
  172. package/dist/detectors/registry/attachment-missing.js.map +1 -0
  173. package/dist/detectors/registry/bare-url-mix.d.ts +3 -0
  174. package/dist/detectors/registry/bare-url-mix.d.ts.map +1 -0
  175. package/dist/detectors/registry/bare-url-mix.js +3 -0
  176. package/dist/detectors/registry/bare-url-mix.js.map +1 -0
  177. package/dist/detectors/registry/caps-shouting.d.ts +3 -0
  178. package/dist/detectors/registry/caps-shouting.d.ts.map +1 -0
  179. package/dist/detectors/registry/caps-shouting.js +3 -0
  180. package/dist/detectors/registry/caps-shouting.js.map +1 -0
  181. package/dist/detectors/registry/code-fence-language.d.ts +3 -0
  182. package/dist/detectors/registry/code-fence-language.d.ts.map +1 -0
  183. package/dist/detectors/registry/code-fence-language.js +3 -0
  184. package/dist/detectors/registry/code-fence-language.js.map +1 -0
  185. package/dist/detectors/registry/currency-notation.d.ts +3 -0
  186. package/dist/detectors/registry/currency-notation.d.ts.map +1 -0
  187. package/dist/detectors/registry/currency-notation.js +3 -0
  188. package/dist/detectors/registry/currency-notation.js.map +1 -0
  189. package/dist/detectors/registry/dangling-opener.d.ts +3 -0
  190. package/dist/detectors/registry/dangling-opener.d.ts.map +1 -0
  191. package/dist/detectors/registry/dangling-opener.js +3 -0
  192. package/dist/detectors/registry/dangling-opener.js.map +1 -0
  193. package/dist/detectors/registry/defined-name-repeated.d.ts +3 -0
  194. package/dist/detectors/registry/defined-name-repeated.d.ts.map +1 -0
  195. package/dist/detectors/registry/defined-name-repeated.js +3 -0
  196. package/dist/detectors/registry/defined-name-repeated.js.map +1 -0
  197. package/dist/detectors/registry/defined-term-form.d.ts +3 -0
  198. package/dist/detectors/registry/defined-term-form.d.ts.map +1 -0
  199. package/dist/detectors/registry/defined-term-form.js +3 -0
  200. package/dist/detectors/registry/defined-term-form.js.map +1 -0
  201. package/dist/detectors/registry/email-frame.d.ts +3 -0
  202. package/dist/detectors/registry/email-frame.d.ts.map +1 -0
  203. package/dist/detectors/registry/email-frame.js +3 -0
  204. package/dist/detectors/registry/email-frame.js.map +1 -0
  205. package/dist/detectors/registry/email-subject.d.ts +3 -0
  206. package/dist/detectors/registry/email-subject.d.ts.map +1 -0
  207. package/dist/detectors/registry/email-subject.js +3 -0
  208. package/dist/detectors/registry/email-subject.js.map +1 -0
  209. package/dist/detectors/registry/ending-run.d.ts +3 -0
  210. package/dist/detectors/registry/ending-run.d.ts.map +1 -0
  211. package/dist/detectors/registry/ending-run.js +3 -0
  212. package/dist/detectors/registry/ending-run.js.map +1 -0
  213. package/dist/detectors/registry/footnote-mismatch.d.ts +3 -0
  214. package/dist/detectors/registry/footnote-mismatch.d.ts.map +1 -0
  215. package/dist/detectors/registry/footnote-mismatch.js +3 -0
  216. package/dist/detectors/registry/footnote-mismatch.js.map +1 -0
  217. package/dist/detectors/registry/heading-command.d.ts +3 -0
  218. package/dist/detectors/registry/heading-command.d.ts.map +1 -0
  219. package/dist/detectors/registry/heading-command.js +3 -0
  220. package/dist/detectors/registry/heading-command.js.map +1 -0
  221. package/dist/detectors/registry/heading-end-punctuation.d.ts +3 -0
  222. package/dist/detectors/registry/heading-end-punctuation.d.ts.map +1 -0
  223. package/dist/detectors/registry/heading-end-punctuation.js +3 -0
  224. package/dist/detectors/registry/heading-end-punctuation.js.map +1 -0
  225. package/dist/detectors/registry/hyphen-dash.d.ts +3 -0
  226. package/dist/detectors/registry/hyphen-dash.d.ts.map +1 -0
  227. package/dist/detectors/registry/hyphen-dash.js +3 -0
  228. package/dist/detectors/registry/hyphen-dash.js.map +1 -0
  229. package/dist/detectors/registry/image-file-name-alt.d.ts +3 -0
  230. package/dist/detectors/registry/image-file-name-alt.d.ts.map +1 -0
  231. package/dist/detectors/registry/image-file-name-alt.js +3 -0
  232. package/dist/detectors/registry/image-file-name-alt.js.map +1 -0
  233. package/dist/detectors/registry/impossible-date.d.ts +3 -0
  234. package/dist/detectors/registry/impossible-date.d.ts.map +1 -0
  235. package/dist/detectors/registry/impossible-date.js +3 -0
  236. package/dist/detectors/registry/impossible-date.js.map +1 -0
  237. package/dist/detectors/registry/impossible-time.d.ts +3 -0
  238. package/dist/detectors/registry/impossible-time.d.ts.map +1 -0
  239. package/dist/detectors/registry/impossible-time.js +3 -0
  240. package/dist/detectors/registry/impossible-time.js.map +1 -0
  241. package/dist/detectors/registry/latin-abbreviation.d.ts +3 -0
  242. package/dist/detectors/registry/latin-abbreviation.d.ts.map +1 -0
  243. package/dist/detectors/registry/latin-abbreviation.js +3 -0
  244. package/dist/detectors/registry/latin-abbreviation.js.map +1 -0
  245. package/dist/detectors/registry/link-text-url-mismatch.d.ts +3 -0
  246. package/dist/detectors/registry/link-text-url-mismatch.d.ts.map +1 -0
  247. package/dist/detectors/registry/link-text-url-mismatch.js +3 -0
  248. package/dist/detectors/registry/link-text-url-mismatch.js.map +1 -0
  249. package/dist/detectors/registry/list-marker.d.ts +3 -0
  250. package/dist/detectors/registry/list-marker.d.ts.map +1 -0
  251. package/dist/detectors/registry/list-marker.js +3 -0
  252. package/dist/detectors/registry/list-marker.js.map +1 -0
  253. package/dist/detectors/registry/mark-consistency.d.ts +3 -0
  254. package/dist/detectors/registry/mark-consistency.d.ts.map +1 -0
  255. package/dist/detectors/registry/mark-consistency.js +3 -0
  256. package/dist/detectors/registry/mark-consistency.js.map +1 -0
  257. package/dist/detectors/registry/numeral-opener.d.ts +3 -0
  258. package/dist/detectors/registry/numeral-opener.d.ts.map +1 -0
  259. package/dist/detectors/registry/numeral-opener.js +3 -0
  260. package/dist/detectors/registry/numeral-opener.js.map +1 -0
  261. package/dist/detectors/registry/ordinal-suffix.d.ts +3 -0
  262. package/dist/detectors/registry/ordinal-suffix.d.ts.map +1 -0
  263. package/dist/detectors/registry/ordinal-suffix.js +3 -0
  264. package/dist/detectors/registry/ordinal-suffix.js.map +1 -0
  265. package/dist/detectors/registry/origin-particle.d.ts +3 -0
  266. package/dist/detectors/registry/origin-particle.d.ts.map +1 -0
  267. package/dist/detectors/registry/origin-particle.js +3 -0
  268. package/dist/detectors/registry/origin-particle.js.map +1 -0
  269. package/dist/detectors/registry/range-notation.d.ts +3 -0
  270. package/dist/detectors/registry/range-notation.d.ts.map +1 -0
  271. package/dist/detectors/registry/range-notation.js +3 -0
  272. package/dist/detectors/registry/range-notation.js.map +1 -0
  273. package/dist/detectors/registry/relative-pronoun-mix.d.ts +3 -0
  274. package/dist/detectors/registry/relative-pronoun-mix.d.ts.map +1 -0
  275. package/dist/detectors/registry/relative-pronoun-mix.js +3 -0
  276. package/dist/detectors/registry/relative-pronoun-mix.js.map +1 -0
  277. package/dist/detectors/registry/request-without-deadline.d.ts +3 -0
  278. package/dist/detectors/registry/request-without-deadline.d.ts.map +1 -0
  279. package/dist/detectors/registry/request-without-deadline.js +3 -0
  280. package/dist/detectors/registry/request-without-deadline.js.map +1 -0
  281. package/dist/detectors/registry/sentence-spacing.d.ts +3 -0
  282. package/dist/detectors/registry/sentence-spacing.d.ts.map +1 -0
  283. package/dist/detectors/registry/sentence-spacing.js +3 -0
  284. package/dist/detectors/registry/sentence-spacing.js.map +1 -0
  285. package/dist/detectors/registry/share-over-hundred.d.ts +3 -0
  286. package/dist/detectors/registry/share-over-hundred.d.ts.map +1 -0
  287. package/dist/detectors/registry/share-over-hundred.js +3 -0
  288. package/dist/detectors/registry/share-over-hundred.js.map +1 -0
  289. package/dist/detectors/registry/shell-prompt.d.ts +3 -0
  290. package/dist/detectors/registry/shell-prompt.d.ts.map +1 -0
  291. package/dist/detectors/registry/shell-prompt.js +3 -0
  292. package/dist/detectors/registry/shell-prompt.js.map +1 -0
  293. package/dist/detectors/registry/step-reference.d.ts +3 -0
  294. package/dist/detectors/registry/step-reference.d.ts.map +1 -0
  295. package/dist/detectors/registry/step-reference.js +3 -0
  296. package/dist/detectors/registry/step-reference.js.map +1 -0
  297. package/dist/detectors/registry/step-statement.d.ts +3 -0
  298. package/dist/detectors/registry/step-statement.d.ts.map +1 -0
  299. package/dist/detectors/registry/step-statement.js +3 -0
  300. package/dist/detectors/registry/step-statement.js.map +1 -0
  301. package/dist/detectors/registry/table-row-overflow.d.ts +3 -0
  302. package/dist/detectors/registry/table-row-overflow.d.ts.map +1 -0
  303. package/dist/detectors/registry/table-row-overflow.js +3 -0
  304. package/dist/detectors/registry/table-row-overflow.js.map +1 -0
  305. package/dist/detectors/registry/toc-heading.d.ts +3 -0
  306. package/dist/detectors/registry/toc-heading.d.ts.map +1 -0
  307. package/dist/detectors/registry/toc-heading.js +3 -0
  308. package/dist/detectors/registry/toc-heading.js.map +1 -0
  309. package/dist/detectors/registry/unclosed-code-fence.d.ts +3 -0
  310. package/dist/detectors/registry/unclosed-code-fence.d.ts.map +1 -0
  311. package/dist/detectors/registry/unclosed-code-fence.js +3 -0
  312. package/dist/detectors/registry/unclosed-code-fence.js.map +1 -0
  313. package/dist/detectors/registry/unpaired-parallel.d.ts +3 -0
  314. package/dist/detectors/registry/unpaired-parallel.d.ts.map +1 -0
  315. package/dist/detectors/registry/unpaired-parallel.js +3 -0
  316. package/dist/detectors/registry/unpaired-parallel.js.map +1 -0
  317. package/dist/detectors/registry/unrendered-emphasis.d.ts +3 -0
  318. package/dist/detectors/registry/unrendered-emphasis.d.ts.map +1 -0
  319. package/dist/detectors/registry/unrendered-emphasis.js +3 -0
  320. package/dist/detectors/registry/unrendered-emphasis.js.map +1 -0
  321. package/dist/detectors/relative-pronoun-mix.d.ts +19 -0
  322. package/dist/detectors/relative-pronoun-mix.d.ts.map +1 -0
  323. package/dist/detectors/relative-pronoun-mix.js +60 -0
  324. package/dist/detectors/relative-pronoun-mix.js.map +1 -0
  325. package/dist/detectors/request-owner.d.ts +23 -0
  326. package/dist/detectors/request-owner.d.ts.map +1 -0
  327. package/dist/detectors/request-owner.js +110 -0
  328. package/dist/detectors/request-owner.js.map +1 -0
  329. package/dist/detectors/share-over-hundred.d.ts +20 -0
  330. package/dist/detectors/share-over-hundred.d.ts.map +1 -0
  331. package/dist/detectors/share-over-hundred.js +55 -0
  332. package/dist/detectors/share-over-hundred.js.map +1 -0
  333. package/dist/detectors/step-reference.d.ts +20 -0
  334. package/dist/detectors/step-reference.d.ts.map +1 -0
  335. package/dist/detectors/step-reference.js +57 -0
  336. package/dist/detectors/step-reference.js.map +1 -0
  337. package/dist/detectors/technical-docs.d.ts +32 -0
  338. package/dist/detectors/technical-docs.d.ts.map +1 -0
  339. package/dist/detectors/technical-docs.js +199 -0
  340. package/dist/detectors/technical-docs.js.map +1 -0
  341. package/dist/detectors/toc-heading.d.ts +10 -0
  342. package/dist/detectors/toc-heading.d.ts.map +1 -0
  343. package/dist/detectors/toc-heading.js +99 -0
  344. package/dist/detectors/toc-heading.js.map +1 -0
  345. package/dist/extension/pack-read.d.ts +21 -0
  346. package/dist/extension/pack-read.d.ts.map +1 -0
  347. package/dist/extension/pack-read.js +117 -0
  348. package/dist/extension/pack-read.js.map +1 -0
  349. package/dist/extension/plugin-load.d.ts.map +1 -1
  350. package/dist/extension/plugin-load.js +44 -10
  351. package/dist/extension/plugin-load.js.map +1 -1
  352. package/dist/extension/plugin-parse.d.ts.map +1 -1
  353. package/dist/extension/plugin-parse.js +10 -3
  354. package/dist/extension/plugin-parse.js.map +1 -1
  355. package/dist/facts/address-values.d.ts +5 -0
  356. package/dist/facts/address-values.d.ts.map +1 -0
  357. package/dist/facts/address-values.js +64 -0
  358. package/dist/facts/address-values.js.map +1 -0
  359. package/dist/facts/cross-facts.d.ts +20 -0
  360. package/dist/facts/cross-facts.d.ts.map +1 -0
  361. package/dist/facts/cross-facts.js +41 -0
  362. package/dist/facts/cross-facts.js.map +1 -0
  363. package/dist/fix-plan/chosen-depth.d.ts +10 -0
  364. package/dist/fix-plan/chosen-depth.d.ts.map +1 -0
  365. package/dist/fix-plan/chosen-depth.js +24 -0
  366. package/dist/fix-plan/chosen-depth.js.map +1 -0
  367. package/dist/fix-plan/mode.d.ts +13 -4
  368. package/dist/fix-plan/mode.d.ts.map +1 -1
  369. package/dist/fix-plan/mode.js +38 -7
  370. package/dist/fix-plan/mode.js.map +1 -1
  371. package/dist/fix-plan/plan.d.ts +20 -0
  372. package/dist/fix-plan/plan.d.ts.map +1 -1
  373. package/dist/fix-plan/plan.js +23 -5
  374. package/dist/fix-plan/plan.js.map +1 -1
  375. package/dist/fix-plan/render.d.ts.map +1 -1
  376. package/dist/fix-plan/render.js +24 -0
  377. package/dist/fix-plan/render.js.map +1 -1
  378. package/dist/fix-plan/text.d.ts +8 -0
  379. package/dist/fix-plan/text.d.ts.map +1 -1
  380. package/dist/fix-plan/text.js +36 -10
  381. package/dist/fix-plan/text.js.map +1 -1
  382. package/dist/genre-load.d.ts +1 -1
  383. package/dist/genre-load.d.ts.map +1 -1
  384. package/dist/genre-load.js +7 -4
  385. package/dist/genre-load.js.map +1 -1
  386. package/dist/genre-parse.d.ts +6 -0
  387. package/dist/genre-parse.d.ts.map +1 -1
  388. package/dist/genre-parse.js +18 -0
  389. package/dist/genre-parse.js.map +1 -1
  390. package/dist/init.js +10 -10
  391. package/dist/init.js.map +1 -1
  392. package/dist/known-genres.d.ts +8 -0
  393. package/dist/known-genres.d.ts.map +1 -0
  394. package/dist/known-genres.js +17 -0
  395. package/dist/known-genres.js.map +1 -0
  396. package/dist/link-shape.d.ts +20 -0
  397. package/dist/link-shape.d.ts.map +1 -0
  398. package/dist/link-shape.js +58 -0
  399. package/dist/link-shape.js.map +1 -0
  400. package/dist/list-marks.d.ts +21 -0
  401. package/dist/list-marks.d.ts.map +1 -0
  402. package/dist/list-marks.js +82 -0
  403. package/dist/list-marks.js.map +1 -0
  404. package/dist/reasons.d.ts.map +1 -1
  405. package/dist/reasons.js +15 -4
  406. package/dist/reasons.js.map +1 -1
  407. package/dist/render/explain.d.ts.map +1 -1
  408. package/dist/render/explain.js +11 -2
  409. package/dist/render/explain.js.map +1 -1
  410. package/dist/render/friendly.js +1 -1
  411. package/dist/render/friendly.js.map +1 -1
  412. package/dist/render/rules-json.d.ts.map +1 -1
  413. package/dist/render/rules-json.js +12 -3
  414. package/dist/render/rules-json.js.map +1 -1
  415. package/dist/render/rules-table.js +6 -6
  416. package/dist/render/rules-table.js.map +1 -1
  417. package/dist/render/semantic.js +2 -2
  418. package/dist/render/semantic.js.map +1 -1
  419. package/dist/render/suppressions.js +2 -2
  420. package/dist/render/suppressions.js.map +1 -1
  421. package/dist/rewrite-depth.d.ts +22 -0
  422. package/dist/rewrite-depth.d.ts.map +1 -0
  423. package/dist/rewrite-depth.js +42 -0
  424. package/dist/rewrite-depth.js.map +1 -0
  425. package/dist/rule-fields.d.ts +28 -0
  426. package/dist/rule-fields.d.ts.map +1 -0
  427. package/dist/rule-fields.js +85 -0
  428. package/dist/rule-fields.js.map +1 -0
  429. package/dist/rule-guide.d.ts +3 -0
  430. package/dist/rule-guide.d.ts.map +1 -1
  431. package/dist/rule-guide.js +6 -0
  432. package/dist/rule-guide.js.map +1 -1
  433. package/dist/rule-load.d.ts.map +1 -1
  434. package/dist/rule-load.js +13 -5
  435. package/dist/rule-load.js.map +1 -1
  436. package/dist/rule-offs.d.ts +14 -0
  437. package/dist/rule-offs.d.ts.map +1 -0
  438. package/dist/rule-offs.js +45 -0
  439. package/dist/rule-offs.js.map +1 -0
  440. package/dist/structure/build.d.ts.map +1 -1
  441. package/dist/structure/build.js +14 -1
  442. package/dist/structure/build.js.map +1 -1
  443. package/dist/structure/cross-definitions.d.ts +23 -0
  444. package/dist/structure/cross-definitions.d.ts.map +1 -0
  445. package/dist/structure/cross-definitions.js +34 -0
  446. package/dist/structure/cross-definitions.js.map +1 -0
  447. package/genres.yaml +14 -313
  448. package/package.json +3 -3
  449. package/rules/absolute-exception.yaml +3 -1
  450. package/rules/abstract-length.yaml +7 -1
  451. package/rules/acronym-expansion-conflict.yaml +2 -1
  452. package/rules/adverb-overuse.yaml +7 -1
  453. package/rules/adverb-without-negation.yaml +60 -0
  454. package/rules/adversative-ga-repeat.yaml +2 -1
  455. package/rules/agentless-passive.yaml +10 -1
  456. package/rules/agreement-slip.yaml +4 -1
  457. package/rules/ai-generated-composite.yaml +6 -1
  458. package/rules/ai-structure.yaml +8 -2
  459. package/rules/ai-tell.yaml +7 -1
  460. package/rules/all-caps-shouting.yaml +61 -0
  461. package/rules/announced-count-mismatch.yaml +3 -1
  462. package/rules/announcing-opener.yaml +7 -1
  463. package/rules/article-sound.yaml +6 -2
  464. package/rules/assistant-residue.yaml +4 -1
  465. package/rules/attachment-not-attached.yaml +112 -0
  466. package/rules/baito-keigo.yaml +64 -0
  467. package/rules/bare-url-mix.yaml +86 -0
  468. package/rules/bold-density.yaml +1 -0
  469. package/rules/bold-label-list.yaml +9 -1
  470. package/rules/bracket-nesting.yaml +2 -1
  471. package/rules/bracket-width-consistency.yaml +64 -0
  472. package/rules/broken-link.yaml +1 -1
  473. package/rules/chat-citation-residue.yaml +2 -1
  474. package/rules/citation-reference-mismatch.yaml +7 -1
  475. package/rules/citation-style-mix.yaml +7 -1
  476. package/rules/clause-chain.yaml +6 -1
  477. package/rules/cliche.yaml +67 -0
  478. package/rules/closing-cliche.yaml +7 -0
  479. package/rules/code-fence-language-mix.yaml +176 -0
  480. package/rules/colloquial-opener.yaml +62 -0
  481. package/rules/colon-lead-in.yaml +7 -1
  482. package/rules/concrete-evidence-density.yaml +6 -0
  483. package/rules/contraction-consistency.yaml +4 -1
  484. package/rules/contrast-framing.yaml +7 -1
  485. package/rules/cross-doc-duplicate-definition.yaml +70 -0
  486. package/rules/cross-doc-fact-conflict.yaml +64 -0
  487. package/rules/currency-notation-consistency.yaml +77 -0
  488. package/rules/cushion-phrase-density.yaml +8 -1
  489. package/rules/dangling-figure-reference.yaml +3 -1
  490. package/rules/dangling-participle.yaml +58 -0
  491. package/rules/dangling-reference.yaml +3 -1
  492. package/rules/date-format-consistency.yaml +3 -2
  493. package/rules/date-order.yaml +3 -1
  494. package/rules/date-range-reversed.yaml +3 -1
  495. package/rules/date-weekday-mismatch.yaml +3 -1
  496. package/rules/date-without-year.yaml +4 -3
  497. package/rules/defined-name-repeated.yaml +97 -0
  498. package/rules/defined-term-form.yaml +113 -0
  499. package/rules/demonstrative-opener-run.yaml +2 -1
  500. package/rules/double-keigo.yaml +6 -1
  501. package/rules/double-negative.yaml +5 -1
  502. package/rules/doubled-nado.yaml +2 -1
  503. package/rules/doubled-punctuation.yaml +1 -1
  504. package/rules/doubled-word.yaml +4 -1
  505. package/rules/duplicate-definition.yaml +3 -1
  506. package/rules/duplicate-heading.yaml +1 -1
  507. package/rules/duration-mismatch.yaml +2 -1
  508. package/rules/elapsed-years-mismatch.yaml +4 -1
  509. package/rules/email-greeting-closing.yaml +122 -0
  510. package/rules/email-subject-length.yaml +112 -0
  511. package/rules/emoji-density.yaml +3 -1
  512. package/rules/emoji-heading.yaml +7 -1
  513. package/rules/empty-intensifier.yaml +4 -0
  514. package/rules/empty-list-item.yaml +2 -1
  515. package/rules/empty-section.yaml +1 -1
  516. package/rules/empty-table-cell.yaml +2 -1
  517. package/rules/excessive-hedging.yaml +7 -1
  518. package/rules/exclamation-width-consistency.yaml +64 -0
  519. package/rules/expletive-construction.yaml +7 -1
  520. package/rules/fact-conflict.yaml +4 -1
  521. package/rules/figure-reference-order.yaml +7 -1
  522. package/rules/footnote-mismatch.yaml +91 -0
  523. package/rules/fullwidth-alnum-consistency.yaml +2 -2
  524. package/rules/hankaku-kana.yaml +1 -1
  525. package/rules/heading-command-missing.yaml +109 -0
  526. package/rules/heading-echo.yaml +6 -1
  527. package/rules/heading-end-punctuation-mix.yaml +158 -0
  528. package/rules/heading-level-skip.yaml +3 -3
  529. package/rules/heading-numbering-mix.yaml +4 -3
  530. package/rules/hiragana-fukushi.yaml +4 -1
  531. package/rules/homophone-slip.yaml +5 -1
  532. package/rules/humble-for-others.yaml +63 -0
  533. package/rules/hyphen-as-dash.yaml +59 -0
  534. package/rules/idiom-misuse.yaml +4 -1
  535. package/rules/image-file-name-alt.yaml +83 -0
  536. package/rules/impossible-date.yaml +79 -0
  537. package/rules/impossible-time.yaml +76 -0
  538. package/rules/internal-jargon.yaml +1 -1
  539. package/rules/invisible-character.yaml +1 -1
  540. package/rules/katakana-density.yaml +2 -1
  541. package/rules/katakana-long-vowel.yaml +1 -1
  542. package/rules/known-misspelling.yaml +2 -1
  543. package/rules/kutoten-consistency.yaml +1 -1
  544. package/rules/latin-abbreviation-form.yaml +58 -0
  545. package/rules/latin-spacing.yaml +6 -2
  546. package/rules/less-for-fewer.yaml +59 -0
  547. package/rules/link-text-url-mismatch.yaml +72 -0
  548. package/rules/list-item-form-mix.yaml +7 -1
  549. package/rules/list-marker-mix.yaml +111 -0
  550. package/rules/max-kanji-continuous.yaml +10 -2
  551. package/rules/max-paragraph-length.yaml +4 -1
  552. package/rules/max-sentence-length.yaml +29 -21
  553. package/rules/max-ten.yaml +4 -1
  554. package/rules/misconversion.yaml +2 -1
  555. package/rules/modal-conflict.yaml +2 -1
  556. package/rules/name-variant.yaml +4 -3
  557. package/rules/ngram-repetition.yaml +5 -0
  558. package/rules/no-doubled-joshi.yaml +5 -1
  559. package/rules/no-em-dash.yaml +4 -1
  560. package/rules/no-lead.yaml +6 -1
  561. package/rules/no-mixed-desumasu.yaml +5 -2
  562. package/rules/no-nakaguro-parallel.yaml +3 -0
  563. package/rules/nominalization.yaml +5 -1
  564. package/rules/nonstandard-word.yaml +80 -0
  565. package/rules/number-style-consistency.yaml +6 -1
  566. package/rules/number-without-unit.yaml +2 -1
  567. package/rules/numbering-gap.yaml +1 -1
  568. package/rules/numeral-sentence-start.yaml +56 -0
  569. package/rules/one-sentence-paragraph-run.yaml +5 -1
  570. package/rules/ordinal-suffix-mismatch.yaml +65 -0
  571. package/rules/orthographic-variant.yaml +4 -1
  572. package/rules/outside-announced-list.yaml +2 -1
  573. package/rules/oxford-comma-consistency.yaml +3 -1
  574. package/rules/padded-intro.yaml +7 -0
  575. package/rules/paragraph-length-variance.yaml +7 -1
  576. package/rules/paragraph-restatement.yaml +6 -1
  577. package/rules/percent-sum-mismatch.yaml +3 -1
  578. package/rules/person-consistency.yaml +4 -1
  579. package/rules/preamble-length.yaml +6 -0
  580. package/rules/preferred-term.yaml +1 -1
  581. package/rules/proper-noun-density.yaml +8 -1
  582. package/rules/quote-style-consistency.yaml +66 -0
  583. package/rules/ra-nuki.yaml +4 -1
  584. package/rules/range-notation-consistency.yaml +73 -0
  585. package/rules/redundant-expression.yaml +4 -1
  586. package/rules/reference-title-mismatch.yaml +4 -3
  587. package/rules/reference-topic-missing.yaml +2 -1
  588. package/rules/relative-date-mismatch.yaml +2 -1
  589. package/rules/repeated-conjunction.yaml +4 -1
  590. package/rules/repeated-sentence-ending.yaml +62 -0
  591. package/rules/repeated-sentence-head.yaml +4 -0
  592. package/rules/request-without-deadline.yaml +92 -0
  593. package/rules/required-sections.yaml +1 -1
  594. package/rules/requirement-modal.yaml +1 -1
  595. package/rules/requirement-smell.yaml +12 -4
  596. package/rules/rule-of-three.yaml +8 -1
  597. package/rules/sasete-itadaku.yaml +7 -1
  598. package/rules/section-length-uniformity.yaml +7 -1
  599. package/rules/sentence-initial-conjunction-run.yaml +5 -1
  600. package/rules/sentence-initial-so.yaml +6 -1
  601. package/rules/sentence-rhythm.yaml +8 -1
  602. package/rules/sentence-spacing-consistency.yaml +63 -0
  603. package/rules/share-over-hundred.yaml +65 -0
  604. package/rules/shell-prompt-mix.yaml +128 -0
  605. package/rules/space-before-punctuation.yaml +1 -1
  606. package/rules/spelling-consistency.yaml +3 -1
  607. package/rules/step-reference-missing.yaml +85 -0
  608. package/rules/step-statement-mix.yaml +76 -0
  609. package/rules/stock-transition.yaml +8 -1
  610. package/rules/stray-space.yaml +2 -2
  611. package/rules/summary-fact-mismatch.yaml +4 -1
  612. package/rules/table-header-variant.yaml +2 -1
  613. package/rules/table-row-overflow.yaml +90 -0
  614. package/rules/taigen-dome-in-prose.yaml +6 -0
  615. package/rules/tari-unpaired.yaml +58 -0
  616. package/rules/teki-overuse.yaml +2 -1
  617. package/rules/title-case-consistency.yaml +1 -1
  618. package/rules/title-length.yaml +7 -1
  619. package/rules/toc-heading-mismatch.yaml +136 -0
  620. package/rules/topic-predicate-distance.yaml +6 -1
  621. package/rules/total-mismatch.yaml +3 -1
  622. package/rules/translationese-density.yaml +4 -1
  623. package/rules/unbalanced-bracket.yaml +3 -3
  624. package/rules/unclosed-code-fence.yaml +107 -0
  625. package/rules/uncomparable-graded.yaml +65 -0
  626. package/rules/undefined-acronym.yaml +8 -1
  627. package/rules/unfilled-placeholder.yaml +1 -1
  628. package/rules/unit-mismatch.yaml +2 -1
  629. package/rules/unknown-word.yaml +4 -1
  630. package/rules/unqualified-superlative.yaml +6 -1
  631. package/rules/unrendered-emphasis.yaml +73 -0
  632. package/rules/unused-definition.yaml +4 -3
  633. package/rules/url-run-on.yaml +1 -1
  634. package/rules/use-before-definition.yaml +4 -3
  635. package/rules/vague-figure-reference.yaml +3 -1
  636. package/rules/vague-link-text.yaml +2 -1
  637. package/rules/vague-word-density.yaml +2 -1
  638. package/rules/very-adjective.yaml +5 -1
  639. package/rules/weasel-word.yaml +6 -1
  640. package/rules/which-that-consistency.yaml +64 -0
  641. package/rules/wordy-phrase.yaml +6 -1
  642. package/rules/yori-as-from.yaml +61 -0
  643. package/skills/chaff/SKILL.md +7 -7
  644. package/src/api.ts +13 -1
  645. package/src/cli-text.ts +22 -7
  646. package/src/cli.ts +8 -9
  647. package/src/command-help.ts +26 -0
  648. package/src/commands/eval.ts +2 -2
  649. package/src/commands/fix-plan.ts +13 -1
  650. package/src/commands/help.ts +8 -0
  651. package/src/commands/init-command.ts +13 -0
  652. package/src/commands/plugin-init.ts +150 -0
  653. package/src/commands/plugin-test.ts +111 -0
  654. package/src/config/load.ts +3 -0
  655. package/src/config/option-problems.ts +2 -2
  656. package/src/config/rule-problems.ts +2 -2
  657. package/src/custom/checked.ts +8 -0
  658. package/src/custom/dsl-fields.ts +102 -0
  659. package/src/custom/load.ts +2 -0
  660. package/src/custom/parse.ts +72 -50
  661. package/src/custom/problems.ts +16 -2
  662. package/src/detectors/amount-for-count.ts +90 -0
  663. package/src/detectors/blank-structure.ts +12 -4
  664. package/src/detectors/clause-shape.ts +199 -0
  665. package/src/detectors/code-fences.ts +113 -0
  666. package/src/detectors/cross-definitions.ts +49 -0
  667. package/src/detectors/cross-facts.ts +60 -0
  668. package/src/detectors/cross-registry/cross-doc-duplicate-definition.ts +4 -0
  669. package/src/detectors/cross-registry/cross-doc-fact-conflict.ts +4 -0
  670. package/src/detectors/currency-notation.ts +90 -0
  671. package/src/detectors/custom.ts +10 -2
  672. package/src/detectors/dangling-opener.ts +74 -0
  673. package/src/detectors/defined-term-form.ts +200 -0
  674. package/src/detectors/email-letter.ts +262 -0
  675. package/src/detectors/footnote-mismatch.ts +72 -0
  676. package/src/detectors/form-minority.ts +21 -0
  677. package/src/detectors/heading-end-mark.ts +82 -0
  678. package/src/detectors/impossible-date.ts +141 -0
  679. package/src/detectors/impossible-time.ts +151 -0
  680. package/src/detectors/link-shape.ts +70 -0
  681. package/src/detectors/list-marker.ts +60 -0
  682. package/src/detectors/mark-shape.ts +202 -0
  683. package/src/detectors/markdown-slip.ts +140 -0
  684. package/src/detectors/numeral-opener.ts +72 -0
  685. package/src/detectors/ordinal-suffix.ts +40 -0
  686. package/src/detectors/range-notation.ts +76 -0
  687. package/src/detectors/registry/adverb-polarity.ts +4 -0
  688. package/src/detectors/registry/amount-for-count.ts +4 -0
  689. package/src/detectors/registry/attachment-missing.ts +4 -0
  690. package/src/detectors/registry/bare-url-mix.ts +4 -0
  691. package/src/detectors/registry/caps-shouting.ts +4 -0
  692. package/src/detectors/registry/code-fence-language.ts +4 -0
  693. package/src/detectors/registry/currency-notation.ts +4 -0
  694. package/src/detectors/registry/dangling-opener.ts +4 -0
  695. package/src/detectors/registry/defined-name-repeated.ts +4 -0
  696. package/src/detectors/registry/defined-term-form.ts +4 -0
  697. package/src/detectors/registry/email-frame.ts +4 -0
  698. package/src/detectors/registry/email-subject.ts +4 -0
  699. package/src/detectors/registry/ending-run.ts +4 -0
  700. package/src/detectors/registry/footnote-mismatch.ts +4 -0
  701. package/src/detectors/registry/heading-command.ts +4 -0
  702. package/src/detectors/registry/heading-end-punctuation.ts +4 -0
  703. package/src/detectors/registry/hyphen-dash.ts +4 -0
  704. package/src/detectors/registry/image-file-name-alt.ts +4 -0
  705. package/src/detectors/registry/impossible-date.ts +4 -0
  706. package/src/detectors/registry/impossible-time.ts +4 -0
  707. package/src/detectors/registry/latin-abbreviation.ts +4 -0
  708. package/src/detectors/registry/link-text-url-mismatch.ts +4 -0
  709. package/src/detectors/registry/list-marker.ts +4 -0
  710. package/src/detectors/registry/mark-consistency.ts +4 -0
  711. package/src/detectors/registry/numeral-opener.ts +4 -0
  712. package/src/detectors/registry/ordinal-suffix.ts +4 -0
  713. package/src/detectors/registry/origin-particle.ts +4 -0
  714. package/src/detectors/registry/range-notation.ts +4 -0
  715. package/src/detectors/registry/relative-pronoun-mix.ts +4 -0
  716. package/src/detectors/registry/request-without-deadline.ts +4 -0
  717. package/src/detectors/registry/sentence-spacing.ts +4 -0
  718. package/src/detectors/registry/share-over-hundred.ts +4 -0
  719. package/src/detectors/registry/shell-prompt.ts +4 -0
  720. package/src/detectors/registry/step-reference.ts +4 -0
  721. package/src/detectors/registry/step-statement.ts +4 -0
  722. package/src/detectors/registry/table-row-overflow.ts +4 -0
  723. package/src/detectors/registry/toc-heading.ts +4 -0
  724. package/src/detectors/registry/unclosed-code-fence.ts +4 -0
  725. package/src/detectors/registry/unpaired-parallel.ts +4 -0
  726. package/src/detectors/registry/unrendered-emphasis.ts +4 -0
  727. package/src/detectors/relative-pronoun-mix.ts +79 -0
  728. package/src/detectors/request-owner.ts +141 -0
  729. package/src/detectors/share-over-hundred.ts +75 -0
  730. package/src/detectors/step-reference.ts +83 -0
  731. package/src/detectors/technical-docs.ts +230 -0
  732. package/src/detectors/toc-heading.ts +125 -0
  733. package/src/extension/pack-read.ts +128 -0
  734. package/src/extension/plugin-load.ts +49 -9
  735. package/src/extension/plugin-parse.ts +10 -3
  736. package/src/facts/address-values.ts +74 -0
  737. package/src/facts/cross-facts.ts +54 -0
  738. package/src/fix-plan/chosen-depth.ts +28 -0
  739. package/src/fix-plan/mode.ts +57 -11
  740. package/src/fix-plan/plan.ts +37 -5
  741. package/src/fix-plan/render.ts +24 -0
  742. package/src/fix-plan/text.ts +47 -10
  743. package/src/genre-load.ts +7 -4
  744. package/src/genre-parse.ts +19 -0
  745. package/src/init.ts +10 -10
  746. package/src/known-genres.ts +19 -0
  747. package/src/link-shape.ts +62 -0
  748. package/src/list-marks.ts +106 -0
  749. package/src/reasons.ts +16 -4
  750. package/src/render/explain.ts +14 -2
  751. package/src/render/friendly.ts +1 -1
  752. package/src/render/rules-json.ts +14 -3
  753. package/src/render/rules-table.ts +6 -6
  754. package/src/render/semantic.ts +2 -2
  755. package/src/render/suppressions.ts +2 -2
  756. package/src/rewrite-depth.ts +60 -0
  757. package/src/rule-fields.ts +123 -0
  758. package/src/rule-guide.ts +9 -0
  759. package/src/rule-load.ts +10 -4
  760. package/src/rule-offs.ts +50 -0
  761. package/src/structure/build.ts +21 -1
  762. package/src/structure/cross-definitions.ts +60 -0
@@ -0,0 +1,151 @@
1
+ import type { Detector, Finding, ProseDocument } from "../plugin.ts";
2
+ import { escapeRegExp } from "../orthography.ts";
3
+ import { quoteAround } from "./quote-around.ts";
4
+
5
+ // 時計に無い時刻(午後13時、13 PM、10:75、9時75分)。午前・午後が付けば時は 12 まで、分と秒は 59 まで。
6
+ // 午前・午後の語とその置き場所は語彙表 clock-meridiem、時・分・秒の単位は date-time-unit(大きい順の四つ目から)が言う。
7
+ // 午前・午後の無い「25:00」は、深夜の番組表のように 24 を超えて数える書き方があるので言わない。
8
+
9
+ /** 時刻を読むのに要る語。before は時刻の前に書く午前・午後、after は後ろに書く AM・PM、units は時・分・秒の単位。 */
10
+ export type ClockWords = { readonly before: readonly string[]; readonly after: readonly string[]; readonly units: readonly string[] };
11
+
12
+ /** 時計に無い時刻 1 つ。reason は、午前・午後の付いた時が 12 を超える(hour)か、分か秒が 59 を超える(minute)か。 */
13
+ export type ImpossibleTime = { readonly offset: number; readonly written: string; readonly reason: "hour" | "minute" };
14
+
15
+ const DIGIT = "[0-90-9]";
16
+ const COLON = "[::]";
17
+ /** 数の前後に来ない字。英字の語や番号の中(abc10:75、v10:75)は時刻ではない。日本語の字のすぐ後ろ(会議は10:75)は読む。 */
18
+ const NOT_AFTER_CLOCK = "(?<![A-Za-z0-90-9_::.])";
19
+ const NOT_BEFORE_CLOCK = "(?![A-Za-z0-90-9_::])";
20
+ const HALF_DAY = 12;
21
+ const MINUTES_IN_HOUR = 60;
22
+ /** 午前・午後の付いた時で、時刻の書き損じと読む上限。50 pm(ピコメートル)のような量を時刻と読まない。 */
23
+ const HOURS_IN_DAY = 24;
24
+ const FULLWIDTH_OFFSET = 0xfee0;
25
+
26
+ const halfWidth = (text: string): string => text.replace(/[0-9]/gu, (char) => String.fromCharCode(char.charCodeAt(0) - FULLWIDTH_OFFSET));
27
+
28
+ type Groups = Readonly<Record<string, string | undefined>>;
29
+
30
+ const numberIn = (groups: Groups, name: string): number | undefined => {
31
+ const written = groups[name];
32
+ return written === undefined ? undefined : Number(halfWidth(written));
33
+ };
34
+
35
+ /** 分か秒が 59 を超えるか。 */
36
+ const isPastMinute = (groups: Groups): boolean =>
37
+ ["m", "colonMinute", "s"].some((name) => {
38
+ const value = numberIn(groups, name);
39
+ return value !== undefined && value >= MINUTES_IN_HOUR;
40
+ });
41
+
42
+ /** 午前・午後の付いた時が 12 を超えるか(24 まで)。 */
43
+ const isPastHalfDay = (groups: Groups): boolean => {
44
+ const hour = numberIn(groups, "h") ?? 0;
45
+ return hour > HALF_DAY && hour <= HOURS_IN_DAY;
46
+ };
47
+
48
+ /** 時刻の書き方と、その中の書き損じの見分け方。 */
49
+ type Shape = { readonly pattern: RegExp; readonly problem: (groups: Groups, text: string, at: number) => ImpossibleTime["reason"] | undefined };
50
+
51
+ const withMeridiem = (groups: Groups): ImpossibleTime["reason"] | undefined => {
52
+ if (isPastHalfDay(groups)) return "hour";
53
+ return isPastMinute(groups) ? "minute" : undefined;
54
+ };
55
+
56
+ const minutesOnly = (groups: Groups): ImpossibleTime["reason"] | undefined => (isPastMinute(groups) ? "minute" : undefined);
57
+
58
+ /** 「Genesis 24:67」「Science, 12:75」のような章と節、巻と頁は時刻ではない。頭が大文字の語や区切りの印のすぐ後ろは読まない。 */
59
+ const isCitationBefore = (before: string): boolean => {
60
+ const trimmed = before.trimEnd();
61
+ if ([",", ";", "."].includes(trimmed.slice(-1))) return true;
62
+ const lastWord = trimmed.split(/\s/u).at(-1) ?? "";
63
+ return /^\p{Lu}/u.test(lastWord);
64
+ };
65
+
66
+ /** 時を 2 以上か 0 埋め(09:75)で書いた、時と分だけの時刻。1:75 は縮尺や比。 */
67
+ const bareClock = (groups: Groups, text: string, at: number): ImpossibleTime["reason"] | undefined => {
68
+ const hourText = groups["h"] ?? "";
69
+ const hour = Number(halfWidth(hourText));
70
+ const looksLikeClock = hour <= HOURS_IN_DAY && (hour >= 2 || /^[00]/u.test(hourText));
71
+ if (!looksLikeClock || isCitationBefore(text.slice(Math.max(0, at - HALF_DAY), at))) return undefined;
72
+ return minutesOnly(groups);
73
+ };
74
+
75
+ const alternation = (words: readonly string[]): string => words.map(escapeRegExp).join("|");
76
+
77
+ const meridiemShapes = (words: ClockWords): Shape[] => {
78
+ const [hourUnit, minuteUnit] = words.units.map(escapeRegExp);
79
+ const before = alternation(words.before);
80
+ const after = alternation(words.after);
81
+ const minuteTail = minuteUnit === undefined ? "" : `(?:\\s?(?<m>${DIGIT}{1,2})${minuteUnit})?`;
82
+ const unitTail = hourUnit === undefined ? "" : `${hourUnit}${minuteTail}|`;
83
+ return [
84
+ ...(after === ""
85
+ ? []
86
+ : [
87
+ {
88
+ pattern: new RegExp(`${NOT_AFTER_CLOCK}(?<![,])(?<h>${DIGIT}{1,2})(?:${COLON}(?<m>${DIGIT}{2}))?\\s?(?:${after})(?![\\p{L}\\p{N}])`, "gu"),
89
+ problem: withMeridiem,
90
+ },
91
+ ]),
92
+ ...(before === ""
93
+ ? []
94
+ : [{ pattern: new RegExp(`(?:${before})\\s?(?<h>${DIGIT}{1,2})(?:${unitTail}[::](?<colonMinute>${DIGIT}{2}))`, "gu"), problem: withMeridiem }]),
95
+ ];
96
+ };
97
+
98
+ const unitShapes = (words: ClockWords): Shape[] => {
99
+ const [hourUnit, minuteUnit, secondUnit] = words.units.map(escapeRegExp);
100
+ if (hourUnit === undefined || minuteUnit === undefined) return [];
101
+ const seconds = secondUnit === undefined ? "" : `(?:\\s?(?<s>${DIGIT}{1,2})${secondUnit})?`;
102
+ return [{ pattern: new RegExp(`(?<!${DIGIT})(?<h>${DIGIT}{1,2})${hourUnit}\\s?(?<m>${DIGIT}{1,2})${minuteUnit}${seconds}`, "gu"), problem: minutesOnly }];
103
+ };
104
+
105
+ const shapesOf = (words: ClockWords): Shape[] => [
106
+ ...meridiemShapes(words),
107
+ ...unitShapes(words),
108
+ {
109
+ pattern: new RegExp(`${NOT_AFTER_CLOCK}(?<h>${DIGIT}{1,3})${COLON}(?<m>${DIGIT}{2})${COLON}(?<s>${DIGIT}{2})${NOT_BEFORE_CLOCK}`, "gu"),
110
+ problem: minutesOnly,
111
+ },
112
+ { pattern: new RegExp(`${NOT_AFTER_CLOCK}(?<![,/])(?<h>${DIGIT}{1,2})${COLON}(?<m>${DIGIT}{2})${NOT_BEFORE_CLOCK}`, "gu"), problem: bareClock },
113
+ ];
114
+
115
+ const impossibleIn = (text: string, shape: Shape): ImpossibleTime[] =>
116
+ [...text.matchAll(shape.pattern)].flatMap((match) => {
117
+ const reason = shape.problem(match.groups ?? {}, text, match.index);
118
+ return reason === undefined ? [] : [{ offset: match.index, written: match[0].trim(), reason }];
119
+ });
120
+
121
+ const overlaps = (left: ImpossibleTime, right: ImpossibleTime): boolean =>
122
+ left.offset < right.offset + right.written.length && right.offset < left.offset + left.written.length;
123
+
124
+ /** 文字列の中の、時計に無い時刻。重なる所に二つの形が当たれば、先の形(午前・午後の付いたもの)だけ。 */
125
+ export const impossibleTimes = (text: string, words: ClockWords): ImpossibleTime[] =>
126
+ shapesOf(words)
127
+ .flatMap((shape) => impossibleIn(text, shape))
128
+ .reduce<ImpossibleTime[]>((kept, found) => (kept.some((earlier) => overlaps(earlier, found)) ? kept : [...kept, found]), [])
129
+ .toSorted((left, right) => left.offset - right.offset);
130
+
131
+ const meridiemOf = (doc: ProseDocument, position: "before" | "after"): string[] =>
132
+ (doc.lexicons["clock-meridiem"] ?? []).filter((entry) => entry.position === position).map((entry) => entry.pattern);
133
+
134
+ export const clockWordsOf = (doc: ProseDocument): ClockWords => ({
135
+ before: meridiemOf(doc, "before"),
136
+ after: meridiemOf(doc, "after"),
137
+ units: (doc.lexicons["date-time-unit"] ?? []).map((entry) => entry.pattern).slice(3),
138
+ });
139
+
140
+ export const impossibleTime: Detector = (doc): Finding[] => {
141
+ const text = doc.prose ?? doc.source;
142
+ return impossibleTimes(text, clockWordsOf(doc)).map((found) => ({
143
+ rule: "",
144
+ severity: "warning",
145
+ line: 0,
146
+ column: 0,
147
+ quote: quoteAround(text, found.offset, found.offset + found.written.length),
148
+ values: { written: found.written, offset: found.offset },
149
+ variant: found.reason,
150
+ }));
151
+ };
@@ -0,0 +1,70 @@
1
+ import type { Detector, Finding, Markup, ProseDocument, Span } from "../plugin.ts";
2
+ import { findingAt, markupOf, quoteOf } from "./markup-finding.ts";
3
+ import { linkTextOf } from "../link-text.ts";
4
+ import { bareUrls } from "../bare-url.ts";
5
+ import { isFileNameAlt, isUrlText, mismatchedHost } from "../link-shape.ts";
6
+ import { minorityWithin } from "../orthography.ts";
7
+ import { readMarkdown, spansOfType } from "../markdown-read.ts";
8
+
9
+ /** A bare URL is reported only while bare URLs are at most this share of the document's links; past it, bare is a style. */
10
+ const BARE_SHARE_PERCENT = 25;
11
+
12
+ /** The link text, or undefined for an autolink, a definition or a link whose text is itself an address. */
13
+ const wordedText = (source: string, link: Span): string | undefined => {
14
+ const text = linkTextOf(source.slice(link.start, link.end));
15
+ return text === undefined || text.trim() === "" || isUrlText(text) ? undefined : text;
16
+ };
17
+
18
+ /** An inline link the reader clicks on words (`[text](url)`). A reference definition's span does not show the words used with it, so it is not counted. */
19
+ const isWorded = (source: string, link: Span): boolean => wordedText(source, link) !== undefined;
20
+
21
+ /** A link destination the parser gave up on (`[a](https://…/{{ page }})`, a space in it): what is left is a link, not a bare URL. */
22
+ const FAILED_DESTINATION = "](";
23
+
24
+ const inside = (span: Span, regions: readonly Span[]): boolean => regions.some((region) => span.start >= region.start && span.start < region.end);
25
+
26
+ /**
27
+ * The bare URLs the reader sees as text (outside links, code and quotations). A URL in a table cell is a value the table lists
28
+ * (an example request, a field's content), not a link the prose points to, so tables are left out.
29
+ */
30
+ const bareUrlSpans = (source: string, markup: Markup): Span[] => {
31
+ const tables = spansOfType(readMarkdown(source).root, "table");
32
+ return markup.texts.flatMap((text) =>
33
+ bareUrls(source.slice(text.start, text.end))
34
+ .map((url) => ({ start: text.start + url.start, end: text.start + url.end }))
35
+ .filter((url) => !source.startsWith(FAILED_DESTINATION, url.start - FAILED_DESTINATION.length) && !inside(url, tables)),
36
+ );
37
+ };
38
+
39
+ type Address = Span & { readonly bare: boolean };
40
+
41
+ /**
42
+ * A URL written bare (`https://…`) in a document whose other links are words (`[the guide](https://…)`). Reported only when the
43
+ * document has at least `limit` worded links and the bare ones are the few; a document that writes its URLs bare throughout is not.
44
+ */
45
+ export const bareUrlMix: Detector = (doc: ProseDocument, options): Finding[] => {
46
+ const markup = markupOf(doc);
47
+ if (markup === undefined) return [];
48
+ const worded: Address[] = markup.links.filter((link) => isWorded(doc.source, link)).map((link) => ({ ...link, bare: false }));
49
+ if (worded.length < options.limit) return [];
50
+ const bare: Address[] = bareUrlSpans(doc.source, markup).map((url) => ({ ...url, bare: true }));
51
+ return minorityWithin([...worded, ...bare], (address) => address.bare, BARE_SHARE_PERCENT)
52
+ .filter((address) => address.bare)
53
+ .map((address) => findingAt(doc, address, { url: quoteOf(doc.source, address), count: worded.length }));
54
+ };
55
+
56
+ /** A link whose text is an address on one host and whose destination is on another (`[https://a.example](https://b.example)`). */
57
+ export const linkTextUrlMismatch: Detector = (doc): Finding[] =>
58
+ (markupOf(doc)?.links ?? []).flatMap((link) => {
59
+ const text = linkTextOf(doc.source.slice(link.start, link.end));
60
+ const hosts = text === undefined ? undefined : mismatchedHost(text, link.destination);
61
+ return hosts === undefined ? [] : [findingAt(doc, link, hosts)];
62
+ });
63
+
64
+ /** An image whose alt text is its file's name (`IMG_1234.png`, `Screenshot 2026-04-01`). The name prefixes come from the lexicon. */
65
+ export const imageFileNameAlt: Detector = (doc, options): Finding[] => {
66
+ const prefixes = (options.lexicon ?? []).map((entry) => entry.pattern);
67
+ return (markupOf(doc)?.images ?? []).flatMap((image) =>
68
+ image.alt !== undefined && isFileNameAlt(image.alt, prefixes) ? [findingAt(doc, image, { alt: image.alt.trim() })] : [],
69
+ );
70
+ };
@@ -0,0 +1,60 @@
1
+ import type { Detector, Finding, ProseDocument } from "../plugin.ts";
2
+ import { findingAt, markupOf } from "./markup-finding.ts";
3
+ import { readMarkdown } from "../markdown-read.ts";
4
+ import { readLists, type ListMark } from "../list-marks.ts";
5
+
6
+ /** Fewer lists than this at one depth, and the majority bullet is not yet a habit. */
7
+ const MIN_LISTS = 3;
8
+
9
+ /** Past this share, the document uses both bullets on purpose. */
10
+ const MINORITY_PERCENT = 25;
11
+
12
+ const PERCENT = 100;
13
+
14
+ /** The length of the bullet quoted: the bullet itself. */
15
+ const BULLET_LENGTH = 1;
16
+
17
+ const at = (doc: ProseDocument, start: number, values: Readonly<Record<string, string | number>>, variant: string): Finding =>
18
+ findingAt(doc, { start, end: start + BULLET_LENGTH }, values, variant);
19
+
20
+ /** The bullet most lists use; undefined on a tie. */
21
+ const majorityOf = (lists: readonly ListMark[]): { readonly mark: string; readonly count: number } | undefined => {
22
+ const counts = [...lists.reduce((acc, list) => acc.set(list.mark, (acc.get(list.mark) ?? 0) + 1), new Map<string, number>())].toSorted(
23
+ ([, left], [, right]) => right - left,
24
+ );
25
+ const [first, second] = counts;
26
+ return first === undefined || second?.[1] === first[1] ? undefined : { mark: first[0], count: first[1] };
27
+ };
28
+
29
+ /** Lists whose bullet differs from the one most lists of the same depth use, while those are the few. */
30
+ const minorityAt = (doc: ProseDocument, lists: readonly ListMark[]): Finding[] => {
31
+ const majority = lists.length < MIN_LISTS ? undefined : majorityOf(lists);
32
+ if (majority === undefined) return [];
33
+ const odd = lists.filter((list) => list.mark !== majority.mark);
34
+ if (odd.length * PERCENT > MINORITY_PERCENT * lists.length) return [];
35
+ return odd.map((list) => at(doc, list.start, { mark: list.mark, usual: majority.mark, count: majority.count }, "document"));
36
+ };
37
+
38
+ /** A list written right after another in the same place with another bullet: Markdown starts a new list where the bullet changes. */
39
+ const splitAt = (doc: ProseDocument, list: ListMark): Finding[] =>
40
+ list.previous === undefined || list.previous === list.mark ? [] : [at(doc, list.start, { mark: list.mark, usual: list.previous }, "split")];
41
+
42
+ /**
43
+ * Bullets mixed in one list or one document: a list that changes bullet partway (`-` then `*`, which Markdown splits in two),
44
+ * a `・` line inside a list item (Markdown joins it to the item), and a list whose bullet differs from most lists at its depth.
45
+ */
46
+ export const listMarkerMix: Detector = (doc): Finding[] => {
47
+ if (markupOf(doc) === undefined) return [];
48
+ const { lists, foldedTyped } = readLists(doc.source, readMarkdown(doc.source).root);
49
+ const split = lists.flatMap((list) => splitAt(doc, list));
50
+ const folded = foldedTyped.map((start) => at(doc, start, { mark: "・" }, "folded"));
51
+ const reported = new Set(split.map((finding) => finding.values["offset"]));
52
+ const byDepth = new Map<number, ListMark[]>();
53
+ lists.forEach((list) => {
54
+ const same = byDepth.get(list.depth) ?? [];
55
+ same.push(list);
56
+ byDepth.set(list.depth, same);
57
+ });
58
+ const minority = [...byDepth.values()].flatMap((same) => minorityAt(doc, same)).filter((finding) => !reported.has(finding.values["offset"]));
59
+ return [...split, ...folded, ...minority].toSorted((left, right) => Number(left.values["offset"]) - Number(right.values["offset"]));
60
+ };
@@ -0,0 +1,202 @@
1
+ import type { Detector, Finding, Lexicon, ProseDocument, Sentence } from "../plugin.ts";
2
+ import { escapeRegExp } from "../orthography.ts";
3
+ import { minorityReport } from "../spacing-minority.ts";
4
+ import { isWithinAny, quotedSpans } from "../quoted-span.ts";
5
+
6
+ /** The letters a mark must touch to count as part of the document's text, by language. */
7
+ const SCRIPT: Readonly<Record<string, RegExp>> = {
8
+ ja: /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}ー々]/u,
9
+ en: /\p{Script=Latin}/u,
10
+ };
11
+
12
+ /** One way of writing a mark: its kind, which of the two ways it is (side), and how the other way writes it. */
13
+ type Form = { readonly kind: string; readonly side: boolean; readonly other: string };
14
+
15
+ /**
16
+ * The two ways of writing each kind of mark, from the lexicon: pattern is one way, instead_of the other, group the kind.
17
+ * Two patterns may share one instead_of (“ and ” both against "), so the other way of a shared mark is both patterns.
18
+ */
19
+ export const formsOf = (lexicon: Lexicon): ReadonlyMap<string, Form> => {
20
+ const forms = new Map<string, Form>();
21
+ lexicon.forEach((entry) => {
22
+ const kind = entry.group ?? entry.pattern;
23
+ const shared = entry.instead_of ?? "";
24
+ forms.set(entry.pattern, { kind, side: true, other: shared });
25
+ const known = forms.get(shared);
26
+ forms.set(shared, { kind, side: false, other: `${known?.side === false ? known.other : ""}${entry.pattern}` });
27
+ });
28
+ return forms;
29
+ };
30
+
31
+ export type Mark = { readonly sentence: Sentence; readonly char: string; readonly form: Form; readonly offset: number; readonly spaced: boolean };
32
+
33
+ /** Punctuation that may stand between a closing or opening mark and the word it belongs to ("draft." and “draft”.). */
34
+ const BETWEEN = /[.,;:!?。、]/u;
35
+
36
+ /** The letter beside a mark on one side (step -1 or 1), past one punctuation mark. */
37
+ const besideLetter = (text: string, at: number, step: number, script: RegExp): boolean => {
38
+ const next = text.charAt(at + step);
39
+ return script.test(next) || (BETWEEN.test(next) && script.test(text.charAt(at + step * 2)));
40
+ };
41
+
42
+ const touchesScript = (text: string, at: number, script: RegExp): boolean => besideLetter(text, at, -1, script) || besideLetter(text, at, 1, script);
43
+
44
+ /** The marks in a sentence that touch a letter of the document's script, outside 「」『』 quotations. spaced is the lexicon's pattern side. */
45
+ export const marksIn = (sentence: Sentence, forms: ReadonlyMap<string, Form>, script: RegExp): Mark[] => {
46
+ const quoted = quotedSpans(sentence.text);
47
+ return sentence.text.split("").flatMap((char, at) => {
48
+ const form = forms.get(char);
49
+ if (form === undefined || !touchesScript(sentence.text, at, script)) return [];
50
+ if (isWithinAny(quoted, { start: at, end: at })) return [];
51
+ return [{ sentence, char, form, offset: sentence.span.start + at, spaced: form.side }];
52
+ });
53
+ };
54
+
55
+ /** Something written one of two ways (spaced is which), at a place in a sentence. */
56
+ type Sided = { readonly sentence: Sentence; readonly offset: number; readonly spaced: boolean };
57
+
58
+ const sidedFinding = (item: Sided, values: Finding["values"], variant?: string): Finding => ({
59
+ rule: "",
60
+ severity: "info",
61
+ line: 0,
62
+ column: 0,
63
+ quote: item.sentence.text.trim(),
64
+ ...(variant === undefined ? {} : { variant }),
65
+ values: { ...values, offset: item.offset },
66
+ });
67
+
68
+ /** The less common way, one finding each, or one finding for a document written both ways (spacing-minority.ts). */
69
+ const minorityFindings = <T extends Sided>(items: readonly T[], limit: number, valuesOf: (item: T) => Finding["values"]): Finding[] => {
70
+ const report = minorityReport(items, limit);
71
+ if (report === undefined) return [];
72
+ if (report.mode === "mixed") {
73
+ const { first, odd, spaced, touching } = report;
74
+ return [sidedFinding(first, { ...valuesOf(first), spaced, touching, count: odd, of: items.length, limit }, "mixed")];
75
+ }
76
+ return report.odd.map((item) => sidedFinding(item, { ...valuesOf(item), count: report.odd.length, of: items.length, limit }));
77
+ };
78
+
79
+ /** Sentences written in the document's own language; a quoted sentence in another language follows its own marks. */
80
+ const ownSentences = (doc: ProseDocument): readonly Sentence[] => doc.sentences.filter((sentence) => sentence.embeddedLanguage === undefined);
81
+
82
+ /**
83
+ * A mark written two ways in one document (( and (, “ and "), the less common way reported. The two ways come from the
84
+ * lexicon; neither is called right. Only marks next to a letter of the document's script count, so a bracket in code-like
85
+ * text (f(x)) or an English aside in a Japanese document does not.
86
+ */
87
+ export const markConsistency: Detector = (doc, options): Finding[] => {
88
+ const script = SCRIPT[doc.language];
89
+ if (script === undefined) return [];
90
+ const forms = formsOf(options.lexicon ?? []);
91
+ const marks = ownSentences(doc).flatMap((sentence) => marksIn(sentence, forms, script));
92
+ const kinds = [...new Set(marks.map((mark) => mark.form.kind))];
93
+ return kinds.flatMap((kind) =>
94
+ minorityFindings(
95
+ marks.filter((mark) => mark.form.kind === kind),
96
+ options.limit,
97
+ (mark) => ({ mark: mark.char, usual: mark.form.other }),
98
+ ),
99
+ );
100
+ };
101
+
102
+ /**
103
+ * A sentence that surely ends: a lower-case word of two letters or more, then the mark. A number ("11.1."), an abbreviation
104
+ * ("Req.", "e.g.") or a capital ("U.S.") may be a split the adapter made inside a sentence, where one space is usual.
105
+ */
106
+ const SENTENCE_END = /(?<![\p{L}\d.])\p{Ll}{2,}[.!?]["'”’)\]]?$/u;
107
+ /** The next sentence starts with a capital, past an opening quote: not a citation ("[ISO99]") or an aside in brackets. */
108
+ const SENTENCE_START = /^["'“‘]?\p{Lu}/u;
109
+ const ONE_SPACE = " ";
110
+ const TWO_SPACES = " ";
111
+
112
+ /** The space between two sentences on one line, when the first ends with a period, question or exclamation mark. spaced is two spaces. */
113
+ export const sentenceGaps = (doc: ProseDocument): Sided[] =>
114
+ ownSentences(doc).flatMap((sentence, at, sentences) => {
115
+ const next = sentences[at + 1];
116
+ if (next === undefined || !SENTENCE_END.test(sentence.text) || !SENTENCE_START.test(next.text)) return [];
117
+ const gap = doc.source.slice(sentence.span.end, next.span.start);
118
+ if (gap !== ONE_SPACE && gap !== TWO_SPACES) return [];
119
+ return [{ sentence: next, spaced: gap === TWO_SPACES, offset: sentence.span.end }];
120
+ });
121
+
122
+ /** One space between sentences and two, mixed in one document: the less common is reported. */
123
+ export const sentenceSpacing: Detector = (doc, options): Finding[] =>
124
+ minorityFindings(sentenceGaps(doc), options.limit, (gap) => ({
125
+ spaces: gap.spaced ? TWO_SPACES.length : ONE_SPACE.length,
126
+ usual: gap.spaced ? ONE_SPACE.length : TWO_SPACES.length,
127
+ }));
128
+
129
+ type Hit = { readonly sentence: Sentence; readonly matched: string; readonly offset: number };
130
+
131
+ const hitFinding = (hit: Hit, count: number, limit: number): Finding => ({
132
+ rule: "",
133
+ severity: "info",
134
+ line: 0,
135
+ column: 0,
136
+ quote: hit.sentence.text.trim(),
137
+ values: { matched: hit.matched, count, limit, offset: hit.offset },
138
+ });
139
+
140
+ const hitsOf = (sentences: readonly Sentence[], pattern: RegExp): Hit[] =>
141
+ sentences.flatMap((sentence) =>
142
+ [...sentence.text.matchAll(pattern)].map((match) => ({ sentence, matched: match[0].trim(), offset: sentence.span.start + match.index })),
143
+ );
144
+
145
+ const reportFrom = (hits: readonly Hit[], limit: number): Finding[] => (hits.length < limit ? [] : hits.map((hit) => hitFinding(hit, hits.length, limit)));
146
+
147
+ /** A spaced hyphen or a double hyphen between words, standing for a dash ("late - very late", "late--very late"). */
148
+ const HYPHEN_DASH = /(?<=[\p{L},;)]) -{1,2} (?=[\p{L}(])|(?<=\p{L})--(?=\p{L})/gu;
149
+
150
+ export const hyphenDash: Detector = (doc, options): Finding[] => reportFrom(hitsOf(ownSentences(doc), HYPHEN_DASH), options.limit);
151
+
152
+ /** Every way of writing an abbreviation with some of its periods dropped but not all (e.g. → e.g, eg.). */
153
+ export const partialForms = (abbreviation: string): string[] => {
154
+ const dots = [...abbreviation].flatMap((char, at) => (char === "." ? [at] : []));
155
+ const subsets = Array.from({ length: 2 ** dots.length }, (_unused, mask) => dots.filter((_dot, bit) => (mask >> bit) % 2 === 1));
156
+ return subsets
157
+ .filter((dropped) => dropped.length > 0 && dropped.length < dots.length)
158
+ .map((dropped) => [...abbreviation].filter((_char, at) => !dropped.includes(at)).join(""));
159
+ };
160
+
161
+ /** One partial form as a pattern: its first letter in either case, not inside a word, not followed by a period or letter. */
162
+ const partialPattern = (form: string): string =>
163
+ `(?<![\\p{L}.])[${form.charAt(0).toLowerCase()}${form.charAt(0).toUpperCase()}]${escapeRegExp(form.slice(1))}(?![\\p{L}.])`;
164
+
165
+ /** Latin abbreviations from the lexicon (e.g., i.e.) written with only some of their periods (e.g, eg.). */
166
+ export const latinAbbreviation: Detector = (doc, options): Finding[] => {
167
+ const forms = (options.lexicon ?? []).flatMap((entry) => partialForms(entry.pattern));
168
+ if (forms.length === 0) return [];
169
+ const pattern = new RegExp(forms.map(partialPattern).join("|"), "gu");
170
+ return reportFrom(hitsOf(ownSentences(doc), pattern), options.limit);
171
+ };
172
+
173
+ /** How many all-capital words in a row read as shouting rather than a run of acronyms. */
174
+ const SHOUT_WORDS = 3;
175
+ const CAPS_RUN = new RegExp(`\\b[A-Z][A-Z']*(?:,? [A-Z][A-Z']*){${String(SHOUT_WORDS - 1)},}\\b`, "gu");
176
+ /** One-letter words (I, A) may sit in a run, but a run of them alone ("A B C") is a list of labels. */
177
+ const MIN_LONG_WORDS = 2;
178
+
179
+ const LATIN_WORD = /[A-Za-z][A-Za-z']*/gu;
180
+
181
+ /** The words a source writes at least once not wholly in capitals (do, Do), in lower case: words, not acronyms. */
182
+ export const lowerCaseWords = (source: string): ReadonlySet<string> =>
183
+ new Set((source.match(LATIN_WORD) ?? []).filter((word) => word !== word.toUpperCase()).map((word) => word.toLowerCase()));
184
+
185
+ /**
186
+ * Whether more than half the words of a capital run are also written elsewhere in lower case. Words the lexicon lists as
187
+ * written in capitals by convention (MUST NOT in a specification) count like acronyms.
188
+ */
189
+ export const isShouting = (run: string, known: ReadonlySet<string>, conventional: ReadonlySet<string>): boolean => {
190
+ const words = run.split(/,? /u);
191
+ const ordinary = words.filter((word) => !conventional.has(word) && known.has(word.toLowerCase()));
192
+ const long = words.filter((word) => word.length > 1);
193
+ return long.length >= MIN_LONG_WORDS && ordinary.length * 2 > words.length;
194
+ };
195
+
196
+ /** Runs of three or more all-capital words that are ordinary words, not acronyms ("DO NOT DELETE THIS"). */
197
+ export const capsShouting: Detector = (doc, options): Finding[] => {
198
+ const known = lowerCaseWords(doc.prose ?? doc.source);
199
+ const conventional = new Set((options.lexicon ?? []).map((entry) => entry.pattern));
200
+ const hits = hitsOf(ownSentences(doc), CAPS_RUN).filter((hit) => isShouting(hit.matched, known, conventional));
201
+ return reportFrom(hits, options.limit);
202
+ };
@@ -0,0 +1,140 @@
1
+ // Markdown that does not render the way it was written: a table row with more cells than its header (GFM drops the
2
+ // extra ones), a code fence that is never closed (the rest of the document becomes code), and strong-emphasis marks
3
+ // (`**`) that Markdown cannot pair and shows as they are. Pure; reads the source's Markdown tree.
4
+ import type { Detector, Finding, ProseDocument, Span } from "../plugin.ts";
5
+ import { readMarkdown } from "../markdown-read.ts";
6
+ import { spanOf, type MarkdownNode } from "../markdown-node.ts";
7
+ import { findingAt, markupOf } from "./markup-finding.ts";
8
+
9
+ /** Quotations are someone else's text, and their markup is theirs. */
10
+ const QUOTED = "blockquote";
11
+
12
+ /** Nodes of one type outside quotations, in source order. Walks with its own stack: Markdown nests without limit. */
13
+ const nodesOf = (root: MarkdownNode, type: string): MarkdownNode[] => {
14
+ const found: MarkdownNode[] = [];
15
+ const pending: MarkdownNode[] = [root];
16
+ for (let node = pending.pop(); node !== undefined; node = pending.pop()) {
17
+ if (node.type === QUOTED) continue;
18
+ if (node.type === type) found.push(node);
19
+ (node.children ?? []).toReversed().forEach((child) => pending.push(child));
20
+ }
21
+ return found;
22
+ };
23
+
24
+ const markdownRoot = (doc: ProseDocument): MarkdownNode | undefined => (markupOf(doc) === undefined ? undefined : readMarkdown(doc.source).root);
25
+
26
+ /** An HTML comment: a cell holding only one shows the reader nothing, and losing it loses nothing. */
27
+ const COMMENT = /<!--[\s\S]*?-->/gu;
28
+
29
+ /** Whether a cell shows the reader anything: its source without the bars, comments and spaces. */
30
+ const cellShows = (source: string, cell: MarkdownNode): boolean => {
31
+ const span = spanOf(cell);
32
+ return span !== undefined && source.slice(span.start, span.end).replaceAll("|", "").replace(COMMENT, "").trim() !== "";
33
+ };
34
+
35
+ /**
36
+ * A body row with more cells than the header: GFM shows only as many cells as the header has, and drops the rest. Extra cells
37
+ * that are empty or hold only a comment lose nothing and are not reported.
38
+ */
39
+ export const tableRowOverflow: Detector = (doc): Finding[] => {
40
+ const root = markdownRoot(doc);
41
+ if (root === undefined) return [];
42
+ return nodesOf(root, "table").flatMap((table) => {
43
+ const [header, ...body] = table.children ?? [];
44
+ const columns = header?.children?.length ?? 0;
45
+ return body.flatMap((row) => {
46
+ const cells = row.children ?? [];
47
+ const span = spanOf(row);
48
+ const lost = cells.slice(columns).some((cell) => cellShows(doc.source, cell));
49
+ return lost && span !== undefined ? [findingAt(doc, span, { cells: cells.length, columns })] : [];
50
+ });
51
+ });
52
+ };
53
+
54
+ /** A fence opening a code block: three or more backticks or tildes, after up to three spaces. */
55
+ const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/u;
56
+
57
+ /** A closing fence may sit at most this many columns further in than the container's text; four in, it is code. */
58
+ const MAX_FENCE_INDENT = 3;
59
+
60
+ const TAB_WIDTH = 4;
61
+
62
+ const indentOf = (line: string): number =>
63
+ Array.from(line.slice(0, line.length - line.trimStart().length)).reduce((width, char) => width + (char === "\t" ? TAB_WIDTH : 1), 0);
64
+
65
+ /** Whether a line closes a fence: not indented past the limit, the same character, at least as many, and nothing after but spaces. */
66
+ const closes = (line: string, fence: string, maxIndent: number): boolean => {
67
+ const trimmed = line.trim();
68
+ const mark = fence.charAt(0);
69
+ return indentOf(line) <= maxIndent && trimmed.length >= fence.length && [...trimmed].every((char) => char === mark);
70
+ };
71
+
72
+ /** The opening fence of a code block that is never closed; undefined for a closed or an indented block. */
73
+ const unclosedFenceOf = (source: string, span: Span): Span | undefined => {
74
+ const lines = source.slice(span.start, span.end).split("\n");
75
+ const first = lines[0] ?? "";
76
+ const fence = FENCE_OPEN.exec(first)?.[1];
77
+ if (fence === undefined) return undefined;
78
+ const column = span.start - (source.lastIndexOf("\n", span.start - 1) + 1);
79
+ const last = lines.length > 1 ? (lines.at(-1) ?? "") : "";
80
+ return closes(last, fence, column + MAX_FENCE_INDENT) ? undefined : { start: span.start, end: span.start + first.trimEnd().length };
81
+ };
82
+
83
+ /**
84
+ * A fenced code block with no closing fence. Markdown runs it to the end of the document (or of the list item it is in), so
85
+ * everything after the opening fence shows as code. Reported at the opening fence.
86
+ */
87
+ export const unclosedCodeFence: Detector = (doc): Finding[] => {
88
+ const root = markdownRoot(doc);
89
+ if (root === undefined) return [];
90
+ return nodesOf(root, "code").flatMap((code) => {
91
+ const span = spanOf(code);
92
+ const open = span === undefined ? undefined : unclosedFenceOf(doc.source, span);
93
+ return open === undefined ? [] : [findingAt(doc, open, { fence: doc.source.slice(open.start, open.end).trim() })];
94
+ });
95
+ };
96
+
97
+ /** `**` or `__` left in text: Markdown would have made it strong emphasis if it could pair it. Longer runs are rules or blanks to fill. */
98
+ const STRONG_RUN = /(?<![*_])(?:\*\*|__)(?![*_])/gu;
99
+
100
+ /** A run with Latin letters or digits on both sides (`2**10`, `snake__case`) is part of the word. Japanese has no spaces, so 「は**金」 is a mark. */
101
+ const WORD_CHAR = /[A-Za-z0-9]/u;
102
+
103
+ const isSpace = (char: string): boolean => char === "" || /\s/u.test(char);
104
+
105
+ /** Python's dunder names (`__init__`) written outside code. */
106
+ const DUNDER = /__\w+__/u;
107
+
108
+ const dunderAround = (text: string, run: Span): boolean => {
109
+ const start = text.lastIndexOf(" ", run.start) + 1;
110
+ const end = text.indexOf(" ", run.end);
111
+ return DUNDER.test(text.slice(start, end === -1 ? text.length : end));
112
+ };
113
+
114
+ /** The strong-emphasis runs in one text node that Markdown left as text. */
115
+ export const strayStrongRuns = (text: string): Span[] =>
116
+ [...text.matchAll(STRONG_RUN)].flatMap((match) => {
117
+ const run = { start: match.index, end: match.index + match[0].length };
118
+ const before = text.charAt(run.start - 1);
119
+ const after = text.charAt(run.end);
120
+ if (before === "\\" || (WORD_CHAR.test(before) && WORD_CHAR.test(after))) return [];
121
+ if (isSpace(before) && isSpace(after)) return [];
122
+ return dunderAround(text, run) ? [] : [run];
123
+ });
124
+
125
+ /**
126
+ * `**` or `__` that Markdown shows as it is: strong emphasis left open (`**bold`), or one CommonMark cannot close because a
127
+ * bracket or a quotation mark touches the mark from inside while a letter touches it from outside (`**「重要」**です`).
128
+ */
129
+ export const unrenderedEmphasis: Detector = (doc): Finding[] => {
130
+ const root = markdownRoot(doc);
131
+ if (root === undefined) return [];
132
+ return nodesOf(root, "text").flatMap((node) => {
133
+ const span = spanOf(node);
134
+ if (span === undefined) return [];
135
+ const written = doc.source.slice(span.start, span.end);
136
+ return strayStrongRuns(written).map((run) =>
137
+ findingAt(doc, { start: span.start + run.start, end: span.start + run.end }, { mark: written.slice(run.start, run.end) }),
138
+ );
139
+ });
140
+ };