chaffjs 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/dist/address-chain.d.ts +4 -1
  2. package/dist/address-chain.d.ts.map +1 -1
  3. package/dist/address-chain.js +5 -2
  4. package/dist/address-chain.js.map +1 -1
  5. package/dist/cli-text.d.ts +4 -1
  6. package/dist/cli-text.d.ts.map +1 -1
  7. package/dist/cli-text.js +14 -2
  8. package/dist/cli-text.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +21 -21
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/eval.d.ts +4 -0
  13. package/dist/commands/eval.d.ts.map +1 -1
  14. package/dist/commands/eval.js +20 -6
  15. package/dist/commands/eval.js.map +1 -1
  16. package/dist/commands/feedback.d.ts.map +1 -1
  17. package/dist/commands/feedback.js +2 -1
  18. package/dist/commands/feedback.js.map +1 -1
  19. package/dist/commands/test.d.ts.map +1 -1
  20. package/dist/commands/test.js +3 -2
  21. package/dist/commands/test.js.map +1 -1
  22. package/dist/commands/tree.d.ts.map +1 -1
  23. package/dist/commands/tree.js +2 -1
  24. package/dist/commands/tree.js.map +1 -1
  25. package/dist/detectors/acronym-compound.d.ts +8 -0
  26. package/dist/detectors/acronym-compound.d.ts.map +1 -0
  27. package/dist/detectors/acronym-compound.js +8 -0
  28. package/dist/detectors/acronym-compound.js.map +1 -0
  29. package/dist/detectors/acronym-context.d.ts +6 -0
  30. package/dist/detectors/acronym-context.d.ts.map +1 -1
  31. package/dist/detectors/acronym-context.js +24 -1
  32. package/dist/detectors/acronym-context.js.map +1 -1
  33. package/dist/detectors/acronym-expansion.d.ts +10 -0
  34. package/dist/detectors/acronym-expansion.d.ts.map +1 -1
  35. package/dist/detectors/acronym-expansion.js +63 -5
  36. package/dist/detectors/acronym-expansion.js.map +1 -1
  37. package/dist/detectors/char-shape.d.ts.map +1 -1
  38. package/dist/detectors/char-shape.js +38 -16
  39. package/dist/detectors/char-shape.js.map +1 -1
  40. package/dist/detectors/citation-key.d.ts +13 -0
  41. package/dist/detectors/citation-key.d.ts.map +1 -0
  42. package/dist/detectors/citation-key.js +9 -0
  43. package/dist/detectors/citation-key.js.map +1 -0
  44. package/dist/detectors/cited-name.d.ts +11 -0
  45. package/dist/detectors/cited-name.d.ts.map +1 -0
  46. package/dist/detectors/cited-name.js +15 -0
  47. package/dist/detectors/cited-name.js.map +1 -0
  48. package/dist/detectors/concrete-evidence.d.ts +7 -0
  49. package/dist/detectors/concrete-evidence.d.ts.map +1 -0
  50. package/dist/detectors/concrete-evidence.js +21 -0
  51. package/dist/detectors/concrete-evidence.js.map +1 -0
  52. package/dist/detectors/conjugated-form.d.ts +3 -0
  53. package/dist/detectors/conjugated-form.d.ts.map +1 -0
  54. package/dist/detectors/conjugated-form.js +29 -0
  55. package/dist/detectors/conjugated-form.js.map +1 -0
  56. package/dist/detectors/counter-edge.d.ts +8 -0
  57. package/dist/detectors/counter-edge.d.ts.map +1 -0
  58. package/dist/detectors/counter-edge.js +27 -0
  59. package/dist/detectors/counter-edge.js.map +1 -0
  60. package/dist/detectors/dotted-name.d.ts +13 -0
  61. package/dist/detectors/dotted-name.d.ts.map +1 -0
  62. package/dist/detectors/dotted-name.js +10 -0
  63. package/dist/detectors/dotted-name.js.map +1 -0
  64. package/dist/detectors/doubled-word.d.ts +6 -1
  65. package/dist/detectors/doubled-word.d.ts.map +1 -1
  66. package/dist/detectors/doubled-word.js +22 -2
  67. package/dist/detectors/doubled-word.js.map +1 -1
  68. package/dist/detectors/en-shape.d.ts +1 -1
  69. package/dist/detectors/en-shape.d.ts.map +1 -1
  70. package/dist/detectors/en-shape.js +11 -22
  71. package/dist/detectors/en-shape.js.map +1 -1
  72. package/dist/detectors/enumerated-runs.d.ts +22 -0
  73. package/dist/detectors/enumerated-runs.d.ts.map +1 -0
  74. package/dist/detectors/enumerated-runs.js +53 -0
  75. package/dist/detectors/enumerated-runs.js.map +1 -0
  76. package/dist/detectors/gram-place.d.ts +0 -4
  77. package/dist/detectors/gram-place.d.ts.map +1 -1
  78. package/dist/detectors/gram-place.js +6 -1
  79. package/dist/detectors/gram-place.js.map +1 -1
  80. package/dist/detectors/gram-predicate.d.ts +7 -0
  81. package/dist/detectors/gram-predicate.d.ts.map +1 -0
  82. package/dist/detectors/gram-predicate.js +44 -0
  83. package/dist/detectors/gram-predicate.js.map +1 -0
  84. package/dist/detectors/heading-case.d.ts +21 -0
  85. package/dist/detectors/heading-case.d.ts.map +1 -0
  86. package/dist/detectors/heading-case.js +38 -0
  87. package/dist/detectors/heading-case.js.map +1 -0
  88. package/dist/detectors/heading-echo.d.ts.map +1 -1
  89. package/dist/detectors/heading-echo.js +4 -1
  90. package/dist/detectors/heading-echo.js.map +1 -1
  91. package/dist/detectors/lexicon.d.ts.map +1 -1
  92. package/dist/detectors/lexicon.js +7 -4
  93. package/dist/detectors/lexicon.js.map +1 -1
  94. package/dist/detectors/middle-dot.d.ts +6 -0
  95. package/dist/detectors/middle-dot.d.ts.map +1 -0
  96. package/dist/detectors/middle-dot.js +9 -0
  97. package/dist/detectors/middle-dot.js.map +1 -0
  98. package/dist/detectors/name-numeral.d.ts +16 -0
  99. package/dist/detectors/name-numeral.d.ts.map +1 -0
  100. package/dist/detectors/name-numeral.js +30 -0
  101. package/dist/detectors/name-numeral.js.map +1 -0
  102. package/dist/detectors/name-part.d.ts +8 -0
  103. package/dist/detectors/name-part.d.ts.map +1 -0
  104. package/dist/detectors/name-part.js +20 -0
  105. package/dist/detectors/name-part.js.map +1 -0
  106. package/dist/detectors/name-run.d.ts +8 -0
  107. package/dist/detectors/name-run.d.ts.map +1 -0
  108. package/dist/detectors/name-run.js +13 -0
  109. package/dist/detectors/name-run.js.map +1 -0
  110. package/dist/detectors/orthography.d.ts.map +1 -1
  111. package/dist/detectors/orthography.js +18 -6
  112. package/dist/detectors/orthography.js.map +1 -1
  113. package/dist/detectors/placeholder.d.ts +14 -0
  114. package/dist/detectors/placeholder.d.ts.map +1 -0
  115. package/dist/detectors/placeholder.js +24 -0
  116. package/dist/detectors/placeholder.js.map +1 -0
  117. package/dist/detectors/polite-word.d.ts +4 -0
  118. package/dist/detectors/polite-word.d.ts.map +1 -0
  119. package/dist/detectors/polite-word.js +10 -0
  120. package/dist/detectors/polite-word.js.map +1 -0
  121. package/dist/detectors/quoted-variant.d.ts +7 -0
  122. package/dist/detectors/quoted-variant.d.ts.map +1 -0
  123. package/dist/detectors/quoted-variant.js +18 -0
  124. package/dist/detectors/quoted-variant.js.map +1 -0
  125. package/dist/detectors/register.d.ts +7 -5
  126. package/dist/detectors/register.d.ts.map +1 -1
  127. package/dist/detectors/register.js +6 -4
  128. package/dist/detectors/register.js.map +1 -1
  129. package/dist/detectors/roman-numeral.d.ts +6 -0
  130. package/dist/detectors/roman-numeral.d.ts.map +1 -0
  131. package/dist/detectors/roman-numeral.js +6 -0
  132. package/dist/detectors/roman-numeral.js.map +1 -0
  133. package/dist/detectors/sentence-ending.d.ts.map +1 -1
  134. package/dist/detectors/sentence-ending.js +14 -4
  135. package/dist/detectors/sentence-ending.js.map +1 -1
  136. package/dist/detectors/series-label.d.ts +13 -0
  137. package/dist/detectors/series-label.d.ts.map +1 -0
  138. package/dist/detectors/series-label.js +26 -0
  139. package/dist/detectors/series-label.js.map +1 -0
  140. package/dist/detectors/signals.d.ts.map +1 -1
  141. package/dist/detectors/signals.js +37 -25
  142. package/dist/detectors/signals.js.map +1 -1
  143. package/dist/detectors/structure-tree.js +1 -1
  144. package/dist/detectors/structure-tree.js.map +1 -1
  145. package/dist/detectors/structure.d.ts.map +1 -1
  146. package/dist/detectors/structure.js +2 -1
  147. package/dist/detectors/structure.js.map +1 -1
  148. package/dist/detectors/superlative-name.d.ts +9 -0
  149. package/dist/detectors/superlative-name.d.ts.map +1 -0
  150. package/dist/detectors/superlative-name.js +24 -0
  151. package/dist/detectors/superlative-name.js.map +1 -0
  152. package/dist/detectors/token-shape.d.ts.map +1 -1
  153. package/dist/detectors/token-shape.js +10 -2
  154. package/dist/detectors/token-shape.js.map +1 -1
  155. package/dist/document.d.ts.map +1 -1
  156. package/dist/document.js +10 -4
  157. package/dist/document.js.map +1 -1
  158. package/dist/genre-check.d.ts +34 -0
  159. package/dist/genre-check.d.ts.map +1 -0
  160. package/dist/genre-check.js +21 -0
  161. package/dist/genre-check.js.map +1 -0
  162. package/dist/genre.d.ts +8 -1
  163. package/dist/genre.d.ts.map +1 -1
  164. package/dist/genre.js +29 -5
  165. package/dist/genre.js.map +1 -1
  166. package/dist/init.d.ts +0 -1
  167. package/dist/init.d.ts.map +1 -1
  168. package/dist/init.js +0 -12
  169. package/dist/init.js.map +1 -1
  170. package/dist/joined-view.d.ts +25 -0
  171. package/dist/joined-view.d.ts.map +1 -0
  172. package/dist/joined-view.js +76 -0
  173. package/dist/joined-view.js.map +1 -0
  174. package/dist/line-continues.d.ts +6 -0
  175. package/dist/line-continues.d.ts.map +1 -0
  176. package/dist/line-continues.js +33 -0
  177. package/dist/line-continues.js.map +1 -0
  178. package/dist/measure.d.ts +1 -0
  179. package/dist/measure.d.ts.map +1 -1
  180. package/dist/measure.js +5 -1
  181. package/dist/measure.js.map +1 -1
  182. package/dist/number-name.d.ts +7 -3
  183. package/dist/number-name.d.ts.map +1 -1
  184. package/dist/number-name.js +51 -15
  185. package/dist/number-name.js.map +1 -1
  186. package/dist/orthography.d.ts +4 -0
  187. package/dist/orthography.d.ts.map +1 -1
  188. package/dist/orthography.js +11 -9
  189. package/dist/orthography.js.map +1 -1
  190. package/dist/plain-source.d.ts +6 -0
  191. package/dist/plain-source.d.ts.map +1 -0
  192. package/dist/plain-source.js +6 -0
  193. package/dist/plain-source.js.map +1 -0
  194. package/dist/plugin.d.ts +18 -1
  195. package/dist/plugin.d.ts.map +1 -1
  196. package/dist/preamble-paragraphs.d.ts +7 -0
  197. package/dist/preamble-paragraphs.d.ts.map +1 -0
  198. package/dist/preamble-paragraphs.js +7 -0
  199. package/dist/preamble-paragraphs.js.map +1 -0
  200. package/dist/quoted-span.d.ts +10 -0
  201. package/dist/quoted-span.d.ts.map +1 -0
  202. package/dist/quoted-span.js +20 -0
  203. package/dist/quoted-span.js.map +1 -0
  204. package/dist/render/eval.d.ts +2 -1
  205. package/dist/render/eval.d.ts.map +1 -1
  206. package/dist/render/eval.js +84 -43
  207. package/dist/render/eval.js.map +1 -1
  208. package/dist/resolve-genre.d.ts +11 -0
  209. package/dist/resolve-genre.d.ts.map +1 -0
  210. package/dist/resolve-genre.js +18 -0
  211. package/dist/resolve-genre.js.map +1 -0
  212. package/dist/soft-break.d.ts +19 -0
  213. package/dist/soft-break.d.ts.map +1 -0
  214. package/dist/soft-break.js +78 -0
  215. package/dist/soft-break.js.map +1 -0
  216. package/dist/structure/build.d.ts.map +1 -1
  217. package/dist/structure/build.js +23 -10
  218. package/dist/structure/build.js.map +1 -1
  219. package/dist/structure/date-order.d.ts +3 -0
  220. package/dist/structure/date-order.d.ts.map +1 -1
  221. package/dist/structure/date-order.js +9 -1
  222. package/dist/structure/date-order.js.map +1 -1
  223. package/dist/structure/heading-leaves.d.ts +7 -0
  224. package/dist/structure/heading-leaves.d.ts.map +1 -0
  225. package/dist/structure/heading-leaves.js +6 -0
  226. package/dist/structure/heading-leaves.js.map +1 -0
  227. package/dist/structure/issues.d.ts +1 -1
  228. package/dist/structure/issues.d.ts.map +1 -1
  229. package/dist/structure/issues.js +14 -3
  230. package/dist/structure/issues.js.map +1 -1
  231. package/dist/structure/listed-tags.d.ts +3 -0
  232. package/dist/structure/listed-tags.d.ts.map +1 -0
  233. package/dist/structure/listed-tags.js +6 -0
  234. package/dist/structure/listed-tags.js.map +1 -0
  235. package/dist/structure/number-in-sentence.d.ts +15 -0
  236. package/dist/structure/number-in-sentence.d.ts.map +1 -0
  237. package/dist/structure/number-in-sentence.js +13 -0
  238. package/dist/structure/number-in-sentence.js.map +1 -0
  239. package/dist/structure/unit-word.d.ts +8 -0
  240. package/dist/structure/unit-word.d.ts.map +1 -0
  241. package/dist/structure/unit-word.js +10 -0
  242. package/dist/structure/unit-word.js.map +1 -0
  243. package/dist/structure/wrapped-tail.d.ts +18 -0
  244. package/dist/structure/wrapped-tail.d.ts.map +1 -0
  245. package/dist/structure/wrapped-tail.js +11 -0
  246. package/dist/structure/wrapped-tail.js.map +1 -0
  247. package/package.json +3 -3
  248. package/rules/dangling-reference.yaml +2 -2
  249. package/rules/date-order.yaml +2 -2
  250. package/rules/duplicate-definition.yaml +2 -2
  251. package/rules/latin-spacing.yaml +4 -2
  252. package/rules/max-kanji-continuous.yaml +1 -1
  253. package/rules/no-doubled-joshi.yaml +2 -2
  254. package/rules/no-nakaguro-parallel.yaml +2 -2
  255. package/rules/title-case-consistency.yaml +2 -2
  256. package/rules/undefined-acronym.yaml +15 -1
  257. package/rules/unqualified-superlative.yaml +1 -1
  258. package/src/address-chain.ts +5 -2
  259. package/src/cli-text.ts +19 -3
  260. package/src/cli.ts +22 -19
  261. package/src/commands/eval.ts +26 -5
  262. package/src/commands/feedback.ts +2 -1
  263. package/src/commands/test.ts +3 -2
  264. package/src/commands/tree.ts +2 -1
  265. package/src/detectors/acronym-compound.ts +9 -0
  266. package/src/detectors/acronym-context.ts +32 -1
  267. package/src/detectors/acronym-expansion.ts +72 -5
  268. package/src/detectors/char-shape.ts +44 -17
  269. package/src/detectors/citation-key.ts +13 -0
  270. package/src/detectors/cited-name.ts +23 -0
  271. package/src/detectors/concrete-evidence.ts +31 -0
  272. package/src/detectors/conjugated-form.ts +40 -0
  273. package/src/detectors/counter-edge.ts +29 -0
  274. package/src/detectors/dotted-name.ts +14 -0
  275. package/src/detectors/doubled-word.ts +25 -2
  276. package/src/detectors/en-shape.ts +17 -30
  277. package/src/detectors/enumerated-runs.ts +68 -0
  278. package/src/detectors/gram-place.ts +8 -1
  279. package/src/detectors/gram-predicate.ts +55 -0
  280. package/src/detectors/heading-case.ts +41 -0
  281. package/src/detectors/heading-echo.ts +5 -1
  282. package/src/detectors/lexicon.ts +8 -5
  283. package/src/detectors/middle-dot.ts +10 -0
  284. package/src/detectors/name-numeral.ts +37 -0
  285. package/src/detectors/name-part.ts +24 -0
  286. package/src/detectors/name-run.ts +16 -0
  287. package/src/detectors/orthography.ts +20 -9
  288. package/src/detectors/placeholder.ts +38 -0
  289. package/src/detectors/polite-word.ts +14 -0
  290. package/src/detectors/quoted-variant.ts +20 -0
  291. package/src/detectors/register.ts +11 -7
  292. package/src/detectors/roman-numeral.ts +5 -0
  293. package/src/detectors/sentence-ending.ts +23 -6
  294. package/src/detectors/series-label.ts +33 -0
  295. package/src/detectors/signals.ts +44 -31
  296. package/src/detectors/structure-tree.ts +2 -2
  297. package/src/detectors/structure.ts +2 -1
  298. package/src/detectors/superlative-name.ts +30 -0
  299. package/src/detectors/token-shape.ts +12 -3
  300. package/src/document.ts +12 -5
  301. package/src/genre-check.ts +46 -0
  302. package/src/genre.ts +34 -5
  303. package/src/init.ts +0 -13
  304. package/src/joined-view.ts +96 -0
  305. package/src/line-continues.ts +43 -0
  306. package/src/measure.ts +9 -1
  307. package/src/number-name.ts +65 -15
  308. package/src/orthography.ts +15 -9
  309. package/src/plain-source.ts +5 -0
  310. package/src/plugin.ts +23 -2
  311. package/src/preamble-paragraphs.ts +9 -0
  312. package/src/quoted-span.ts +25 -0
  313. package/src/render/eval.ts +101 -38
  314. package/src/resolve-genre.ts +20 -0
  315. package/src/soft-break.ts +94 -0
  316. package/src/structure/build.ts +29 -17
  317. package/src/structure/date-order.ts +16 -1
  318. package/src/structure/heading-leaves.ts +7 -0
  319. package/src/structure/issues.ts +18 -3
  320. package/src/structure/listed-tags.ts +7 -0
  321. package/src/structure/number-in-sentence.ts +20 -0
  322. package/src/structure/unit-word.ts +11 -0
  323. package/src/structure/wrapped-tail.ts +22 -0
