@chaffjs/lang-en 0.5.0 → 0.7.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.
- package/README.md +1 -1
- package/dist/citation.d.ts +15 -0
- package/dist/citation.d.ts.map +1 -0
- package/dist/citation.js +79 -0
- package/dist/citation.js.map +1 -0
- package/dist/definitions.d.ts +5 -0
- package/dist/definitions.d.ts.map +1 -0
- package/dist/definitions.js +33 -0
- package/dist/definitions.js.map +1 -0
- package/dist/depth.d.ts +4 -0
- package/dist/depth.d.ts.map +1 -0
- package/dist/depth.js +4 -0
- package/dist/depth.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/reference-list.d.ts +7 -0
- package/dist/reference-list.d.ts.map +1 -0
- package/dist/reference-list.js +56 -0
- package/dist/reference-list.js.map +1 -0
- package/dist/roman.d.ts +3 -0
- package/dist/roman.d.ts.map +1 -0
- package/dist/roman.js +10 -0
- package/dist/roman.js.map +1 -0
- package/dist/structure.d.ts +3 -0
- package/dist/structure.d.ts.map +1 -0
- package/dist/structure.js +359 -0
- package/dist/structure.js.map +1 -0
- package/package.json +1 -1
- package/src/citation.ts +85 -0
- package/src/definitions.ts +43 -0
- package/src/depth.ts +3 -0
- package/src/index.ts +2 -0
- package/src/reference-list.ts +66 -0
- package/src/roman.ts +9 -0
- package/src/structure.ts +393 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { citedDocumentAfter } from "./citation.js";
|
|
2
|
+
import { membersAfter } from "./reference-list.js";
|
|
3
|
+
import { parseRoman } from "./roman.js";
|
|
4
|
+
import { definitionScopeDepth, definitions, opensDefinitionScope } from "./definitions.js";
|
|
5
|
+
import { CHAPTER_DEPTH, PART_DEPTH } from "./depth.js";
|
|
6
|
+
// Contracts, specifications and statutes in English. core nests what this reads; it does not know
|
|
7
|
+
// how English numbers its articles.
|
|
8
|
+
const numberOf = (text) => {
|
|
9
|
+
if (text === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (/^\d{1,3}(?:\.\d{1,3}){0,5}$/u.test(text))
|
|
12
|
+
return text;
|
|
13
|
+
const roman = parseRoman(text);
|
|
14
|
+
return roman === undefined ? undefined : String(roman);
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A heading is the number followed by nothing, punctuation, or a capitalised title.
|
|
18
|
+
* "Article 3 shall apply to…" at the start of a line is a sentence, not the heading of Article 3.
|
|
19
|
+
*/
|
|
20
|
+
const titleOf = (rest) => {
|
|
21
|
+
const trimmed = rest.replace(/^[.:\-–—\s]+/u, "").trim();
|
|
22
|
+
if (trimmed !== "" && /^\p{Ll}/u.test(trimmed))
|
|
23
|
+
return undefined;
|
|
24
|
+
return trimmed;
|
|
25
|
+
};
|
|
26
|
+
const ARTICLE = /^\s{0,3}(?:ARTICLE|Article)\s+(?<n>\d{1,3}|[IVXLC]{1,7})\b(?<rest>.*)$/u;
|
|
27
|
+
const SECTION = /^\s{0,3}(?:SECTION|Section|§)\s*(?<n>\d{1,3}(?:\.\d{1,3}){0,5})\b(?<rest>.*)$/u;
|
|
28
|
+
/**
|
|
29
|
+
* An amendment inserts a subsection between two others and numbers it "(A1)" or "(2A)". It is a subsection, written
|
|
30
|
+
* outside the sequence: it has no ordinal, so "(1)" after "(A1)" is still the first.
|
|
31
|
+
*/
|
|
32
|
+
const INSERTED = "\\d{1,3}[A-Z]{1,2}|[A-Z]{1,2}\\d{1,3}";
|
|
33
|
+
const LETTERED = new RegExp(`^\\s{0,6}\\((?<n>[a-z]{1,4}|\\d{1,3}|${INSERTED})\\)\\s+(?<rest>\\S.*)$`, "u");
|
|
34
|
+
const IS_INSERTED = new RegExp(`^(?:${INSERTED})$`, "u");
|
|
35
|
+
const MULTI_ROMAN = /^(?:ii|iii|iv|vi|vii|viii|ix)$/u;
|
|
36
|
+
const headed = (pattern, line, numbering, label) => {
|
|
37
|
+
const groups = pattern.exec(line)?.groups;
|
|
38
|
+
const number = numberOf(groups?.["n"]);
|
|
39
|
+
const heading = titleOf(groups?.["rest"] ?? "");
|
|
40
|
+
if (groups === undefined || number === undefined || heading === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
const parts = number.split(".");
|
|
43
|
+
return {
|
|
44
|
+
kind: "article",
|
|
45
|
+
depth: parts.length,
|
|
46
|
+
number,
|
|
47
|
+
absolute: true,
|
|
48
|
+
label: label(groups["n"] ?? ""),
|
|
49
|
+
heading,
|
|
50
|
+
rest: heading,
|
|
51
|
+
ordinal: Number(parts.at(-1)),
|
|
52
|
+
numbering,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* 開いている項目の書き方。"(ii)" はローマ数字、"(b)" は英字。一文字の "(i)" はどちらにも読めるので、
|
|
57
|
+
* 開いたときの読みを覚えておく代わりに、一つ上に英字が開いていたかで決め直す。
|
|
58
|
+
*/
|
|
59
|
+
const styleOfLabel = (open, index, all) => {
|
|
60
|
+
const inner = /^\((?<n>[A-Za-z0-9]{1,5})\)$/u.exec(open.label)?.groups?.["n"];
|
|
61
|
+
if (inner === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (/^\d+$/u.test(inner) || IS_INSERTED.test(inner))
|
|
64
|
+
return "digit";
|
|
65
|
+
if (MULTI_ROMAN.test(inner))
|
|
66
|
+
return "roman";
|
|
67
|
+
const above = all[index - 1];
|
|
68
|
+
return /^[ivx]$/u.test(inner) && above !== undefined && styleOfLabel(above, index - 1, all) === "letter" && above.depth < open.depth ? "roman" : "letter";
|
|
69
|
+
};
|
|
70
|
+
const styles = (context) => context.open.map((open, index, all) => styleOfLabel(open, index, all));
|
|
71
|
+
/** "(h)" の次の "(i)" は英字。開いている英字の次の文字なら、ローマ数字とは読まない。 */
|
|
72
|
+
const followsLetter = (raw, context) => context.open.some((open, index) => styles(context)[index] === "letter" && open.number.charCodeAt(0) + 1 === raw.charCodeAt(0));
|
|
73
|
+
/** "(i)" is a roman numeral right under "(a)", or when a roman list is already open; the letter i otherwise. */
|
|
74
|
+
const styleOf = (raw, context) => {
|
|
75
|
+
if (/^\d+$/u.test(raw) || IS_INSERTED.test(raw))
|
|
76
|
+
return "digit";
|
|
77
|
+
if (MULTI_ROMAN.test(raw))
|
|
78
|
+
return "roman";
|
|
79
|
+
if (!/^[ivx]$/u.test(raw) || followsLetter(raw, context))
|
|
80
|
+
return "letter";
|
|
81
|
+
const open = styles(context);
|
|
82
|
+
return open.includes("roman") || open.at(-1) === "letter" ? "roman" : "letter";
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* A sibling has the depth of the open item written the same way: "(b)" closes "(i)" and sits beside "(a)".
|
|
86
|
+
* A new way of numbering goes one deeper than whatever is open.
|
|
87
|
+
*/
|
|
88
|
+
const depthFor = (style, context) => {
|
|
89
|
+
const open = styles(context);
|
|
90
|
+
const sibling = [...context.open].reverse().find((_item, reversed) => open[context.open.length - 1 - reversed] === style);
|
|
91
|
+
return sibling?.depth ?? (context.open.at(-1)?.depth ?? 0) + 1;
|
|
92
|
+
};
|
|
93
|
+
const LETTER_BEFORE_A = "a".charCodeAt(0) - 1;
|
|
94
|
+
/** "(b)" は 2 番目、"(ii)" も 2 番目。二文字以上の英字("(aa)")は並びが決まらないので付けない。 */
|
|
95
|
+
const ordinalOf = (raw, style) => {
|
|
96
|
+
if (style === "digit")
|
|
97
|
+
return IS_INSERTED.test(raw) ? undefined : Number(raw);
|
|
98
|
+
if (style === "roman")
|
|
99
|
+
return parseRoman(raw);
|
|
100
|
+
return raw.length === 1 ? raw.charCodeAt(0) - LETTER_BEFORE_A : undefined;
|
|
101
|
+
};
|
|
102
|
+
const lettered = (line, context) => {
|
|
103
|
+
const groups = LETTERED.exec(line)?.groups;
|
|
104
|
+
const raw = groups?.["n"];
|
|
105
|
+
if (groups === undefined || raw === undefined)
|
|
106
|
+
return undefined;
|
|
107
|
+
const style = styleOf(raw, context);
|
|
108
|
+
// 番地は書かれたままの "ii" を使う。参照「Section 4.2(a)(ii)」も同じ形で書かれるので、そのまま引ける。
|
|
109
|
+
return {
|
|
110
|
+
kind: "item",
|
|
111
|
+
depth: depthFor(style, context),
|
|
112
|
+
ordinal: ordinalOf(raw, style),
|
|
113
|
+
number: raw,
|
|
114
|
+
absolute: false,
|
|
115
|
+
label: `(${raw})`,
|
|
116
|
+
heading: "",
|
|
117
|
+
rest: groups["rest"]?.trim() ?? "",
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
/** Chapters restart in each part, so a chapter continues its part's address: PART II, CHAPTER 1 → pt2.ch1. */
|
|
121
|
+
const PART = /^\s{0,3}(?:PART|Part)\s+(?<n>\d{1,3}|[IVXLC]{1,7})\b(?<rest>.*)$/u;
|
|
122
|
+
const CHAPTER = /^\s{0,3}(?:CHAPTER|Chapter)\s+(?<n>\d{1,3}|[IVXLC]{1,7})\b(?<rest>.*)$/u;
|
|
123
|
+
const chapter = (pattern, line, depth, prefix, word) => {
|
|
124
|
+
const groups = pattern.exec(line)?.groups;
|
|
125
|
+
const number = numberOf(groups?.["n"]);
|
|
126
|
+
const heading = titleOf(groups?.["rest"] ?? "");
|
|
127
|
+
if (groups === undefined || number === undefined || heading === undefined)
|
|
128
|
+
return undefined;
|
|
129
|
+
const label = `${word} ${groups["n"] ?? ""}`;
|
|
130
|
+
return { kind: "chapter", depth, number: prefix + number, absolute: false, label, heading, rest: heading, ordinal: Number(number) };
|
|
131
|
+
};
|
|
132
|
+
const numbered = (line, context) => chapter(PART, line, PART_DEPTH, "pt", "Part") ??
|
|
133
|
+
chapter(CHAPTER, line, CHAPTER_DEPTH, "ch", "Chapter") ??
|
|
134
|
+
headed(ARTICLE, line, "article", (n) => `Article ${n}`) ??
|
|
135
|
+
headed(SECTION, line, "section", (n) => `Section ${n}`) ??
|
|
136
|
+
lettered(line, context);
|
|
137
|
+
const REFERENCE = /(?<word>\b[Ss]ections?|\b[Aa]rticles?|§) ?(?<n>\d{1,3}(?:\.\d{1,3}){0,5}|[IVXLC]{1,7})\b/gu;
|
|
138
|
+
/** "(a)", "(ii)", "(3)", and an inserted "(A1)" or "(2A)": the same labels the tree reads. */
|
|
139
|
+
const SUBDIVISION = new RegExp(`^\\((?<p>[a-z0-9]{1,4}|${INSERTED})\\)`, "u");
|
|
140
|
+
/** The longest label, with its parentheses: "(ZZ999)". */
|
|
141
|
+
const MAX_SUBDIVISION_LENGTH = 7;
|
|
142
|
+
/** "(a)(ii)(3)" is as deep as a reference goes; more parentheses are text, not a deeper address. */
|
|
143
|
+
const MAX_SUBDIVISIONS = 4;
|
|
144
|
+
/**
|
|
145
|
+
* "(a)(ii)" のような続きの括弧を、正規表現を複雑にせずに一つずつ読む。
|
|
146
|
+
* 再帰にしないのは、括弧が延々と続く行でスタックを使い切らないため。
|
|
147
|
+
*/
|
|
148
|
+
const subdivisions = (text, from) => {
|
|
149
|
+
const parts = [];
|
|
150
|
+
let end = from;
|
|
151
|
+
while (parts.length < MAX_SUBDIVISIONS) {
|
|
152
|
+
const part = SUBDIVISION.exec(text.slice(end, end + MAX_SUBDIVISION_LENGTH))?.groups?.["p"];
|
|
153
|
+
if (part === undefined)
|
|
154
|
+
break;
|
|
155
|
+
parts.push(part);
|
|
156
|
+
end += part.length + 2;
|
|
157
|
+
}
|
|
158
|
+
return { parts, end };
|
|
159
|
+
};
|
|
160
|
+
/** Parentheses opened and not yet closed in `between`. */
|
|
161
|
+
const PAREN_STEP = { "(": 1, ")": -1 };
|
|
162
|
+
const advance = (gloss, text, to) => {
|
|
163
|
+
for (let index = gloss.scanned; index < to; index += 1) {
|
|
164
|
+
gloss.depth += PAREN_STEP[text[index] ?? ""] ?? 0;
|
|
165
|
+
while ((gloss.anchors.at(-1)?.depth ?? -Infinity) > gloss.depth)
|
|
166
|
+
gloss.anchors.pop();
|
|
167
|
+
}
|
|
168
|
+
gloss.scanned = Math.max(gloss.scanned, to);
|
|
169
|
+
};
|
|
170
|
+
const glossedDocument = (gloss) => {
|
|
171
|
+
const anchor = gloss.anchors.at(-1);
|
|
172
|
+
return anchor !== undefined && gloss.depth > anchor.depth ? anchor.document : undefined;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* "Section 4.2(a)" → 4.2.a, "Article III" → 3. The same addresses the tree gives.
|
|
176
|
+
* "Section 9 of the Master Agreement" carries the other document's name, and is not looked up in this tree.
|
|
177
|
+
*/
|
|
178
|
+
const references = (text) => {
|
|
179
|
+
const gloss = { depth: 0, scanned: 0, anchors: [] };
|
|
180
|
+
return [...text.matchAll(REFERENCE)].flatMap((match) => {
|
|
181
|
+
const main = numberOf(match.groups?.["n"]);
|
|
182
|
+
if (main === undefined)
|
|
183
|
+
return [];
|
|
184
|
+
const { parts, end } = subdivisions(text, match.index + match[0].length);
|
|
185
|
+
advance(gloss, text, match.index);
|
|
186
|
+
const cited = citedDocumentAfter(text, end);
|
|
187
|
+
const document = cited ?? glossedDocument(gloss);
|
|
188
|
+
gloss.scanned = Math.max(gloss.scanned, end);
|
|
189
|
+
if (cited !== undefined)
|
|
190
|
+
gloss.anchors.push({ document: cited, depth: gloss.depth });
|
|
191
|
+
const numbering = /^[Aa]/u.test(match.groups?.["word"] ?? "") ? "article" : "section";
|
|
192
|
+
const shared = { numbering, ...(document === undefined ? {} : { document }) };
|
|
193
|
+
const first = { start: match.index, end, attrs: { target: [main, ...parts].join("."), label: text.slice(match.index, end), ...shared } };
|
|
194
|
+
const [plural, roman] = [/s$/u.test(match.groups?.["word"] ?? ""), /^[IVXLC]+$/u.test(match.groups?.["n"] ?? "")];
|
|
195
|
+
return [first, ...membersAfter(text, end, [main, ...parts], shared, plural, roman)];
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
/** Longest first, and never inside a word: "shall not" is not also "shall", "mayor" is not "may". */
|
|
199
|
+
const MARKERS = [
|
|
200
|
+
["is required to", "must"],
|
|
201
|
+
["shall not", "must-not"],
|
|
202
|
+
["must not", "must-not"],
|
|
203
|
+
["may not", "must-not"],
|
|
204
|
+
["agrees to", "must"],
|
|
205
|
+
["shall", "must"],
|
|
206
|
+
["must", "must"],
|
|
207
|
+
["may", "may"],
|
|
208
|
+
];
|
|
209
|
+
const isWordChar = (char) => char !== undefined && /[\p{L}\p{N}_]/u.test(char);
|
|
210
|
+
/** Every whole-word occurrence. A loop, not recursion: a line with thousands of "may" must not exhaust the stack. */
|
|
211
|
+
const wordAt = (lower, word) => {
|
|
212
|
+
const found = [];
|
|
213
|
+
for (let at = lower.indexOf(word); at !== -1; at = lower.indexOf(word, at + 1)) {
|
|
214
|
+
if (!isWordChar(lower[at - 1]) && !isWordChar(lower[at + word.length]))
|
|
215
|
+
found.push(at);
|
|
216
|
+
}
|
|
217
|
+
return found;
|
|
218
|
+
};
|
|
219
|
+
const SENTENCE_OPENERS = new Set([".", "!", "?", ":", ";", '"', "“", "("]);
|
|
220
|
+
/** "May" in the middle of a sentence is the month: "published in May 2023". At the start it can be the modal. */
|
|
221
|
+
const isMonthName = (text, at) => {
|
|
222
|
+
if (!text.startsWith("May", at))
|
|
223
|
+
return false;
|
|
224
|
+
const before = text.slice(Math.max(0, at - 4), at).trimEnd();
|
|
225
|
+
return before !== "" && !SENTENCE_OPENERS.has(before.at(-1) ?? "");
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Longest marker first; a hit that overlaps one already kept is dropped. Overlap is checked with a mark
|
|
229
|
+
* per character, not by comparing every pair, which would go quadratic on a line with thousands of markers.
|
|
230
|
+
*/
|
|
231
|
+
const obligations = (text) => {
|
|
232
|
+
const lower = text.toLowerCase();
|
|
233
|
+
const taken = new Uint8Array(text.length);
|
|
234
|
+
const kept = [];
|
|
235
|
+
MARKERS.forEach(([marker, type]) => {
|
|
236
|
+
wordAt(lower, marker).forEach((start) => {
|
|
237
|
+
const end = start + marker.length;
|
|
238
|
+
if (isMonthName(text, start))
|
|
239
|
+
return;
|
|
240
|
+
if (taken.subarray(start, end).some((mark) => mark === 1))
|
|
241
|
+
return;
|
|
242
|
+
taken.fill(1, start, end);
|
|
243
|
+
kept.push({ start, end, attrs: { marker, type } });
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
return kept.sort((left, right) => left.start - right.start);
|
|
247
|
+
};
|
|
248
|
+
/** Find the number first, then look at what is right before (a currency) and right after (a unit). */
|
|
249
|
+
const NUMBER_RUN = /\d[\d,.]{0,15}/gu;
|
|
250
|
+
const UNITS = [
|
|
251
|
+
"business days",
|
|
252
|
+
"business day",
|
|
253
|
+
"calendar days",
|
|
254
|
+
"calendar day",
|
|
255
|
+
"days",
|
|
256
|
+
"day",
|
|
257
|
+
"weeks",
|
|
258
|
+
"week",
|
|
259
|
+
"months",
|
|
260
|
+
"month",
|
|
261
|
+
"years",
|
|
262
|
+
"year",
|
|
263
|
+
"hours",
|
|
264
|
+
"hour",
|
|
265
|
+
"minutes",
|
|
266
|
+
"minute",
|
|
267
|
+
"percent",
|
|
268
|
+
"times",
|
|
269
|
+
"%",
|
|
270
|
+
];
|
|
271
|
+
const CURRENCIES = ["USD", "EUR", "$", "€", "£"];
|
|
272
|
+
/** "30." の終わりの点は文の終わり。数の一部にしない。 */
|
|
273
|
+
const withoutTrailingPunctuation = (run) => {
|
|
274
|
+
let end = run.length;
|
|
275
|
+
while (end > 0 && (run[end - 1] === "." || run[end - 1] === ","))
|
|
276
|
+
end -= 1;
|
|
277
|
+
return run.slice(0, end);
|
|
278
|
+
};
|
|
279
|
+
/** One space or tab may sit between a number and its unit or currency. */
|
|
280
|
+
const isGap = (char) => char === " " || char === "\t";
|
|
281
|
+
const unitAfter = (text, from) => {
|
|
282
|
+
const gap = isGap(text[from]) ? 1 : 0;
|
|
283
|
+
const unit = UNITS.find((candidate) => text.startsWith(candidate, from + gap) && !isWordChar(text[from + gap + candidate.length]));
|
|
284
|
+
return unit;
|
|
285
|
+
};
|
|
286
|
+
/** The currency just before the number, allowing one space. Looks back a few characters, never at the whole line. */
|
|
287
|
+
const currencyBefore = (text, at) => {
|
|
288
|
+
const end = isGap(text[at - 1]) ? at - 1 : at;
|
|
289
|
+
return CURRENCIES.find((currency) => text.startsWith(currency, end - currency.length));
|
|
290
|
+
};
|
|
291
|
+
const quantities = (text) => [...text.matchAll(NUMBER_RUN)].flatMap((match) => {
|
|
292
|
+
const digits = withoutTrailingPunctuation(match[0]);
|
|
293
|
+
const value = Number(digits.replace(/,/gu, ""));
|
|
294
|
+
const end = match.index + digits.length;
|
|
295
|
+
const unit = unitAfter(text, end) ?? currencyBefore(text, match.index);
|
|
296
|
+
return unit === undefined || Number.isNaN(value) || isWordChar(text[match.index - 1]) ? [] : [{ start: match.index, end, attrs: { value, unit } }];
|
|
297
|
+
});
|
|
298
|
+
const MONTHS = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"];
|
|
299
|
+
const MONTH_WORD = /\b(?<month>[A-Z][a-z]{2,8})\b/gu;
|
|
300
|
+
const ISO_DATE = /\b(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})\b/gu;
|
|
301
|
+
const DAY_BEFORE = /(?<d>\d{1,2})(?:st|nd|rd|th)? $/u;
|
|
302
|
+
const DAY_YEAR_AFTER = /^ (?<d>\d{1,2})(?:st|nd|rd|th)?,? (?<y>\d{4})\b/u;
|
|
303
|
+
const YEAR_AFTER = /^,? (?<y>\d{4})\b/u;
|
|
304
|
+
const pad = (value) => value.padStart(2, "0");
|
|
305
|
+
/** "1 April 2024": the day written before the month. */
|
|
306
|
+
const dayBefore = (text, at) => {
|
|
307
|
+
const found = DAY_BEFORE.exec(text.slice(Math.max(0, at - 6), at));
|
|
308
|
+
const day = found?.groups?.["d"];
|
|
309
|
+
return found === null || day === undefined ? undefined : { day, start: at - found[0].length };
|
|
310
|
+
};
|
|
311
|
+
/** "April 1, 2024" → 2024-04-01. */
|
|
312
|
+
const monthDayYear = (text, at, end, month) => {
|
|
313
|
+
const found = DAY_YEAR_AFTER.exec(text.slice(end, end + 16));
|
|
314
|
+
if (found?.groups === undefined)
|
|
315
|
+
return undefined;
|
|
316
|
+
const value = `${found.groups["y"] ?? ""}-${pad(String(month))}-${pad(found.groups["d"] ?? "")}`;
|
|
317
|
+
return { start: at, end: end + found[0].length, attrs: { value } };
|
|
318
|
+
};
|
|
319
|
+
/** "1 April 2024" → 2024-04-01, "April 2024" → 2024-04. */
|
|
320
|
+
const monthYear = (text, at, end, month) => {
|
|
321
|
+
const found = YEAR_AFTER.exec(text.slice(end, end + 8));
|
|
322
|
+
const year = found?.groups?.["y"];
|
|
323
|
+
if (found === null || year === undefined)
|
|
324
|
+
return undefined;
|
|
325
|
+
const before = dayBefore(text, at);
|
|
326
|
+
const value = [year, pad(String(month)), ...(before === undefined ? [] : [pad(before.day)])].join("-");
|
|
327
|
+
return { start: before?.start ?? at, end: end + found[0].length, attrs: { value } };
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* A month name alone is not a date: "May" is also the modal verb, so it counts only with a year beside it.
|
|
331
|
+
* The month is found first and its neighbours read with anchored patterns, never one long alternation.
|
|
332
|
+
*/
|
|
333
|
+
const namedDate = (text, match) => {
|
|
334
|
+
const month = MONTHS.indexOf((match.groups?.["month"] ?? "").toLowerCase()) + 1;
|
|
335
|
+
if (month === 0)
|
|
336
|
+
return undefined;
|
|
337
|
+
const end = match.index + match[0].length;
|
|
338
|
+
return monthDayYear(text, match.index, end, month) ?? monthYear(text, match.index, end, month);
|
|
339
|
+
};
|
|
340
|
+
const isoDate = (match) => ({
|
|
341
|
+
start: match.index,
|
|
342
|
+
end: match.index + match[0].length,
|
|
343
|
+
attrs: { value: `${match.groups?.["y"] ?? ""}-${match.groups?.["m"] ?? ""}-${match.groups?.["d"] ?? ""}` },
|
|
344
|
+
});
|
|
345
|
+
const dates = (text) => [...[...text.matchAll(MONTH_WORD)].flatMap((match) => namedDate(text, match) ?? []), ...[...text.matchAll(ISO_DATE)].map(isoDate)].sort((left, right) => left.start - right.start);
|
|
346
|
+
/** "2.5 days" and "1.5 times" are amounts, not section 2.5 titled "days". */
|
|
347
|
+
const countedAfter = (_number, rest) => unitAfter(` ${rest}`, 0) !== undefined;
|
|
348
|
+
export const structure = {
|
|
349
|
+
numbered,
|
|
350
|
+
definitions,
|
|
351
|
+
references,
|
|
352
|
+
obligations,
|
|
353
|
+
quantities,
|
|
354
|
+
dates,
|
|
355
|
+
countedAfter,
|
|
356
|
+
opensDefinitionScope,
|
|
357
|
+
definitionScopeDepth,
|
|
358
|
+
};
|
|
359
|
+
//# sourceMappingURL=structure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structure.js","sourceRoot":"","sources":["../src/structure.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/citation.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// A reference into another document: "Section 9 of the Master Agreement" is not this document's Section 9.
|
|
2
|
+
|
|
3
|
+
const OF = /^,? of (?:the |that |those )?/u;
|
|
4
|
+
/** "section 4(2)(a) (exception to liability …) of the Damages (Scotland) Act 2011": the gloss sits between the number and the name. */
|
|
5
|
+
const GLOSS = /^ \([^()]{1,100}\)/u;
|
|
6
|
+
const CONNECTOR = /^(?:,? (?:to|and|or)|,) /u;
|
|
7
|
+
/** "9", "29(2)", "(g)", and a roman "V" for a list of Articles. The roman numeral must end the word: "VIII", not "Vendor". */
|
|
8
|
+
const LISTED_NUMBERS = [/^\d{1,3}[A-Z]{0,2}(?:\([a-z0-9]{1,4}\))*/u, /^(?:\([a-z0-9]{1,4}\))+/u, /^[IVXLC]{1,7}\b/u];
|
|
9
|
+
|
|
10
|
+
const listedNumber = (text: string): string | undefined => LISTED_NUMBERS.map((pattern) => pattern.exec(text)?.[0]).find((found) => found !== undefined);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A listed number is a reference only where the list goes on or ends: "Sections 1, 2 and 9 of" or "Section 3 and 4." —
|
|
14
|
+
* not "Section 3 and 4 days", where the 4 counts days.
|
|
15
|
+
*/
|
|
16
|
+
const MEMBER_END = /^(?:$|[,;.:)]|\s(?:to|and|or|of)\b|\s\()/u;
|
|
17
|
+
|
|
18
|
+
export type ListMember = { readonly start: number; readonly text: string };
|
|
19
|
+
|
|
20
|
+
/** Every number listed after a reference, with where it ends. `start` is from the start of `rest`. */
|
|
21
|
+
const listed = (rest: string): (ListMember & { readonly end: number })[] => {
|
|
22
|
+
const found: (ListMember & { end: number })[] = [];
|
|
23
|
+
let end = 0;
|
|
24
|
+
for (;;) {
|
|
25
|
+
const connector = CONNECTOR.exec(rest.slice(end))?.[0];
|
|
26
|
+
const number = connector === undefined ? undefined : listedNumber(rest.slice(end + connector.length));
|
|
27
|
+
if (connector === undefined || number === undefined) return found;
|
|
28
|
+
const start = end + connector.length;
|
|
29
|
+
end = start + number.length;
|
|
30
|
+
found.push({ start, text: number, end });
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** "Article 58(2)(c) to (g) and (j) of the UK GDPR": the list runs on to the name that governs all of it. */
|
|
35
|
+
const listEnd = (rest: string): number => listed(rest).at(-1)?.end ?? 0;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The members after a reference, "2" and "9" in "Sections 1, 2 and 9". After a plural ("Sections") every listed number
|
|
39
|
+
* is one; after a singular ("Section 3 and 4 days") only one where the list goes on or ends.
|
|
40
|
+
*/
|
|
41
|
+
export const listMembers = (rest: string, plural: boolean): ListMember[] =>
|
|
42
|
+
listed(rest)
|
|
43
|
+
.filter((member) => plural || MEMBER_END.test(rest.slice(member.end)))
|
|
44
|
+
.map(({ start, text }) => ({ start, text }));
|
|
45
|
+
|
|
46
|
+
const CAPITALISED = /^[A-Z][\w'’-]*/u;
|
|
47
|
+
/**
|
|
48
|
+
* How a document names itself. "Section 3 of the Agreement" in an agreement means this one.
|
|
49
|
+
* "the Act" and "the Code" are left out: a statute calls itself "this Act", and a contract's "the Act" is a law it cites.
|
|
50
|
+
*/
|
|
51
|
+
const SELF = new Set(["Agreement", "Contract", "Terms", "Policy"]);
|
|
52
|
+
/** Words that may sit inside a title: "Code of Federal Regulations". */
|
|
53
|
+
const JOINERS = new Set(["of", "and", "for", "on"]);
|
|
54
|
+
const MAX_WORDS = 8;
|
|
55
|
+
|
|
56
|
+
/** Capitalised words from the start of `rest`, allowing a joiner between two of them. Punctuation ends the title. */
|
|
57
|
+
const titleWords = (rest: string): string[] => {
|
|
58
|
+
const tokens = rest.split(" ");
|
|
59
|
+
const words: string[] = [];
|
|
60
|
+
for (let index = 0; index < tokens.length && words.length < MAX_WORDS; index += 1) {
|
|
61
|
+
const token = tokens[index] ?? "";
|
|
62
|
+
const word = CAPITALISED.exec(token)?.[0];
|
|
63
|
+
const joins = word === undefined && words.length > 0 && JOINERS.has(token) && CAPITALISED.test(tokens[index + 1] ?? "");
|
|
64
|
+
if (word === undefined && !joins) break;
|
|
65
|
+
words.push(word ?? token);
|
|
66
|
+
if (word !== undefined && word.length < token.length) break;
|
|
67
|
+
}
|
|
68
|
+
return words;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The document named right after a reference, or undefined when the reference is into this document.
|
|
73
|
+
* "of this Agreement" and "of the Agreement" are this document; "of the Master Agreement" is another.
|
|
74
|
+
*/
|
|
75
|
+
export const citedDocumentAfter = (text: string, end: number): string | undefined => {
|
|
76
|
+
const rest = text.slice(end, end + 200);
|
|
77
|
+
const afterList = listEnd(rest);
|
|
78
|
+
const listed = afterList + (GLOSS.exec(rest.slice(afterList))?.[0].length ?? 0);
|
|
79
|
+
const of = OF.exec(rest.slice(listed));
|
|
80
|
+
if (of === null) return undefined;
|
|
81
|
+
const words = titleWords(rest.slice(listed + of[0].length));
|
|
82
|
+
if (words.length === 0) return undefined;
|
|
83
|
+
const name = words.join(" ");
|
|
84
|
+
return words.length === 1 && SELF.has(name) ? undefined : name;
|
|
85
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Mention } from "chaffjs/plugin";
|
|
2
|
+
import { CHAPTER_DEPTH, PART_DEPTH } from "./depth.ts";
|
|
3
|
+
|
|
4
|
+
// Definitions in English documents, and how far a definition holds.
|
|
5
|
+
|
|
6
|
+
const DEFINITIONS = [
|
|
7
|
+
/["“](?<term>[^"”\n]{1,60})["”] (?:means|shall mean|refers to|has the meaning)\b/gu,
|
|
8
|
+
/\((?:the |hereinafter )?["“](?<term>[^"”\n]{1,60})["”]\)/gu,
|
|
9
|
+
/\(hereinafter referred to as ["“](?<term>[^"”\n]{1,60})["”]\)/gu,
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* "In this Part—" and "This section applies where a person ("the seller")…": a statute defines the same word again
|
|
14
|
+
* in the next Part. A Part's definitions are compared within the Part, a section's within the section.
|
|
15
|
+
*/
|
|
16
|
+
const DEFINITION_SCOPE = /\b(?:In this (?:section|subsection|Part|Chapter|Schedule|Article)\b|This (?:section|Part|Chapter) defines\b)/u;
|
|
17
|
+
/** "This section applies where a person ("the seller") …" names a party in parentheses for this section only. */
|
|
18
|
+
const APPLIES = /\bThis section applies\b/u;
|
|
19
|
+
/** "In this Part" and "This Part defines" hold in the Part; "In this Chapter" in the Chapter; the rest in the section. */
|
|
20
|
+
const WIDER_SCOPE: readonly (readonly [RegExp, number])[] = [
|
|
21
|
+
[/\b(?:In this|This) Part\b/u, PART_DEPTH],
|
|
22
|
+
[/\b(?:In this|This) Chapter\b/u, CHAPTER_DEPTH],
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const opensDefinitionScope = (text: string): boolean => DEFINITION_SCOPE.test(text);
|
|
26
|
+
|
|
27
|
+
export const definitionScopeDepth = (text: string): number | undefined =>
|
|
28
|
+
DEFINITION_SCOPE.test(text) ? WIDER_SCOPE.find(([pattern]) => pattern.test(text))?.[1] : undefined;
|
|
29
|
+
|
|
30
|
+
/** "has the meaning given in section 3" points at a definition elsewhere instead of making one. */
|
|
31
|
+
const POINTER = /^ has the meaning given (?:in|by)\b/u;
|
|
32
|
+
const isPointer = (text: string, end: number): boolean => POINTER.test(text.slice(end - " has the meaning".length));
|
|
33
|
+
|
|
34
|
+
export const definitions = (text: string): Mention[] =>
|
|
35
|
+
DEFINITIONS.flatMap((pattern) =>
|
|
36
|
+
[...text.matchAll(pattern)].flatMap((match) => {
|
|
37
|
+
const term = match.groups?.["term"];
|
|
38
|
+
if (term === undefined) return [];
|
|
39
|
+
const end = match.index + match[0].length;
|
|
40
|
+
const namesAParty = match[0].startsWith("(") && APPLIES.test(text);
|
|
41
|
+
return [{ start: match.index, end, attrs: { term, ...(isPointer(text, end) || namesAParty ? { scope: "local" } : {}) } }];
|
|
42
|
+
}),
|
|
43
|
+
);
|
package/src/depth.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { split, SentenceSplitterSyntax } from "sentence-splitter";
|
|
2
2
|
import { loadLexicons } from "./lexicons.ts";
|
|
3
|
+
import { structure } from "./structure.ts";
|
|
3
4
|
import { isReady, prepare, tokenize } from "./pos.ts";
|
|
4
5
|
import type { AdapterNeeds, LanguageAdapter, Segmentation, Sentence } from "chaffjs/plugin";
|
|
5
6
|
|
|
@@ -48,6 +49,7 @@ export const adapter: LanguageAdapter = {
|
|
|
48
49
|
return [...source.matchAll(LATIN_LETTER)].length / total;
|
|
49
50
|
},
|
|
50
51
|
lexicons: loadLexicons(),
|
|
52
|
+
structure,
|
|
51
53
|
segment: (text: string): Segmentation => {
|
|
52
54
|
const sentences: Sentence[] = split(text)
|
|
53
55
|
.filter((node) => node.type === SentenceSplitterSyntax.Sentence)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Mention } from "chaffjs/plugin";
|
|
2
|
+
import { listMembers } from "./citation.ts";
|
|
3
|
+
import { parseRoman } from "./roman.ts";
|
|
4
|
+
|
|
5
|
+
// The later members of a reference list: "Sections 1, 2 and 9" names Section 9 too.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A member is a number, as in the reference itself ("45A" is not read there either), a roman numeral in a list that
|
|
9
|
+
* began with one ("Articles IV, V and VI"), or only parentheses.
|
|
10
|
+
*/
|
|
11
|
+
const MEMBER = /^(?:(?<main>\d{1,3})|(?<roman>[IVXLC]{1,7}))?(?<parens>(?:\([a-z0-9]{1,4}\))*)$/u;
|
|
12
|
+
const PAREN = /\(([a-z0-9]{1,4})\)/gu;
|
|
13
|
+
|
|
14
|
+
const isDigits = (part: string): boolean => /^\d+$/u.test(part);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The part of the address before that a parenthesised member stands beside: the last one written the same way
|
|
18
|
+
* (digits with digits, letters with letters). 45.3.b and (5) → 45.5; 58.2.c and (g) → 58.2.g. The main number stays.
|
|
19
|
+
*/
|
|
20
|
+
const replaceLast = (previous: readonly string[] | undefined, parens: readonly string[]): string[] | undefined => {
|
|
21
|
+
const first = parens[0];
|
|
22
|
+
if (previous === undefined || first === undefined) return undefined;
|
|
23
|
+
const at = previous.findLastIndex((part, index) => index > 0 && isDigits(part) === isDigits(first));
|
|
24
|
+
return at === -1 ? undefined : [...previous.slice(0, at), ...parens];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** The main number of a member: digits as written, a roman numeral as its value, but only in a roman list. */
|
|
28
|
+
const mainOf = (groups: Readonly<Record<string, string | undefined>>, romanList: boolean): string | undefined => {
|
|
29
|
+
const roman = groups["roman"];
|
|
30
|
+
if (roman === undefined) return groups["main"];
|
|
31
|
+
const value = romanList ? parseRoman(roman) : undefined;
|
|
32
|
+
return value === undefined ? undefined : String(value);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** A member's address, from the member before it when it is only parentheses. */
|
|
36
|
+
const addressOf = (member: string, previous: readonly string[] | undefined, romanList: boolean): string[] | undefined => {
|
|
37
|
+
const groups = MEMBER.exec(member)?.groups ?? {};
|
|
38
|
+
const parens = [...(groups["parens"] ?? "").matchAll(PAREN)].map((part) => part[1] ?? "");
|
|
39
|
+
const written = groups["main"] ?? groups["roman"];
|
|
40
|
+
if (written === undefined) return replaceLast(previous, parens);
|
|
41
|
+
const main = mainOf(groups, romanList);
|
|
42
|
+
return main === undefined ? undefined : [main, ...parens];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* "Sections 1, 2 and 9" → 2 and 9 as references too, with the list's numbering and document (`shared`).
|
|
47
|
+
* `first` is the address of the reference that opens the list, and `end` where it ends in `text`.
|
|
48
|
+
*/
|
|
49
|
+
export const membersAfter = (
|
|
50
|
+
text: string,
|
|
51
|
+
end: number,
|
|
52
|
+
first: readonly string[],
|
|
53
|
+
shared: Readonly<Record<string, string>>,
|
|
54
|
+
plural: boolean,
|
|
55
|
+
romanList: boolean,
|
|
56
|
+
): Mention[] => {
|
|
57
|
+
const mentions: Mention[] = [];
|
|
58
|
+
let previous: readonly string[] | undefined = first;
|
|
59
|
+
listMembers(text.slice(end), plural).forEach((member) => {
|
|
60
|
+
previous = addressOf(member.text, previous, romanList);
|
|
61
|
+
if (previous === undefined) return;
|
|
62
|
+
const start = end + member.start;
|
|
63
|
+
mentions.push({ start, end: start + member.text.length, attrs: { target: previous.join("."), label: member.text, ...shared } });
|
|
64
|
+
});
|
|
65
|
+
return mentions;
|
|
66
|
+
};
|
package/src/roman.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const ROMAN: Readonly<Record<string, number>> = { i: 1, v: 5, x: 10, l: 50, c: 100 };
|
|
2
|
+
|
|
3
|
+
/** "IV" → 4, "xii" → 12. Undefined for anything that is not a roman numeral. */
|
|
4
|
+
export const parseRoman = (text: string): number | undefined => {
|
|
5
|
+
const values = [...text.toLowerCase()].map((char) => ROMAN[char]);
|
|
6
|
+
if (values.length === 0 || values.some((value) => value === undefined)) return undefined;
|
|
7
|
+
const known = values.filter((value) => value !== undefined);
|
|
8
|
+
return known.reduce((total, value, index) => ((known[index + 1] ?? 0) > value ? total - value : total + value), 0);
|
|
9
|
+
};
|