chaffjs 0.8.0 → 0.10.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 (190) hide show
  1. package/dist/address-chain.d.ts +9 -0
  2. package/dist/address-chain.d.ts.map +1 -0
  3. package/dist/address-chain.js +105 -0
  4. package/dist/address-chain.js.map +1 -0
  5. package/dist/cli-args.d.ts.map +1 -1
  6. package/dist/cli-args.js +1 -1
  7. package/dist/cli-args.js.map +1 -1
  8. package/dist/cli-text.d.ts.map +1 -1
  9. package/dist/cli-text.js +4 -0
  10. package/dist/cli-text.js.map +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +19 -1
  13. package/dist/cli.js.map +1 -1
  14. package/dist/commands/eval.d.ts.map +1 -1
  15. package/dist/commands/eval.js +2 -1
  16. package/dist/commands/eval.js.map +1 -1
  17. package/dist/commands/feedback.d.ts +35 -0
  18. package/dist/commands/feedback.d.ts.map +1 -0
  19. package/dist/commands/feedback.js +157 -0
  20. package/dist/commands/feedback.js.map +1 -0
  21. package/dist/commands/test.d.ts.map +1 -1
  22. package/dist/commands/test.js +3 -2
  23. package/dist/commands/test.js.map +1 -1
  24. package/dist/commands/tree.d.ts.map +1 -1
  25. package/dist/commands/tree.js +3 -1
  26. package/dist/commands/tree.js.map +1 -1
  27. package/dist/config/by-path.d.ts +5 -1
  28. package/dist/config/by-path.d.ts.map +1 -1
  29. package/dist/config/by-path.js +1 -4
  30. package/dist/config/by-path.js.map +1 -1
  31. package/dist/config/load.d.ts +2 -0
  32. package/dist/config/load.d.ts.map +1 -1
  33. package/dist/config/load.js +3 -1
  34. package/dist/config/load.js.map +1 -1
  35. package/dist/detectors/char-shape.d.ts.map +1 -1
  36. package/dist/detectors/char-shape.js +3 -2
  37. package/dist/detectors/char-shape.js.map +1 -1
  38. package/dist/detectors/index.d.ts.map +1 -1
  39. package/dist/detectors/index.js +4 -1
  40. package/dist/detectors/index.js.map +1 -1
  41. package/dist/detectors/lexicon.d.ts.map +1 -1
  42. package/dist/detectors/lexicon.js +8 -1
  43. package/dist/detectors/lexicon.js.map +1 -1
  44. package/dist/detectors/structure-tree.d.ts +5 -0
  45. package/dist/detectors/structure-tree.d.ts.map +1 -1
  46. package/dist/detectors/structure-tree.js +50 -1
  47. package/dist/detectors/structure-tree.js.map +1 -1
  48. package/dist/detectors/team.d.ts.map +1 -1
  49. package/dist/detectors/team.js +7 -2
  50. package/dist/detectors/team.js.map +1 -1
  51. package/dist/detectors/word-list.d.ts +3 -0
  52. package/dist/detectors/word-list.d.ts.map +1 -0
  53. package/dist/detectors/word-list.js +3 -0
  54. package/dist/detectors/word-list.js.map +1 -0
  55. package/dist/document.d.ts +2 -2
  56. package/dist/document.d.ts.map +1 -1
  57. package/dist/document.js +4 -2
  58. package/dist/document.js.map +1 -1
  59. package/dist/feedback/draft.d.ts +28 -0
  60. package/dist/feedback/draft.d.ts.map +1 -0
  61. package/dist/feedback/draft.js +66 -0
  62. package/dist/feedback/draft.js.map +1 -0
  63. package/dist/feedback/excerpt.d.ts +10 -0
  64. package/dist/feedback/excerpt.d.ts.map +1 -0
  65. package/dist/feedback/excerpt.js +20 -0
  66. package/dist/feedback/excerpt.js.map +1 -0
  67. package/dist/legal-address.d.ts +2 -0
  68. package/dist/legal-address.d.ts.map +1 -0
  69. package/dist/legal-address.js +16 -0
  70. package/dist/legal-address.js.map +1 -0
  71. package/dist/orthography.d.ts.map +1 -1
  72. package/dist/orthography.js +15 -0
  73. package/dist/orthography.js.map +1 -1
  74. package/dist/plugin.d.ts +98 -0
  75. package/dist/plugin.d.ts.map +1 -1
  76. package/dist/profile/for-file.d.ts +7 -0
  77. package/dist/profile/for-file.d.ts.map +1 -0
  78. package/dist/profile/for-file.js +6 -0
  79. package/dist/profile/for-file.js.map +1 -0
  80. package/dist/profile/load.d.ts +4 -0
  81. package/dist/profile/load.d.ts.map +1 -0
  82. package/dist/profile/load.js +25 -0
  83. package/dist/profile/load.js.map +1 -0
  84. package/dist/profile/parse.d.ts +19 -0
  85. package/dist/profile/parse.d.ts.map +1 -0
  86. package/dist/profile/parse.js +95 -0
  87. package/dist/profile/parse.js.map +1 -0
  88. package/dist/profile/select.d.ts +22 -0
  89. package/dist/profile/select.d.ts.map +1 -0
  90. package/dist/profile/select.js +21 -0
  91. package/dist/profile/select.js.map +1 -0
  92. package/dist/reasons.d.ts +1 -0
  93. package/dist/reasons.d.ts.map +1 -1
  94. package/dist/reasons.js +2 -0
  95. package/dist/reasons.js.map +1 -1
  96. package/dist/render/compact.d.ts.map +1 -1
  97. package/dist/render/compact.js +3 -1
  98. package/dist/render/compact.js.map +1 -1
  99. package/dist/run.d.ts.map +1 -1
  100. package/dist/run.js +40 -7
  101. package/dist/run.js.map +1 -1
  102. package/dist/structure/build.d.ts +2 -1
  103. package/dist/structure/build.d.ts.map +1 -1
  104. package/dist/structure/build.js +105 -11
  105. package/dist/structure/build.js.map +1 -1
  106. package/dist/structure/date-order.d.ts +12 -0
  107. package/dist/structure/date-order.d.ts.map +1 -0
  108. package/dist/structure/date-order.js +30 -0
  109. package/dist/structure/date-order.js.map +1 -0
  110. package/dist/structure/issues.d.ts.map +1 -1
  111. package/dist/structure/issues.js +9 -2
  112. package/dist/structure/issues.js.map +1 -1
  113. package/dist/structure/of.d.ts +2 -1
  114. package/dist/structure/of.d.ts.map +1 -1
  115. package/dist/structure/of.js +7 -1
  116. package/dist/structure/of.js.map +1 -1
  117. package/dist/structure/relative-find.d.ts +8 -0
  118. package/dist/structure/relative-find.d.ts.map +1 -0
  119. package/dist/structure/relative-find.js +190 -0
  120. package/dist/structure/relative-find.js.map +1 -0
  121. package/dist/structure/relative-resolve.d.ts +3 -0
  122. package/dist/structure/relative-resolve.d.ts.map +1 -0
  123. package/dist/structure/relative-resolve.js +175 -0
  124. package/dist/structure/relative-resolve.js.map +1 -0
  125. package/dist/structure/runs.d.ts +20 -0
  126. package/dist/structure/runs.d.ts.map +1 -0
  127. package/dist/structure/runs.js +63 -0
  128. package/dist/structure/runs.js.map +1 -0
  129. package/dist/structure/total.d.ts +14 -0
  130. package/dist/structure/total.d.ts.map +1 -0
  131. package/dist/structure/total.js +119 -0
  132. package/dist/structure/total.js.map +1 -0
  133. package/dist/structure/unnumbered.d.ts +15 -0
  134. package/dist/structure/unnumbered.d.ts.map +1 -0
  135. package/dist/structure/unnumbered.js +28 -0
  136. package/dist/structure/unnumbered.js.map +1 -0
  137. package/dist/structure/unread.d.ts +8 -0
  138. package/dist/structure/unread.d.ts.map +1 -0
  139. package/dist/structure/unread.js +34 -0
  140. package/dist/structure/unread.js.map +1 -0
  141. package/dist/structure/weekday.d.ts +7 -0
  142. package/dist/structure/weekday.d.ts.map +1 -0
  143. package/dist/structure/weekday.js +21 -0
  144. package/dist/structure/weekday.js.map +1 -0
  145. package/package.json +4 -3
  146. package/profiles/statute.yaml +66 -0
  147. package/rules/date-order.yaml +26 -0
  148. package/rules/date-weekday-mismatch.yaml +26 -0
  149. package/rules/internal-jargon.yaml +1 -1
  150. package/rules/total-mismatch.yaml +26 -0
  151. package/skills/chaff/SKILL.md +24 -4
  152. package/src/address-chain.ts +113 -0
  153. package/src/cli-args.ts +1 -1
  154. package/src/cli-text.ts +4 -0
  155. package/src/cli.ts +20 -1
  156. package/src/commands/eval.ts +2 -1
  157. package/src/commands/feedback.ts +210 -0
  158. package/src/commands/test.ts +3 -2
  159. package/src/commands/tree.ts +3 -1
  160. package/src/config/by-path.ts +11 -3
  161. package/src/config/load.ts +5 -1
  162. package/src/detectors/char-shape.ts +5 -4
  163. package/src/detectors/index.ts +4 -1
  164. package/src/detectors/lexicon.ts +11 -1
  165. package/src/detectors/structure-tree.ts +67 -1
  166. package/src/detectors/team.ts +7 -2
  167. package/src/detectors/word-list.ts +2 -0
  168. package/src/document.ts +11 -3
  169. package/src/feedback/draft.ts +102 -0
  170. package/src/feedback/excerpt.ts +23 -0
  171. package/src/orthography.ts +15 -0
  172. package/src/plugin.ts +95 -0
  173. package/src/profile/for-file.ts +11 -0
  174. package/src/profile/load.ts +26 -0
  175. package/src/profile/parse.ts +119 -0
  176. package/src/profile/select.ts +35 -0
  177. package/src/reasons.ts +5 -0
  178. package/src/render/compact.ts +4 -1
  179. package/src/run.ts +42 -7
  180. package/src/structure/build.ts +135 -15
  181. package/src/structure/date-order.ts +44 -0
  182. package/src/structure/issues.ts +10 -2
  183. package/src/structure/of.ts +15 -3
  184. package/src/structure/relative-find.ts +237 -0
  185. package/src/structure/relative-resolve.ts +209 -0
  186. package/src/structure/runs.ts +71 -0
  187. package/src/structure/total.ts +157 -0
  188. package/src/structure/unnumbered.ts +33 -0
  189. package/src/structure/unread.ts +39 -0
  190. package/src/structure/weekday.ts +25 -0
