chaffjs 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/dist/address-chain.d.ts +4 -1
  2. package/dist/address-chain.d.ts.map +1 -1
  3. package/dist/address-chain.js +5 -2
  4. package/dist/address-chain.js.map +1 -1
  5. package/dist/cli-text.d.ts +4 -1
  6. package/dist/cli-text.d.ts.map +1 -1
  7. package/dist/cli-text.js +14 -2
  8. package/dist/cli-text.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +21 -21
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/eval.d.ts +4 -0
  13. package/dist/commands/eval.d.ts.map +1 -1
  14. package/dist/commands/eval.js +20 -6
  15. package/dist/commands/eval.js.map +1 -1
  16. package/dist/commands/feedback.d.ts.map +1 -1
  17. package/dist/commands/feedback.js +2 -1
  18. package/dist/commands/feedback.js.map +1 -1
  19. package/dist/commands/test.d.ts.map +1 -1
  20. package/dist/commands/test.js +3 -2
  21. package/dist/commands/test.js.map +1 -1
  22. package/dist/commands/tree.d.ts.map +1 -1
  23. package/dist/commands/tree.js +2 -1
  24. package/dist/commands/tree.js.map +1 -1
  25. package/dist/detectors/acronym-compound.d.ts +8 -0
  26. package/dist/detectors/acronym-compound.d.ts.map +1 -0
  27. package/dist/detectors/acronym-compound.js +8 -0
  28. package/dist/detectors/acronym-compound.js.map +1 -0
  29. package/dist/detectors/acronym-context.d.ts +4 -0
  30. package/dist/detectors/acronym-context.d.ts.map +1 -1
  31. package/dist/detectors/acronym-context.js +17 -1
  32. package/dist/detectors/acronym-context.js.map +1 -1
  33. package/dist/detectors/acronym-expansion.d.ts +10 -0
  34. package/dist/detectors/acronym-expansion.d.ts.map +1 -1
  35. package/dist/detectors/acronym-expansion.js +63 -5
  36. package/dist/detectors/acronym-expansion.js.map +1 -1
  37. package/dist/detectors/char-shape.d.ts.map +1 -1
  38. package/dist/detectors/char-shape.js +36 -14
  39. package/dist/detectors/char-shape.js.map +1 -1
  40. package/dist/detectors/citation-key.d.ts +13 -0
  41. package/dist/detectors/citation-key.d.ts.map +1 -0
  42. package/dist/detectors/citation-key.js +9 -0
  43. package/dist/detectors/citation-key.js.map +1 -0
  44. package/dist/detectors/cited-name.d.ts +11 -0
  45. package/dist/detectors/cited-name.d.ts.map +1 -0
  46. package/dist/detectors/cited-name.js +15 -0
  47. package/dist/detectors/cited-name.js.map +1 -0
  48. package/dist/detectors/concrete-evidence.d.ts +7 -0
  49. package/dist/detectors/concrete-evidence.d.ts.map +1 -0
  50. package/dist/detectors/concrete-evidence.js +21 -0
  51. package/dist/detectors/concrete-evidence.js.map +1 -0
  52. package/dist/detectors/counter-edge.d.ts +8 -0
  53. package/dist/detectors/counter-edge.d.ts.map +1 -0
  54. package/dist/detectors/counter-edge.js +27 -0
  55. package/dist/detectors/counter-edge.js.map +1 -0
  56. package/dist/detectors/dotted-name.d.ts +13 -0
  57. package/dist/detectors/dotted-name.d.ts.map +1 -0
  58. package/dist/detectors/dotted-name.js +10 -0
  59. package/dist/detectors/dotted-name.js.map +1 -0
  60. package/dist/detectors/doubled-word.d.ts +6 -1
  61. package/dist/detectors/doubled-word.d.ts.map +1 -1
  62. package/dist/detectors/doubled-word.js +22 -2
  63. package/dist/detectors/doubled-word.js.map +1 -1
  64. package/dist/detectors/en-shape.d.ts +1 -1
  65. package/dist/detectors/en-shape.d.ts.map +1 -1
  66. package/dist/detectors/en-shape.js +11 -22
  67. package/dist/detectors/en-shape.js.map +1 -1
  68. package/dist/detectors/gram-place.d.ts +0 -4
  69. package/dist/detectors/gram-place.d.ts.map +1 -1
  70. package/dist/detectors/gram-place.js +6 -1
  71. package/dist/detectors/gram-place.js.map +1 -1
  72. package/dist/detectors/gram-predicate.d.ts +7 -0
  73. package/dist/detectors/gram-predicate.d.ts.map +1 -0
  74. package/dist/detectors/gram-predicate.js +44 -0
  75. package/dist/detectors/gram-predicate.js.map +1 -0
  76. package/dist/detectors/heading-case.d.ts +21 -0
  77. package/dist/detectors/heading-case.d.ts.map +1 -0
  78. package/dist/detectors/heading-case.js +38 -0
  79. package/dist/detectors/heading-case.js.map +1 -0
  80. package/dist/detectors/heading-echo.d.ts.map +1 -1
  81. package/dist/detectors/heading-echo.js +4 -1
  82. package/dist/detectors/heading-echo.js.map +1 -1
  83. package/dist/detectors/lexicon.d.ts.map +1 -1
  84. package/dist/detectors/lexicon.js +7 -4
  85. package/dist/detectors/lexicon.js.map +1 -1
  86. package/dist/detectors/name-numeral.d.ts +16 -0
  87. package/dist/detectors/name-numeral.d.ts.map +1 -0
  88. package/dist/detectors/name-numeral.js +30 -0
  89. package/dist/detectors/name-numeral.js.map +1 -0
  90. package/dist/detectors/name-part.d.ts +8 -0
  91. package/dist/detectors/name-part.d.ts.map +1 -0
  92. package/dist/detectors/name-part.js +20 -0
  93. package/dist/detectors/name-part.js.map +1 -0
  94. package/dist/detectors/name-run.d.ts +8 -0
  95. package/dist/detectors/name-run.d.ts.map +1 -0
  96. package/dist/detectors/name-run.js +13 -0
  97. package/dist/detectors/name-run.js.map +1 -0
  98. package/dist/detectors/orthography.d.ts.map +1 -1
  99. package/dist/detectors/orthography.js +15 -5
  100. package/dist/detectors/orthography.js.map +1 -1
  101. package/dist/detectors/placeholder.d.ts +14 -0
  102. package/dist/detectors/placeholder.d.ts.map +1 -0
  103. package/dist/detectors/placeholder.js +24 -0
  104. package/dist/detectors/placeholder.js.map +1 -0
  105. package/dist/detectors/quoted-variant.d.ts +7 -0
  106. package/dist/detectors/quoted-variant.d.ts.map +1 -0
  107. package/dist/detectors/quoted-variant.js +18 -0
  108. package/dist/detectors/quoted-variant.js.map +1 -0
  109. package/dist/detectors/series-label.d.ts +13 -0
  110. package/dist/detectors/series-label.d.ts.map +1 -0
  111. package/dist/detectors/series-label.js +26 -0
  112. package/dist/detectors/series-label.js.map +1 -0
  113. package/dist/detectors/signals.d.ts.map +1 -1
  114. package/dist/detectors/signals.js +29 -24
  115. package/dist/detectors/signals.js.map +1 -1
  116. package/dist/detectors/structure-tree.js +1 -1
  117. package/dist/detectors/structure-tree.js.map +1 -1
  118. package/dist/detectors/superlative-name.d.ts +9 -0
  119. package/dist/detectors/superlative-name.d.ts.map +1 -0
  120. package/dist/detectors/superlative-name.js +24 -0
  121. package/dist/detectors/superlative-name.js.map +1 -0
  122. package/dist/detectors/token-shape.d.ts.map +1 -1
  123. package/dist/detectors/token-shape.js +10 -2
  124. package/dist/detectors/token-shape.js.map +1 -1
  125. package/dist/document.d.ts.map +1 -1
  126. package/dist/document.js +10 -4
  127. package/dist/document.js.map +1 -1
  128. package/dist/genre-check.d.ts +34 -0
  129. package/dist/genre-check.d.ts.map +1 -0
  130. package/dist/genre-check.js +21 -0
  131. package/dist/genre-check.js.map +1 -0
  132. package/dist/genre.d.ts +8 -1
  133. package/dist/genre.d.ts.map +1 -1
  134. package/dist/genre.js +29 -5
  135. package/dist/genre.js.map +1 -1
  136. package/dist/init.d.ts +0 -1
  137. package/dist/init.d.ts.map +1 -1
  138. package/dist/init.js +0 -12
  139. package/dist/init.js.map +1 -1
  140. package/dist/joined-view.d.ts +25 -0
  141. package/dist/joined-view.d.ts.map +1 -0
  142. package/dist/joined-view.js +76 -0
  143. package/dist/joined-view.js.map +1 -0
  144. package/dist/line-continues.d.ts +6 -0
  145. package/dist/line-continues.d.ts.map +1 -0
  146. package/dist/line-continues.js +33 -0
  147. package/dist/line-continues.js.map +1 -0
  148. package/dist/measure.d.ts +1 -0
  149. package/dist/measure.d.ts.map +1 -1
  150. package/dist/measure.js +5 -1
  151. package/dist/measure.js.map +1 -1
  152. package/dist/number-name.d.ts +6 -3
  153. package/dist/number-name.d.ts.map +1 -1
  154. package/dist/number-name.js +31 -14
  155. package/dist/number-name.js.map +1 -1
  156. package/dist/orthography.d.ts +4 -0
  157. package/dist/orthography.d.ts.map +1 -1
  158. package/dist/orthography.js +11 -9
  159. package/dist/orthography.js.map +1 -1
  160. package/dist/plain-source.d.ts +6 -0
  161. package/dist/plain-source.d.ts.map +1 -0
  162. package/dist/plain-source.js +6 -0
  163. package/dist/plain-source.js.map +1 -0
  164. package/dist/plugin.d.ts +16 -1
  165. package/dist/plugin.d.ts.map +1 -1
  166. package/dist/quoted-span.d.ts +10 -0
  167. package/dist/quoted-span.d.ts.map +1 -0
  168. package/dist/quoted-span.js +20 -0
  169. package/dist/quoted-span.js.map +1 -0
  170. package/dist/render/eval.d.ts +2 -1
  171. package/dist/render/eval.d.ts.map +1 -1
  172. package/dist/render/eval.js +84 -43
  173. package/dist/render/eval.js.map +1 -1
  174. package/dist/resolve-genre.d.ts +11 -0
  175. package/dist/resolve-genre.d.ts.map +1 -0
  176. package/dist/resolve-genre.js +18 -0
  177. package/dist/resolve-genre.js.map +1 -0
  178. package/dist/soft-break.d.ts +19 -0
  179. package/dist/soft-break.d.ts.map +1 -0
  180. package/dist/soft-break.js +78 -0
  181. package/dist/soft-break.js.map +1 -0
  182. package/dist/structure/build.d.ts.map +1 -1
  183. package/dist/structure/build.js +19 -9
  184. package/dist/structure/build.js.map +1 -1
  185. package/dist/structure/date-order.d.ts +3 -0
  186. package/dist/structure/date-order.d.ts.map +1 -1
  187. package/dist/structure/date-order.js +9 -1
  188. package/dist/structure/date-order.js.map +1 -1
  189. package/dist/structure/issues.d.ts +1 -1
  190. package/dist/structure/issues.d.ts.map +1 -1
  191. package/dist/structure/issues.js +14 -3
  192. package/dist/structure/issues.js.map +1 -1
  193. package/dist/structure/listed-tags.d.ts +3 -0
  194. package/dist/structure/listed-tags.d.ts.map +1 -0
  195. package/dist/structure/listed-tags.js +6 -0
  196. package/dist/structure/listed-tags.js.map +1 -0
  197. package/dist/structure/number-in-sentence.d.ts +15 -0
  198. package/dist/structure/number-in-sentence.d.ts.map +1 -0
  199. package/dist/structure/number-in-sentence.js +13 -0
  200. package/dist/structure/number-in-sentence.js.map +1 -0
  201. package/dist/structure/unit-word.d.ts +8 -0
  202. package/dist/structure/unit-word.d.ts.map +1 -0
  203. package/dist/structure/unit-word.js +10 -0
  204. package/dist/structure/unit-word.js.map +1 -0
  205. package/dist/structure/wrapped-tail.d.ts +18 -0
  206. package/dist/structure/wrapped-tail.d.ts.map +1 -0
  207. package/dist/structure/wrapped-tail.js +11 -0
  208. package/dist/structure/wrapped-tail.js.map +1 -0
  209. package/package.json +3 -3
  210. package/rules/dangling-reference.yaml +2 -2
  211. package/rules/date-order.yaml +2 -2
  212. package/rules/latin-spacing.yaml +4 -2
  213. package/rules/max-kanji-continuous.yaml +1 -1
  214. package/rules/no-doubled-joshi.yaml +2 -2
  215. package/rules/title-case-consistency.yaml +2 -2
  216. package/rules/undefined-acronym.yaml +14 -1
  217. package/rules/unqualified-superlative.yaml +1 -1
  218. package/src/address-chain.ts +5 -2
  219. package/src/cli-text.ts +19 -3
  220. package/src/cli.ts +22 -19
  221. package/src/commands/eval.ts +26 -5
  222. package/src/commands/feedback.ts +2 -1
  223. package/src/commands/test.ts +3 -2
  224. package/src/commands/tree.ts +2 -1
  225. package/src/detectors/acronym-compound.ts +9 -0
  226. package/src/detectors/acronym-context.ts +22 -1
  227. package/src/detectors/acronym-expansion.ts +72 -5
  228. package/src/detectors/char-shape.ts +42 -14
  229. package/src/detectors/citation-key.ts +13 -0
  230. package/src/detectors/cited-name.ts +23 -0
  231. package/src/detectors/concrete-evidence.ts +31 -0
  232. package/src/detectors/counter-edge.ts +29 -0
  233. package/src/detectors/dotted-name.ts +14 -0
  234. package/src/detectors/doubled-word.ts +25 -2
  235. package/src/detectors/en-shape.ts +17 -30
  236. package/src/detectors/gram-place.ts +8 -1
  237. package/src/detectors/gram-predicate.ts +55 -0
  238. package/src/detectors/heading-case.ts +41 -0
  239. package/src/detectors/heading-echo.ts +5 -1
  240. package/src/detectors/lexicon.ts +8 -5
  241. package/src/detectors/name-numeral.ts +37 -0
  242. package/src/detectors/name-part.ts +24 -0
  243. package/src/detectors/name-run.ts +16 -0
  244. package/src/detectors/orthography.ts +17 -8
  245. package/src/detectors/placeholder.ts +38 -0
  246. package/src/detectors/quoted-variant.ts +20 -0
  247. package/src/detectors/series-label.ts +33 -0
  248. package/src/detectors/signals.ts +35 -30
  249. package/src/detectors/structure-tree.ts +2 -2
  250. package/src/detectors/superlative-name.ts +30 -0
  251. package/src/detectors/token-shape.ts +12 -3
  252. package/src/document.ts +12 -5
  253. package/src/genre-check.ts +46 -0
  254. package/src/genre.ts +34 -5
  255. package/src/init.ts +0 -13
  256. package/src/joined-view.ts +96 -0
  257. package/src/line-continues.ts +43 -0
  258. package/src/measure.ts +9 -1
  259. package/src/number-name.ts +35 -13
  260. package/src/orthography.ts +15 -9
  261. package/src/plain-source.ts +5 -0
  262. package/src/plugin.ts +21 -2
  263. package/src/quoted-span.ts +25 -0
  264. package/src/render/eval.ts +101 -38
  265. package/src/resolve-genre.ts +20 -0
  266. package/src/soft-break.ts +94 -0
  267. package/src/structure/build.ts +19 -10
  268. package/src/structure/date-order.ts +16 -1
  269. package/src/structure/issues.ts +18 -3
  270. package/src/structure/listed-tags.ts +7 -0
  271. package/src/structure/number-in-sentence.ts +20 -0
  272. package/src/structure/unit-word.ts +11 -0
  273. package/src/structure/wrapped-tail.ts +22 -0
