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
@@ -3,7 +3,11 @@ import { wordsOf } from "./structure.ts";
3
3
  import { compacted, placeOf } from "./gram-place.ts";
4
4
  import { notAcronymSpansOf, type NotAcronymSpans } from "./acronym-context.ts";
5
5
  import { expansionAt, type ExpandedAt } from "./acronym-expansion.ts";
6
- import type { Detector, Finding, ProseDocument, Section, Sentence, Token } from "../plugin.ts";
6
+ import { isExplained } from "./acronym-compound.ts";
7
+ import { conjugatedForms } from "./conjugated-form.ts";
8
+ import { evidenceSpans, hasNumeral, startsWithin } from "./concrete-evidence.ts";
9
+ import { hasPredicateIn } from "./gram-predicate.ts";
10
+ import type { Detector, Finding, ProseDocument, Section, Sentence } from "../plugin.ts";
7
11
 
8
12
  const PER = 1000;
9
13
 
@@ -97,29 +101,15 @@ const gramsOf = (doc: ProseDocument): Map<string, number> => {
97
101
  /** 数えたときと同じ形の文。char 単位では空白を詰めてから数えている。 */
98
102
  const gramText = (sentence: Sentence, unit: ProseDocument["lengthUnit"]): string => compacted(sentence.text, unit).text;
99
103
 
100
- /** 動詞と助動詞。言い回し(ることができます、it is important to)にはこれが入り、主題の名前(state and local tax)には入らない。 */
101
- const PREDICATE_POS = new Set(["VERB", "AUX"]);
102
-
103
- const NOUNS = new Set(["NOUN", "PROPN", "ADJ"]);
104
-
105
- /**
106
- * 英語で名詞のすぐ前の動詞は、名詞を飾る語(形容詞を挟んでも同じ)(the upcoming fiscal year、the Disclosing Party、the borrow checker)で、
107
- * 述語ではない。日本語の名詞の前の動詞(〜にあるコンポーネント)は言い回しの一部になるので、word 単位だけで見る。
108
- */
109
- const modifiesNoun = (tokens: readonly Token[], index: number, unit: ProseDocument["lengthUnit"]): boolean =>
110
- unit === "word" && tokens[index]?.pos === "VERB" && NOUNS.has(tokens[index + 1]?.pos ?? "");
111
-
112
104
  /**
113
- * 語句が文の中で占める範囲に、動詞か助動詞があるか。範囲で見る。文字で探すと、同じ文の別の所の動詞「use」が、語句の中の名詞「use」に当たる。
105
+ * 語句が文の中で占める範囲に、述語になる動詞か助動詞があるか(gram-predicate.ts)。
114
106
  * 品詞が無ければ見分けられないので、これまでどおり言い回しとして数える。
115
107
  */
116
108
  const hasPredicate = (gram: string, sentence: Sentence, unit: ProseDocument["lengthUnit"]): boolean => {
117
109
  if (sentence.tokens === undefined) return true;
118
110
  const place = placeOf(compacted(sentence.text, unit), gram);
119
111
  if (place === undefined) return true;
120
- const [start, end] = [sentence.span.start + place.start, sentence.span.start + place.end];
121
- const tokens = sentence.tokens;
122
- return tokens.some((token, index) => PREDICATE_POS.has(token.pos) && token.span.start < end && start < token.span.end && !modifiesNoun(tokens, index, unit));
112
+ return hasPredicateIn(sentence.tokens, { start: sentence.span.start + place.start, end: sentence.span.start + place.end }, unit);
123
113
  };
124
114
 
125
115
  export const ngramRepetition: Detector = (doc, options): Finding[] => {
@@ -149,10 +139,14 @@ export const ngramRepetition: Detector = (doc, options): Finding[] => {
149
139
  * 展開は「略語の直前か直後の括弧」か「括弧の中の略語」で書かれる。
150
140
  *
151
141
  * & で繋いだ大文字は 1 語として読む(ATT&CK、M&IE)。割ると CK や IE が別の略語に見える。
142
+ * - で繋いだ略語どうしも 1 語(RT-PCR)。割ると RT が別の略語に見える。数字を含むもの(COVID-19)は識別子として外す。
152
143
  */
153
144
  const EDGE_BEFORE = String.raw`(?<![A-Za-z0-9_&])`;
154
145
  const EDGE_AFTER = String.raw`(?![A-Za-z0-9_&])`;
155
- const ACRONYM = new RegExp(String.raw`${EDGE_BEFORE}(?:[A-Z]+(?:&[A-Z]+)+|[A-Z]{2,6})${EDGE_AFTER}`, "gu");
146
+ /** 略語として読む大文字の長さの上限。これより長い大文字の語(BILLING)は、普通の語を大文字で書いたもの。 */
147
+ const ACRONYM_MAX = 6;
148
+ const ACRONYM_PART = String.raw`(?:[A-Z]+(?:&[A-Z]+)+|[A-Z]{2,${ACRONYM_MAX}})`;
149
+ const ACRONYM = new RegExp(String.raw`${EDGE_BEFORE}${ACRONYM_PART}(?:-${ACRONYM_PART})*${EDGE_AFTER}`, "gu");
156
150
 
157
151
  /**
158
152
  * 大文字で書かれていても略語ではないもの。
@@ -161,9 +155,17 @@ const ACRONYM = new RegExp(String.raw`${EDGE_BEFORE}(?:[A-Z]+(?:&[A-Z]+)+|[A-Z]{
161
155
  * 強調は読点や括弧をまたいで続くので(THE AUTHOR, THE COMPANY (IF ANY))、そういう続きを含む
162
156
  * 「大文字以外の文字を含まない一続き」を丸ごと強調と見なす。小文字の語が挟まれば切れるので、
163
157
  * 「SRE, SLO, MTTR」のような略語の並びは残る。引用符に包まれた大文字の句(“AS IS”)も強調。
158
+ * 2 語でも、片方が略語にしては長い語なら強調(BILLING CODE)。略語 2 つの並び(NIST SP)は残る。
164
159
  */
165
160
  const CAPS_WORD = String.raw`[A-Z]+(?:&[A-Z]+)*`;
166
- const SHOUTED_RUN = new RegExp(String.raw`${EDGE_BEFORE}${CAPS_WORD}(?:[\s"“”'‘’]+${CAPS_WORD}){2,}${EDGE_AFTER}`, "u");
161
+ const LONG_CAPS_WORD = String.raw`[A-Z]{${ACRONYM_MAX + 1},}`;
162
+ const CAPS_GAP = String.raw`[\s"“”'‘’]+`;
163
+ const SHOUTED_RUN = new RegExp(String.raw`${EDGE_BEFORE}${CAPS_WORD}(?:${CAPS_GAP}${CAPS_WORD}){2,}${EDGE_AFTER}`, "u");
164
+ /** 2 語の強調は、その 2 語だけを外す。続きまで外すと、区切りの後ろの略語(NEW GUIDELINES: SRE)まで消える。 */
165
+ const SHOUTED_PAIR = new RegExp(
166
+ String.raw`${EDGE_BEFORE}(?:${LONG_CAPS_WORD}${CAPS_GAP}${CAPS_WORD}|${CAPS_WORD}${CAPS_GAP}${LONG_CAPS_WORD})${EDGE_AFTER}`,
167
+ "gu",
168
+ );
167
169
  const UNCASED_STRETCH = /(?:[A-Z]|\P{L})+/gu;
168
170
  const QUOTED_CAPS = new RegExp(String.raw`["“'‘]${CAPS_WORD}(?:\s+${CAPS_WORD})+["”'’]`, "gu");
169
171
 
@@ -186,7 +188,7 @@ const spanOf = (match: RegExpExecArray): Span => ({ start: match.index, end: mat
186
188
  const spansOf = (text: string, notation: NotAcronymSpans): Span[] => [
187
189
  ...[...text.matchAll(UNCASED_STRETCH)].filter((match) => SHOUTED_RUN.test(match[0])).map(spanOf),
188
190
  ...[...text.matchAll(IDENTIFIER)].filter((match) => /\d/u.test(match[0])).map(spanOf),
189
- ...[QUOTED_CAPS, REQUIREMENT_WORD, LICENCE].flatMap((pattern) => [...text.matchAll(pattern)].map(spanOf)),
191
+ ...[SHOUTED_PAIR, QUOTED_CAPS, REQUIREMENT_WORD, LICENCE].flatMap((pattern) => [...text.matchAll(pattern)].map(spanOf)),
190
192
  ...notation(text),
191
193
  ];
192
194
 
@@ -216,17 +218,28 @@ const notationOf = (doc: ProseDocument): NotAcronymSpans =>
216
218
  currencies: patternsOf(doc, "currency-code"),
217
219
  usStates: patternsOf(doc, "us-state-code"),
218
220
  emphasis: patternsOf(doc, "emphasis-word"),
221
+ divisions: patternsOf(doc, "numbered-division"),
222
+ honorifics: patternsOf(doc, "honorific"),
223
+ dateTimeUnits: patternsOf(doc, "date-time-unit"),
219
224
  });
220
225
 
226
+ /** 定義の語は、語彙表の形(という)と、この文書で活用して書かれた形(といいます)の両方で照らす。 */
227
+ const definitionVerbsOf = (doc: ProseDocument): string[] => {
228
+ const verbs = patternsOf(doc, "definition-verb");
229
+ return [...verbs, ...conjugatedForms(doc.sentences, verbs)];
230
+ };
231
+
221
232
  export const undefinedAcronym: Detector = (doc, options): Finding[] => {
222
233
  const body = bodyOf(doc);
223
- const common = new Set((options.lexicon ?? []).map((entry) => entry.pattern));
234
+ // HTTP のメソッド名(GET)は略語ではないので、通じる略語と同じく展開を求めない。
235
+ const common = new Set([...(options.lexicon ?? []).map((entry) => entry.pattern), ...patternsOf(doc, "http-method")]);
224
236
  const seen = new Map<string, Hit>();
225
237
  acronymsOf(doc, notationOf(doc)).forEach(({ word, hit }) => {
226
- if (!common.has(word) && !seen.has(word)) seen.set(word, hit);
238
+ if (!seen.has(word)) seen.set(word, hit);
227
239
  });
228
- const expandedAt = expansionAt({ markers: patternsOf(doc, "definition-marker"), verbs: patternsOf(doc, "definition-verb") });
229
- const bare = [...seen.entries()].filter(([acronym]) => !isExpanded(body, acronym, expandedAt));
240
+ const expandedAt = expansionAt({ markers: patternsOf(doc, "definition-marker"), verbs: definitionVerbsOf(doc) });
241
+ const explainedAlone = (word: string): boolean => common.has(word) || isExpanded(body, word, expandedAt);
242
+ const bare = [...seen.entries()].filter(([acronym]) => !isExplained(acronym, explainedAlone));
230
243
  if (bare.length < options.limit) return [];
231
244
  return bare.map(([acronym, hit]) => ({
232
245
  rule: "undefined-acronym",
@@ -257,16 +270,16 @@ const openingOf = (sentence: Sentence | undefined, unit: ProseDocument["lengthUn
257
270
  return pieces.length <= OPENING[unit] ? pieces.join(joiner) : `${pieces.slice(0, OPENING[unit]).join(joiner)}…`;
258
271
  };
259
272
 
260
- /**
261
- * 形態素解析が数と読んだ語(UD の NumType=Card)。漢数字の「二割」「十五分」も、算用数字と同じ具体的な数。
262
- * 言い回しの「二人三脚」「一人ひとり」は、辞書が一語として持つので数にならない。品詞が無ければ、数字だけを見る。
263
- */
264
- const hasNumeral = (section: Section): boolean =>
265
- section.sentences.some((sentence) => (sentence.tokens ?? []).some((token) => token.features?.["NumType"] === "Card"));
273
+ const hasNumeralIn = (section: Section): boolean => section.sentences.some((sentence) => hasNumeral(sentence.tokens ?? []));
266
274
 
267
275
  export const concreteEvidence: Detector = (doc, options): Finding[] => {
268
276
  const sections = doc.sections.filter((section) => section.sentences.length >= 3);
269
- const empty = sections.filter((section) => !CONCRETE.test(doc.source.slice(section.span.start, section.span.end)) && !hasNumeral(section));
277
+ const bare = sections.filter(
278
+ (section) => !CONCRETE.test(doc.source.slice(section.span.start, section.span.end)) && !hasNumeralIn(section) && !startsWithin(section.span, doc.links),
279
+ );
280
+ // 構造の木は作るのに手間がかかる。指摘に届かない文書では作らない。
281
+ const nodes = bare.length < options.limit ? [] : evidenceSpans(doc.structure);
282
+ const empty = bare.filter((section) => !startsWithin(section.span, nodes));
270
283
  if (empty.length < options.limit) return [];
271
284
  return empty.map((section) => ({
272
285
  rule: "concrete-evidence-density",
@@ -17,11 +17,11 @@ const quoteAt = (source: string, offset: number): string => {
17
17
  };
18
18
 
19
19
  const findingsOf =
20
- (rule: string, issuesOf: (tree: NonNullable<ProseDocument["structure"]>) => StructureIssue[]): Detector =>
20
+ (rule: string, issuesOf: (tree: NonNullable<ProseDocument["structure"]>, source: string) => StructureIssue[]): Detector =>
21
21
  (doc): Finding[] =>
22
22
  doc.structure === undefined
23
23
  ? []
24
- : issuesOf(doc.structure).map((issue) => ({
24
+ : issuesOf(doc.structure, doc.source).map((issue) => ({
25
25
  rule,
26
26
  severity: "error",
27
27
  line: 0,
@@ -2,6 +2,7 @@ import { charLength, lengthOf } from "../measure.ts";
2
2
  import type { BulletList, Detector, Finding, Paragraph, ProseDocument, Section, Span } from "../plugin.ts";
3
3
  import { dateStampIndexes, stampCandidates } from "../date-stamp.ts";
4
4
  import { inDocumentOrder } from "../structure/issues.ts";
5
+ import { preambleParagraphs } from "../preamble-paragraphs.ts";
5
6
 
6
7
  /**
7
8
  * ばらつきは変動係数(標準偏差 ÷ 平均)で測る。
@@ -116,7 +117,7 @@ const withoutDateStamps = (doc: ProseDocument, paragraphs: readonly Paragraph[])
116
117
  export const preambleLength: Detector = (doc, options): Finding[] => {
117
118
  const body = doc.sections.find((section) => section.depth >= 2);
118
119
  if (body === undefined) return [];
119
- const all = doc.paragraphs.filter((paragraph) => paragraph.span.start < body.span.start);
120
+ const all = preambleParagraphs(doc.paragraphs, body.span.start);
120
121
  if (all.length <= options.limit) return [];
121
122
  const before = withoutDateStamps(doc, all);
122
123
  if (before.length <= options.limit) return [];
@@ -0,0 +1,30 @@
1
+ import type { Lexicon, Token } from "../plugin.ts";
2
+ import type { TokenRange } from "./lexicon-match.ts";
3
+
4
+ /** 記号(最高!)は名詞と付いても語を作らない。 */
5
+ const LETTER = /\p{L}/u;
6
+
7
+ const joinsAsNoun = (previous: Token, token: Token | undefined): token is Token =>
8
+ token !== undefined && token.pos === "NOUN" && LETTER.test(token.surface) && previous.span.end === token.span.start;
9
+
10
+ /** 空白も助詞も挟まずに続く名詞の連なり(最大|瞬間|風速)の最後の語。連なりが無ければ undefined。 */
11
+ const lastJoined = (tokens: readonly Token[], at: number): Token | undefined => {
12
+ const previous = tokens[at - 1];
13
+ const token = tokens[at];
14
+ if (previous === undefined || !joinsAsNoun(previous, token)) return undefined;
15
+ return lastJoined(tokens, at + 1) ?? token;
16
+ };
17
+
18
+ const measures = (noun: Token, quantities: Lexicon): boolean => quantities.some((entry) => entry.pattern === noun.surface);
19
+
20
+ /**
21
+ * 最上級の名詞がそのまま名詞と 1 語になり、その連なりが測る量の名詞で終わるか(最大風速・最高気温・最大値・最大駐車台数)。
22
+ * これは量の名前で、何かが一番だという主張ではない。「最大の効果」「最も速い」は助詞や用言を挟むので当たらない。
23
+ * ほかの名詞で終わる語(最高品質・最高精度・最大効果)は一番だという主張のまま。量の名詞は言語パッケージの語彙表が持つ。
24
+ */
25
+ export const namesQuantity = (tokens: readonly Token[], range: TokenRange, quantities: Lexicon): boolean => {
26
+ const superlative = tokens[range.start];
27
+ const single = range.end - range.start === 1 && superlative?.pos === "NOUN";
28
+ const last = single ? lastJoined(tokens, range.end) : undefined;
29
+ return last !== undefined && measures(last, quantities);
30
+ };
@@ -1,5 +1,6 @@
1
1
  import { hasParticle, isClosed, lastContent } from "../sentence-shape.ts";
2
- import type { Detector, Finding, Span, Token } from "../plugin.ts";
2
+ import { isWithinAny, quotedSpans } from "../quoted-span.ts";
3
+ import type { Detector, Finding, Sentence, Span, Token } from "../plugin.ts";
3
4
 
4
5
  /**
5
6
  * 文末が名詞で終わる(体言止め)。箇条書きでは普通だが、本文では文が途中で切れて読める。
@@ -57,17 +58,25 @@ const BREAK: Run = { surface: "", tokens: [] };
57
58
  /**
58
59
  * 名詞は連なりの中身なので飛ばす。読点と、語彙表に無い助詞は連なりを切る。
59
60
  * 「弊社のAはBの」を 2 回続いたと数えないため。「は」でいちど句が閉じている。
61
+ *
62
+ * 鉤括弧で引いたもの(括弧ごと)は、名詞と同じく連なりの中身として飛ばす。引いた題名(「食品中のウイルスの制御のための…ガイドライン」)や
63
+ * 発言は書き手が言い換えられないので、中の助詞は数えない。題名も名前なので、外の連なり(弊社の「新製品」の販売の計画)は続けて数える。
60
64
  */
61
- const runsOf = (tokens: readonly Token[], nesting: readonly string[]): Run[] =>
65
+ const runsOf = (tokens: readonly Token[], nesting: readonly string[], quoted: readonly Span[]): Run[] =>
62
66
  tokens.reduce<Run[]>((acc, token) => {
67
+ if (isWithinAny(quoted, token.span)) return acc;
63
68
  if (nesting.includes(token.surface)) return extend(acc, token);
64
69
  return token.pos === "PUNCT" || PARTICLE.has(token.pos) ? [...acc, BREAK] : acc;
65
70
  }, []);
66
71
 
72
+ /** 文の中の、鉤括弧で引いたものを括弧ごと、文書全体の座標で。 */
73
+ const quotedIn = (sentence: Sentence): Span[] =>
74
+ quotedSpans(sentence.text).map((span) => ({ start: sentence.span.start + span.start - 1, end: sentence.span.start + span.end + 1 }));
75
+
67
76
  export const doubledParticle: Detector = (doc, options): Finding[] => {
68
77
  const nesting = (options.lexicon ?? []).map((entry) => entry.pattern);
69
78
  return doc.sentences.flatMap((sentence) =>
70
- runsOf(sentence.tokens ?? [], nesting)
79
+ runsOf(sentence.tokens ?? [], nesting, quotedIn(sentence))
71
80
  .filter((run) => run.tokens.length > options.limit)
72
81
  .map((run) => ({
73
82
  rule: "no-doubled-joshi",
package/src/document.ts CHANGED
@@ -6,6 +6,8 @@ import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
6
6
  import { atxHeadingText, headingText } from "./heading-text.ts";
7
7
  import { hasTitle } from "./heading-title.ts";
8
8
  import { maskSpans } from "./mask.ts";
9
+ import { spansWithin, unmaskedSoftBreaks } from "./soft-break.ts";
10
+ import { segmentJoined } from "./joined-view.ts";
9
11
  import { buildTree, type Outline } from "./structure/build.ts";
10
12
  import { isMarkdownPath } from "./structure/markdown-path.ts";
11
13
  import { pageFurniture, textOutline } from "./page-furniture.ts";
@@ -187,15 +189,17 @@ const spansOfType = (root: Node, type: string, keep: (node: Node) => boolean = (
187
189
  */
188
190
  const shift = (span: Span, by: number): Span => ({ start: by + span.start, end: by + span.end });
189
191
 
190
- const sentencesOf = (prose: string, paragraphs: readonly Span[], adapter: LanguageAdapter): Sentence[] =>
192
+ const breaksWithin = (breaks: readonly Span[], paragraph: Span): Span[] => spansWithin(breaks, paragraph).map((span) => shift(span, -paragraph.start));
193
+
194
+ const sentencesOf = (prose: string, paragraphs: readonly Span[], adapter: LanguageAdapter, softBreaks: readonly Span[]): Sentence[] =>
191
195
  paragraphs.flatMap((paragraph) =>
192
- adapter
193
- .segment(prose.slice(paragraph.start, paragraph.end))
194
- .sentences.filter((sentence) => sentence.text.trim().length > 0)
196
+ segmentJoined(prose.slice(paragraph.start, paragraph.end), breaksWithin(softBreaks, paragraph), (text) => adapter.segment(text))
197
+ .filter((sentence) => sentence.text.trim().length > 0)
195
198
  .map((sentence) => ({
196
199
  span: shift(sentence.span, paragraph.start),
197
200
  text: sentence.text,
198
201
  ...(sentence.tokens === undefined ? {} : { tokens: sentence.tokens.map((token) => ({ ...token, span: shift(token.span, paragraph.start) })) }),
202
+ ...(sentence.wrapBreaks === undefined ? {} : { wrapBreaks: sentence.wrapBreaks.map((span) => shift(span, paragraph.start)) }),
199
203
  })),
200
204
  );
201
205
 
@@ -279,7 +283,9 @@ export const buildDocument = (
279
283
  // ページの案内は段落としても数えない。数えると、目次の行が「本題までの段落」に入る。
280
284
  const paragraphSpans = spansOfType(root, "paragraph", (node) => !isInPageNavigation(node, anchors));
281
285
  const listItems = spansOfType(root, "listItem");
282
- const sentences = sentencesOf(prose, paragraphSpans, adapter);
286
+ // Markdown は段落を流し込んで表示するので、段落の中の改行は読み手に見えない。テキストの文書は行をそのまま見せる(法令は 1 行 1 号)。
287
+ const softBreaks = isMarkdownPath(path) ? unmaskedSoftBreaks(source, prose) : [];
288
+ const sentences = sentencesOf(prose, paragraphSpans, adapter, softBreaks);
283
289
  const lexicons = {
284
290
  ...adapter.lexicons,
285
291
  "internal-jargon": team.jargon.map((pattern) => ({ pattern })),
@@ -301,6 +307,7 @@ export const buildDocument = (
301
307
  listSpans: listItems,
302
308
  paragraphs: paragraphsOf(paragraphSpans, sentences, listItems),
303
309
  lists: listsOf(root, source, anchors),
310
+ links: [...spansOfType(root, "link"), ...spansOfType(root, "linkReference")],
304
311
  lexicons: tagged ? tokenizedLexicons(lexicons, adapter) : lexicons,
305
312
  requiredSections: team.requiredSections,
306
313
  // 構造の rule(参照先が無い・番号の抜け)が読む木。どの rule も読まなければ作らない。何万行の契約書で、他の rule の lint に代金を払わせない。
@@ -0,0 +1,46 @@
1
+ import { frontMatterValues } from "./genre.ts";
2
+
3
+ /** Where a genre was set: the command line, chaff.yaml's genre, or one of its by_path entries. */
4
+ export type GenreSetting = "--genre" | "config" | "by_path";
5
+
6
+ type UnknownGenre = {
7
+ readonly where: GenreSetting;
8
+ readonly genre: string;
9
+ /** The by_path entry's files, so the message can say which entry; empty elsewhere. */
10
+ readonly files: readonly string[];
11
+ };
12
+
13
+ type PathGenre = { readonly files: readonly string[]; readonly genre: string | undefined };
14
+
15
+ type WrittenGenres = { readonly flag: string | undefined; readonly config: string | undefined; readonly byPath: readonly PathGenre[] };
16
+
17
+ /** Commands that never read a genre from chaff.yaml. A wrong one there must not stop them: genres is how to find the right name. */
18
+ const GENRE_FREE: ReadonlySet<string> = new Set(["init", "genres", "skill", "relax", "strict", "off", "tree", "cite"]);
19
+
20
+ /** The genres a command would read: --genre always (every command refuses a wrong one), chaff.yaml's only where the command reads a genre. */
21
+ export const writtenGenres = (
22
+ command: string,
23
+ flag: string | undefined,
24
+ config: { readonly genre: string | undefined; readonly byPath: readonly PathGenre[] },
25
+ ): WrittenGenres => (GENRE_FREE.has(command) ? { flag, config: undefined, byPath: [] } : { flag, config: config.genre, byPath: config.byPath });
26
+
27
+ const setting = (where: GenreSetting, genre: string | undefined, files: readonly string[] = []): UnknownGenre[] =>
28
+ genre === undefined ? [] : [{ where, genre, files }];
29
+
30
+ /**
31
+ * Every genre a setting names that chaff does not know: no rule's use_for matches one, so nothing would run and nothing be listed as not run.
32
+ * All of them, not only the one a file would use, because a wrong setting is wrong for every file.
33
+ */
34
+ export const unknownGenres = (written: WrittenGenres, known: readonly string[]): UnknownGenre[] =>
35
+ [
36
+ ...setting("--genre", written.flag),
37
+ ...setting("config", written.config),
38
+ ...written.byPath.flatMap((entry) => setting("by_path", entry.genre, entry.files)),
39
+ ].filter((entry) => !known.includes(entry.genre));
40
+
41
+ /**
42
+ * The front matter's genre that was not read. Front matter is the document's and often another tool's, so this warns and does not stop:
43
+ * the file falls back to the path, the content or the default. type is not looked at (Zenn's tech / idea mean something else).
44
+ */
45
+ export const unknownFrontMatterGenre = (source: string, known: readonly string[]): string | undefined =>
46
+ frontMatterValues(source, "genre").find((value) => value !== "" && !known.includes(value));
package/src/genre.ts CHANGED
@@ -1,3 +1,17 @@
1
+ /** chaff が知っているジャンル。rule の use_for はこの頭の部分で当てる。 */
2
+ export const GENRES: readonly string[] = [
3
+ "technical/spec",
4
+ "technical/readme",
5
+ "blog/tech",
6
+ "blog/essay",
7
+ "blog/owned-media",
8
+ "business/proposal",
9
+ "business/report",
10
+ "business/email",
11
+ "business/press-release",
12
+ "business/meeting-notes",
13
+ ];
14
+
1
15
  export type GenreGuess = { readonly genre: string; readonly from: "front-matter" | "path" | "content" };
2
16
 
3
17
  /**
@@ -35,9 +49,24 @@ export const guessGenre = (path: string, source: string, front: string | undefin
35
49
  return undefined;
36
50
  };
37
51
 
38
- /** front matter の genre / type を拾う。失敗しても落とさない。 */
39
- export const frontMatterGenre = (source: string): string | undefined => {
40
- const match = /^---\n([\s\S]*?)\n---/u.exec(source);
41
- const line = match?.[1] === undefined ? undefined : /^(?:genre|type):\s*(\S+)\s*$/mu.exec(match[1]);
42
- return line?.[1];
52
+ /** YAML の 1 行の値。行末のコメントと、値を囲む引用符を外す。 */
53
+ const scalarOf = (raw: string): string => {
54
+ const comment = raw.search(/\s#/u);
55
+ const value = (comment === -1 ? raw : raw.slice(0, comment)).trim();
56
+ return /^(["'])(.*)\1$/u.exec(value)?.[2] ?? value;
57
+ };
58
+
59
+ const valuesOf = (block: string, key: string): string[] => [...block.matchAll(new RegExp(`^${key}:(.*)$`, "gmu"))].map((line) => scalarOf(line[1] ?? ""));
60
+
61
+ /** front matter の 1 つの欄に書かれた値。front matter が無ければ空。 */
62
+ export const frontMatterValues = (source: string, key: string): string[] => {
63
+ const block = /^---\n([\s\S]*?)\n---/u.exec(source)?.[1];
64
+ return block === undefined ? [] : valuesOf(block, key);
43
65
  };
66
+
67
+ /**
68
+ * front matter の genre / type を拾う。失敗しても落とさない。genre が type より先。
69
+ * 知っているジャンルだけを返す。Zenn の type(tech / idea)のような別の意味の値をジャンルにすると、どの rule も当てはまらず「指摘なし」になる。
70
+ */
71
+ export const frontMatterGenre = (source: string): string | undefined =>
72
+ [...frontMatterValues(source, "genre"), ...frontMatterValues(source, "type")].find((value) => GENRES.includes(value));
package/src/init.ts CHANGED
@@ -125,16 +125,3 @@ export const runInit = (dir: string, genre: string, ui: UiLanguage = "ja"): stri
125
125
  "",
126
126
  ];
127
127
  };
128
-
129
- export const GENRES: readonly string[] = [
130
- "technical/spec",
131
- "technical/readme",
132
- "blog/tech",
133
- "blog/essay",
134
- "blog/owned-media",
135
- "business/proposal",
136
- "business/report",
137
- "business/email",
138
- "business/press-release",
139
- "business/meeting-notes",
140
- ];
@@ -0,0 +1,96 @@
1
+ import { spansWithin, withoutSpans } from "./soft-break.ts";
2
+ import { continuedBreaks, wrapBreaks } from "./line-continues.ts";
3
+ import type { Segmentation, Sentence, Span, Token } from "./plugin.ts";
4
+
5
+ /**
6
+ * 消える改行を取り除いた文字列と、その上の範囲を元の文字列の範囲に戻す写像。
7
+ *
8
+ * 解析はつないだ文字列で行い、指摘の位置は元の文字列で出す。行・列も、引用して見せる範囲も、元の文書のまま。
9
+ */
10
+ export type JoinedView = { readonly text: string; readonly toSource: (span: Span) => Span };
11
+
12
+ type Removed = { readonly at: number; readonly removedBefore: number };
13
+
14
+ /** 取り除いた各範囲が、つないだ文字列のどこにあったか。removedBefore はそこまでに取り除いた長さ(その範囲を含む)。 */
15
+ const removedPlaces = (breaks: readonly Span[]): Removed[] => {
16
+ const places: Removed[] = [];
17
+ breaks.reduce((before, span) => {
18
+ const removedBefore = before + span.end - span.start;
19
+ places.push({ at: span.start - before, removedBefore });
20
+ return removedBefore;
21
+ }, 0);
22
+ return places;
23
+ };
24
+
25
+ /** at が offset 以下(inclusive)または未満の、最後の範囲までに取り除いた長さ。places は at の昇順。 */
26
+ const removedUpTo = (places: readonly Removed[], offset: number, inclusive: boolean): number => {
27
+ const search = (low: number, high: number, found: number): number => {
28
+ if (low > high) return found;
29
+ const middle = (low + high) >> 1;
30
+ const place = places[middle];
31
+ if (place === undefined) return found;
32
+ const reached = inclusive ? place.at <= offset : place.at < offset;
33
+ return reached ? search(middle + 1, high, place.removedBefore) : search(low, middle - 1, found);
34
+ };
35
+ return search(0, places.length - 1, 0);
36
+ };
37
+
38
+ /**
39
+ * 始まりは、そこで取り除いた改行の後ろ(次の文字)へ。終わりは、そこで取り除いた改行の手前へ。
40
+ * 改行をまたぐ語(「関\nする」)は、改行ごと覆う範囲になる。
41
+ */
42
+ export const joinedView = (text: string, breaks: readonly Span[]): JoinedView => {
43
+ const places = removedPlaces(breaks);
44
+ const toSource = (span: Span): Span => {
45
+ const start = span.start + removedUpTo(places, span.start, true);
46
+ const end = span.end + removedUpTo(places, span.end, false);
47
+ return { start, end: Math.max(start, end) };
48
+ };
49
+ return { text: withoutSpans(text, breaks), toSource };
50
+ };
51
+
52
+ const tokenInSource =
53
+ (view: JoinedView) =>
54
+ (token: Token): Token => ({ ...token, span: view.toSource(token.span) });
55
+
56
+ const sentenceInSource =
57
+ (text: string, view: JoinedView) =>
58
+ (sentence: Sentence): Sentence => {
59
+ const span = view.toSource(sentence.span);
60
+ return {
61
+ span,
62
+ text: text.slice(span.start, span.end),
63
+ ...(sentence.tokens === undefined ? {} : { tokens: sentence.tokens.map(tokenInSource(view)) }),
64
+ };
65
+ };
66
+
67
+ type Segment = (text: string) => Segmentation;
68
+
69
+ /** breaks を除いて segment し、文と語の範囲を text の上に戻す。文の text は元の文字列の切り出しのまま。 */
70
+ const segmentWithout = (text: string, breaks: readonly Span[], segment: Segment): Sentence[] => {
71
+ const view = joinedView(text, breaks);
72
+ return segment(view.text).sentences.map(sentenceInSource(text, view));
73
+ };
74
+
75
+ const withWrapBreaks = (sentence: Sentence, wrapped: readonly Span[]): Sentence => {
76
+ const inside = spansWithin(wrapped, sentence.span);
77
+ return inside.length === 0 ? sentence : { ...sentence, wrapBreaks: inside };
78
+ };
79
+
80
+ const tokensOf = (sentences: readonly Sentence[]): Token[] => sentences.flatMap((sentence) => sentence.tokens ?? []);
81
+
82
+ /**
83
+ * 消える改行を、行が続いていると言えるものだけ除いて segment する。
84
+ * 一度すべて除いて解析し、その語で決めて、残す改行があれば読み直す。
85
+ * 語が無ければ(品詞を読まない adapter)決められないので、前と同じく改行ごと渡す。
86
+ * 除く改行が無ければ segment の結果をそのまま返す。つながない段落の文は、前と同じものになる。
87
+ */
88
+ export const segmentJoined = (text: string, breaks: readonly Span[], segment: Segment): readonly Sentence[] => {
89
+ if (breaks.length === 0) return segment(text).sentences;
90
+ const joined = segmentWithout(text, breaks, segment);
91
+ const kept = continuedBreaks(breaks, tokensOf(joined));
92
+ if (kept.length === 0) return segment(text).sentences;
93
+ const sentences = kept.length === breaks.length ? joined : segmentWithout(text, kept, segment);
94
+ const wrapped = wrapBreaks(kept, tokensOf(sentences));
95
+ return sentences.map((sentence) => withWrapBreaks(sentence, wrapped));
96
+ };
@@ -0,0 +1,43 @@
1
+ import { firstEndingAfter } from "./soft-break.ts";
2
+ import type { Span, Token } from "./plugin.ts";
3
+
4
+ /**
5
+ * 消える改行のうち、どれを消して読むか。決めるのは解析器が読んだ語(全部の改行を消して読んだもの)。
6
+ *
7
+ * Markdown の段落は改行を消して表示されるが、書いた人は改行で区切ったつもりのことがある。
8
+ * 番号だけの見出しの行(「2.1 注文の登録\n登録は」)、宛先や署名を 1 行に 1 つ書いた並び。
9
+ * そこを消して読むと「登録登録」が語の重なりになる。行が続いていると語で言えるときだけ消す。
10
+ */
11
+
12
+ const inside = (token: Token | undefined, part: Span): boolean => token !== undefined && token.span.start < part.start && token.span.end > part.end;
13
+
14
+ type Around = { readonly across: Token | undefined; readonly before: Token | undefined };
15
+
16
+ /** 各改行の位置で、改行をまたぐ語(あれば)と、改行の直前で終わる語。tokens は並び順(重ならない)。 */
17
+ const aroundEach = (breaks: readonly Span[], tokens: readonly Token[]): Around[] => {
18
+ const spans = tokens.map((token) => token.span);
19
+ return breaks.map((part) => {
20
+ const at = firstEndingAfter(spans, part.start);
21
+ return { across: tokens[at], before: tokens[at - 1] };
22
+ });
23
+ };
24
+
25
+ /** 解析器が語の途中に置いた改行。語をまたぐなら、行を折り返しただけ。 */
26
+ export const wrapBreaks = (breaks: readonly Span[], tokens: readonly Token[]): Span[] => {
27
+ const around = aroundEach(breaks, tokens);
28
+ return breaks.filter((part, index) => inside(around[index]?.across, part));
29
+ };
30
+
31
+ /** 句を閉じない語。助詞・接続助詞・接続詞で終わる行は、次の行へ続く。 */
32
+ const CONTINUING_POS = new Set(["ADP", "SCONJ", "CCONJ"]);
33
+ /** 読点で終わる行も続く。句点・括弧・名詞で終わる行は、見出しや 1 行 1 項目の終わりと見分けられない。 */
34
+ const COMMAS = new Set(["、", ","]);
35
+
36
+ const continuesPast = (token: Token | undefined, part: Span): boolean =>
37
+ token !== undefined && token.span.end === part.start && (CONTINUING_POS.has(token.pos) || COMMAS.has(token.surface));
38
+
39
+ /** 行が続いている改行: 語をまたぐものと、句を閉じない語の直後にあるもの。 */
40
+ export const continuedBreaks = (breaks: readonly Span[], tokens: readonly Token[]): Span[] => {
41
+ const around = aroundEach(breaks, tokens);
42
+ return breaks.filter((part, index) => inside(around[index]?.across, part) || continuesPast(around[index]?.before, part));
43
+ };
package/src/measure.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { withoutSpans } from "./soft-break.ts";
1
2
  import type { Sentence } from "./plugin.ts";
2
3
 
3
4
  /**
@@ -9,7 +10,14 @@ import type { Sentence } from "./plugin.ts";
9
10
  *
10
11
  * 日本語は文字数、英語は語数。単位は adapter が宣言する。
11
12
  */
12
- export const proseText = (sentence: Sentence): string => sentence.text.replace(/\s+/gu, " ").trim();
13
+ /** 語の途中で折り返した改行(wrapBreaks)は空白にせず除く。「関\nする」を「関 する」と読まない。 */
14
+ export const proseText = (sentence: Sentence): string =>
15
+ withoutSpans(
16
+ sentence.text,
17
+ (sentence.wrapBreaks ?? []).map((span) => ({ start: span.start - sentence.span.start, end: span.end - sentence.span.start })),
18
+ )
19
+ .replace(/\s+/gu, " ")
20
+ .trim();
13
21
 
14
22
  export const charLength = (sentence: Sentence): number => sentence.text.replace(/\s+/gu, "").length;
15
23