@@ -1,9 +1,13 @@
1
1
  import type { Heading } from "../document.ts";
2
2
  import { atxHeadingText, headingText } from "../heading-text.ts";
3
+ import { addressSpans } from "../address-chain.ts";
4
+ import { relativeMentions, substitutedDocuments } from "./relative-find.ts";
5
+ import { resolveRelative } from "./relative-resolve.ts";
3
6
  import { maskSpans } from "../mask.ts";
4
- import type { Mention, NumberedLine, NumberingContext, Span, StructureKind, StructureNode, StructurePatterns } from "../plugin.ts";
7
+ import type { DocumentProfile, Mention, NumberedLine, NumberingContext, Span, StructureKind, StructureNode, StructurePatterns } from "../plugin.ts";
5
8
  import { lineNumberAt, linesOf, type Line } from "./lines.ts";
6
9
  import { dottedNumber } from "./universal.ts";
10
+ import { unnumberedUnit, type OpenUnit } from "./unnumbered.ts";
7
11
 
8
12
  /** 組み立て中の節点。できあがったら StructureNode に固める。 */
9
13
  type Draft = {
@@ -17,10 +21,11 @@ type Draft = {
17
21
  readonly ordinal?: number | undefined;
18
22
  readonly level?: number | undefined;
19
23
  readonly ordinalTo?: number | undefined;
24
+ readonly numbering?: string | undefined;
20
25
  };
21
26
 
22
27
  /** 開いている節点。rank の大きいものほど内側。 */
23
- type Frame = { readonly draft: Draft; readonly rank: number; readonly numbered?: NumberedLine | undefined };
28
+ type Frame = { readonly draft: Draft; readonly rank: number; readonly numbered?: NumberedLine | undefined; readonly bodyOnLine?: boolean };
24
29
 
25
30
  /**
26
31
  * 見出しは深さ 1〜6 を rank にする。番号のまとまりは必ず見出しの内側に入るので、
@@ -32,6 +37,13 @@ type State = {
32
37
  readonly stack: Frame[];
33
38
  /** 見出しの深さごとの通し番号。番号の無い見出しの番地 h2.1 を作る。 */
34
39
  readonly headingCounts: number[];
40
+ /** opensDefinitionScope の行を含んだまとまり(ふつうは条)。ここに入る定義は範囲の中でだけ比べる。 */
41
+ readonly scopedArticles: Set<Draft>;
42
+ /** 範囲が条より広い(「In this Part—」)条と、その広いまとまり。その条の定義は、広いまとまりの中で比べる。 */
43
+ readonly widerScopes: Map<Draft, Draft>;
44
+ readonly profile: DocumentProfile | undefined;
45
+ /** 見出しだけの行(文書の種類の caption)の行番号と、その言葉。すぐ次の行の条が見出しに使う。 */
46
+ readonly captions: Map<number, string>;
35
47
  };