@@ -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,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,5 +1,6 @@
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 { isWithinAny, quotedSpans } from "../quoted-span.ts";
3
4
  import { digitRunAround, isNumberName, sequenceLabelStarts } from "../number-name.ts";
4
5
 
5
6
  /** チームが chaff.yaml の prefer に書いた「使わない書き方」。書いていなければ何も言わない。 */
@@ -31,12 +32,18 @@ const digitBeside = (boundary: Boundary): number => {
31
32
  };
32
33
 
33
34
  /** 番号・識別子として書かれた数の境目は、空け方の好みではないので数えない(number-name.ts)。 */
34
- const isCounted = (sentence: Sentence, boundary: Boundary, sequence: ReadonlySet<number>): 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, sequence);
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 はそろっているかだけを見る。
@@ -44,11 +51,13 @@ const isCounted = (sentence: Sentence, boundary: Boundary, sequence: ReadonlySet
44
51
  export const latinSpacing: Detector = (doc, options): Finding[] => {
45
52
  // 覆った文(prose)で探す。コードの中の「1 件」は並びに入れない。
46
53
  const sequence = sequenceLabelStarts(doc.prose ?? doc.source);
47
- const located: Located[] = doc.sentences.flatMap((sentence) =>
48
- latinBoundaries(sentence.text, doc.source.slice(sentence.span.start, sentence.span.end))
49
- .filter((boundary) => isCounted(sentence, boundary, sequence))
50
- .map((boundary) => ({ sentence, ...boundary })),
51
- );
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
+ });
52
61
  return KINDS.flatMap((kind) => {
53
62
  const ofKind = located.filter((entry) => entry.kind === kind);
54
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,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);
@@ -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 }));
@@ -3,8 +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 { isExplained } from "./acronym-compound.ts";
6
7
  import { conjugatedForms } from "./conjugated-form.ts";
