chaffjs 0.10.0 → 0.11.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 (128) hide show
  1. package/dist/cli-text.d.ts.map +1 -1
  2. package/dist/cli-text.js +2 -0
  3. package/dist/cli-text.js.map +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +5 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/detectors/char-shape.d.ts.map +1 -1
  8. package/dist/detectors/char-shape.js +34 -2
  9. package/dist/detectors/char-shape.js.map +1 -1
  10. package/dist/detectors/en-shape.d.ts.map +1 -1
  11. package/dist/detectors/en-shape.js +110 -9
  12. package/dist/detectors/en-shape.js.map +1 -1
  13. package/dist/detectors/gram-place.d.ts +14 -0
  14. package/dist/detectors/gram-place.d.ts.map +1 -0
  15. package/dist/detectors/gram-place.js +39 -0
  16. package/dist/detectors/gram-place.js.map +1 -0
  17. package/dist/detectors/lexicon-match.d.ts +9 -0
  18. package/dist/detectors/lexicon-match.d.ts.map +1 -0
  19. package/dist/detectors/lexicon-match.js +40 -0
  20. package/dist/detectors/lexicon-match.js.map +1 -0
  21. package/dist/detectors/lexicon.d.ts.map +1 -1
  22. package/dist/detectors/lexicon.js +13 -20
  23. package/dist/detectors/lexicon.js.map +1 -1
  24. package/dist/detectors/phrase-match.d.ts.map +1 -1
  25. package/dist/detectors/phrase-match.js +7 -6
  26. package/dist/detectors/phrase-match.js.map +1 -1
  27. package/dist/detectors/repeated-head.d.ts.map +1 -1
  28. package/dist/detectors/repeated-head.js +15 -5
  29. package/dist/detectors/repeated-head.js.map +1 -1
  30. package/dist/detectors/sentence-ending.d.ts.map +1 -1
  31. package/dist/detectors/sentence-ending.js +7 -3
  32. package/dist/detectors/sentence-ending.js.map +1 -1
  33. package/dist/detectors/signals.d.ts.map +1 -1
  34. package/dist/detectors/signals.js +162 -22
  35. package/dist/detectors/signals.js.map +1 -1
  36. package/dist/document.d.ts.map +1 -1
  37. package/dist/document.js +22 -9
  38. package/dist/document.js.map +1 -1
  39. package/dist/lexicon-tokens.d.ts +4 -0
  40. package/dist/lexicon-tokens.d.ts.map +1 -0
  41. package/dist/lexicon-tokens.js +23 -0
  42. package/dist/lexicon-tokens.js.map +1 -0
  43. package/dist/page-furniture.d.ts +10 -0
  44. package/dist/page-furniture.d.ts.map +1 -0
  45. package/dist/page-furniture.js +35 -0
  46. package/dist/page-furniture.js.map +1 -0
  47. package/dist/plugin.d.ts +4 -0
  48. package/dist/plugin.d.ts.map +1 -1
  49. package/dist/render/compact.d.ts.map +1 -1
  50. package/dist/render/compact.js +11 -2
  51. package/dist/render/compact.js.map +1 -1
  52. package/dist/rule-load.d.ts.map +1 -1
  53. package/dist/rule-load.js +1 -0
  54. package/dist/rule-load.js.map +1 -1
  55. package/dist/run.d.ts.map +1 -1
  56. package/dist/run.js +5 -3
  57. package/dist/run.js.map +1 -1
  58. package/dist/sentence-shape.d.ts +6 -0
  59. package/dist/sentence-shape.d.ts.map +1 -1
  60. package/dist/sentence-shape.js +15 -0
  61. package/dist/sentence-shape.js.map +1 -1
  62. package/dist/structure/bare-numbers.d.ts +14 -0
  63. package/dist/structure/bare-numbers.d.ts.map +1 -0
  64. package/dist/structure/bare-numbers.js +53 -0
  65. package/dist/structure/bare-numbers.js.map +1 -0
  66. package/dist/structure/build.d.ts +2 -0
  67. package/dist/structure/build.d.ts.map +1 -1
  68. package/dist/structure/build.js +6 -3
  69. package/dist/structure/build.js.map +1 -1
  70. package/dist/structure/issues.d.ts.map +1 -1
  71. package/dist/structure/issues.js +11 -2
  72. package/dist/structure/issues.js.map +1 -1
  73. package/dist/structure/of.d.ts.map +1 -1
  74. package/dist/structure/of.js +4 -2
  75. package/dist/structure/of.js.map +1 -1
  76. package/dist/structure/total.d.ts +1 -0
  77. package/dist/structure/total.d.ts.map +1 -1
  78. package/dist/structure/total.js +37 -13
  79. package/dist/structure/total.js.map +1 -1
  80. package/dist/structure/universal.d.ts +2 -2
  81. package/dist/structure/universal.d.ts.map +1 -1
  82. package/dist/structure/universal.js +13 -4
  83. package/dist/structure/universal.js.map +1 -1
  84. package/dist/version.d.ts +5 -0
  85. package/dist/version.d.ts.map +1 -1
  86. package/dist/version.js +17 -2
  87. package/dist/version.js.map +1 -1
  88. package/package.json +3 -3
  89. package/rules/ai-tell.yaml +1 -0
  90. package/rules/closing-cliche.yaml +1 -0
  91. package/rules/concrete-evidence-density.yaml +2 -0
  92. package/rules/cushion-phrase-density.yaml +1 -0
  93. package/rules/double-keigo.yaml +1 -0
  94. package/rules/empty-intensifier.yaml +1 -0
  95. package/rules/excessive-hedging.yaml +1 -0
  96. package/rules/hiragana-fukushi.yaml +1 -0
  97. package/rules/max-kanji-continuous.yaml +2 -0
  98. package/rules/ngram-repetition.yaml +2 -0
  99. package/rules/padded-intro.yaml +1 -0
  100. package/rules/repeated-conjunction.yaml +1 -0
  101. package/rules/sasete-itadaku.yaml +1 -0
  102. package/rules/unqualified-superlative.yaml +1 -0
  103. package/src/cli-text.ts +2 -0
  104. package/src/cli.ts +5 -1
  105. package/src/detectors/char-shape.ts +38 -3
  106. package/src/detectors/en-shape.ts +138 -7
  107. package/src/detectors/gram-place.ts +41 -0
  108. package/src/detectors/lexicon-match.ts +44 -0
  109. package/src/detectors/lexicon.ts +15 -22
  110. package/src/detectors/phrase-match.ts +8 -7
  111. package/src/detectors/repeated-head.ts +21 -7
  112. package/src/detectors/sentence-ending.ts +7 -3
  113. package/src/detectors/signals.ts +188 -21
  114. package/src/document.ts +25 -9
  115. package/src/lexicon-tokens.ts +28 -0
  116. package/src/page-furniture.ts +45 -0
  117. package/src/plugin.ts +9 -1
  118. package/src/render/compact.ts +12 -2
  119. package/src/rule-load.ts +1 -0
  120. package/src/run.ts +6 -3
  121. package/src/sentence-shape.ts +16 -0
  122. package/src/structure/bare-numbers.ts +78 -0
  123. package/src/structure/build.ts +9 -3
  124. package/src/structure/issues.ts +16 -2
  125. package/src/structure/of.ts +4 -2
  126. package/src/structure/total.ts +45 -19
  127. package/src/structure/universal.ts +14 -3
  128. package/src/version.ts +21 -3