36
48
 
37
49
  const draftOf = (kind: StructureKind, address: string, line: Line, attrs: Readonly<Record<string, string | number>>): Draft => ({
@@ -85,7 +97,7 @@ const headingAddress = (state: State, depth: number): string => {
85
97
  * 見出しに書いた番号(「## 第3条」)は、見出しの深さで入れ子にする。本文の番号は見出しの内側に入る。
86
98
  * 見出しの深さを捨てると、「## 第3条」の下の「### 詳細」が条の外に出てしまう。
87
99
  */
88
- const openNumbered = (state: State, line: Line, numbered: NumberedLine, headingDepth: number | undefined): void => {
100
+ const openNumbered = (state: State, line: Line, numbered: NumberedLine, headingDepth: number | undefined, bodyOnLine: boolean): void => {
89
101
  const attrs = { label: numbered.label, ...(numbered.heading === "" ? {} : { heading: numbered.heading }) };
90
102
  const rank = headingDepth ?? NUMBERED_RANK + numbered.depth;
91
103
  // 番地は、自分と同じか内側を閉じてから決める。閉じる前だと、同じ深さの前の条を親と取り違える。
@@ -95,8 +107,9 @@ const openNumbered = (state: State, line: Line, numbered: NumberedLine, headingD
95
107
  ordinal: numbered.ordinal,
96
108
  level: numbered.depth,
97
109
  ordinalTo: numbered.ordinalTo,
110
+ numbering: numbered.numbering,
98
111
  };
99
- open(state, { draft, rank, numbered });
112
+ open(state, { draft, rank, numbered, bodyOnLine });
100
113
  };
101
114
 
102
115
  const openSection = (state: State, line: Line, heading: Heading): void => {
@@ -104,11 +117,30 @@ const openSection = (state: State, line: Line, heading: Heading): void => {
104
117
  open(state, { draft: draftOf("section", headingAddress(state, heading.depth), line, { heading: heading.text }), rank: heading.depth });
105
118
  };
106
119
 
107
- const LEAVES: readonly { readonly kind: StructureKind; readonly find: (patterns: StructurePatterns, text: string) => readonly Mention[] }[] = [
120
+ /** 番地(文書の種類が決める「前二項」など)の中の数は、数量ではない。 */
121
+ const outsideAddresses = (mentions: readonly Mention[], text: string, profile: DocumentProfile | undefined): readonly Mention[] => {
122
+ const spans = addressSpans(text, profile);
123
+ return spans.length === 0 ? mentions : mentions.filter((mention) => !spans.some((span) => span.start <= mention.start && mention.end <= span.end));
124
+ };
125
+
126
+ /** 番地を名指しした参照(第百五十七条第一項)の一部は、相対の参照ではない。 */
127
+ const withoutAbsolute = (relative: readonly Mention[], absolute: readonly Mention[]): readonly Mention[] =>
128
+ relative.filter((mention) => !absolute.some((other) => mention.start < other.end && other.start < mention.end));
129
+
130
+ type LeafFinder = (patterns: StructurePatterns, text: string, profile: DocumentProfile | undefined) => readonly Mention[];
131
+
132
+ const LEAVES: readonly { readonly kind: StructureKind; readonly find: LeafFinder }[] = [
108
133
  { kind: "definition", find: (patterns, text) => patterns.definitions(text) },
109
- { kind: "reference", find: (patterns, text) => patterns.references(text) },
134
+ { kind: "reference", find: (patterns, text, profile) => substitutedDocuments(patterns.references(text), text, profile) },
135
+ {
136
+ kind: "reference",
137
+ find: (patterns, text, profile) => {
138
+ const absolute = patterns.references(text);
139
+ return withoutAbsolute(relativeMentions(text, profile, patterns.number, absolute), absolute);
140
+ },
141
+ },
110
142
  { kind: "obligation", find: (patterns, text) => patterns.obligations(text) },
111
- { kind: "quantity", find: (patterns, text) => patterns.quantities(text) },
143
+ { kind: "quantity", find: (patterns, text, profile) => outsideAddresses(patterns.quantities(text), text, profile) },
112
144
  { kind: "date", find: (patterns, text) => patterns.dates?.(text) ?? [] },
113
145
  ];
114
146
 
@@ -119,13 +151,54 @@ const universalNumber = (patterns: StructurePatterns, text: string, context: Num
119
151
  };
120
152
 
121
153
  /** 行の中の定義・参照・義務・数量を、いま開いている最も内側の節点の子にする。 */
154
+ const enclosingArticle = (state: State): Draft | undefined => [...state.stack].reverse().find((frame) => frame.draft.kind === "article")?.draft;
155
+
156
+ /** 条より広い範囲(「In this Part」)なら、その深さの開いているまとまり。開いていなければ(Part の見出しの無い文書)無い。 */
157
+ const widerScope = (state: State, patterns: StructurePatterns, text: string): Draft | undefined => {
158
+ const depth = patterns.definitionScopeDepth?.(text);
159
+ return depth === undefined ? undefined : [...state.stack].reverse().find((frame) => frame.numbered?.depth === depth)?.draft;
160
+ };
161
+
162
+ /** 範囲を宣言した行を持つまとまり。条の中なら条、条の外(Markdown の見出しの下、Part の直下)なら開いている一番内側のもの。 */
163
+ const holderOf = (state: State): Draft => enclosingArticle(state) ?? top(state).draft;
164
+
165
+ /**
166
+ * 定義の範囲。範囲を宣言した行(In this section— / In this Part—)を含むまとまりの定義が対象。宣言が Part なら、比べるのは
167
+ * その Part の中(within に番地を残す)。条の外で宣言したものは、そのまとまりの中。宣言の無いところの定義は、文書全体で比べる。
168
+ */
169
+ const scopeOf = (state: State, patterns: StructurePatterns, text: string): Readonly<Record<string, string>> => {
170
+ const holder = holderOf(state);
171
+ if (patterns.opensDefinitionScope?.(text) === true) {
172
+ state.scopedArticles.add(holder);
173
+ const wider = widerScope(state, patterns, text);
174
+ if (wider !== undefined) state.widerScopes.set(holder, wider);
175
+ }
176
+ if (!state.scopedArticles.has(holder)) return {};
177
+ const within = state.widerScopes.get(holder) ?? (holder.kind === "article" ? undefined : holder);
178
+ return within === undefined ? { scope: "local" } : { scope: "local", within: within.address };
179
+ };
180
+
122
181
  const addLeaves = (state: State, patterns: StructurePatterns, line: Line, text: string, offset: number): void => {
123
182
  const parent = top(state).draft;
183
+ const scope = scopeOf(state, patterns, text);
124
184
  const leaves = LEAVES.flatMap(({ kind, find }) =>
125
- find(patterns, text).map((mention) => ({ kind, start: line.start + offset + mention.start, end: line.start + offset + mention.end, attrs: mention.attrs })),
185
+ find(patterns, text, state.profile).map((mention) => ({
186
+ kind,
187
+ start: line.start + offset + mention.start,
188
+ end: line.start + offset + mention.end,
189
+ attrs: mention.attrs,
190
+ })),
126
191
  ).sort((left, right) => left.start - right.start);
127
192
  leaves.forEach((leaf) =>
128
- parent.children.push({ kind: leaf.kind, address: "", start: leaf.start, end: leaf.end, line: line.number, attrs: leaf.attrs, children: [] }),
193
+ parent.children.push({
194
+ kind: leaf.kind,
195
+ address: "",
196
+ start: leaf.start,
197
+ end: leaf.end,
198
+ line: line.number,
199
+ attrs: leaf.kind === "definition" ? { ...leaf.attrs, ...scope } : leaf.attrs,
200
+ children: [],
201
+ }),
129
202
  );
130
203
  };
131
204
 
@@ -145,6 +218,7 @@ const freeze = (draft: Draft): StructureNode => ({
145
218
  ...(draft.ordinal === undefined ? {} : { ordinal: draft.ordinal }),
146
219
  ...(draft.level === undefined ? {} : { level: draft.level }),
147
220
  ...(draft.ordinalTo === undefined ? {} : { ordinalTo: draft.ordinalTo }),
221
+ ...(draft.numbering === undefined ? {} : { numbering: draft.numbering }),
148
222
  });
149
223
 
150
224
  /** Markdown から取った手がかり。見出しと、中を読まない範囲(コード)。.txt はどちらも空。 */
@@ -152,7 +226,13 @@ export type Outline = { readonly headings: readonly Heading[]; readonly opaque:
152
226
 
153
227
  export const NO_OUTLINE: Outline = { headings: [], opaque: [] };
154
228
 
155
- export type StructureInput = { readonly path: string; readonly source: string; readonly language: string; readonly outline: Outline };
229
+ export type StructureInput = {
230
+ readonly path: string;
231
+ readonly source: string;
232
+ readonly language: string;
233
+ readonly outline: Outline;
234
+ readonly profile?: DocumentProfile | undefined;
235
+ };
156
236
 
157
237
  /** 見出しのある行を、行番号から引けるようにする。見出しごとに全行を探し直さない。 */
158
238
  const headingsByLine = (lines: readonly Line[], headings: readonly Heading[]): Map<number, Heading> =>
@@ -175,14 +255,44 @@ const headingLineText = (text: string): string => {
175
255
  return atxHeadingText(line.replace(/^#{1,6}(?=[ \t]|$)/u, "").trim());
176
256
  };
177
257
 
258
+ const captionPatterns = new WeakMap<DocumentProfile, RegExp | undefined>();
259
+
260
+ /** 文書の種類が決めた見出しだけの行なら、その言葉(最初の括弧の中、括弧が無ければ行全体)。 */
261
+ const captionOf = (profile: DocumentProfile | undefined, text: string): string | undefined => {
262
+ if (profile === undefined) return undefined;
263
+ if (!captionPatterns.has(profile)) captionPatterns.set(profile, profile.caption === undefined ? undefined : new RegExp(profile.caption, "u"));
264
+ const found = captionPatterns.get(profile)?.exec(text);
265
+ const words = (found?.[1] ?? found?.[0])?.trim();
266
+ return words === "" ? undefined : words;
267
+ };
268
+
269
+ const openUnits = (state: State): OpenUnit[] =>
270
+ state.stack.flatMap((frame) => (frame.numbered === undefined ? [] : [{ numbered: frame.numbered, bodyOnLine: frame.bodyOnLine === true }]));
271
+
272
+ /** 言語の番号の読み方、文書の種類の番号を書かない単位、言語を問わない通し番号の順に読む。 */
273
+ 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);
275
+
276
+ /** 番号の後ろが見出しでなく本文か。前の行から見出しが付く前の、行そのもので決める。 */
277
+ const carriesBody = (found: NumberedLine | undefined): boolean => found !== undefined && found.heading === "" && found.rest !== "";
278
+
279
+ /** 見出しの無い条のすぐ前の行が見出しだけの行なら、それを条の見出しにする。 */
280
+ const withCaption = (state: State, line: Line, numbered: NumberedLine | undefined): NumberedLine | undefined => {
281
+ const caption = numbered?.kind === "article" && numbered.heading === "" ? state.captions.get(line.number - 1) : undefined;
282
+ return numbered === undefined || caption === undefined ? numbered : { ...numbered, heading: caption };
283
+ };
284
+
178
285
  const readLine = (state: State, patterns: StructurePatterns, line: Line, heading: Heading | undefined): void => {
179
286
  const text = heading === undefined ? line.text : headingLineText(line.text);
180
287
  const openNumbers = state.stack.flatMap((frame) => (frame.numbered === undefined ? [] : [frame.numbered]));
181
288
  const context = { open: openNumbers, isHeading: heading !== undefined };
182
- const numbered = patterns.numbered(text, context) ?? universalNumber(patterns, text, context);
289
+ const found = numberedLine(state, patterns, text, context);
290
+ const numbered = withCaption(state, line, found);
291
+ const caption = numbered === undefined && heading === undefined ? captionOf(state.profile, text) : undefined;
292
+ if (caption !== undefined) state.captions.set(line.number, caption);
183
293
  // 番号付きの見出しも見出しの通し番号を進める。進めないと、その下の「### 詳細」が前の見出しの番地を名乗る。
184
294
  if (numbered !== undefined && heading !== undefined) headingAddress(state, heading.depth);
185
- if (numbered !== undefined) openNumbered(state, line, numbered, heading?.depth);
295
+ if (numbered !== undefined) openNumbered(state, line, numbered, heading?.depth, carriesBody(found));
186
296
  else if (heading !== undefined) openSection(state, line, heading);
187
297
  extend(state, line.start + line.text.length);
188
298
  // 番号付きの行は番号の後ろだけを読む。「第3条(支払)」の「第3条」を自分への参照として拾わない。
@@ -201,13 +311,23 @@ export const buildTree = (input: StructureInput, patterns: StructurePatterns): S
201
311
  // 文中の `第99条` は参照として拾われない。位置は元の文書のまま。
202
312
  const lines = linesOf(maskSpans(input.source, outline.opaque));
203
313
  const headings = headingsByLine(lines, outline.headings);
204
- const doc = draftOf("doc", "", { text: "", start: 0, number: 1 }, { path: input.path, language: input.language });
314
+ const attrs = { path: input.path, language: input.language, ...(input.profile === undefined ? {} : { profile: input.profile.id }) };
315
+ const doc = draftOf("doc", "", { text: "", start: 0, number: 1 }, attrs);
205
316
  doc.end = input.source.length;
206
- const state: State = { stack: [{ draft: doc, rank: 0 }], headingCounts: [] };
317
+ const state: State = {
318
+ stack: [{ draft: doc, rank: 0 }],
319
+ headingCounts: [],
320
+ scopedArticles: new Set(),
321
+ widerScopes: new Map(),
322
+ profile: input.profile,
323
+ captions: new Map(),
324
+ };
207
325
  lines.forEach((line) => {
208
326
  if (line.text.trim() !== "") readLine(state, patterns, line, headings.get(line.number));
209
327
  // コードの行は覆って読まないが、開いている節の中身ではある。節の最後にコードブロックがあっても、範囲をそこまで伸ばす。
210
328
  else if (input.source.slice(line.start, line.start + line.text.length).trim() !== "") extend(state, line.start + line.text.length);
211
329
  });
212
- return freeze(doc);
330
+ const tree = freeze(doc);
331
+ const relative = input.profile?.relative;
332
+ return relative === undefined ? tree : resolveRelative(tree, relative.implicitFirst);
213
333
  };
@@ -0,0 +1,44 @@
1
+ import type { StructureIssue } from "./issues.ts";
2
+ import { runsOf, type Line } from "./runs.ts";
3
+
4
+ /**
5
+ * 日程として並べた日付の順番。箇条書きの続いた項目か、表の続いた行で、日付をちょうど一つ持つものを並びとして読む。
6
+ * 並びの向き(古い順か新しい順か)は多いほうで決め、それに逆らう一歩だけを言う。新しい順に並べた履歴は正しい並び。
7
+ * 向きが決まらない並び(上がりと下がりが同じ数)は何も言わない。逆らう一歩を言えるのは、日付が 4 つ以上の並びだけになる。
8
+ */
9
+ export type DatedPoint = { readonly offset: number; readonly value: string };
10
+
11
+ /** 並びとして比べられる日付の書き方。年月日、年月、月日は、同じ書き方どうしでしか比べない。 */
12
+ const PRECISIONS = [/^\d{4}-\d{2}-\d{2}$/u, /^\d{4}-\d{2}$/u, /^\d{2}-\d{2}$/u];
13
+
14
+ /** 行ごとの日付。ちょうど一つの行だけを並びに入れる。二つある行(期間)はどちらを並べたのか決められない。 */
15
+ const datedLines = (run: readonly Line[], points: readonly DatedPoint[]): DatedPoint[] =>
16
+ run.flatMap((line) => {
17
+ const inside = points.filter((point) => point.offset >= line.start && point.offset <= line.end);
18
+ return inside.length === 1 && inside[0] !== undefined ? [inside[0]] : [];
19
+ });
20
+
21
+ const samePrecision = (dated: readonly DatedPoint[]): boolean => PRECISIONS.some((pattern) => dated.every((point) => pattern.test(point.value)));
22
+
23
+ /** 上がりが多ければ 1、下がりが多ければ -1、同じ数なら 0(向きを決めない)。 */
24
+ const majorityOf = (up: number, down: number): number => Math.sign(up - down);
25
+
26
+ /** 多いほうの向きに逆らう一歩。後ろの項目の日付を指す。 */
27
+ const againstMajority = (dated: readonly DatedPoint[]): StructureIssue[] => {
28
+ const steps = dated
29
+ .slice(1)
30
+ .map((point, index) => ({ point, previous: dated[index], sign: Math.sign(point.value.localeCompare(dated[index]?.value ?? "")) }));
31
+ const up = steps.filter((step) => step.sign > 0).length;
32
+ const down = steps.filter((step) => step.sign < 0).length;
33
+ const majority = majorityOf(up, down);
34
+ if (majority === 0) return [];
35
+ return steps
36
+ .filter((step) => step.sign === -majority)
37
+ .map((step) => ({ offset: step.point.offset, values: { date: step.point.value, previous: step.previous?.value ?? "" } }));
38
+ };
39
+
40
+ export const dateOrderBreaks = (source: string, points: readonly DatedPoint[]): StructureIssue[] =>
41
+ runsOf(source).flatMap((run) => {
42
+ const dated = datedLines(run, points);
43
+ return samePrecision(dated) ? againstMajority(dated) : [];
44
+ });
@@ -41,8 +41,15 @@ export const danglingReferences = (tree: StructureNode): StructureIssue[] => {
41
41
  // 参照は条を指す。条を一つも持たない文書(契約書に付ける承諾書のひな形など)の「契約書第6条」は、別の文書の条。
42
42
  if (!nodes.some((node) => node.kind === "article")) return [];
43
43
  const addresses = new Set(nodes.filter((node) => NUMBERED.includes(node.kind)).map((node) => node.address));
44
+ // Section で組んだ法令が "Articles 13 to 21 of the UK GDPR" と別の文書の Article を名指ししていれば、名の無い "Article 6(3)" もそちら。
45
+ // 名指しがあれば、名の無い "Article 9" が書き間違いか向こうの条かは区別できないので黙る。名指しが無い文書では報告する。
46
+ const numbering = (node: StructureNode): unknown => node.attrs["numbering"];
47
+ const numberings = new Set(nodes.flatMap((node) => (node.kind === "article" && node.numbering !== undefined ? [node.numbering] : [])));
48
+ const citedElsewhere = new Set(nodes.filter((node) => node.kind === "reference" && node.attrs["document"] !== undefined).map(numbering));
49
+ const otherNumbering = (node: StructureNode): boolean =>
50
+ numberings.size > 0 && typeof numbering(node) === "string" && !numberings.has(String(numbering(node))) && citedElsewhere.has(numbering(node));
44
51
  return nodes
45
- .filter((node) => node.kind === "reference" && node.attrs["document"] === undefined)
52
+ .filter((node) => node.kind === "reference" && node.attrs["document"] === undefined && !otherNumbering(node))
46
53
  .filter((node) => !resolves(node, addresses))
47
54
  .map((node) => ({ offset: node.span.start, values: { label: textOf(node, "label"), target: textOf(node, "target") } }));
48
55
  };
@@ -71,7 +78,8 @@ export const duplicateDefinitions = (tree: StructureNode): StructureIssue[] => {
71
78
  const first = new Map<string, StructureNode>();
72
79
  return definitionsInOrder(tree).flatMap(({ node, article }) => {
73
80
  const term = textOf(node, "term");
74
- const key = node.attrs["scope"] === "local" ? `${article}\u0000${term}` : term;
81
+ const within = node.attrs["within"] === undefined ? article : textOf(node, "within");
82
+ const key = node.attrs["scope"] === "local" ? `${within}\u0000${term}` : term;
75
83
  const earlier = first.get(key);
76
84
  if (earlier === undefined) {
77
85
  first.set(key, node);
@@ -1,8 +1,14 @@
1
1
  import { markdownOutline } from "../document.ts";
2
- import type { StructureNode, StructurePatterns } from "../plugin.ts";
2
+ import type { DocumentProfile, StructureNode, StructurePatterns } from "../plugin.ts";
3
3
  import { buildTree, NO_OUTLINE } from "./build.ts";
4
4
 
5
- export type SourceInput = { readonly path: string; readonly source: string; readonly language: string; readonly markdown: boolean };
5
+ export type SourceInput = {
6
+ readonly path: string;
7
+ readonly source: string;
8
+ readonly language: string;
9
+ readonly markdown: boolean;
10
+ readonly profile?: DocumentProfile | undefined;
11
+ };
6
12
 
7
13
  /**
8
14
  * 文字列から木を作る。Markdown なら見出しとコードの範囲を先に取る。
@@ -10,6 +16,12 @@ export type SourceInput = { readonly path: string; readonly source: string; read
10
16
  */
11
17
  export const buildStructure = (input: SourceInput, patterns: StructurePatterns): StructureNode =>
12
18
  buildTree(
13
- { path: input.path, source: input.source, language: input.language, outline: input.markdown ? markdownOutline(input.source) : NO_OUTLINE },
19
+ {
20
+ path: input.path,
21
+ source: input.source,
22
+ language: input.language,
23
+ outline: input.markdown ? markdownOutline(input.source) : NO_OUTLINE,
24
+ profile: input.profile,
25
+ },
14
26
  patterns,
15
27
  );
@@ -0,0 +1,237 @@
1
+ import type { DocumentProfile, Mention, RelativeVocabulary, Span } from "../plugin.ts";
2
+
3
+ /**
4
+ * 前条・次項・同号・本条・前各項・前二項・前条第二項、条を書かない「第一項」を行の中から探す。語は文書の種類(profiles/*.yaml)が決める。
5
+ * ここでは番地を決めず、何をどれだけ指すかだけを attrs に残す。番地は木ができてから relative-resolve.ts が決める。
6
+ */
7
+ type RelativeWay = "before" | "after" | "same" | "current" | "continue";
8
+
9
+ type NumberReader = (text: string) => number | undefined;
10
+
11
+ const escape = (word: string): string => word.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
12
+
13
+ const oneOf = (words: readonly string[]): string => (words.length === 0 ? "(?!)" : words.map(escape).join("|"));
14
+
15
+ type Compiled = {
16
+ readonly named: RegExp;
17
+ readonly bare: RegExp | undefined;
18
+ readonly suffix: RegExp;
19
+ readonly notAfter: RegExp | undefined;
20
+ readonly joins: RegExp;
21
+ readonly ways: ReadonlyMap<string, RelativeWay>;
22
+ };
23
+
24
+ const waysOf = (vocabulary: RelativeVocabulary): Map<string, RelativeWay> =>
25
+ new Map<string, RelativeWay>([
26
+ ...vocabulary.before.map((word): [string, RelativeWay] => [word, "before"]),
27
+ ...vocabulary.after.map((word): [string, RelativeWay] => [word, "after"]),
28
+ ...vocabulary.same.map((word): [string, RelativeWay] => [word, "same"]),
29
+ ...vocabulary.current.map((word): [string, RelativeWay] => [word, "current"]),
30
+ ]);
31
+
32
+ /** 後ろの決まり: 番地の並びと同じく、つなぎの語を挟んで address_end。「前条件」の「前条」を参照にしない。 */
33
+ const endOf = (profile: DocumentProfile): string => {
34
+ const connectives = profile.connectives.length === 0 ? "" : `(?:${oneOf(profile.connectives)})*`;
35
+ return profile.addressEnd === undefined ? "" : `(?=${connectives}(?:${profile.addressEnd}))`;
36
+ };
37
+
38
+ const compile = (profile: DocumentProfile, vocabulary: RelativeVocabulary): Compiled => {
39
+ const units = oneOf(Object.keys(vocabulary.units));
40
+ const suffix = `${escape(vocabulary.suffixPrefix)}(?:${vocabulary.count})(?:${units})`;
41
+ const end = endOf(profile);
42
+ const start = vocabulary.notAfter === undefined ? "" : `(?<!${vocabulary.notAfter})`;
43
+ const way = oneOf([...vocabulary.before, ...vocabulary.after, ...vocabulary.same, ...vocabulary.current]);
44
+ const count = `${oneOf(vocabulary.every)}|${vocabulary.count}`;
45
+ const inside = `${escape(vocabulary.suffixPrefix)}(?:${vocabulary.count})(?:${oneOf(vocabulary.inside)})`;
46
+ return {
47
+ named: new RegExp(`${start}(?<way>${way})(?<count>${count})?(?<unit>${units})(?<suffix>(?:${suffix})*)${end}`, "gu"),
48
+ // 前の文字は見ない。語に続くもの(別表第一第一号)も見つけて、並びの続きを止めるのに使う。
49
+ bare: vocabulary.inside.length === 0 ? undefined : new RegExp(`(?<suffix>${inside}(?:${suffix})*)${end}`, "gu"),
50
+ suffix: new RegExp(`${escape(vocabulary.suffixPrefix)}(?<n>${vocabulary.count})(?<unit>${units})`, "gu"),
51
+ notAfter: vocabulary.notAfter === undefined ? undefined : new RegExp(`(?:${vocabulary.notAfter})$`, "u"),
52
+ joins: new RegExp(`(?:${[...vocabulary.joiners, ...profile.connectives.map(escape)].join("|") || "(?!)"})`, "gu"),
53
+ ways: waysOf(vocabulary),
54
+ };
55
+ };
56
+
57
+ const compiled = new WeakMap<DocumentProfile, Compiled>();
58
+
59
+ /** 「第二項第三号」を「2:2,3:3」(深さ:番号)にする。読めない数があれば undefined。 */
60
+ const suffixOf = (text: string, patterns: Compiled, vocabulary: RelativeVocabulary, number: NumberReader): string | undefined => {
61
+ const parts = [...text.matchAll(patterns.suffix)].map((match) => {
62
+ const value = number(match.groups?.["n"] ?? "");
63
+ const depth = vocabulary.units[match.groups?.["unit"] ?? ""];
64
+ return value === undefined || depth === undefined ? undefined : `${String(depth)}:${String(value)}`;
65
+ });
66
+ return parts.includes(undefined) ? undefined : parts.join(",");
67
+ };
68
+
69
+ /** 「各」は前の全部(0)、数が無ければ 1。 */
70
+ const countOf = (count: string | undefined, vocabulary: RelativeVocabulary, number: NumberReader): number | undefined => {
71
+ if (count === undefined) return 1;
72
+ return vocabulary.every.includes(count) ? 0 : number(count);
73
+ };
74
+
75
+ type Substitution = NonNullable<RelativeVocabulary["substitution"]>;
76
+
77
+ /** 文字と、その位置(UTF-16 の単位で)。絵文字のような 2 単位の文字も 1 文字として読む。 */
78
+ const characters = (text: string): { readonly char: string; readonly at: number }[] => {
79
+ const found: { char: string; at: number }[] = [];
80
+ let at = 0;
81
+ for (const char of text) {
82
+ found.push({ char, at });
83
+ at += char.length;
84
+ }
85
+ return found;
86
+ };
87
+
88
+ /** 一番外側の括弧の範囲。閉じの無い開きは捨てる。 */
89
+ const outerQuotes = (text: string, quote: Substitution): Span[] => {
90
+ const spans: Span[] = [];
91
+ const opens: number[] = [];
92
+ characters(text).forEach(({ char, at }) => {
93
+ if (char === quote.open) opens.push(at);
94
+ const start = char === quote.close ? opens.pop() : undefined;
95
+ if (start !== undefined && opens.length === 0) spans.push({ start, end: at + char.length });
96
+ });
97
+ return spans;
98
+ };
99
+
100
+ /** 読み替えの括弧(閉じの直後か開きの直前に目印があるもの)の範囲。 */
101
+ const substitutionSpans = (text: string, quote: Substitution | undefined): Span[] =>
102
+ quote === undefined
103
+ ? []
104
+ : outerQuotes(text, quote).filter(
105
+ (span) => quote.after.some((marker) => text.startsWith(marker, span.end)) || quote.before.some((marker) => text.slice(0, span.start).endsWith(marker)),
106
+ );
107
+
108
+ /** 読み替えの括弧の中の、文書を名指ししていない参照に、読み替える先の文書の名前を付ける。 */
109
+ export const substitutedDocuments = (mentions: readonly Mention[], text: string, profile: DocumentProfile | undefined): readonly Mention[] => {
110
+ const quote = profile?.relative?.substitution;
111
+ const document = quote?.document;
112
+ if (document === undefined) return mentions;
113
+ const spans = substitutionSpans(text, quote);
114
+ return mentions.map((mention) =>
115
+ mention.attrs["document"] === undefined && spans.some((span) => span.start <= mention.start && mention.start < span.end)
116
+ ? { ...mention, attrs: { ...mention.attrs, document } }
117
+ : mention,
118
+ );
119
+ };
120
+
121
+ /** 閉じた括弧書きを内側から外す。 */
122
+ const withoutAsides = (gap: string, aside: RelativeVocabulary["aside"]): string => {
123
+ if (aside === undefined) return gap;
124
+ const inner = new RegExp(`${escape(aside.open)}[^${escape(aside.open)}${escape(aside.close)}]*${escape(aside.close)}`, "gu");
125
+ const once = gap.replace(inner, "");
126
+ return once === gap ? gap : withoutAsides(once, aside);
127
+ };
128
+
129
+ /** あいだが、並びをつなぐもの・つなぎの語・閉じた括弧書き・空白だけか。 */
130
+ const onlyJoins = (gap: string, patterns: Compiled, vocabulary: RelativeVocabulary): boolean =>
131
+ withoutAsides(gap, vocabulary.aside).replace(patterns.joins, "").trim() === "";
132
+
133
+ const overlaps = (start: number, end: number, others: readonly Mention[]): boolean => others.some((other) => start < other.end && other.start < end);
134
+
135
+ /** 行き先を決められない番地。並びの続きを止めるためだけに覚え、参照にはしない。 */
136
+ const OPAQUE = "opaque";
137
+
138
+ /** すぐ前が not_after に当たる(「別表第一」の「一」に続く「第一号」)。 */
139
+ const glued = (text: string, start: number, patterns: Compiled): boolean => patterns.notAfter?.test(text.slice(0, start)) === true;
140
+
141
+ type Bare = { readonly start: number; readonly end: number; readonly label: string; readonly suffix: string; readonly first: number };
142
+
143
+ const bareCandidates = (text: string, patterns: Compiled, vocabulary: RelativeVocabulary, number: NumberReader): Bare[] =>
144
+ patterns.bare === undefined
145
+ ? []
146
+ : [...text.matchAll(patterns.bare)].flatMap((match) => {
147
+ const suffix = suffixOf(match.groups?.["suffix"] ?? "", patterns, vocabulary, number);
148
+ const first = Number(suffix?.split(":")[0]);
149
+ return suffix === undefined || !Number.isInteger(first)
150
+ ? []
151
+ : [{ start: match.index, end: match.index + match[0].length, label: match[0], suffix, first }];
152
+ });
153
+
154
+ /** 並びの中での扱い: 前の参照の続きか、今いるところの中か、行き先を決められないか。 */
155
+ type BareWay = { readonly way: string; readonly continues?: Mention | undefined };
156
+
157
+ /** 並びの中での扱い: 前の参照の続きか、今いるところの中か、行き先を決められないか。続きなら、どの参照の続きか。 */
158
+ const wayOfBare = (text: string, bare: Bare, known: readonly Mention[], patterns: Compiled, vocabulary: RelativeVocabulary): BareWay => {
159
+ if (glued(text, bare.start, patterns)) return { way: OPAQUE };
160
+ const depth = openAsides(text, bare.start, vocabulary).length;
161
+ const sameDepth = known.filter((other) => other.end <= bare.start && openAsides(text, other.end, vocabulary).length === depth);
162
+ const previous = sameDepth.toSorted((left, right) => left.end - right.end).at(-1);
163
+ const joined = previous !== undefined && onlyJoins(text.slice(previous.end, bare.start), patterns, vocabulary) ? previous : undefined;
164
+ const continues = joined ?? ownerOfAside(text, bare.start, known, vocabulary);
165
+ if (continues === undefined) return { way: "current" };
166
+ return continues.attrs["relative"] === OPAQUE ? { way: OPAQUE } : { way: "continue", continues };
167
+ };
168
+
169
+ /** at の位置で開いたままの括弧書きの開きの位置。並びは同じ深さの参照どうしでつながる。 */
170
+ const openAsides = (text: string, at: number, vocabulary: RelativeVocabulary): number[] => {
171
+ const aside = vocabulary.aside;
172
+ const opens: number[] = [];
173
+ if (aside === undefined) return opens;
174
+ characters(text.slice(0, at)).forEach(({ char, at: position }) => {
175
+ if (char === aside.open) opens.push(position);
176
+ if (char === aside.close) opens.pop();
177
+ });
178
+ return opens;
179
+ };
180
+
181
+ /**
182
+ * 参照の直後に開いた括弧書きの中なら、その参照。「第二十七条(第四項を除き、第五項及び第六項の規定を…)」の番地は、どれも第二十七条の中。
183
+ */
184
+ const ownerOfAside = (text: string, at: number, known: readonly Mention[], vocabulary: RelativeVocabulary): Mention | undefined => {
185
+ const open = openAsides(text, at, vocabulary).at(-1);
186
+ return open === undefined ? undefined : known.find((other) => other.end === open);
187
+ };
188
+
189
+ /**
190
+ * 条を書かない「第一項」。すぐ前の参照とのあいだがつなぎの語だけなら、その参照の続き(continue)。
191
+ * そうでなければ、書いた場所を含む一つ上のまとまり(current)の中。
192
+ */
193
+ const bareMentions = (text: string, patterns: Compiled, vocabulary: RelativeVocabulary, number: NumberReader, before: readonly Mention[]): Mention[] => {
194
+ const found: Mention[] = [];
195
+ bareCandidates(text, patterns, vocabulary, number).forEach((bare) => {
196
+ const known = [...before, ...found];
197
+ if (overlaps(bare.start, bare.end, known)) return;
198
+ const { way, continues } = wayOfBare(text, bare, known, patterns, vocabulary);
199
+ // 条を書かない番地は、その一つ上(level)ではなく、書いた単位(level + 1)から名指しする。「第一項」は「同条」の行き先を変えない。
200
+ // continues は、続きの元の参照がどれだけ前から始まるか。木の中で、その参照を位置で引く。
201
+ const back = continues === undefined ? {} : { continues: bare.start - continues.start };
202
+ const attrs = { label: bare.label, relative: way, level: bare.first - 1, count: 1, suffix: bare.suffix, names: bare.first, ...back };
203
+ found.push({ start: bare.start, end: bare.end, attrs });
204
+ });
205
+ return found.filter((mention) => mention.attrs["relative"] !== OPAQUE);
206
+ };
207
+
208
+ const namedMentions = (text: string, patterns: Compiled, vocabulary: RelativeVocabulary, number: NumberReader): Mention[] =>
209
+ [...text.matchAll(patterns.named)].flatMap((match) => {
210
+ const groups = match.groups ?? {};
211
+ const way = patterns.ways.get(groups["way"] ?? "");
212
+ const level = vocabulary.units[groups["unit"] ?? ""];
213
+ const count = countOf(groups["count"], vocabulary, number);
214
+ const suffix = suffixOf(groups["suffix"] ?? "", patterns, vocabulary, number);
215
+ if (way === undefined || level === undefined || count === undefined || suffix === undefined) return [];
216
+ return [{ start: match.index, end: match.index + match[0].length, attrs: { label: match[0], relative: way, level, count, suffix } }];
217
+ });
218
+
219
+ /** absolute は同じ行の番地を名指しした参照。条を書かない番地が、その続きかどうかを決めるのに使う。 */
220
+ export const relativeMentions = (
221
+ text: string,
222
+ profile: DocumentProfile | undefined,
223
+ number: NumberReader | undefined,
224
+ absolute: readonly Mention[] = [],
225
+ ): Mention[] => {
226
+ const vocabulary = profile?.relative;
227
+ if (profile === undefined || vocabulary === undefined || number === undefined) return [];
228
+ if (!compiled.has(profile)) compiled.set(profile, compile(profile, vocabulary));
229
+ const patterns = compiled.get(profile);
230
+ if (patterns === undefined) return [];
231
+ const substituted = substitutionSpans(text, vocabulary.substitution);
232
+ const named = namedMentions(text, patterns, vocabulary, number);
233
+ const bare = bareMentions(text, patterns, vocabulary, number, [...absolute, ...named]);
234
+ return [...named, ...bare]
235
+ .filter((mention) => !substituted.some((span) => span.start <= mention.start && mention.start < span.end))
236
+ .sort((left, right) => left.start - right.start);
237
+ };