7
- import type { Detector, Finding, ProseDocument, Section, Sentence, Token } from "../plugin.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";
8
11
 
9
12
  const PER = 1000;
10
13
 
@@ -98,29 +101,15 @@ const gramsOf = (doc: ProseDocument): Map<string, number> => {
98
101
  /** 数えたときと同じ形の文。char 単位では空白を詰めてから数えている。 */
99
102
  const gramText = (sentence: Sentence, unit: ProseDocument["lengthUnit"]): string => compacted(sentence.text, unit).text;
100
103
 
101
- /** 動詞と助動詞。言い回し(ることができます、it is important to)にはこれが入り、主題の名前(state and local tax)には入らない。 */
102
- const PREDICATE_POS = new Set(["VERB", "AUX"]);
103
-
104
- const NOUNS = new Set(["NOUN", "PROPN", "ADJ"]);
105
-
106
- /**
107
- * 英語で名詞のすぐ前の動詞は、名詞を飾る語(形容詞を挟んでも同じ)(the upcoming fiscal year、the Disclosing Party、the borrow checker)で、
108
- * 述語ではない。日本語の名詞の前の動詞(〜にあるコンポーネント)は言い回しの一部になるので、word 単位だけで見る。
109
- */
110
- const modifiesNoun = (tokens: readonly Token[], index: number, unit: ProseDocument["lengthUnit"]): boolean =>
111
- unit === "word" && tokens[index]?.pos === "VERB" && NOUNS.has(tokens[index + 1]?.pos ?? "");
112
-
113
104
  /**
114
- * 語句が文の中で占める範囲に、動詞か助動詞があるか。範囲で見る。文字で探すと、同じ文の別の所の動詞「use」が、語句の中の名詞「use」に当たる。
105
+ * 語句が文の中で占める範囲に、述語になる動詞か助動詞があるか(gram-predicate.ts)。
115
106
  * 品詞が無ければ見分けられないので、これまでどおり言い回しとして数える。
116
107
  */
117
108
  const hasPredicate = (gram: string, sentence: Sentence, unit: ProseDocument["lengthUnit"]): boolean => {
118
109
  if (sentence.tokens === undefined) return true;
119
110
  const place = placeOf(compacted(sentence.text, unit), gram);
120
111
  if (place === undefined) return true;
121
- const [start, end] = [sentence.span.start + place.start, sentence.span.start + place.end];
122
- const tokens = sentence.tokens;
123
- 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);
124
113
  };