@@ -0,0 +1,55 @@
1
+ import { isNamePart } from "./name-part.ts";
2
+ import type { LengthUnit, Span, Token } from "../plugin.ts";
3
+
4
+ /** 動詞と助動詞。言い回し(ることができます、it is important to)にはこれが入り、主題の名前(state and local tax)には入らない。 */
5
+ const PREDICATE_POS = new Set(["VERB", "AUX"]);
6
+
7
+ const NOUNS = new Set(["NOUN", "PROPN", "ADJ"]);
8
+
9
+ /** 中身を持たない語。名前の前後に付いても、名前の繰り返しを言い回しに変えない。 */
10
+ const FUNCTION_POS = new Set(["ADP", "DET", "CCONJ", "SCONJ", "PART", "PUNCT", "SYM", "X"]);
11
+
12
+ /**
13
+ * 英語で名詞のすぐ前の動詞は、名詞を飾る語(形容詞を挟んでも同じ)(the upcoming fiscal year、the Disclosing Party、the borrow checker)で、
14
+ * 述語ではない。日本語の名詞の前の動詞(〜にあるコンポーネント)は言い回しの一部になるので、word 単位だけで見る。
15
+ */
16
+ const modifiesNoun = (tokens: readonly Token[], index: number, unit: LengthUnit): boolean =>
17
+ unit === "word" && tokens[index]?.pos === "VERB" && NOUNS.has(tokens[index + 1]?.pos ?? "");
18
+
19
+ /** 英語で大文字の名前の一部になった動詞(Procedures Guide、the Location Object)も、述語ではない。 */
20
+ const namePartAt = (tokens: readonly Token[], index: number, unit: LengthUnit): boolean => unit === "word" && isNamePart(tokens, index);
21
+
22
+ const isName = (tokens: readonly Token[], index: number, unit: LengthUnit): boolean => {
23
+ const token = tokens[index];
24
+ return token !== undefined && (token.pos === "PROPN" || token.features?.["NameType"] !== undefined || namePartAt(tokens, index, unit));
25
+ };
26
+
27
+ const overlaps = (token: Token, window: Span): boolean => token.span.start < window.end && window.start < token.span.end;
28
+
29
+ const isCut = (token: Token, window: Span): boolean => token.span.start < window.start || window.end < token.span.end;
30
+
31
+ /**
32
+ * 窓が名前だけを繰り返しているか。predicate(添字)のほかに窓が触れる語が、名前と中身の無い語だけで、名前が 1 つはある。
33
+ * 窓の端で切れた動詞("ed in the NSF Propos" の identified、"ed by Applicable Law" の prohibited)は語尾しか入っておらず、
34
+ * 動詞は出てくるたびに違ってよい。繰り返されているのは名前のほう。
35
+ */
36
+ const onlyNamesBeside = (tokens: readonly Token[], predicate: number, window: Span, unit: LengthUnit): boolean => {
37
+ const touched = tokens.flatMap((token, index) => (index !== predicate && overlaps(token, window) ? [index] : []));
38
+ const names = touched.filter((index) => isName(tokens, index, unit));
39
+ const rest = touched.filter((index) => !isName(tokens, index, unit));
40
+ return names.length > 0 && rest.every((index) => FUNCTION_POS.has(tokens[index]?.pos ?? ""));
41
+ };
42
+
43
+ const countsAsPredicate = (tokens: readonly Token[], index: number, window: Span, unit: LengthUnit): boolean => {
44
+ const token = tokens[index];
45
+ if (token === undefined || !PREDICATE_POS.has(token.pos) || !overlaps(token, window)) return false;
46
+ if (modifiesNoun(tokens, index, unit) || namePartAt(tokens, index, unit)) return false;
47
+ return !isCut(token, window) || !onlyNamesBeside(tokens, index, window, unit);
48
+ };
49
+
50
+ /**
51
+ * 語句が文の中で占める範囲(window、文書の中の位置)に、述語になる動詞か助動詞があるか。
52
+ * 範囲で見る。文字で探すと、同じ文の別の所の動詞「use」が、語句の中の名詞「use」に当たる。
53
+ */
54
+ export const hasPredicateIn = (tokens: readonly Token[], window: Span, unit: LengthUnit): boolean =>
55
+ tokens.some((_, index) => countsAsPredicate(tokens, index, window, unit));
@@ -0,0 +1,41 @@
1
+ const WORD = /[A-Za-z][A-Za-z'-]*/gu;
2
+
3
+ /** 小さい語は Title Case でも小文字のままなので、大文字化の判定から外す。 */
4
+ const MINOR = new Set(["a", "an", "the", "and", "or", "but", "of", "in", "on", "at", "to", "for", "with", "as", "by", "from", "is"]);
5
+
6
+ const TITLE_DEPTH = 1;
7
+
8
+ /** 見出しが Title Case か。判定できなければ undefined。 */
9
+ export const isTitleCase = (heading: string): boolean | undefined => {
10
+ const words = [...heading.matchAll(WORD)].map((match) => match[0]).filter((word) => !MINOR.has(word.toLowerCase()));
11
+ // 1 語の見出しは、どちらの流儀でも先頭が大文字になる。判定できない。
12
+ if (words.length < 2) return undefined;
13
+ const capitalized = words.filter((word) => word[0] === word[0]?.toUpperCase()).length;
14
+ return capitalized === words.length;
15
+ };
16
+
17
+ /**
18
+ * 文書の題名。最初の見出しが深さ 1 で、深さ 1 の見出しがほかに無いときの、その見出し。
19
+ * 深さ 1 が複数あると、最初のものが題名なのか章なのかを構造からは決められないので、題名とは呼ばない。
20
+ * 見出しより前の導入部(深さ 0)は見出しではないので数えない。
21
+ */
22
+ export const pageTitleOf = <T extends { readonly depth: number }>(sections: readonly T[]): T | undefined => {
23
+ const headed = sections.filter((section) => section.depth > 0);
24
+ const first = headed[0];
25
+ if (first?.depth !== TITLE_DEPTH) return undefined;
26
+ return headed.filter((section) => section.depth === TITLE_DEPTH).length === 1 ? first : undefined;
27
+ };
28
+
29
+ export type CaseCounts = { readonly titleCase: number; readonly sentenceCase: number };
30
+
31
+ /**
32
+ * 節の見出しのうち少数派の流儀(true なら Title Case)。少数派が無ければ undefined。
33
+ * 題名は数に入れない。題名は名前なので、節と流儀を分けるのはよくある揃えかた。
34
+ * 同数のときだけ題名の流儀で決める。題名と同じ流儀のほうが、書き手の選んだ流儀である見込みが高い。
35
+ */
36
+ export const minorityCase = (sections: CaseCounts, pageTitleCase: boolean | undefined): boolean | undefined => {
37
+ if (sections.titleCase === sections.sentenceCase) return sections.titleCase === 0 || pageTitleCase === undefined ? undefined : !pageTitleCase;
38
+ const minorityIsTitle = sections.titleCase < sections.sentenceCase;
39
+ const few = minorityIsTitle ? sections.titleCase : sections.sentenceCase;
40
+ return few === 0 ? undefined : minorityIsTitle;
41
+ };
@@ -1,5 +1,6 @@
1
1
  import { lengthOf } from "../measure.ts";
2
2
  import { handsOver } from "./lead-in.ts";
3
+ import { withoutQuotedVariants } from "./quoted-variant.ts";
3
4
  import type { Detector, Finding, LengthUnit, ProseDocument, Section } from "../plugin.ts";
4
5
 
5
6
  /** 見出しが短すぎると、偶然の一致で 100% になる。これ未満の見出しは見ない。 */
@@ -58,12 +59,15 @@ const leadsIn = (doc: ProseDocument, section: Section, phrases: readonly string[
58
59
  return first !== undefined && handsOver(first.text, doc.source.slice(first.span.end, section.span.end), phrases);
59
60
  };
60
61
 
62
+ /** 見出しとの重なりを測る文。用語集や表記の手引きは見出しの語の別の書き方を引用する(Not “datacentre”)ので、それは数えない。 */
63
+ const echoedText = (section: Section): string => withoutQuotedVariants(section.firstSentence?.text ?? "", section.heading);
64
+
61
65
  export const headingEcho: Detector = (doc, options): Finding[] => {
62
66
  const leadIns = (doc.lexicons["lead-in"] ?? []).map((entry) => entry.pattern);
63
67
  return doc.sections
64
68
  .filter((section) => section.heading.length > 0 && section.firstSentence !== undefined && addsLittle(section, doc.lengthUnit))
65
69
  .filter((section) => !leadsIn(doc, section, leadIns))
66
- .map((section) => ({ section, overlap: Math.round(containment(trigrams(section.heading), trigrams(section.firstSentence?.text ?? "")) * 100) }))
70
+ .map((section) => ({ section, overlap: Math.round(containment(trigrams(section.heading), trigrams(echoedText(section))) * 100) }))
67
71
  .filter(({ overlap }) => overlap >= options.limit)
68
72
  .map(({ section, overlap }) => ({
69
73
  rule: "heading-echo",
@@ -3,6 +3,7 @@ import { wordsOf } from "./structure.ts";
3
3
  import type { Detector, Finding, Lexicon, LexiconEntry, ProseDocument, Sentence } from "../plugin.ts";
4
4
  import { entryIn, entryOpens, entryRanges } from "./lexicon-match.ts";
5
5
  import { scoped, scopeMarkersOf, type ScopeMarkers } from "./superlative-scope.ts";
6
+ import { namesQuantity } from "./superlative-name.ts";
6
7
 
7
8
  const PER = 1000;
8
9
 
@@ -42,21 +43,23 @@ export const cushionDensity = densityRule("cushion-phrase-density");
42
43
  /** 数字は語ではないので言語を問わない。数があれば測った結果を言っている。品詞の数(NUM)は "the best one" の one まで含むので使わない。 */
43
44
  const DIGIT = /\d/u;
44
45
 
45
- type Qualifiers = { readonly comparison: Lexicon; readonly scope: ScopeMarkers };
46
+ type Qualifiers = { readonly comparison: Lexicon; readonly scope: ScopeMarkers; readonly quantityNouns: Lexicon };
46
47
 
47
48
  const qualifiersOf = (doc: ProseDocument): Qualifiers => ({
48
49
  comparison: doc.lexicons["comparison-marker"] ?? [],
49
50
  scope: scopeMarkersOf(doc.lexicons["superlative-scope"] ?? []),
51
+ quantityNouns: doc.lexicons["quantity-noun"] ?? [],
50
52
  });
51
53
 
52
- /** どの出現も範囲を持つときだけ。1 つでも範囲の無い出現があれば、その文には限定の無い最上級がある。 */
53
- const everyScoped = (sentence: Sentence, entry: LexiconEntry, scope: ScopeMarkers): boolean => {
54
+ /** どの出現も範囲を持つか量の名前のときだけ。1 つでもそうでない出現があれば、その文には限定の無い最上級がある。 */
55
+ const everyQualified = (sentence: Sentence, entry: LexiconEntry, qualifiers: Qualifiers): boolean => {
56
+ const tokens = sentence.tokens ?? [];
54
57
  const ranges = entryRanges(sentence, entry);
55
- return ranges.length > 0 && ranges.every((range) => scoped(sentence.tokens ?? [], range, scope));
58
+ return ranges.length > 0 && ranges.every((range) => scoped(tokens, range, qualifiers.scope) || namesQuantity(tokens, range, qualifiers.quantityNouns));
56
59
  };
57
60
 
58
61
  const qualified = (sentence: Sentence, entry: LexiconEntry, qualifiers: Qualifiers): boolean =>
59
- DIGIT.test(sentence.text) || qualifiers.comparison.some((marker) => entryIn(sentence, marker)) || everyScoped(sentence, entry, qualifiers.scope);
62
+ DIGIT.test(sentence.text) || qualifiers.comparison.some((marker) => entryIn(sentence, marker)) || everyQualified(sentence, entry, qualifiers);
60
63
 
61
64
  const bareHits = (doc: ProseDocument, lexicon: Lexicon): Hit[] => {
62
65
  const qualifiers = qualifiersOf(doc);
@@ -0,0 +1,10 @@
1
+ const MIDDLE_DOT = /・/gu;
2
+
3
+ /** 文頭か行頭(前に空白だけ)の「・」。前に並べる項目が無いので、箇条書きの印。 */
4
+ const BULLET_DOT = /(^|\n)[^\S\n]*・/gu;
5
+
6
+ /**
7
+ * 項目を並べる中黒の数。箇条書きの印の「・」は数えない。
8
+ * 「・ 水分を補給すること」が何行続いても、どこまでが 1 つの項目かは改行で読める。
9
+ */
10
+ export const parallelDotCount = (text: string): number => [...text.replace(BULLET_DOT, "$1").matchAll(MIDDLE_DOT)].length;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 大文字で始まる語のすぐ後ろの、I・V・X だけで書いたローマ数字(Engineer II、Leopold III、World War II)。名前に付く
3
+ * 番号で、略語ではない。語の前に文字が無ければ(文の頭、箇条書きの番号の後ろ)、大文字は名前の印ではなく文頭の書き方なので
4
+ * 外さない(Start IV fluids、1. Give IV fluids)。
5
+ * CD、MD、DC、CI、MIX のように C・D・L・M を含む数字は略語であることが多いので、この位置でも略語のまま数える。
6
+ */
7
+
8
+ type Span = { readonly start: number; readonly end: number };
9
+
10
+ const SMALL_NUMERAL = "(?=[IVX])X{0,3}(?:IX|IV|V?I{0,3})";
11
+ const NAMED_NUMERAL = new RegExp(String.raw`(?<![\p{L}\p{N}_])\p{Lu}\p{Ll}+\s+(?<numeral>${SMALL_NUMERAL})(?![\p{L}\p{N}_&])`, "gu");
12
+ const LETTER = /\p{L}/u;
13
+
14
+ /** 文の中の、名前の後ろの番号の範囲(数字だけ)。 */
15
+ export const nameNumeralSpans = (text: string): Span[] =>
16
+ [...text.matchAll(NAMED_NUMERAL)]
17
+ .filter((match) => LETTER.test(text.slice(0, match.index)))
18
+ .map((match) => {
19
+ const numeral = match.groups?.["numeral"] ?? "";
20
+ const end = match.index + match[0].length;
21
+ return { start: end - numeral.length, end };
22
+ });
23
+
24
+ /**
25
+ * 文の頭か文の終わりの . の後ろに、. を付けて 1 語で立つ数字(Survey of Cool Stars. VII. The Inner Disk、II. Background)。
26
+ * 続き物や区切りの番号で、略語ではない。日本語の文分けは英文の . で切らないので、文の中の . の後ろも見る。
27
+ * C・D・L・M を含む数字(CD.)は略語でもあるので外さない。
28
+ */
29
+ // 前置きは後ろ読みにしない。長さの決まらない後ろ読みは、長い空白の上で位置ごとに遡って二乗の時間がかかる。
30
+ const LONE_NUMERAL = new RegExp(String.raw`(?<lead>^\s*|[.。]\s+)(?<numeral>${SMALL_NUMERAL})(?=\.(?:\s|$))`, "gu");
31
+
32
+ /** 文の中の、1 語で立つ番号の範囲(数字だけ)。 */
33
+ export const loneNumeralSpans = (text: string): Span[] =>
34
+ [...text.matchAll(LONE_NUMERAL)].map((match) => {
35
+ const start = match.index + (match.groups?.["lead"] ?? "").length;
36
+ return { start, end: start + (match.groups?.["numeral"] ?? "").length };
37
+ });
@@ -0,0 +1,24 @@
1
+ import type { Token } from "../plugin.ts";
2
+
3
+ const LETTER = /\p{L}/u;
4
+ const CAPITAL_HEAD = /^\p{Lu}/u;
5
+
6
+ /** 文字を含む語の添字。&、括弧、句読点は名前の並びを切らない(Learning & Development、Guide (PAPPG))。 */
7
+ const wordIndexes = (tokens: readonly Token[]): number[] => tokens.flatMap((token, index) => (LETTER.test(token.surface) ? [index] : []));
8
+
9
+ /** 文の最初の語より後ろで、大文字で始まる語。文頭の大文字は位置のしるしで、名前のしるしではない。 */
10
+ const isCapitalisedAt = (tokens: readonly Token[], words: readonly number[], at: number): boolean => {
11
+ const index = words[at];
12
+ return at > 0 && index !== undefined && CAPITAL_HEAD.test(tokens[index]?.surface ?? "");
13
+ };
14
+
15
+ /**
16
+ * 語(添字)が、大文字で書いた名前の一部か。文の途中で大文字で始まり、隣の語も文の途中の大文字(Procedures Guide、
17
+ * the Location Object、Learning & Development)。解析器はこういう語を動詞と読むことがあるが、述語ではない。
18
+ * 文頭の語は隣に数えない。Select Save (if applicable) の Save は述語のまま。
19
+ */
20
+ export const isNamePart = (tokens: readonly Token[], index: number): boolean => {
21
+ const words = wordIndexes(tokens);
22
+ const at = words.indexOf(index);
23
+ return isCapitalisedAt(tokens, words, at) && (isCapitalisedAt(tokens, words, at - 1) || isCapitalisedAt(tokens, words, at + 1));
24
+ };
@@ -0,0 +1,16 @@
1
+ import type { Token } from "../plugin.ts";
2
+
3
+ const nameType = (token: Token | undefined): string | undefined => token?.features?.["NameType"];
4
+
5
+ /** 人の姓と名(田中太郎)。姓が 2 つ続けば(田中山田)、名前の並び。 */
6
+ const isFullName = (named: readonly (Token | undefined)[]): boolean => named.length === 2 && nameType(named[0]) === "Sur" && nameType(named[1]) === "Giv";
7
+
8
+ /**
9
+ * 漢字の連なりの語(covering: tokens の添字)が 1 つの名前か。辞書が固有名詞と読む 1 語(日本銀行)か、人の姓と名。
10
+ * 名前は書き手が変えられない。固有名詞に普通の語が続けば(武蔵野美術大学造形構想学部、東京都知事選挙管理委員会事務局)、
11
+ * 名前から組み立てた語で、どこで切れるかは読み手に見えない。名前が続けば(日本銀行日本電信電話)、並び。どちらも名前とは言わない。
12
+ */
13
+ export const isOneName = (tokens: readonly Token[], covering: readonly number[]): boolean => {
14
+ const named = covering.map((index) => tokens[index]);
15
+ return (named.length === 1 && named[0]?.pos === "PROPN") || isFullName(named);
16
+ };
@@ -1,6 +1,7 @@
1
- import type { Detector, Finding, Sentence } from "../plugin.ts";
1
+ import type { Detector, Finding, Sentence, Span } from "../plugin.ts";
2
2
  import { latinBoundaries, minorityStyle, occurrencesOutside, type Boundary, type SpacingKind } from "../orthography.ts";
3
- import { digitRunAround, isNumberName } from "../number-name.ts";
3
+ import { isWithinAny, quotedSpans } from "../quoted-span.ts";
4
+ import { digitRunAround, isNumberName, sequenceLabelStarts } from "../number-name.ts";
4
5
 
5
6
  /** チームが chaff.yaml の prefer に書いた「使わない書き方」。書いていなければ何も言わない。 */
6
7
  export const preferredTerm: Detector = (doc, options): Finding[] => {
@@ -31,22 +32,32 @@ const digitBeside = (boundary: Boundary): number => {
31
32
  };
32
33
 
33
34
  /** 番号・識別子として書かれた数の境目は、空け方の好みではないので数えない(number-name.ts)。 */
34
- const isCounted = (sentence: Sentence, boundary: Boundary): boolean => {
35
+ const isCounted = (sentence: Sentence, boundary: Boundary, sequence: ReadonlySet<number>, topUnits: ReadonlySet<string>): boolean => {
35
36
  if (boundary.kind === "letter") return true;
36
37
  const run = digitRunAround(sentence.text, digitBeside(boundary));
37
- return run === undefined || !isNumberName(sentence.text, run, sentence.tokens, sentence.span.start);
38
+ return run === undefined || !isNumberName(sentence.text, run, sentence.tokens, sentence.span.start, sequence, topUnits);
38
39
  };
39
40
 
41
+ /**
42
+ * 鉤括弧で引いた題名や発言(「…ガイドライン ver. 1.1」)の中の境目。空け方は引いた元のもので、書き手の書き方ではない。
43
+ * 括弧は日本語の字でも英数字でもないので、境目はまるごと括弧の中か外にある。
44
+ */
45
+ const isQuoted = (quoted: readonly Span[], boundary: Boundary): boolean => isWithinAny(quoted, { start: boundary.offset, end: boundary.offset });
46
+
40
47
  /**
41
48
  * 日本語と英字・数字のあいだを、空けるか詰めるか。文書の中で混ざっていたら、少ないほうを指摘する。
42
49
  * どちらが正しいかは決めない。決めるのはチームで、chaff はそろっているかだけを見る。
43
50
  */
44
51
  export const latinSpacing: Detector = (doc, options): Finding[] => {
45
- const located: Located[] = doc.sentences.flatMap((sentence) =>
46
- latinBoundaries(sentence.text, doc.source.slice(sentence.span.start, sentence.span.end))
47
- .filter((boundary) => isCounted(sentence, boundary))
48
- .map((boundary) => ({ sentence, ...boundary })),
49
- );
52
+ // 覆った文(prose)で探す。コードの中の「1 件」は並びに入れない。
53
+ const sequence = sequenceLabelStarts(doc.prose ?? doc.source);
54
+ const topUnits = new Set((doc.lexicons["prefecture-unit"] ?? []).map((entry) => entry.pattern));
55
+ const located: Located[] = doc.sentences.flatMap((sentence) => {
56
+ const quoted = quotedSpans(sentence.text);
57
+ return latinBoundaries(sentence.text, doc.source.slice(sentence.span.start, sentence.span.end))
58
+ .filter((boundary) => !isQuoted(quoted, boundary) && isCounted(sentence, boundary, sequence, topUnits))
59
+ .map((boundary) => ({ sentence, ...boundary }));
60
+ });
50
61
  return KINDS.flatMap((kind) => {
51
62
  const ofKind = located.filter((entry) => entry.kind === kind);
52
63
  const minority = minorityStyle(ofKind);
@@ -0,0 +1,38 @@
1
+ import { escapeRegExp } from "../orthography.ts";
2
+
3
+ /**
4
+ * 日付や時刻の数字の代わりに置く、同じ大文字の繰り返し(令和YY年MM月DD日 HH:MM、MM/DD/YYYY、YYYY-MM-DD)。
5
+ * 定型文の書式で、略語ではない。繰り返しの語が、日付・時刻の単位の語(年、月、日)の前にあるか、
6
+ * 区切り(/、:、-、.)で別の繰り返しの語か数字と繋がっているときだけ認める。単位の語は言語パッケージの語彙表から読む。
7
+ * AI時代の AI や CI/CD の CD は、同じ文字の繰り返しではないので略語のまま数える。
8
+ */
9
+
10
+ type Span = { readonly start: number; readonly end: number };
11
+
12
+ const REPEATED = /^(?<letter>[A-Z])\k<letter>+$/u;
13
+ const PART = String.raw`(?:[A-Z]{2,}|\d+)`;
14
+ const SEPARATOR = "[//::.-]";
15
+ const JOINED = new RegExp(String.raw`(?<![A-Za-z0-9_&])${PART}(?:${SEPARATOR}${PART})+(?![A-Za-z0-9_&])`, "gu");
16
+ const CAPITALS = /[A-Z]+/gu;
17
+
18
+ const isRepeated = (word: string): boolean => REPEATED.test(word);
19
+
20
+ const spanOf = (match: RegExpExecArray): Span => ({ start: match.index, end: match.index + match[0].length });
21
+
22
+ /** 区切りで繋いだ一続きのうち、大文字の部品がどれも同じ文字の繰り返しのもの(数字だけの 2025/01/31 は略語と関係がない)。 */
23
+ const joinedSpans = (text: string): Span[] =>
24
+ [...text.matchAll(JOINED)]
25
+ .filter((match) => {
26
+ const capitals = match[0].match(CAPITALS) ?? [];
27
+ return capitals.length > 0 && capitals.every(isRepeated);
28
+ })
29
+ .map(spanOf);
30
+
31
+ const beforeUnitSpans = (text: string, units: readonly string[]): Span[] => {
32
+ if (units.length === 0) return [];
33
+ const pattern = new RegExp(String.raw`(?<![A-Za-z0-9_&])[A-Z]{2,}(?=${units.map(escapeRegExp).join("|")})`, "gu");
34
+ return [...text.matchAll(pattern)].filter((match) => isRepeated(match[0])).map(spanOf);
35
+ };
36
+
37
+ /** 文の中の、日付・時刻の書式に置いた大文字の範囲。units は日付・時刻の単位の語。 */
38
+ export const placeholderSpans = (text: string, units: readonly string[]): Span[] => [...joinedSpans(text), ...beforeUnitSpans(text, units)];
@@ -0,0 +1,14 @@
1
+ import type { LexiconEntry, Token } from "../plugin.ts";
2
+
3
+ /** 語彙表の語が解析器で一語になったときの、その語。二語以上(「ございます」)は読みで照らさない。 */
4
+ const singleWord = (entry: LexiconEntry): Token | undefined => (entry.tokens?.length === 1 ? entry.tokens[0] : undefined);
5
+
6
+ /**
7
+ * 読みと品詞が同じなら、表記が違っても同じ語の同じ形。解析器は「下さい」の原形を「下さる」、「ください」の原形を「くださる」と
8
+ * 別の字で返すが、読み(クダサイ)と品詞は同じ。品詞も比べるのは、読みだけだと「升(マス)」が「ます」に当たるから。
9
+ */
10
+ const sameReading = (token: Token, word: Token | undefined): boolean => word?.reading !== undefined && token.reading === word.reading && token.pos === word.pos;
11
+
12
+ /** 語が丁寧語の語彙表の語か。書いた形か原形が語彙表の語と同じか、語彙表の一語と読みと品詞が同じ。 */
13
+ export const isPoliteWord = (token: Token, entries: readonly LexiconEntry[]): boolean =>
14
+ entries.some((entry) => token.surface === entry.pattern || token.lemma === entry.pattern || sameReading(token, singleWord(entry)));
@@ -0,0 +1,20 @@
1
+ // 引用符の中身。英語の ’ と ' はアポストロフィ(organisation’s, don't)にも使われるので、字のあとに続くものは閉じとみなさない。
2
+ const QUOTATIONS: readonly RegExp[] = [
3
+ /“([^“”\n]*)”/gu,
4
+ /‘((?:[^‘’\n]|’(?=\p{L}))*)’(?!\p{L})/gu,
5
+ /"([^"\n]*)"/gu,
6
+ /(?<![\p{L}\p{N}])'((?:[^'\n]|'(?=\p{L}))*)'(?![\p{L}\p{N}])/gu,
7
+ /「([^「」\n]*)」/gu,
8
+ /『([^『』\n]*)』/gu,
9
+ ];
10
+
11
+ // 大文字小文字は畳まない。表記の手引きは大文字小文字だけ違う形も引用して退ける(Not “Open Source software”)。
12
+ const sameText = (left: string, right: string): boolean => left.trim() === right.trim();
13
+
14
+ /**
15
+ * 文から、見出しとは違う形を引用した部分を抜いた文字列。
16
+ * 「Not “datacentre”.」の “datacentre” は語の書き方として挙げたもの(言及)で、見出し「data centre」を繰り返してはいない。
17
+ * 見出しそのままの引用(「利用規約への同意」について説明します)は見出しを指しているので残す。
18
+ */
19
+ export const withoutQuotedVariants = (sentence: string, heading: string): string =>
20
+ QUOTATIONS.reduce((text, quotation) => text.replace(quotation, (whole: string, inner: string) => (sameText(inner, heading) ? whole : " ")), sentence);
@@ -1,4 +1,5 @@
1
- import type { Sentence, Span, Token } from "../plugin.ts";
1
+ import { isPoliteWord } from "./polite-word.ts";
2
+ import type { LexiconEntry, Sentence, Span, Token } from "../plugin.ts";
2
3
 
3
4
  export type Register = "polite" | "plain";
4
5
 
@@ -25,13 +26,12 @@ const withPreceding = (ending: readonly Token[], preceding: readonly Token[]): r
25
26
  * 文末の語の調子。preceding は文末の語より前の語。
26
27
  * 文末に述語(動詞・形容詞・助動詞)が無ければ、ですます調でもである調でもないので undefined。
27
28
  * 「以下の通り。」「円錐形の麦わら帽子。」のような名詞で終わる文を数えると、そのままである調の少数派になっていた。
28
- * 書いた形でも原形でも当てる。「ください」の原形は「くださる」で、原形だけを見ると丁寧な文末を見落とす。
29
+ * 書いた形でも原形でも読みでも当てる(polite-word.ts)。「ください」の原形は「くださる」で、原形だけを見ると丁寧な文末を見落とす。
29
30
  */
30
- export const registerOf = (ending: readonly Token[], preceding: readonly Token[], polite: readonly string[]): Register | undefined => {
31
+ export const registerOf = (ending: readonly Token[], preceding: readonly Token[], polite: readonly LexiconEntry[]): Register | undefined => {
31
32
  const judged = withPreceding(ending, preceding);
32
33
  if (!judged.some(isPredicate)) return undefined;
33
- const isPolite = judged.some((token) => polite.includes(token.surface) || (token.lemma !== undefined && polite.includes(token.lemma)));
34
- return isPolite ? "polite" : "plain";
34
+ return judged.some((token) => isPoliteWord(token, polite)) ? "polite" : "plain";
35
35
  };
36
36
 
37
37
  const EXCLAIMED = /[!?!?]$/u;
@@ -49,7 +49,11 @@ export const outermostList = (offset: number, lists: readonly Span[]): number |
49
49
  .filter((list) => offset >= list.start && offset < list.end)
50
50
  .reduce<number | undefined>((outer, list) => (outer === undefined || list.start < outer ? list.start : outer), undefined);
51
51
 
52
- /** group: 文が入っている箇条書き(outermostList の値)。本文なら undefined。 */
52
+ /** 文が入っているまとまりの始まり。一番外側の箇条書き、それが無ければ番号で始まる段落の並び(runs)。本文なら undefined。 */
53
+ export const groupOf = (offset: number, lists: readonly Span[], runs: readonly Span[]): number | undefined =>
54
+ outermostList(offset, lists) ?? outermostList(offset, runs);
55
+
56
+ /** group: 文が入っているまとまり(groupOf の値)。本文なら undefined。 */
53
57
  export type Judged = { readonly register: Register; readonly group: number | undefined };
54
58
 
55
59
  /** 混ざった文と、その文が属する本文または箇条書きの中の少数派の数。 */
@@ -74,7 +78,7 @@ const slipsInGroup = <T extends Judged>(judged: readonly T[], group: number | un
74
78
  };
75
79
 
76
80
  /**
77
- * 調子が混ざった文を、judged の順で。本文は本文どうし、箇条書きは 1 つずつ、その中で揃っているかを見る。
81
+ * 調子が混ざった文を、judged の順で。本文は本文どうし、箇条書きと番号で始まる段落の並びは 1 つずつ、その中で揃っているかを見る。
78
82
  * ですます調の本文に常体の箇条書きを置くのはよくある書き方で、箇条書きが丸ごと揃っていれば混在ではない。
79
83
  */
80
84
  export const slipsOf = <T extends Judged>(judged: readonly T[], limit: number): Slip<T>[] => {
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 崩れのない書き方のローマ数字(I〜MMMM)。IIII や VX は数として書かれないので、略語の側に残す。
3
+ * 各桁が省けるので空文字にも当たる。1 文字目があることを先読みで求める。
4
+ */
5
+ export const ROMAN_NUMERAL = "(?=[MDCLXVI])M{0,4}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3})";
@@ -1,6 +1,7 @@
1
1
  import { endingTokens, isClosed } from "../sentence-shape.ts";
2
- import { continuesInto, outermostList, registerOf, slipsOf, type Register } from "./register.ts";
3
- import type { Detector, Finding, Sentence } from "../plugin.ts";
2
+ import { enumeratedRuns, enumeratorStarts, numberedStarts } from "./enumerated-runs.ts";
3
+ import { continuesInto, groupOf, registerOf, slipsOf, type Register } from "./register.ts";
4
+ import type { Detector, Finding, LexiconEntry, ProseDocument, Sentence, Span } from "../plugin.ts";
4
5
 
5
6
  /**
6
7
  * 文末の調子が混ざっているかを見る。日本語の「ですます / である」がこれ。
@@ -11,21 +12,37 @@ import type { Detector, Finding, Sentence } from "../plugin.ts";
11
12
  */
12
13
  type Entry = { readonly sentence: Sentence; readonly register: Register; readonly group: number | undefined };
13
14
 
14
- const registerOfSentence = (sentence: Sentence, polite: readonly string[]): Register | undefined => {
15
+ const registerOfSentence = (sentence: Sentence, polite: readonly LexiconEntry[]): Register | undefined => {
15
16
  const ending = endingTokens(sentence);
16
17
  const tokens = sentence.tokens ?? [];
17
18
  const head = ending[0] === undefined ? -1 : tokens.indexOf(ending[0]);
18
19
  return registerOf(ending, head > 0 ? tokens.slice(0, head) : [], polite);
19
20
  };
20
21
 
22
+ /** 番号で始まる段落の並び。木を組むのは高いので、調子が一つだけの文書(混ざりようがない)では組まない。 */
23
+ const runsOf = (doc: ProseDocument, registers: readonly Register[]): Span[] => {
24
+ const tree = new Set(registers).size > 1 ? doc.structure : undefined;
25
+ if (tree === undefined) return [];
26
+ return enumeratedRuns(
27
+ doc.paragraphs.map((paragraph) => paragraph.span),
28
+ enumeratorStarts(numberedStarts(tree), doc.sentences, doc.source),
29
+ doc.source,
30
+ );
31
+ };
32
+
21
33
  export const sentenceEnding: Detector = (doc, options): Finding[] => {
22
- const polite = (options.lexicon ?? []).map((entry) => entry.pattern);
34
+ const polite = options.lexicon ?? [];
23
35
  const lists = doc.lists.map((list) => list.span);
24
- const judged = doc.sentences.flatMap((sentence, index): Entry[] => {
36
+ const found = doc.sentences.flatMap((sentence, index): { sentence: Sentence; register: Register }[] => {
25
37
  if (!isClosed(sentence) || continuesInto(sentence, doc.sentences[index + 1])) return [];
26
38
  const register = registerOfSentence(sentence, polite);
27
- return register === undefined ? [] : [{ sentence, register, group: outermostList(sentence.span.start, lists) }];
39
+ return register === undefined ? [] : [{ sentence, register }];
28
40
  });
41
+ const runs = runsOf(
42
+ doc,
43
+ found.map((entry) => entry.register),
44
+ );
45
+ const judged = found.map((entry): Entry => ({ ...entry, group: groupOf(entry.sentence.span.start, lists, runs) }));
29
46
  return slipsOf(judged, options.limit).map(({ entry: { sentence }, count }) => ({
30
47
  rule: "no-mixed-desumasu",
31
48
  severity: "warning",
@@ -0,0 +1,33 @@
1
+ /**
2
+ * - で区切った文書番号のすぐ前の大文字の語(NIST SP 800-61 の SP、NSF 19-582、IEC 19757-2)。番号の一部で、略語ではない。
3
+ * 空白で離して書くほかは、- で繋いだ識別子(AC-2)と同じ。番号の直前の 1 語だけで、その前の語(NIST)は略語のまま数える。
4
+ * 区切りの無い番号(RFC 9110)、. で区切る版(SDK 3.1)、年の範囲(FY 2024-25)の前の語も略語のまま数える。
5
+ */
6
+
7
+ type Span = { readonly start: number; readonly end: number };
8
+
9
+ const YEAR_RANGE = String.raw`(?:19|20)\d\d-\d`;
10
+ const SERIES_LABEL = new RegExp(String.raw`(?<![A-Za-z0-9_&.-])[A-Z]+ (?!${YEAR_RANGE})(?<number>\d+(?:-\d+)+)[A-Za-z0-9]*(?![A-Za-z0-9_&-])`, "gu");
11
+
12
+ /**
13
+ * 文書番号は長い通し番号を持つ(800-61、19757-2)。2 桁ずつの番号(BOD 25-01)は範囲・得点・日付(SRE 1-2、NFL 3-1、
14
+ * 01-02)と見分けられないので、その前の語は略語のまま数える。同じ桁数で小さいほうから書いた 2 つの数(SLO 100-200、
15
+ * SLA 500-599)は範囲。
16
+ */
17
+ const SERIAL_DIGITS = 3;
18
+
19
+ const isRange = (groups: readonly string[]): boolean => {
20
+ const [low, high, ...rest] = groups;
21
+ return rest.length === 0 && low !== undefined && high !== undefined && low.length === high.length && Number(low) < Number(high);
22
+ };
23
+
24
+ const isDocumentNumber = (number: string): boolean => {
25
+ const groups = number.split("-");
26
+ return groups.some((group) => group.length >= SERIAL_DIGITS) && !isRange(groups);
27
+ };
28
+
29
+ /** 文の中の、番号と、その前の大文字の語の範囲。 */
30
+ export const seriesLabelSpans = (text: string): Span[] =>
31
+ [...text.matchAll(SERIES_LABEL)]
32
+ .filter((match) => isDocumentNumber(match.groups?.["number"] ?? ""))
33
+ .map((match) => ({ start: match.index, end: match.index + match[0].length }));