chaffjs 0.29.0 → 0.30.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/dist/derived/session-hours.d.ts +26 -0
- package/dist/derived/session-hours.d.ts.map +1 -0
- package/dist/derived/session-hours.js +29 -0
- package/dist/derived/session-hours.js.map +1 -0
- package/dist/derived/time-lengths.d.ts +37 -0
- package/dist/derived/time-lengths.d.ts.map +1 -0
- package/dist/derived/time-lengths.js +168 -0
- package/dist/derived/time-lengths.js.map +1 -0
- package/dist/derived/unit-counts.d.ts +7 -0
- package/dist/derived/unit-counts.d.ts.map +1 -0
- package/dist/derived/unit-counts.js +13 -0
- package/dist/derived/unit-counts.js.map +1 -0
- package/dist/derived/working-hours.d.ts +36 -0
- package/dist/derived/working-hours.d.ts.map +1 -0
- package/dist/derived/working-hours.js +55 -0
- package/dist/derived/working-hours.js.map +1 -0
- package/dist/detectors/acronym-expansion.d.ts +1 -1
- package/dist/detectors/acronym-expansion.d.ts.map +1 -1
- package/dist/detectors/acronym-expansion.js +46 -2
- package/dist/detectors/acronym-expansion.js.map +1 -1
- package/dist/detectors/capitalised-term.d.ts +44 -0
- package/dist/detectors/capitalised-term.d.ts.map +1 -0
- package/dist/detectors/capitalised-term.js +101 -0
- package/dist/detectors/capitalised-term.js.map +1 -0
- package/dist/detectors/date-outside-period.d.ts.map +1 -1
- package/dist/detectors/date-outside-period.js +4 -1
- package/dist/detectors/date-outside-period.js.map +1 -1
- package/dist/detectors/derived-numbers.d.ts +2 -0
- package/dist/detectors/derived-numbers.d.ts.map +1 -1
- package/dist/detectors/derived-numbers.js +72 -18
- package/dist/detectors/derived-numbers.js.map +1 -1
- package/dist/detectors/name-variant.d.ts.map +1 -1
- package/dist/detectors/name-variant.js +57 -10
- package/dist/detectors/name-variant.js.map +1 -1
- package/dist/detectors/party-order.d.ts +3 -0
- package/dist/detectors/party-order.d.ts.map +1 -0
- package/dist/detectors/party-order.js +46 -0
- package/dist/detectors/party-order.js.map +1 -0
- package/dist/detectors/registry/connection-time-short.d.ts +3 -0
- package/dist/detectors/registry/connection-time-short.d.ts.map +1 -0
- package/dist/detectors/registry/connection-time-short.js +3 -0
- package/dist/detectors/registry/connection-time-short.js.map +1 -0
- package/dist/detectors/registry/duration-product-mismatch.d.ts +3 -0
- package/dist/detectors/registry/duration-product-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/duration-product-mismatch.js +3 -0
- package/dist/detectors/registry/duration-product-mismatch.js.map +1 -0
- package/dist/detectors/registry/party-order.d.ts +3 -0
- package/dist/detectors/registry/party-order.d.ts.map +1 -0
- package/dist/detectors/registry/party-order.js +3 -0
- package/dist/detectors/registry/party-order.js.map +1 -0
- package/dist/detectors/registry/requirement-lists.d.ts +3 -0
- package/dist/detectors/registry/requirement-lists.d.ts.map +1 -0
- package/dist/detectors/registry/requirement-lists.js +3 -0
- package/dist/detectors/registry/requirement-lists.js.map +1 -0
- package/dist/detectors/registry/return-type.d.ts +3 -0
- package/dist/detectors/registry/return-type.d.ts.map +1 -0
- package/dist/detectors/registry/return-type.js +3 -0
- package/dist/detectors/registry/return-type.js.map +1 -0
- package/dist/detectors/requirement-lists.d.ts +3 -0
- package/dist/detectors/requirement-lists.d.ts.map +1 -0
- package/dist/detectors/requirement-lists.js +46 -0
- package/dist/detectors/requirement-lists.js.map +1 -0
- package/dist/detectors/return-type.d.ts +3 -0
- package/dist/detectors/return-type.d.ts.map +1 -0
- package/dist/detectors/return-type.js +24 -0
- package/dist/detectors/return-type.js.map +1 -0
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +9 -1
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/time-order.d.ts +1 -0
- package/dist/detectors/time-order.d.ts.map +1 -1
- package/dist/detectors/time-order.js +30 -4
- package/dist/detectors/time-order.js.map +1 -1
- package/dist/detectors/undefined-term.d.ts.map +1 -1
- package/dist/detectors/undefined-term.js +19 -3
- package/dist/detectors/undefined-term.js.map +1 -1
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +4 -1
- package/dist/document.js.map +1 -1
- package/dist/facts/table-facts.d.ts +2 -0
- package/dist/facts/table-facts.d.ts.map +1 -1
- package/dist/facts/table-facts.js +2 -0
- package/dist/facts/table-facts.js.map +1 -1
- package/dist/name-variants.d.ts +6 -1
- package/dist/name-variants.d.ts.map +1 -1
- package/dist/name-variants.js +68 -0
- package/dist/name-variants.js.map +1 -1
- package/dist/name-word-order.d.ts +49 -0
- package/dist/name-word-order.d.ts.map +1 -0
- package/dist/name-word-order.js +114 -0
- package/dist/name-word-order.js.map +1 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/return-type.d.ts +33 -0
- package/dist/return-type.d.ts.map +1 -0
- package/dist/return-type.js +302 -0
- package/dist/return-type.js.map +1 -0
- package/dist/structure/connection-times.d.ts +56 -0
- package/dist/structure/connection-times.d.ts.map +1 -0
- package/dist/structure/connection-times.js +165 -0
- package/dist/structure/connection-times.js.map +1 -0
- package/dist/structure/date-order-tie.d.ts +13 -0
- package/dist/structure/date-order-tie.d.ts.map +1 -0
- package/dist/structure/date-order-tie.js +61 -0
- package/dist/structure/date-order-tie.js.map +1 -0
- package/dist/structure/date-order.d.ts +2 -2
- package/dist/structure/date-order.d.ts.map +1 -1
- package/dist/structure/date-order.js +26 -9
- package/dist/structure/date-order.js.map +1 -1
- package/dist/structure/date-outside-period.d.ts +6 -0
- package/dist/structure/date-outside-period.d.ts.map +1 -1
- package/dist/structure/date-outside-period.js +49 -3
- package/dist/structure/date-outside-period.js.map +1 -1
- package/dist/structure/date-range.d.ts +5 -2
- package/dist/structure/date-range.d.ts.map +1 -1
- package/dist/structure/date-range.js +37 -9
- package/dist/structure/date-range.js.map +1 -1
- package/dist/structure/leg-times.d.ts +2 -0
- package/dist/structure/leg-times.d.ts.map +1 -1
- package/dist/structure/leg-times.js +13 -0
- package/dist/structure/leg-times.js.map +1 -1
- package/dist/structure/party-order.d.ts +33 -0
- package/dist/structure/party-order.d.ts.map +1 -0
- package/dist/structure/party-order.js +79 -0
- package/dist/structure/party-order.js.map +1 -0
- package/dist/structure/requirement-lists.d.ts +37 -0
- package/dist/structure/requirement-lists.d.ts.map +1 -0
- package/dist/structure/requirement-lists.js +89 -0
- package/dist/structure/requirement-lists.js.map +1 -0
- package/dist/structure/time-order.d.ts +21 -0
- package/dist/structure/time-order.d.ts.map +1 -1
- package/dist/structure/time-order.js +5 -6
- package/dist/structure/time-order.js.map +1 -1
- package/dist/structure/version-order.d.ts +2 -0
- package/dist/structure/version-order.d.ts.map +1 -1
- package/dist/structure/version-order.js +10 -0
- package/dist/structure/version-order.js.map +1 -1
- package/dist/surname-near.d.ts +6 -0
- package/dist/surname-near.d.ts.map +1 -0
- package/dist/surname-near.js +31 -0
- package/dist/surname-near.js.map +1 -0
- package/dist/table-cells.d.ts +13 -0
- package/dist/table-cells.d.ts.map +1 -0
- package/dist/table-cells.js +36 -0
- package/dist/table-cells.js.map +1 -0
- package/dist/table-names.d.ts +42 -0
- package/dist/table-names.d.ts.map +1 -0
- package/dist/table-names.js +94 -0
- package/dist/table-names.js.map +1 -0
- package/package.json +3 -3
- package/rules/connection-time-short.yaml +52 -0
- package/rules/date-outside-period.yaml +4 -4
- package/rules/duration-mismatch.yaml +7 -4
- package/rules/duration-product-mismatch.yaml +73 -0
- package/rules/name-variant.yaml +8 -2
- package/rules/party-order.yaml +84 -0
- package/rules/required-listed-as-preferred.yaml +102 -0
- package/rules/return-type-mismatch.yaml +79 -0
- package/rules/undefined-term.yaml +21 -7
- package/src/derived/session-hours.ts +62 -0
- package/src/derived/time-lengths.ts +226 -0
- package/src/derived/unit-counts.ts +18 -0
- package/src/derived/working-hours.ts +97 -0
- package/src/detectors/acronym-expansion.ts +57 -2
- package/src/detectors/capitalised-term.ts +147 -0
- package/src/detectors/date-outside-period.ts +4 -1
- package/src/detectors/derived-numbers.ts +94 -21
- package/src/detectors/name-variant.ts +71 -13
- package/src/detectors/party-order.ts +50 -0
- package/src/detectors/registry/connection-time-short.ts +4 -0
- package/src/detectors/registry/duration-product-mismatch.ts +4 -0
- package/src/detectors/registry/party-order.ts +4 -0
- package/src/detectors/registry/requirement-lists.ts +4 -0
- package/src/detectors/registry/return-type.ts +4 -0
- package/src/detectors/requirement-lists.ts +65 -0
- package/src/detectors/return-type.ts +30 -0
- package/src/detectors/structure-tree.ts +12 -1
- package/src/detectors/time-order.ts +40 -5
- package/src/detectors/undefined-term.ts +20 -3
- package/src/document.ts +4 -1
- package/src/facts/table-facts.ts +3 -0
- package/src/name-variants.ts +76 -1
- package/src/name-word-order.ts +149 -0
- package/src/plugin.ts +5 -0
- package/src/return-type.ts +361 -0
- package/src/structure/connection-times.ts +226 -0
- package/src/structure/date-order-tie.ts +86 -0
- package/src/structure/date-order.ts +41 -14
- package/src/structure/date-outside-period.ts +72 -7
- package/src/structure/date-range.ts +45 -9
- package/src/structure/leg-times.ts +15 -0
- package/src/structure/party-order.ts +115 -0
- package/src/structure/requirement-lists.ts +145 -0
- package/src/structure/time-order.ts +8 -7
- package/src/structure/version-order.ts +10 -0
- package/src/surname-near.ts +31 -0
- package/src/table-cells.ts +41 -0
- package/src/table-names.ts +113 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// One function's return type stated two ways on one page: the signature in a code block against a "Returns:" line, a
|
|
2
|
+
// table's return column, or a sentence ("measure returns a string", 「measure は文字列を返します」). Pure; reads the
|
|
3
|
+
// Markdown source. The words (Returns, 戻り値, returns, 返します, は, and the type words string, 文字列) come from the
|
|
4
|
+
// language's lexicons. A statement is compared only with the signatures of the function it names, and only when both
|
|
5
|
+
// sides are readable. The signature reader is small and TypeScript/Python-shaped; it may later be shared with
|
|
6
|
+
// parameter-table.ts.
|
|
7
|
+
|
|
8
|
+
/** The words of one language that state a return type. `kinds` maps a lower-cased type word to a kind (string, array). */
|
|
9
|
+
export type ReturnWords = {
|
|
10
|
+
readonly labels: readonly string[];
|
|
11
|
+
readonly verbs: readonly string[];
|
|
12
|
+
/** What may follow the function's name before the verb (は, が); none for a language that puts the verb first. */
|
|
13
|
+
readonly subjects: readonly string[];
|
|
14
|
+
readonly kinds: ReadonlyMap<string, string>;
|
|
15
|
+
/** Words opening a clause about when (when, とき): a type word on the far side of one names the input, not the result. */
|
|
16
|
+
readonly clauses: readonly string[];
|
|
17
|
+
/** Words making the type word before them one item of the result (per, ごと). */
|
|
18
|
+
readonly items: readonly string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** A statement that disagrees with every signature of its function: what it says, what a signature says, and where. */
|
|
22
|
+
export type ReturnTypeClash = { readonly fn: string; readonly stated: string; readonly signature: string; readonly offset: number };
|
|
23
|
+
|
|
24
|
+
/** A return type as written: a type in code (`string[]`), or the kind a type word names (a string → string). */
|
|
25
|
+
type Stated = { readonly code: string; readonly written: string } | { readonly kind: string; readonly written: string };
|
|
26
|
+
|
|
27
|
+
type Statement = { readonly fn: string; readonly stated: Stated; readonly offset: number };
|
|
28
|
+
type Signature = { readonly fn: string; readonly params: string; readonly returns: string; readonly offset: number };
|
|
29
|
+
type Line = { readonly text: string; readonly start: number; readonly fenced: boolean; readonly section: number };
|
|
30
|
+
|
|
31
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})/u;
|
|
32
|
+
const HEADING = /^ {0,3}#{1,6}[ \t]/u;
|
|
33
|
+
const TABLE_ROW = /^[ \t]{0,3}\|/u;
|
|
34
|
+
const DELIMITER_ROW = /^[ \t]{0,3}\|?[ \t]*:?-{3,}/u;
|
|
35
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*/u;
|
|
36
|
+
const DECLARATION_HEAD = /^(?:(?:export|default|declare|async|public|static)\s+)*(?:(?:function\*?|def|fn|func)\s+)?/u;
|
|
37
|
+
const ARROW_HEAD = /^(?:(?:export|declare)\s+)*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?/u;
|
|
38
|
+
const RETURN_MARK = /^\s*(?::|->)\s*/u;
|
|
39
|
+
/** What may close a declaration line after its return type: an arrow's `=>` or a body's `{` with what follows, `;`, Python's `:`. */
|
|
40
|
+
const withoutDeclarationTail = (text: string): string => {
|
|
41
|
+
const cut = [text.indexOf("=>"), text.indexOf("{")].filter((at) => at !== -1);
|
|
42
|
+
const head = (cut.length === 0 ? text : text.slice(0, Math.min(...cut))).trim();
|
|
43
|
+
return head.endsWith(";") || head.endsWith(":") ? head.slice(0, -1).trim() : head;
|
|
44
|
+
};
|
|
45
|
+
const GENERICS = /^<[^<>()]*>/u;
|
|
46
|
+
const TYPE_NAME = String.raw`[A-Za-z_$][\w$.]*(?:<[\w$.,\s<>[\]|]*>|\[[\w$.,\s[\]|]*\])?(?:\[\])*`;
|
|
47
|
+
/** A type as code writes it: names, generic arguments, array brackets, and unions of those. */
|
|
48
|
+
const TYPE_TEXT = new RegExp(String.raw`^${TYPE_NAME}(?:\s*\|\s*${TYPE_NAME})*$`, "u");
|
|
49
|
+
/** Where a sentence ends, so a type word of the next sentence is not read as this one's. */
|
|
50
|
+
const SENTENCE_END = /[。!?!?;]|\.(?=\s|$)/u;
|
|
51
|
+
const NOTHING_TYPES: ReadonlySet<string> = new Set(["null", "undefined", "void", "None"]);
|
|
52
|
+
/** The kind of a type name in code; a name not listed (a class, a generic parameter, Promise<…>) has none. */
|
|
53
|
+
const CODE_KINDS: ReadonlyMap<string, string> = new Map([
|
|
54
|
+
["string", "string"],
|
|
55
|
+
["str", "string"],
|
|
56
|
+
["number", "number"],
|
|
57
|
+
["int", "number"],
|
|
58
|
+
["float", "number"],
|
|
59
|
+
["bigint", "number"],
|
|
60
|
+
["boolean", "boolean"],
|
|
61
|
+
["bool", "boolean"],
|
|
62
|
+
["list", "array"],
|
|
63
|
+
["tuple", "array"],
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const linesOf = (source: string): Line[] => {
|
|
67
|
+
const state = { start: 0, fence: "", section: 0 };
|
|
68
|
+
return source.split("\n").map((text) => {
|
|
69
|
+
const start = state.start;
|
|
70
|
+
state.start += text.length + 1;
|
|
71
|
+
const mark = FENCE.exec(text)?.[1];
|
|
72
|
+
const closes = mark !== undefined && state.fence !== "" && mark[0] === state.fence[0] && mark.length >= state.fence.length;
|
|
73
|
+
if (mark !== undefined && (state.fence === "" || closes)) {
|
|
74
|
+
state.fence = closes ? "" : mark;
|
|
75
|
+
return { text, start, fenced: true, section: state.section };
|
|
76
|
+
}
|
|
77
|
+
if (state.fence === "" && HEADING.test(text)) state.section += 1;
|
|
78
|
+
return { text, start, fenced: state.fence !== "", section: state.section };
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** The index of the ) that closes the ( at `open`, or -1. */
|
|
83
|
+
const closingParen = (text: string, open: number): number => {
|
|
84
|
+
const state = { depth: 0, at: -1 };
|
|
85
|
+
Array.from(text.slice(open)).some((char, index) => {
|
|
86
|
+
if (char === "(") state.depth += 1;
|
|
87
|
+
if (char === ")") state.depth -= 1;
|
|
88
|
+
if (state.depth === 0) state.at = open + index;
|
|
89
|
+
return state.depth === 0;
|
|
90
|
+
});
|
|
91
|
+
return state.at;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const squeeze = (text: string): string => text.replace(/\s+/gu, "");
|
|
95
|
+
|
|
96
|
+
/** `Array<string>` is `string[]`: the inner generic first, so Array<Array<string>> becomes string[][]. */
|
|
97
|
+
const withoutArrayGenerics = (type: string): string => {
|
|
98
|
+
const next = type.replace(/(?<![\w$.])Array<([^<>]*)>/u, (_whole, inner: string) => (inner.includes("|") ? `(${inner})[]` : `${inner}[]`));
|
|
99
|
+
return next === type ? type : withoutArrayGenerics(next);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** The members of a union at its top level, outside brackets. */
|
|
103
|
+
const unionMembers = (type: string): string[] => {
|
|
104
|
+
const state = { depth: 0, from: 0 };
|
|
105
|
+
const members: string[] = [];
|
|
106
|
+
Array.from(type).forEach((char, index) => {
|
|
107
|
+
if ("(<[{".includes(char)) state.depth += 1;
|
|
108
|
+
if (")>]}".includes(char)) state.depth -= 1;
|
|
109
|
+
if (char !== "|" || state.depth !== 0) return;
|
|
110
|
+
members.push(type.slice(state.from, index));
|
|
111
|
+
state.from = index + 1;
|
|
112
|
+
});
|
|
113
|
+
return [...members, type.slice(state.from)].filter((member) => member !== "");
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** A code type's members in one spelling: no spaces, Array<T> as T[], sorted. */
|
|
117
|
+
export const typeMembers = (type: string): string[] =>
|
|
118
|
+
unionMembers(withoutArrayGenerics(squeeze(type))).toSorted((left, right) => left.localeCompare(right, "en"));
|
|
119
|
+
|
|
120
|
+
/** The kind of one member, or undefined when its name says nothing a type word could (WrapResult, T, Promise<…>). */
|
|
121
|
+
const kindOfMember = (member: string): string | undefined => {
|
|
122
|
+
if (member.endsWith("[]") || /^(?:list|List|tuple|Tuple)\[/u.test(member)) return "array";
|
|
123
|
+
return CODE_KINDS.get(member);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A type in a sentence or a cell is read only when it can be a type and not a parameter's name: each member has a kind, is
|
|
128
|
+
* null or the like, or is a capitalised name (WrapResult). `text` is a name, and `null` alone says when, not what.
|
|
129
|
+
*/
|
|
130
|
+
const isReadableCode = (type: string): boolean => {
|
|
131
|
+
const members = typeMembers(type);
|
|
132
|
+
const readable = members.every((member) => kindOfMember(member) !== undefined || NOTHING_TYPES.has(member) || /^[A-Z]/u.test(member));
|
|
133
|
+
return TYPE_TEXT.test(type.trim()) && readable && members.some((member) => !NOTHING_TYPES.has(member));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const declaredAt = (line: string): { readonly fn: string; readonly afterName: number } | undefined => {
|
|
137
|
+
const arrow = ARROW_HEAD.exec(line);
|
|
138
|
+
if (arrow?.[1] !== undefined) return { fn: arrow[1], afterName: arrow[0].length };
|
|
139
|
+
const headLength = DECLARATION_HEAD.exec(line)?.[0].length ?? 0;
|
|
140
|
+
const fn = IDENTIFIER.exec(line.slice(headLength))?.[0];
|
|
141
|
+
return fn === undefined ? undefined : { fn, afterName: headLength + fn.length };
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** A declaration with a return type (`wrap(text: string): string[]`, `def f(x) -> str:`); a call has none. */
|
|
145
|
+
export const signatureOf = (line: string, offset = 0): Signature | undefined => {
|
|
146
|
+
const declared = declaredAt(line);
|
|
147
|
+
if (declared === undefined) return undefined;
|
|
148
|
+
const open = declared.afterName + (GENERICS.exec(line.slice(declared.afterName))?.[0].length ?? 0);
|
|
149
|
+
const close = line[open] === "(" ? closingParen(line, open) : -1;
|
|
150
|
+
const mark = close === -1 ? null : RETURN_MARK.exec(line.slice(close + 1));
|
|
151
|
+
if (close === -1 || mark === null) return undefined;
|
|
152
|
+
const returns = withoutDeclarationTail(line.slice(close + 1 + mark[0].length));
|
|
153
|
+
return TYPE_TEXT.test(returns) ? { fn: declared.fn, params: squeeze(line.slice(open + 1, close)), returns, offset } : undefined;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const signaturesOf = (lines: readonly Line[]): Signature[] =>
|
|
157
|
+
lines.flatMap((line) => {
|
|
158
|
+
const lead = line.text.length - line.text.trimStart().length;
|
|
159
|
+
const signature = line.fenced ? signatureOf(line.text.trim(), line.start + lead) : undefined;
|
|
160
|
+
return signature === undefined ? [] : [signature];
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const escaped = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
|
|
164
|
+
|
|
165
|
+
const isSpaced = (word: string): boolean => /^[A-Za-z]/u.test(word);
|
|
166
|
+
|
|
167
|
+
/** A word's occurrences: whole words where words are spaced, any occurrence where they are not. */
|
|
168
|
+
const wordPattern = (word: string): RegExp => (isSpaced(word) ? new RegExp(String.raw`\b${escaped(word)}\b`, "giu") : new RegExp(escaped(word), "gu"));
|
|
169
|
+
|
|
170
|
+
const startsWithWord = (text: string, word: string): boolean =>
|
|
171
|
+
isSpaced(word) ? new RegExp(String.raw`^${escaped(word)}\b`, "iu").test(text) : text.startsWith(word);
|
|
172
|
+
|
|
173
|
+
/** "returns true when …" is a boolean: a literal says its kind in any language, in code or not. */
|
|
174
|
+
const LITERAL_KINDS: ReadonlyMap<string, string> = new Map([
|
|
175
|
+
["true", "boolean"],
|
|
176
|
+
["false", "boolean"],
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
/** Whether `word` stands in `text` as a whole value, not followed by a word making it one item (string per line). */
|
|
180
|
+
const namesWhole = (text: string, word: string, items: readonly string[]): boolean =>
|
|
181
|
+
[...text.matchAll(wordPattern(word))].some((match) => {
|
|
182
|
+
const next = text.slice(match.index + match[0].length).trimStart();
|
|
183
|
+
return !items.some((item) => startsWithWord(next, item));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/** The type words in a stretch of text, outside code spans other than a literal (`false`). */
|
|
187
|
+
const typeWordsIn = (text: string, words: ReturnWords): [string, string][] => {
|
|
188
|
+
const bare = text.replace(/`([^`]*)`/gu, (_whole, inner: string) => (LITERAL_KINDS.has(inner.toLowerCase()) ? ` ${inner} ` : " "));
|
|
189
|
+
return [...words.kinds, ...LITERAL_KINDS].filter(([word]) => namesWhole(bare, word, words.items));
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/** A language with no subject words (en) puts the verb right after the name, and a clause about when after the result. */
|
|
193
|
+
const isVerbFirst = (words: ReturnWords): boolean => words.subjects.length === 0;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The part of a statement that can name what is returned, without a clause about when (when …, …とき): before the first
|
|
197
|
+
* clause word where the verb comes first, after the last where the verb comes last.
|
|
198
|
+
*/
|
|
199
|
+
const returnedPart = (text: string, words: ReturnWords): string => {
|
|
200
|
+
const cuts = words.clauses.flatMap((word) =>
|
|
201
|
+
[...text.matchAll(wordPattern(word))].map((match) => ({ from: match.index, to: match.index + match[0].length })),
|
|
202
|
+
);
|
|
203
|
+
if (cuts.length === 0) return text;
|
|
204
|
+
return isVerbFirst(words) ? text.slice(0, Math.min(...cuts.map((cut) => cut.from))) : text.slice(Math.max(...cuts.map((cut) => cut.to)));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** The one kind a stretch's type words name. */
|
|
208
|
+
const kindIn = (text: string, returnWords: ReturnWords): Stated | undefined => {
|
|
209
|
+
const words = typeWordsIn(text, returnWords);
|
|
210
|
+
const named = new Set(words.map(([, kind]) => kind));
|
|
211
|
+
return named.size === 1 && words[0] !== undefined ? { kind: words[0][1], written: words[0][0] } : undefined;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** What a stretch of text says is returned: one readable code span, else the one kind its type words name. */
|
|
215
|
+
const statedIn = (text: string, words: ReturnWords): Stated | undefined => {
|
|
216
|
+
const trimmed = returnedPart(text, words).trim();
|
|
217
|
+
if (TYPE_TEXT.test(trimmed) && isReadableCode(trimmed)) return { code: trimmed, written: trimmed };
|
|
218
|
+
const spans = new Set([...trimmed.matchAll(/`([^`]+)`/gu)].map((match) => match[1] ?? "").filter(isReadableCode));
|
|
219
|
+
if (spans.size === 1) return { code: [...spans][0] ?? "", written: [...spans][0] ?? "" };
|
|
220
|
+
return spans.size === 0 ? kindIn(trimmed, words) : undefined;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const firstSentence = (text: string): string => text.split(SENTENCE_END)[0] ?? "";
|
|
224
|
+
|
|
225
|
+
const lineOf = (lines: readonly Line[], offset: number): Line | undefined =>
|
|
226
|
+
lines.find((line) => line.start <= offset && offset <= line.start + line.text.length);
|
|
227
|
+
|
|
228
|
+
/** The function each section's "Returns:" line is about: the one function its code blocks declare. */
|
|
229
|
+
const sectionFunctions = (lines: readonly Line[], signatures: readonly Signature[]): Map<number, string> => {
|
|
230
|
+
const bySection = new Map<number, Set<string>>();
|
|
231
|
+
signatures.forEach((signature) => {
|
|
232
|
+
const section = lineOf(lines, signature.offset)?.section ?? -1;
|
|
233
|
+
bySection.set(section, (bySection.get(section) ?? new Set<string>()).add(signature.fn));
|
|
234
|
+
});
|
|
235
|
+
return new Map([...bySection].flatMap(([section, names]): [number, string][] => (names.size === 1 ? [[section, [...names].join("")]] : [])));
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const labelPattern = (labels: readonly string[]): RegExp =>
|
|
239
|
+
new RegExp(String.raw`^[ \t]*(?:[-*+][ \t]+)?(?:\*\*|__)?(?:${labels.map(escaped).join("|")})(?:\*\*|__)?[ \t]*[::](?:\*\*|__)?`, "iu");
|
|
240
|
+
|
|
241
|
+
/** A "Returns:" line under a heading whose code blocks declare one function. */
|
|
242
|
+
const labelStatements = (lines: readonly Line[], functions: ReadonlyMap<number, string>, words: ReturnWords): Statement[] => {
|
|
243
|
+
if (words.labels.length === 0) return [];
|
|
244
|
+
const label = labelPattern(words.labels);
|
|
245
|
+
return lines.flatMap((line) => {
|
|
246
|
+
const head = line.fenced ? null : label.exec(line.text);
|
|
247
|
+
const fn = functions.get(line.section);
|
|
248
|
+
if (head === null || fn === undefined) return [];
|
|
249
|
+
const stated = statedIn(firstSentence(line.text.slice(head[0].length)), words);
|
|
250
|
+
return stated === undefined ? [] : [{ fn, stated, offset: line.start }];
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const cellsOf = (row: string): string[] => row.trim().replace(/^\|/u, "").replace(/\|$/u, "").split("|");
|
|
255
|
+
|
|
256
|
+
const cellText = (cell: string): string => cell.trim().replace(/^(?:\*\*|__)(.*)(?:\*\*|__)$/u, "$1");
|
|
257
|
+
|
|
258
|
+
/** The function a table row names in its first cell: `wrap`, wrap(), `measure(text)`. */
|
|
259
|
+
const rowFunction = (cell: string): string => {
|
|
260
|
+
const name = cellText(cell).replace(/`/gu, "").trim();
|
|
261
|
+
const open = name.indexOf("(");
|
|
262
|
+
return name.endsWith(")") && open > 0 ? name.slice(0, open).trim() : name;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/** A table with a return column (Returns, 戻り値) whose rows name functions in their first cell. */
|
|
266
|
+
const tableStatements = (lines: readonly Line[], known: ReadonlySet<string>, words: ReturnWords): Statement[] => {
|
|
267
|
+
const labels = new Set(words.labels.map((label) => label.toLowerCase()));
|
|
268
|
+
const state = { column: -1 };
|
|
269
|
+
return lines.flatMap((line, index) => {
|
|
270
|
+
if (line.fenced || !TABLE_ROW.test(line.text)) {
|
|
271
|
+
state.column = -1;
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
if (DELIMITER_ROW.test(line.text)) {
|
|
275
|
+
state.column = cellsOf(lines[index - 1]?.text ?? "").findIndex((cell) => labels.has(cellText(cell).toLowerCase()));
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
const cells = cellsOf(line.text);
|
|
279
|
+
const fn = rowFunction(cells[0] ?? "");
|
|
280
|
+
const stated = state.column > 0 && known.has(fn) ? statedIn(cells[state.column] ?? "", words) : undefined;
|
|
281
|
+
return stated === undefined ? [] : [{ fn, stated, offset: line.start }];
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/** `measure returns …` / 「measure は … 返します」: the function's name, an optional subject word, then the verb. */
|
|
286
|
+
const sentencePattern = (fn: string, words: ReturnWords): RegExp => {
|
|
287
|
+
const subject = words.subjects.length === 0 ? "" : String.raw`(?:(?:${words.subjects.map(escaped).join("|")})[、,]?)?`;
|
|
288
|
+
const verbs = words.verbs.map(escaped).join("|");
|
|
289
|
+
return new RegExp(String.raw`(?<![\w$.])\x60?${escaped(fn)}(?:\(\))?\x60?\s*${subject}([^。!?!?;]*?)(?:${verbs})(?![\p{L}\p{N}])`, "gu");
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/** A sentence whose subject is a function's name and whose verb says it returns something. */
|
|
293
|
+
const sentenceStatements = (lines: readonly Line[], known: ReadonlySet<string>, words: ReturnWords): Statement[] => {
|
|
294
|
+
if (words.verbs.length === 0) return [];
|
|
295
|
+
const prose = lines.filter((line) => !line.fenced && !HEADING.test(line.text) && !TABLE_ROW.test(line.text));
|
|
296
|
+
return [...known].flatMap((fn) =>
|
|
297
|
+
prose.flatMap((line) =>
|
|
298
|
+
[...line.text.matchAll(sentencePattern(fn, words))].flatMap((match) => {
|
|
299
|
+
const between = match[1] ?? "";
|
|
300
|
+
// Where the verb comes first, nothing stands between the name and the verb.
|
|
301
|
+
const verbFirst = isVerbFirst(words);
|
|
302
|
+
if (verbFirst && between.trim() !== "") return [];
|
|
303
|
+
const after = firstSentence(line.text.slice(match.index + match[0].length));
|
|
304
|
+
const stated = statedIn(verbFirst ? after : between, words);
|
|
305
|
+
return stated === undefined ? [] : [{ fn, stated, offset: line.start + match.index }];
|
|
306
|
+
}),
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const isSubset = (small: readonly string[], large: readonly string[]): boolean => small.every((member) => large.includes(member));
|
|
312
|
+
|
|
313
|
+
/** Whether a statement can be the signature's return type; true when either side is too vague to say otherwise. */
|
|
314
|
+
const agrees = (stated: Stated, returns: string): boolean => {
|
|
315
|
+
const members = typeMembers(returns);
|
|
316
|
+
if ("code" in stated) {
|
|
317
|
+
const said = typeMembers(stated.code);
|
|
318
|
+
return isSubset(said, members);
|
|
319
|
+
}
|
|
320
|
+
const kinds = members.filter((member) => !NOTHING_TYPES.has(member)).map(kindOfMember);
|
|
321
|
+
return kinds.includes(undefined) || kinds.includes(stated.kind);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const clashOf = (fn: string, stated: string, signature: Signature, offset: number): ReturnTypeClash => ({
|
|
325
|
+
fn,
|
|
326
|
+
stated,
|
|
327
|
+
signature: signature.returns,
|
|
328
|
+
offset,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
/** Two declarations with the same parameters and different return types; overloads (other parameters) are left alone. */
|
|
332
|
+
const signatureClashes = (signatures: readonly Signature[]): ReturnTypeClash[] =>
|
|
333
|
+
signatures.flatMap((later, index) => {
|
|
334
|
+
const earlier = signatures
|
|
335
|
+
.slice(0, index)
|
|
336
|
+
.find(
|
|
337
|
+
(signature) =>
|
|
338
|
+
signature.fn === later.fn && signature.params === later.params && typeMembers(signature.returns).join("|") !== typeMembers(later.returns).join("|"),
|
|
339
|
+
);
|
|
340
|
+
return earlier === undefined ? [] : [clashOf(later.fn, later.returns, earlier, later.offset)];
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
/** Every place on the page that states a function's return type differently from all of the function's signatures. */
|
|
344
|
+
export const returnTypeClashes = (source: string, words: ReturnWords): ReturnTypeClash[] => {
|
|
345
|
+
const lines = linesOf(source);
|
|
346
|
+
const signatures = signaturesOf(lines);
|
|
347
|
+
const known = new Set(signatures.map((signature) => signature.fn));
|
|
348
|
+
const statements = [
|
|
349
|
+
...labelStatements(lines, sectionFunctions(lines, signatures), words),
|
|
350
|
+
...tableStatements(lines, known, words),
|
|
351
|
+
...sentenceStatements(lines, known, words),
|
|
352
|
+
];
|
|
353
|
+
const statementClashes = statements.flatMap((statement) => {
|
|
354
|
+
const own = signatures.filter((signature) => signature.fn === statement.fn);
|
|
355
|
+
const first = own[0];
|
|
356
|
+
return first === undefined || own.some((signature) => agrees(statement.stated, signature.returns))
|
|
357
|
+
? []
|
|
358
|
+
: [clashOf(statement.fn, statement.stated.written, first, statement.offset)];
|
|
359
|
+
});
|
|
360
|
+
return [...signatureClashes(signatures), ...statementClashes].toSorted((left, right) => left.offset - right.offset);
|
|
361
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { StructureIssue } from "./issues.ts";
|
|
2
|
+
import { lineNumberAt, linesOf, type Line } from "./lines.ts";
|
|
3
|
+
import { runsOf } from "./runs.ts";
|
|
4
|
+
import type { Stop } from "./time-order.ts";
|
|
5
|
+
import {
|
|
6
|
+
crossesZones,
|
|
7
|
+
dayShiftOf,
|
|
8
|
+
markBefore,
|
|
9
|
+
markSoonAfter,
|
|
10
|
+
wordOffsets,
|
|
11
|
+
zonesIn,
|
|
12
|
+
type DateSpan,
|
|
13
|
+
type Mark,
|
|
14
|
+
type TimeSpan,
|
|
15
|
+
type TimeWords,
|
|
16
|
+
} from "./time-marks.ts";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 乗り継ぎの間が、文書に書いた移動の時間より短い所。移動の時間は、移動の語(移動、徒歩、地下鉄、by Metro、coach。語彙表
|
|
20
|
+
* travel-time-word)と同じ文に書いた長さ(約10分、about 40 minutes)だけを使う。実際にかかる時間は推し量らない。
|
|
21
|
+
* 移動の始まりと終わりの時刻は二通りに読む。
|
|
22
|
+
* - 移動そのものを書いた予定(「10:00 京都駅から貸切バスで清水寺へ(約30分)」)は、その時刻に発ち、同じ日の次の予定までに着く。
|
|
23
|
+
* - 予定に添えた移動の時間(「09:55 博多駅で合流(福岡空港から地下鉄で約10分)」)や、時刻の無い移動の文に続けて書いた時刻は、
|
|
24
|
+
* すぐ前の着の時刻(「福岡 09:50着」、表の到着の列)から移動して、その時刻に間に合う。
|
|
25
|
+
* 日付が違う、翌日の印がある、時刻が戻る(夜をまたぐ)、時間帯の印が違う所は比べない。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** 移動の時間として読む長さ。minutes は分。 */
|
|
29
|
+
export type TravelLength = { readonly start: number; readonly end: number; readonly minutes: number };
|
|
30
|
+
|
|
31
|
+
/** cues は移動を言う語、bounds は長さを上限として言う語(以内、within)。上限の長さは、それより早く着けるので使わない。 */
|
|
32
|
+
export type TravelWords = { readonly cues: readonly Mark[]; readonly bounds: readonly Mark[] };
|
|
33
|
+
|
|
34
|
+
export type ConnectionInputs = {
|
|
35
|
+
readonly source: string;
|
|
36
|
+
readonly times: readonly TimeSpan[];
|
|
37
|
+
readonly dates: readonly DateSpan[];
|
|
38
|
+
readonly words: TimeWords;
|
|
39
|
+
readonly travel: TravelWords;
|
|
40
|
+
readonly lengths: readonly TravelLength[];
|
|
41
|
+
readonly arrivals: ReadonlySet<number>;
|
|
42
|
+
readonly days: readonly (readonly Stop[])[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const SECONDS_PER_MINUTE = 60;
|
|
46
|
+
|
|
47
|
+
/** 文の切れ目。leg-times.ts と同じ。 */
|
|
48
|
+
const BREAK = /[。.;;!?!?]|\.\s/gu;
|
|
49
|
+
/** 括弧書き。中に括弧を入れ子にしたものは、内側から一つずつ外す。 */
|
|
50
|
+
const BRACKETED = /[((][^()()]*[))]/gu;
|
|
51
|
+
/** 「within 10 minutes」の within と長さの間に挟んでよい字の数。 */
|
|
52
|
+
const BOUND_GAP = 1;
|
|
53
|
+
|
|
54
|
+
/** 発ってから着くまでの間が、書いた移動の時間より短いか。間が負なら(時刻が戻る)、夜をまたいだのか書き間違いか決められないので言わない。 */
|
|
55
|
+
export const isTooShort = (gapSeconds: number, travelMinutes: number): boolean => gapSeconds >= 0 && gapSeconds < travelMinutes * SECONDS_PER_MINUTE;
|
|
56
|
+
|
|
57
|
+
/** 二つの時刻に書いた日付が比べられるか。両方に無いか、同じ書き方で同じ日付なら同じ日。 */
|
|
58
|
+
export const isSameDate = (left: string | undefined, right: string | undefined): boolean => left === right;
|
|
59
|
+
|
|
60
|
+
/** 時間帯の印を見て、二つの行の時刻をそのまま比べられるか。どちらかが時間帯をまたぐか、印が違えば比べない。 */
|
|
61
|
+
export const isSameZone = (left: string, right: string, words: TimeWords): boolean => {
|
|
62
|
+
const [leftZones, rightZones] = [zonesIn(left, words), zonesIn(right, words)];
|
|
63
|
+
if (crossesZones(leftZones) || crossesZones(rightZones)) return false;
|
|
64
|
+
return (leftZones[0]?.text ?? "") === (rightZones[0]?.text ?? "");
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** 括弧書きを外した残り。 */
|
|
68
|
+
export const outsideBrackets = (text: string): string => {
|
|
69
|
+
const stripped = text.replace(BRACKETED, " ");
|
|
70
|
+
return stripped === text ? text : outsideBrackets(stripped);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** text に移動の語が一語として書かれているか。大文字と小文字は区別しない。 */
|
|
74
|
+
export const mentionsTravel = (text: string, cues: readonly Mark[]): boolean => {
|
|
75
|
+
const lower = text.toLowerCase();
|
|
76
|
+
return cues.some((cue) => wordOffsets(lower, cue.pattern.toLowerCase()).length > 0);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** 行の中で、at から to までを含む文の範囲(行の頭からの位置)。 */
|
|
80
|
+
export const sentenceAround = (text: string, at: number, to: number): { start: number; end: number } => {
|
|
81
|
+
const breaks = [...text.matchAll(BREAK)].map((match) => ({ start: match.index, end: match.index + match[0].length }));
|
|
82
|
+
const start = breaks.filter((cut) => cut.end <= at).at(-1)?.end ?? 0;
|
|
83
|
+
const end = breaks.find((cut) => cut.start >= to)?.start ?? text.length;
|
|
84
|
+
return { start, end };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const lineAt = (lines: readonly Line[], offset: number): Line | undefined => {
|
|
88
|
+
const number = lineNumberAt(lines, offset);
|
|
89
|
+
return number === undefined ? undefined : lines[number - 1];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** 長さが上限として書かれている(10分以内、within 10 minutes)か。 */
|
|
93
|
+
const isBound = (inputs: ConnectionInputs, line: Line, length: TravelLength): boolean => {
|
|
94
|
+
const head = inputs.source.slice(line.start, length.start).toLowerCase();
|
|
95
|
+
return markSoonAfter(inputs.source, length.end, inputs.travel.bounds, []) !== undefined || markBefore(head, inputs.travel.bounds, BOUND_GAP) !== undefined;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** 移動の時間を書いた長さか。同じ文に移動の語があり、上限として書いたものではない。 */
|
|
99
|
+
const isTravel = (inputs: ConnectionInputs, line: Line, length: TravelLength): boolean => {
|
|
100
|
+
const sentence = sentenceAround(line.text, length.start - line.start, length.end - line.start);
|
|
101
|
+
return mentionsTravel(line.text.slice(sentence.start, sentence.end), inputs.travel.cues) && !isBound(inputs, line, length);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** 時刻の前の、同じ行に書いた最後の日付。 */
|
|
105
|
+
const dateOf = (inputs: ConnectionInputs, line: Line, time: TimeSpan): string | undefined =>
|
|
106
|
+
inputs.dates.filter((date) => date.start >= line.start && date.end <= time.start).at(-1)?.value;
|
|
107
|
+
|
|
108
|
+
const shiftOf = (inputs: ConnectionInputs, line: Line, time: TimeSpan): number =>
|
|
109
|
+
dayShiftOf(inputs.source.slice(line.start, time.start), inputs.source, time.end, inputs.words);
|
|
110
|
+
|
|
111
|
+
const written = (inputs: ConnectionInputs, span: { start: number; end: number }): string => inputs.source.slice(span.start, span.end);
|
|
112
|
+
|
|
113
|
+
type Leg = { readonly from: TimeSpan; readonly to: TimeSpan };
|
|
114
|
+
|
|
115
|
+
const issueOf = (inputs: ConnectionInputs, leg: Leg, length: TravelLength): StructureIssue => ({
|
|
116
|
+
offset: leg.to.start,
|
|
117
|
+
values: {
|
|
118
|
+
time: written(inputs, leg.to),
|
|
119
|
+
from: written(inputs, leg.from),
|
|
120
|
+
gap: Math.round((leg.to.seconds - leg.from.seconds) / SECONDS_PER_MINUTE),
|
|
121
|
+
travel: written(inputs, length),
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const dayOf = (inputs: ConnectionInputs, line: Line): readonly Stop[] | undefined =>
|
|
126
|
+
inputs.days.find((stops) => stops.some((stop) => stop.offset >= line.start && stop.offset <= line.start + line.text.length));
|
|
127
|
+
|
|
128
|
+
const OPENERS = new Set(["(", "("]);
|
|
129
|
+
const CLOSERS = new Set([")", ")"]);
|
|
130
|
+
|
|
131
|
+
/** at から to までを囲む一番内側の括弧の中身の範囲(行の頭からの位置)。囲む括弧が無ければ undefined。 */
|
|
132
|
+
export const bracketAround = (text: string, at: number, to: number): { start: number; end: number } | undefined => {
|
|
133
|
+
const open = text
|
|
134
|
+
.slice(0, at)
|
|
135
|
+
.split("")
|
|
136
|
+
.reduceRight<{ depth: number; found: number | undefined }>(
|
|
137
|
+
(scan, char, index) => {
|
|
138
|
+
if (scan.found !== undefined) return scan;
|
|
139
|
+
if (CLOSERS.has(char)) return { ...scan, depth: scan.depth + 1 };
|
|
140
|
+
if (!OPENERS.has(char)) return scan;
|
|
141
|
+
return scan.depth === 0 ? { ...scan, found: index } : { ...scan, depth: scan.depth - 1 };
|
|
142
|
+
},
|
|
143
|
+
{ depth: 0, found: undefined },
|
|
144
|
+
).found;
|
|
145
|
+
if (open === undefined) return undefined;
|
|
146
|
+
const close = text
|
|
147
|
+
.slice(to)
|
|
148
|
+
.split("")
|
|
149
|
+
.findIndex((char) => CLOSERS.has(char));
|
|
150
|
+
return close === -1 ? undefined : { start: open + 1, end: to + close };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 移動そのものを書いた予定か。頭に時刻を書いた予定で、移動の語が括弧の外(行の本文)にある。長さを囲む括弧の中に移動の語が
|
|
155
|
+
* あれば(「(福岡空港から地下鉄で約10分)」)、その括弧はこの予定までの移動を添えたもので、予定は移動ではない。
|
|
156
|
+
*/
|
|
157
|
+
const isMoveLine = (inputs: ConnectionInputs, line: Line, length: TravelLength): boolean => {
|
|
158
|
+
const aside = bracketAround(line.text, length.start - line.start, length.end - line.start);
|
|
159
|
+
if (aside !== undefined && mentionsTravel(line.text.slice(aside.start, aside.end), inputs.travel.cues)) return false;
|
|
160
|
+
return dayOf(inputs, line) !== undefined && mentionsTravel(outsideBrackets(line.text), inputs.travel.cues);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** 移動そのものを書いた予定の時刻と、同じ日の次の予定の時刻。 */
|
|
164
|
+
const scheduledLeg = (inputs: ConnectionInputs, line: Line): Leg | undefined => {
|
|
165
|
+
const day = dayOf(inputs, line);
|
|
166
|
+
const index = day?.findIndex((stop) => stop.offset >= line.start) ?? -1;
|
|
167
|
+
const [start, next] = [day?.[index], day?.[index + 1]];
|
|
168
|
+
if (start === undefined || next === undefined || start.shift !== 0 || next.shift !== 0 || start.zone.startsWith("local:")) return undefined;
|
|
169
|
+
const timeAt = (offset: number): TimeSpan | undefined => inputs.times.find((time) => time.start === offset);
|
|
170
|
+
const [from, to] = [timeAt(start.offset), timeAt(next.offset)];
|
|
171
|
+
return from === undefined || to === undefined ? undefined : { from, to };
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/** 時刻と同じ並び(続いた箇条書きや表の行)か同じ行にある、その時刻より前の時刻。 */
|
|
175
|
+
const timesBeside = (inputs: ConnectionInputs, line: Line, to: TimeSpan): TimeSpan[] => {
|
|
176
|
+
const run = runsOf(inputs.source).find((lines) => lines.some((row) => row.start === line.start));
|
|
177
|
+
const inScope = (time: TimeSpan): boolean =>
|
|
178
|
+
(time.start >= line.start && time.end <= to.start) || (run?.some((row) => time.start >= row.start && time.end <= row.end) ?? false);
|
|
179
|
+
return inputs.times.filter((time) => time.end <= to.start && inScope(time));
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** 日付を書いた時刻なら、文書のそれより前の同じ日付の時刻。日付が無ければ、同じ並びか同じ行の、日付の無い前の時刻。 */
|
|
183
|
+
const timesBefore = (inputs: ConnectionInputs, lines: readonly Line[], line: Line, to: TimeSpan): TimeSpan[] => {
|
|
184
|
+
const date = dateOf(inputs, line, to);
|
|
185
|
+
const candidates = date === undefined ? timesBeside(inputs, line, to) : inputs.times.filter((time) => time.end <= to.start);
|
|
186
|
+
return candidates.filter((time) => {
|
|
187
|
+
const own = lineAt(lines, time.start);
|
|
188
|
+
return own !== undefined && isSameDate(dateOf(inputs, own, time), date);
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 移動の時間を添えた時刻。予定の行ならその頭の時刻、文なら移動の文の中かその後ろの、最初の着でない時刻。
|
|
194
|
+
* 移動の文より前の時刻は、移動の前の予定で、移動して着く先ではない。
|
|
195
|
+
*/
|
|
196
|
+
const arrivalTarget = (inputs: ConnectionInputs, line: Line, length: TravelLength): TimeSpan | undefined => {
|
|
197
|
+
const stop = dayOf(inputs, line)?.find((candidate) => candidate.offset >= line.start && candidate.offset <= line.start + line.text.length);
|
|
198
|
+
if (stop !== undefined) return inputs.times.find((time) => time.start === stop.offset);
|
|
199
|
+
const sentenceStart = line.start + sentenceAround(line.text, length.start - line.start, length.end - line.start).start;
|
|
200
|
+
return inputs.times.find((time) => time.start >= sentenceStart && time.end <= line.start + line.text.length && !inputs.arrivals.has(time.start));
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/** 移動の時間を添えた時刻と、そのすぐ前の着の時刻。すぐ前の時刻が着でなければ、どこから移動したのか分からない。 */
|
|
204
|
+
const arrivingLeg = (inputs: ConnectionInputs, lines: readonly Line[], line: Line, length: TravelLength): Leg | undefined => {
|
|
205
|
+
const to = arrivalTarget(inputs, line, length);
|
|
206
|
+
if (to === undefined) return undefined;
|
|
207
|
+
const from = timesBefore(inputs, lines, line, to).at(-1);
|
|
208
|
+
const fromLine = from === undefined ? undefined : lineAt(lines, from.start);
|
|
209
|
+
if (from === undefined || fromLine === undefined || !inputs.arrivals.has(from.start)) return undefined;
|
|
210
|
+
if (shiftOf(inputs, fromLine, from) !== 0 || shiftOf(inputs, line, to) !== 0 || !isSameZone(fromLine.text, line.text, inputs.words)) return undefined;
|
|
211
|
+
return { from, to };
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** 移動の時間を書いた長さごとに、間の足りない乗り継ぎ。同じ時刻は一度だけ言う。 */
|
|
215
|
+
export const shortConnections = (inputs: ConnectionInputs): StructureIssue[] => {
|
|
216
|
+
const lines = linesOf(inputs.source);
|
|
217
|
+
const issues = inputs.lengths.flatMap((length) => {
|
|
218
|
+
const line = lineAt(lines, length.start);
|
|
219
|
+
if (line === undefined || !isTravel(inputs, line, length)) return [];
|
|
220
|
+
const leg = isMoveLine(inputs, line, length) ? scheduledLeg(inputs, line) : arrivingLeg(inputs, lines, line, length);
|
|
221
|
+
return leg !== undefined && isTooShort(leg.to.seconds - leg.from.seconds, length.minutes) ? [issueOf(inputs, leg, length)] : [];
|
|
222
|
+
});
|
|
223
|
+
return issues
|
|
224
|
+
.filter((issue, index) => issues.findIndex((other) => other.offset === issue.offset) === index)
|
|
225
|
+
.toSorted((left, right) => left.offset - right.offset);
|
|
226
|
+
};
|