125
114
 
126
115
  export const ngramRepetition: Detector = (doc, options): Finding[] => {
@@ -150,10 +139,14 @@ export const ngramRepetition: Detector = (doc, options): Finding[] => {
150
139
  * 展開は「略語の直前か直後の括弧」か「括弧の中の略語」で書かれる。
151
140
  *
152
141
  * & で繋いだ大文字は 1 語として読む(ATT&CK、M&IE)。割ると CK や IE が別の略語に見える。
142
+ * - で繋いだ略語どうしも 1 語(RT-PCR)。割ると RT が別の略語に見える。数字を含むもの(COVID-19)は識別子として外す。
153
143
  */
154
144
  const EDGE_BEFORE = String.raw`(?<![A-Za-z0-9_&])`;
155
145
  const EDGE_AFTER = String.raw`(?![A-Za-z0-9_&])`;
156
- 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");
157
150
 
158
151
  /**
159
152
  * 大文字で書かれていても略語ではないもの。
@@ -162,9 +155,17 @@ const ACRONYM = new RegExp(String.raw`${EDGE_BEFORE}(?:[A-Z]+(?:&[A-Z]+)+|[A-Z]{
162
155
  * 強調は読点や括弧をまたいで続くので(THE AUTHOR, THE COMPANY (IF ANY))、そういう続きを含む
163
156
  * 「大文字以外の文字を含まない一続き」を丸ごと強調と見なす。小文字の語が挟まれば切れるので、
164
157
  * 「SRE, SLO, MTTR」のような略語の並びは残る。引用符に包まれた大文字の句(“AS IS”)も強調。
158
+ * 2 語でも、片方が略語にしては長い語なら強調(BILLING CODE)。略語 2 つの並び(NIST SP)は残る。
165
159
  */
166
160
  const CAPS_WORD = String.raw`[A-Z]+(?:&[A-Z]+)*`;
167
- 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
+ );
168
169
  const UNCASED_STRETCH = /(?:[A-Z]|\P{L})+/gu;
169
170
  const QUOTED_CAPS = new RegExp(String.raw`["“'‘]${CAPS_WORD}(?:\s+${CAPS_WORD})+["”'’]`, "gu");
170
171
 
@@ -187,7 +188,7 @@ const spanOf = (match: RegExpExecArray): Span => ({ start: match.index, end: mat
187
188
  const spansOf = (text: string, notation: NotAcronymSpans): Span[] => [
188
189
  ...[...text.matchAll(UNCASED_STRETCH)].filter((match) => SHOUTED_RUN.test(match[0])).map(spanOf),
189
190
  ...[...text.matchAll(IDENTIFIER)].filter((match) => /\d/u.test(match[0])).map(spanOf),
190
- ...[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)),
191
192
  ...notation(text),
192
193
  ];
193
194
 
@@ -218,6 +219,8 @@ const notationOf = (doc: ProseDocument): NotAcronymSpans =>
218
219
  usStates: patternsOf(doc, "us-state-code"),
219
220
  emphasis: patternsOf(doc, "emphasis-word"),
220
221
  divisions: patternsOf(doc, "numbered-division"),
222
+ honorifics: patternsOf(doc, "honorific"),
223
+ dateTimeUnits: patternsOf(doc, "date-time-unit"),
221
224
  });