@@ -44,6 +44,7 @@ type State = {
44
44
  readonly profile: DocumentProfile | undefined;
45
45
  /** 見出しだけの行(文書の種類の caption)の行番号と、その言葉。すぐ次の行の条が見出しに使う。 */
46
46
  readonly captions: Map<number, string>;
47
+ readonly plainText: boolean;
47
48
  };
48
49
 
49
50
  const draftOf = (kind: StructureKind, address: string, line: Line, attrs: Readonly<Record<string, string | number>>): Draft => ({
@@ -145,8 +146,8 @@ const LEAVES: readonly { readonly kind: StructureKind; readonly find: LeafFinder
145
146
  ];
146
147
 
147
148
  /** 言語を問わない通し番号。後ろが単位なら数量なので番号にしない。 */
148
- const universalNumber = (patterns: StructurePatterns, text: string, context: NumberingContext): NumberedLine | undefined => {
149
- const dotted = dottedNumber(text, context);
149
+ const universalNumber = (patterns: StructurePatterns, text: string, context: NumberingContext, plainText: boolean): NumberedLine | undefined => {
150
+ const dotted = dottedNumber(text, context, plainText);
150
151
  return dotted !== undefined && patterns.countedAfter?.(dotted.number, dotted.rest) === true ? undefined : dotted;
151
152
  };
152
153
 
@@ -231,6 +232,8 @@ export type StructureInput = {
231
232
  readonly source: string;
232
233
  readonly language: string;
233
234
  readonly outline: Outline;
235
+ /** Markdown か、テキストか。テキストの仕様書だけの書き方(「5. Security Considerations」)がある。 */
236
+ readonly markdown: boolean;
234
237
  readonly profile?: DocumentProfile | undefined;
235
238
  };
236
239
 
@@ -271,7 +274,9 @@ const openUnits = (state: State): OpenUnit[] =>
271
274
 
272
275
  /** 言語の番号の読み方、文書の種類の番号を書かない単位、言語を問わない通し番号の順に読む。 */
273
276
  const numberedLine = (state: State, patterns: StructurePatterns, text: string, context: NumberingContext): NumberedLine | undefined =>
274
- patterns.numbered(text, context) ?? unnumberedUnit(text, openUnits(state), state.profile?.unnumbered) ?? universalNumber(patterns, text, context);
277
+ patterns.numbered(text, context) ??
278
+ unnumberedUnit(text, openUnits(state), state.profile?.unnumbered) ??
279
+ universalNumber(patterns, text, context, state.plainText);
275
280
 
276
281
  /** 番号の後ろが見出しでなく本文か。前の行から見出しが付く前の、行そのもので決める。 */
277
282
  const carriesBody = (found: NumberedLine | undefined): boolean => found !== undefined && found.heading === "" && found.rest !== "";
@@ -321,6 +326,7 @@ export const buildTree = (input: StructureInput, patterns: StructurePatterns): S
321
326
  widerScopes: new Map(),
322
327
  profile: input.profile,
323
328
  captions: new Map(),
329
+ plainText: !input.markdown,
324
330
  };
325
331
  lines.forEach((line) => {
326
332
  if (line.text.trim() !== "") readLine(state, patterns, line, headings.get(line.number));
@@ -30,7 +30,21 @@ const textOf = (node: StructureNode, key: string): string => {
30
30
  * 日本語の「第4条第1項」は、番号の無い第 1 項を持つ第4条を指しうる。
31
31
  */
32
32
  const resolves = (node: StructureNode, addresses: ReadonlySet<string>): boolean =>
33
- addresses.has(textOf(node, "target")) || (node.attrs["fallback"] !== undefined && addresses.has(textOf(node, "fallback")));
33
+ addresses.has(textOf(node, "target")) || (node.attrs["fallback"] !== undefined && addresses.has(fallbackKey(node)));
34
+
35
+ /**
36
+ * fallbackLabel があれば、fallback はその見出しの番号で書かれた節点だけに当たる。
37
+ * 「第3章」の fallback 3 は「## 3. 構成」には当たり、同じ番地の「第3条」には当たらない。
38
+ */
39
+ const labelled = (address: string, label: string): string => `${address}\u0000${label}`;
40
+ const fallbackKey = (node: StructureNode): string =>
41
+ node.attrs["fallbackLabel"] === undefined ? textOf(node, "fallback") : labelled(textOf(node, "fallback"), textOf(node, "fallbackLabel"));
42
+
43
+ /** 見出しの下の章(h1/ch9)は、見出しの番地を除いた ch9 でも指せる。本文の「第9章」は見出しの番地を書かない。 */
44
+ const addressesOf = (node: StructureNode): string[] =>
45
+ node.kind === "chapter" && node.address.includes("/") ? [node.address, node.address.slice(node.address.lastIndexOf("/") + 1)] : [node.address];
46
+
47
+ const keysOf = (node: StructureNode): string[] => addressesOf(node).flatMap((address) => [address, labelled(address, textOf(node, "label"))]);
34
48
 
35
49
  /**
36
50
  * 参照の番地が木に無い。他の文書の名前が付いた参照(民法第709条、Section 9 of the Master Agreement)は引かない。
@@ -40,7 +54,7 @@ export const danglingReferences = (tree: StructureNode): StructureIssue[] => {
40
54
  const nodes = inDocumentOrder(tree);
41
55
  // 参照は条を指す。条を一つも持たない文書(契約書に付ける承諾書のひな形など)の「契約書第6条」は、別の文書の条。
42
56
  if (!nodes.some((node) => node.kind === "article")) return [];
43
- const addresses = new Set(nodes.filter((node) => NUMBERED.includes(node.kind)).map((node) => node.address));
57
+ const addresses = new Set(nodes.filter((node) => NUMBERED.includes(node.kind)).flatMap(keysOf));
44
58
  // Section で組んだ法令が "Articles 13 to 21 of the UK GDPR" と別の文書の Article を名指ししていれば、名の無い "Article 6(3)" もそちら。
45
59
  // 名指しがあれば、名の無い "Article 9" が書き間違いか向こうの条かは区別できないので黙る。名指しが無い文書では報告する。
46
60
  const numbering = (node: StructureNode): unknown => node.attrs["numbering"];
@@ -1,6 +1,7 @@
1
1
  import { markdownOutline } from "../document.ts";
2
2
  import type { DocumentProfile, StructureNode, StructurePatterns } from "../plugin.ts";
3
- import { buildTree, NO_OUTLINE } from "./build.ts";
3
+ import { buildTree } from "./build.ts";
4
+ import { textOutline } from "../page-furniture.ts";
4
5
 
5
6
  export type SourceInput = {
6
7
  readonly path: string;
@@ -20,7 +21,8 @@ export const buildStructure = (input: SourceInput, patterns: StructurePatterns):
20
21
  path: input.path,
21
22
  source: input.source,
22
23
  language: input.language,
23
- outline: input.markdown ? markdownOutline(input.source) : NO_OUTLINE,
24
+ outline: input.markdown ? markdownOutline(input.source) : textOutline(input.source),
25
+ markdown: input.markdown,
24
26
  profile: input.profile,
25
27
  },
26
28
  patterns,
@@ -1,14 +1,19 @@
1
1
  import type { StructureIssue } from "./issues.ts";
2
2
  import { runsOf, type Line } from "./runs.ts";
3
+ import { bareNumbersIn, CELL_SEPARATOR, NO_UNIT } from "./bare-numbers.ts";
3
4
 
4
5
  /**
5
6
  * 合計の行と、その上に並べた金額の和。箇条書きの続いた項目か、表の続いた行で、合計の語(合計・小計・Total)で始まる行を読む。
6
7
  * 同じ単位で同じ列(箇条書きは一つの列)の金額を上から足し、合計の行の金額と合わなければ言う。
7
8
  * 小計と税のあとの総計のように、足し方が何通りかある。どの足し方でも合わないときだけ言う。
9
+ * 表の升に数だけを書いた列(単位は見出しにある)も足す。そのときは上の行の升がどれも数でなければ足さない。年や番号の列を足さないため。
8
10
  */
9
11
  export type Amount = { readonly offset: number; readonly end: number; readonly value: number; readonly unit: string };
10
12
 
11
13
  const CENTS = 100;
14
+
15
+ /** 区切りのコンマを書かずに 4 桁以上続けた整数部(1205、1205.50)。 */
16
+ const UNGROUPED = /(?<![\d.])\d{4}/u;
12
17
  const MIN_PARTS = 2;
13
18
 
14
19
  /** 金額の前に書いた負の印(-500、▲500、-$500)。行頭の箇条書きの - は印にしない。 */
@@ -22,9 +27,6 @@ type Cell = { readonly column: number; readonly unit: string };
22
27
  type Placed = Amount & Cell & { readonly cents: number | undefined };
23
28
  type Entry = { readonly label: boolean; readonly amounts: readonly Placed[] };
24
29
 
25
- /** 表の列の区切り。\| は列の中に書いた | なので数えない。 */
26
- const CELL_SEPARATOR = /(?<!\\)\|/u;
27
-
28
30
  const columnOf = (source: string, line: Line, offset: number): number => source.slice(line.start, offset).split(CELL_SEPARATOR).length - 1;
29
31
 
30
32
  const isNegative = (source: string, line: Line, amount: Amount): boolean => {
@@ -69,11 +71,14 @@ const sumOf = (values: readonly number[]): number => values.reduce((sum, value)
69
71
 
70
72
  type RowsAbove = { readonly parts: number[]; readonly totals: number[]; sinceLast: number[] };
71
73
 
72
- /** 行の中で、その列と単位の値。無ければ null、二つ以上あるか、列に別の単位しか無いか、符号が決まらなければ undefined。 */
74
+ /**
75
+ * 行の中で、その列と単位の値。無ければ null、二つ以上あるか、列に別の単位しか無いか、符号が決まらなければ undefined。
76
+ * 数だけの列では、数の無い升(空、文字、括弧)も undefined。
77
+ */
73
78
  const valueIn = (entry: Entry, cell: Cell): number | null | undefined => {
74
79
  const inColumn = entry.amounts.filter((amount) => amount.column === cell.column);
75
80
  const matching = inColumn.filter((amount) => sameCell(amount, cell));
76
- if (matching.length === 0) return inColumn.length === 0 ? null : undefined;
81
+ if (matching.length === 0) return inColumn.length === 0 && cell.unit !== NO_UNIT ? null : undefined;
77
82
  return matching.length === 1 ? matching[0]?.cents : undefined;
78
83
  };
79
84
 
@@ -115,43 +120,64 @@ const reportedSum = (above: RowsAbove): number => (above.parts.length === 0 ? su
115
120
  /** 和と呼べるだけの行があるか。項目が二つ以上か、項目が無く小計が二つ以上。 */
116
121
  const enoughRows = (above: RowsAbove): boolean => above.parts.length >= MIN_PARTS || (above.parts.length === 0 && above.totals.length >= MIN_PARTS);
117
122
 
118
- /** 和は、合計の行と同じ小数の桁で見せる($10,160.00 なら $10,260.00)。端数があれば 2 桁。 */
123
+ /** 和は、合計の行と同じ小数の桁と区切りで見せる($10,160.00 なら $10,260.00、1205 なら 1305)。端数があれば 2 桁。 */
119
124
  const formatCents = (cents: number, written: string): string => {
120
125
  const digits = Math.max(/\.\d{2}/u.test(written) ? 2 : 0, cents % CENTS === 0 ? 0 : 2);
121
- return (cents / CENTS).toLocaleString("en-US", { minimumFractionDigits: digits, maximumFractionDigits: digits });
126
+ const useGrouping = !UNGROUPED.test(written);
127
+ return (cents / CENTS).toLocaleString("en-US", { minimumFractionDigits: digits, maximumFractionDigits: digits, useGrouping });
122
128
  };
123
129
 
124
130
  /** 数の前に書いた単位($1,200、$ 1,200)の始まり。木の数量は数から始まるので、見せるときは前の単位も入れる。 */
125
131
  const unitStartBefore = (source: string, amount: Amount): number | undefined => {
132
+ if (amount.unit === NO_UNIT) return undefined;
126
133
  const gap = /[ \t]/u.test(source[amount.offset - 1] ?? "") ? 1 : 0;
127
134
  const start = amount.offset - gap - amount.unit.length;
128
135
  return start >= 0 && source.slice(start, amount.offset - gap) === amount.unit ? start : undefined;
129
136
  };
130
137
 
131
- /** 合計の行に書いた金額と、上の和を、同じ書き方で見せる。 */
132
- const shownAmounts = (source: string, total: Amount, sumCents: number): Record<string, string> => {
138
+ /** 負の金額の前に書いた印(-、▲)。見せる金額に入れる。 */
139
+ const SIGN_BEFORE = /[-−▲△][ \t]?$/u;
140
+ const SIGN_REACH = 2;
141
+
142
+ const signBefore = (source: string, total: Placed, start: number): string =>
143
+ total.cents !== undefined && total.cents < 0 ? (SIGN_BEFORE.exec(source.slice(Math.max(0, start - SIGN_REACH), start))?.[0] ?? "") : "";
144
+
145
+ /** 合計の行に書いた金額と、上の和を、同じ書き方で見せる。負の和には、合計に書いた印(無ければ -)を付ける。 */
146
+ const shownAmounts = (source: string, total: Placed, sumCents: number): Record<string, string> => {
133
147
  const unitStart = unitStartBefore(source, total);
134
- const written = source.slice(unitStart ?? total.offset, total.end);
135
- const sum = formatCents(sumCents, written);
136
- if (unitStart !== undefined) return { written, sum: `${source.slice(unitStart, total.offset)}${sum}` };
137
- return { written, sum: written.endsWith(total.unit) ? `${sum}${total.unit}` : sum };
148
+ const start = unitStart ?? total.offset;
149
+ const sign = signBefore(source, total, start);
150
+ const written = source.slice(start - sign.length, total.end);
151
+ const sumSign = sumCents < 0 ? sign || "-" : "";
152
+ const sum = formatCents(Math.abs(sumCents), written);
153
+ if (unitStart !== undefined) return { written, sum: `${sumSign}${source.slice(unitStart, total.offset)}${sum}` };
154
+ return { written, sum: `${sumSign}${sum}${written.endsWith(total.unit) ? total.unit : ""}` };
138
155
  };
139
156
 
157
+ /**
158
+ * 数だけの列で、合計の行の数が項目のどれかより大きくなければ、和ではなく平均や率や年の列と読んで足さない。
159
+ * 項目に負の数があれば、和が項目より小さいこともあるので足す。
160
+ */
161
+ const readsAsSum = (cell: Cell, writtenCents: number, above: RowsAbove): boolean =>
162
+ cell.unit !== NO_UNIT || above.parts.some((part) => part < 0) || above.parts.every((part) => part < writtenCents);
163
+
140
164
  const mismatchesIn = (source: string, entries: readonly Entry[]): StructureIssue[] =>
141
165
  entries.flatMap((entry, index) => {
142
166
  if (!entry.label) return [];
143
167
  return entry.amounts.flatMap((total) => {
144
168
  const writtenCents = valueIn(entry, total);
145
169
  const above = windowAbove(entries.slice(0, index), total);
146
- if (typeof writtenCents !== "number" || above === undefined || !enoughRows(above) || candidateSums(above).includes(writtenCents)) return [];
170
+ if (typeof writtenCents !== "number" || above === undefined || !enoughRows(above) || !readsAsSum(total, writtenCents, above)) return [];
171
+ if (candidateSums(above).includes(writtenCents)) return [];
147
172
  return [{ offset: total.offset, values: shownAmounts(source, total, reportedSum(above)) }];
148
173
  });
149
174
  });
150
175
 
151
176
  export const totalMismatches = (source: string, amounts: readonly Amount[], labels: readonly string[]): StructureIssue[] =>
152
- runsOf(source).flatMap((run) =>
153
- mismatchesIn(
177
+ runsOf(source).flatMap((run) => {
178
+ const inRun = [...amounts, ...bareNumbersIn(source, run)];
179
+ return mismatchesIn(
154
180
  source,
155
- run.map((line) => entryOf(source, line, amounts, labels)),
156
- ),
157
- );
181
+ run.map((line) => entryOf(source, line, inRun, labels)),
182
+ );
183
+ });
@@ -7,15 +7,26 @@ import type { NumberedLine, NumberingContext } from "../plugin.ts";
7
7
  const DOTTED = /^[ \t]{0,3}(?<number>\d{1,3}(?:\.\d{1,3}){0,5})\.?[ \t\u3000]+(?<rest>\S.*)$/u;
8
8
 
9
9
  /**
10
- * 本文の「1. 」は箇条書きで、見出しの「1. 」は章番号。点を含まない番号は、見出しの行でだけ読む。
10
+ * RFC のようなテキストの仕様書(Markdown でないもの)の章見出し「5. Security Considerations」。Markdown では、行内のコードを伏せた空白が
11
+ * 同じ形に見えるので読まない。行頭から、点の後ろに空白が 2 つ以上あり、
12
+ * 見出しは文として終わらない(句点・ピリオド・読点で終わらない)短い語句。箇条書きの「1. 」は空白 1 つで、文で終わる。
13
+ */
14
+ const TOP_SECTION = /^\d{1,3}\. {2,}\S/u;
15
+ const SENTENCE_END = /[.。:;,、!?!?]$/u;
16
+ const MAX_TITLE = 80;
17
+
18
+ const isTopSection = (line: string, rest: string): boolean => TOP_SECTION.test(line) && rest.length <= MAX_TITLE && !SENTENCE_END.test(rest);
19
+
20
+ /**
21
+ * 本文の「1. 」は箇条書きで、見出しの「1. 」は章番号。点を含まない番号は、見出しの行か、テキストの章見出しの形でだけ読む。
11
22
  * 「4.2 」のように点を含むものは、.txt の仕様書でも見出しとして書かれるので本文でも読む。
12
23
  */
13
- export const dottedNumber = (line: string, context: NumberingContext): NumberedLine | undefined => {
24
+ export const dottedNumber = (line: string, context: NumberingContext, plainText = false): NumberedLine | undefined => {
14
25
  const groups = DOTTED.exec(line)?.groups;
15
26
  const number = groups?.["number"];
16
27
  if (number === undefined) return undefined;
17
28
  const depth = number.split(".").length;
18
- if (depth < 2 && !context.isHeading) return undefined;
19
29
  const rest = groups?.["rest"]?.trim() ?? "";
30
+ if (depth < 2 && !context.isHeading && !(plainText && isTopSection(line, rest))) return undefined;
20
31
  return { kind: "article", depth, number, absolute: true, label: number, heading: rest, rest, ordinal: Number(number.split(".").at(-1)) };
21
32
  };
package/src/version.ts CHANGED
@@ -8,7 +8,25 @@ import { dirname, join } from "node:path";
8
8
  */
9
9
  const MANIFEST = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
10
10
 
11
- const versionOf = (raw: unknown): string =>
12
- typeof raw === "object" && raw !== null && "version" in raw && typeof raw.version === "string" ? raw.version : "0.0.0";
11
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
13
12
 
14
- export const VERSION = versionOf(JSON.parse(readFileSync(MANIFEST, "utf8")));
13
+ const versionOf = (raw: unknown): string => (isRecord(raw) && typeof raw["version"] === "string" ? raw["version"] : "0.0.0");
14
+
15
+ const BUNDLED = /^@chaffjs\/lang-/u;
16
+
17
+ /**
18
+ * `chaff --version` の行。chaffjs と、同梱の言語パッケージ。chaffjs は言語パッケージをちょうどの版で依存に書くので、manifest がそのまま答え。
19
+ */
20
+ export const versionLines = (raw: unknown): string[] => {
21
+ const dependencies = isRecord(raw) && isRecord(raw["dependencies"]) ? raw["dependencies"] : {};
22
+ const bundled = Object.entries(dependencies)
23
+ .filter((entry): entry is [string, string] => BUNDLED.test(entry[0]) && typeof entry[1] === "string")
24
+ .sort(([left], [right]) => left.localeCompare(right))
25
+ .map(([name, version]) => `${name} ${version}`);
26
+ return [`chaffjs ${versionOf(raw)}`, ...bundled];
27
+ };
28
+
29
+ const manifest: unknown = JSON.parse(readFileSync(MANIFEST, "utf8"));
30
+
31
+ export const VERSION = versionOf(manifest);
32
+ export const VERSION_LINES = versionLines(manifest);