222
225
 
223
226
  /** 定義の語は、語彙表の形(という)と、この文書で活用して書かれた形(といいます)の両方で照らす。 */
@@ -228,13 +231,15 @@ const definitionVerbsOf = (doc: ProseDocument): string[] => {
228
231
 
229
232
  export const undefinedAcronym: Detector = (doc, options): Finding[] => {
230
233
  const body = bodyOf(doc);
231
- 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")]);
232
236
  const seen = new Map<string, Hit>();
233
237
  acronymsOf(doc, notationOf(doc)).forEach(({ word, hit }) => {
234
- if (!common.has(word) && !seen.has(word)) seen.set(word, hit);
238
+ if (!seen.has(word)) seen.set(word, hit);
235
239
  });
236
240
  const expandedAt = expansionAt({ markers: patternsOf(doc, "definition-marker"), verbs: definitionVerbsOf(doc) });
237
- const bare = [...seen.entries()].filter(([acronym]) => !isExpanded(body, acronym, expandedAt));
241
+ const explainedAlone = (word: string): boolean => common.has(word) || isExpanded(body, word, expandedAt);
242
+ const bare = [...seen.entries()].filter(([acronym]) => !isExplained(acronym, explainedAlone));
238
243
  if (bare.length < options.limit) return [];
239
244
  return bare.map(([acronym, hit]) => ({
240
245
  rule: "undefined-acronym",
@@ -265,16 +270,16 @@ const openingOf = (sentence: Sentence | undefined, unit: ProseDocument["lengthUn
265
270
  return pieces.length <= OPENING[unit] ? pieces.join(joiner) : `${pieces.slice(0, OPENING[unit]).join(joiner)}…`;
266
271
  };
267
272
 
268
- /**
269
- * 形態素解析が数と読んだ語(UD の NumType=Card)。漢数字の「二割」「十五分」も、算用数字と同じ具体的な数。
270
- * 言い回しの「二人三脚」「一人ひとり」は、辞書が一語として持つので数にならない。品詞が無ければ、数字だけを見る。
271
- */
272
- const hasNumeral = (section: Section): boolean =>
273
- 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 ?? []));
274
274
 
275
275
  export const concreteEvidence: Detector = (doc, options): Finding[] => {
276
276
  const sections = doc.sections.filter((section) => section.sentences.length >= 3);
277
- 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));
278
283
  if (empty.length < options.limit) return [];
279
284
  return empty.map((section) => ({
280
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,
@@ -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));