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,50 @@
|
|
|
1
|
+
// Party names listed in the reverse of the document's usual order (乙及び甲 where the contract otherwise writes 甲及び乙). The
|
|
2
|
+
// names are the parties the document defines and the language package's standard ones (party-order-label); the words that list
|
|
3
|
+
// them are its party-order-joiner list. A direction (乙から甲へ, 甲が乙に) is no listing and is not read.
|
|
4
|
+
import { definedTerms } from "../structure/definition-use.ts";
|
|
5
|
+
import { partiesOf } from "../structure/party-role.ts";
|
|
6
|
+
import { partyListings, reversedListings, type ListingWords } from "../structure/party-order.ts";
|
|
7
|
+
import { quoteAt } from "./structure-tree.ts";
|
|
8
|
+
import type { Detector, Finding, ProseDocument } from "../plugin.ts";
|
|
9
|
+
|
|
10
|
+
/** The usual order is the one written at least this many times as often as its reverse: a tie or near-tie is no convention. */
|
|
11
|
+
const MIN_USUAL_RATIO = 2;
|
|
12
|
+
const SERIES_GROUP = "series";
|
|
13
|
+
|
|
14
|
+
const patternsOf = (doc: ProseDocument, lexicon: string): string[] => (doc.lexicons[lexicon] ?? []).map((entry) => entry.pattern);
|
|
15
|
+
|
|
16
|
+
const listingWords = (doc: ProseDocument): ListingWords => {
|
|
17
|
+
const standard = patternsOf(doc, "party-order-label");
|
|
18
|
+
const terms = doc.structure === undefined ? [] : definedTerms(doc.structure);
|
|
19
|
+
const parties = partiesOf(doc.source, terms, {
|
|
20
|
+
roles: patternsOf(doc, "party-role"),
|
|
21
|
+
shortNames: patternsOf(doc, "party-short-name"),
|
|
22
|
+
companyForms: patternsOf(doc, "company-form"),
|
|
23
|
+
});
|
|
24
|
+
const joiners = doc.lexicons["party-order-joiner"] ?? [];
|
|
25
|
+
return {
|
|
26
|
+
labels: [...new Set([...standard, ...parties.map((party) => party.term)])],
|
|
27
|
+
bareLabels: standard,
|
|
28
|
+
joiners: joiners.filter((entry) => entry.group !== SERIES_GROUP).map((entry) => entry.pattern),
|
|
29
|
+
seriesMarks: joiners.filter((entry) => entry.group === SERIES_GROUP).map((entry) => entry.pattern),
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const partyOrder: Detector = (doc): Finding[] => {
|
|
34
|
+
const texts = doc.sentences.map((sentence) => ({ start: sentence.span.start, text: sentence.text }));
|
|
35
|
+
const textOf = (listing: { readonly start: number; readonly end: number }): string => doc.source.slice(listing.start, listing.end);
|
|
36
|
+
return reversedListings(partyListings(texts, listingWords(doc)), MIN_USUAL_RATIO).map((slip) => ({
|
|
37
|
+
rule: "party-order",
|
|
38
|
+
severity: "info",
|
|
39
|
+
line: 0,
|
|
40
|
+
column: 0,
|
|
41
|
+
quote: quoteAt(doc.source, slip.listing.start),
|
|
42
|
+
values: {
|
|
43
|
+
written: textOf(slip.listing),
|
|
44
|
+
usual: textOf(slip.usual),
|
|
45
|
+
usualCount: slip.usualCount,
|
|
46
|
+
reverseCount: slip.reverseCount,
|
|
47
|
+
offset: slip.listing.start,
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// required-listed-as-preferred: the same skill under a required heading or label and again under a preferred one
|
|
2
|
+
// (structure/requirement-lists.ts). The headings and labels are requirement-heading's; the length of experience is read
|
|
3
|
+
// with requirement-length-unit, requirement-length-word and number-word; requirement-item-ending ends an item.
|
|
4
|
+
import type { Detector, Finding, ProseDocument, Span } from "../plugin.ts";
|
|
5
|
+
import { requiredListedAsPreferred, type RequirementBlock, type RequirementWords, type ValuedWord } from "../structure/requirement-lists.ts";
|
|
6
|
+
import { quoteAround } from "./quote-around.ts";
|
|
7
|
+
|
|
8
|
+
const patternsOf = (doc: ProseDocument, lexicon: string, group?: string): string[] =>
|
|
9
|
+
(doc.lexicons[lexicon] ?? []).filter((entry) => group === undefined || entry.group === group).map((entry) => entry.pattern);
|
|
10
|
+
|
|
11
|
+
const valuedOf = (doc: ProseDocument, lexicon: string, group?: string): ValuedWord[] =>
|
|
12
|
+
(doc.lexicons[lexicon] ?? []).flatMap((entry) =>
|
|
13
|
+
entry.weight === undefined || (group !== undefined && entry.group !== group) ? [] : [{ word: entry.pattern, value: entry.weight }],
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const wordsOf = (doc: ProseDocument): RequirementWords => ({
|
|
17
|
+
required: patternsOf(doc, "requirement-heading", "required"),
|
|
18
|
+
preferred: patternsOf(doc, "requirement-heading", "preferred"),
|
|
19
|
+
units: valuedOf(doc, "requirement-length-unit"),
|
|
20
|
+
before: patternsOf(doc, "requirement-length-word", "before"),
|
|
21
|
+
after: patternsOf(doc, "requirement-length-word", "after"),
|
|
22
|
+
numbers: valuedOf(doc, "number-word", "digit"),
|
|
23
|
+
endings: patternsOf(doc, "requirement-item-ending"),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const LIST_MARKER = /^\s*(?:[-*+・•]|\d+[.).)])\s+(?:\[[ xX]\]\s+)?/u;
|
|
27
|
+
|
|
28
|
+
const headingsOf = (doc: ProseDocument): RequirementBlock[] =>
|
|
29
|
+
doc.sections
|
|
30
|
+
.filter((section) => section.depth > 0)
|
|
31
|
+
.map((section) => ({ kind: "heading", depth: section.depth, text: section.heading, offset: section.span.start }));
|
|
32
|
+
|
|
33
|
+
/** An item's own lines, joined, without its marker: the lines it wraps onto, up to a nested item. */
|
|
34
|
+
const itemText = (written: string): string => {
|
|
35
|
+
const [first = "", ...rest] = written.split("\n");
|
|
36
|
+
const nested = rest.findIndex((line) => LIST_MARKER.test(line));
|
|
37
|
+
return [first.replace(LIST_MARKER, ""), ...(nested === -1 ? rest : rest.slice(0, nested))].map((line) => line.trim()).join(" ");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const itemsOf = (doc: ProseDocument): RequirementBlock[] =>
|
|
41
|
+
doc.lists.flatMap((list) => list.itemSpans.map((span) => ({ kind: "item", text: itemText(doc.source.slice(span.start, span.end)), offset: span.start })));
|
|
42
|
+
|
|
43
|
+
const inside = (offset: number, spans: readonly Span[]): boolean => spans.some((span) => offset >= span.start && offset < span.end);
|
|
44
|
+
|
|
45
|
+
/** The lines outside lists, with code, headings and emphasis marks blanked out: the places a label (必須:) stands alone. */
|
|
46
|
+
const linesOf = (doc: ProseDocument): RequirementBlock[] => {
|
|
47
|
+
const prose = doc.prose ?? doc.source;
|
|
48
|
+
const starts = [0, ...[...prose.matchAll(/\n/gu)].map((match) => match.index + 1)];
|
|
49
|
+
return starts.flatMap((start) => {
|
|
50
|
+
const line = prose.slice(start, prose.indexOf("\n", start) === -1 ? prose.length : prose.indexOf("\n", start));
|
|
51
|
+
return line.trim() === "" || inside(start, doc.listSpans) ? [] : [{ kind: "line", text: line, offset: start }];
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const requirementLists: Detector = (doc): Finding[] => {
|
|
56
|
+
const blocks = [...headingsOf(doc), ...linesOf(doc), ...itemsOf(doc)].toSorted((left, right) => left.offset - right.offset);
|
|
57
|
+
return requiredListedAsPreferred(blocks, wordsOf(doc)).map((issue) => ({
|
|
58
|
+
rule: "required-listed-as-preferred",
|
|
59
|
+
severity: "warning",
|
|
60
|
+
line: 0,
|
|
61
|
+
column: 0,
|
|
62
|
+
quote: quoteAround(doc.source, issue.offset),
|
|
63
|
+
values: { ...issue.values, offset: issue.offset },
|
|
64
|
+
}));
|
|
65
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// return-type-mismatch: one function's return type stated two ways on a page, the signature against a "Returns:" line, a
|
|
2
|
+
// table's return column or a sentence (return-type.ts). The words come from the return-label, return-verb, return-subject,
|
|
3
|
+
// return-type-word, return-clause and return-item lexicons.
|
|
4
|
+
import { quoteAt } from "./structure-tree.ts";
|
|
5
|
+
import { returnTypeClashes, type ReturnWords } from "../return-type.ts";
|
|
6
|
+
import type { Detector, Finding, Lexicon } from "../plugin.ts";
|
|
7
|
+
|
|
8
|
+
const patterns = (lexicon: Lexicon | undefined): string[] => (lexicon ?? []).map((entry) => entry.pattern);
|
|
9
|
+
|
|
10
|
+
const kindsOf = (lexicon: Lexicon | undefined): Map<string, string> =>
|
|
11
|
+
new Map((lexicon ?? []).flatMap((entry): [string, string][] => (entry.group === undefined ? [] : [[entry.pattern, entry.group]])));
|
|
12
|
+
|
|
13
|
+
const wordsOf = (lexicons: Readonly<Record<string, Lexicon>>): ReturnWords => ({
|
|
14
|
+
labels: patterns(lexicons["return-label"]),
|
|
15
|
+
verbs: patterns(lexicons["return-verb"]),
|
|
16
|
+
subjects: patterns(lexicons["return-subject"]),
|
|
17
|
+
kinds: kindsOf(lexicons["return-type-word"]),
|
|
18
|
+
clauses: patterns(lexicons["return-clause"]),
|
|
19
|
+
items: patterns(lexicons["return-item"]),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const returnType: Detector = (doc): Finding[] =>
|
|
23
|
+
returnTypeClashes(doc.source, wordsOf(doc.lexicons)).map((clash) => ({
|
|
24
|
+
rule: "return-type-mismatch",
|
|
25
|
+
severity: "warning",
|
|
26
|
+
line: 0,
|
|
27
|
+
column: 0,
|
|
28
|
+
quote: quoteAt(doc.source, clash.offset),
|
|
29
|
+
values: { function: clash.fn, stated: clash.stated, signature: clash.signature, offset: clash.offset },
|
|
30
|
+
}));
|
|
@@ -9,6 +9,7 @@ import { totalMismatches, type Amount } from "../structure/total.ts";
|
|
|
9
9
|
import { proseTotalMismatches } from "../structure/prose-total.ts";
|
|
10
10
|
import { countedAmounts, type SummedCounter } from "../structure/counted-amounts.ts";
|
|
11
11
|
import { rangeFrameOf, reversedRanges, type DatedSpan, type RangeWords } from "../structure/date-range.ts";
|
|
12
|
+
import { afterLabel } from "../structure/stated-period.ts";
|
|
12
13
|
import { percentSumMismatches, type ShareWords } from "../structure/percent-sum.ts";
|
|
13
14
|
import { proseShareMismatches } from "../structure/percent-sum-prose.ts";
|
|
14
15
|
import { isQuotedAlone } from "../quoted-span.ts";
|
|
@@ -227,11 +228,21 @@ const rangeWordsOf = (doc: ProseDocument): RangeWords => ({
|
|
|
227
228
|
weekdays: (doc.lexicons["weekday"] ?? []).map((entry) => entry.pattern),
|
|
228
229
|
});
|
|
229
230
|
|
|
231
|
+
/** その位置が、期間の語(period-label)で始まる行の、語より後ろにあるか。 */
|
|
232
|
+
const labelledAt =
|
|
233
|
+
(doc: ProseDocument) =>
|
|
234
|
+
(offset: number): boolean => {
|
|
235
|
+
const lineStart = doc.source.lastIndexOf("\n", offset - 1) + 1;
|
|
236
|
+
const lineEnd = doc.source.indexOf("\n", offset);
|
|
237
|
+
const after = afterLabel(doc.source.slice(lineStart, lineEnd === -1 ? doc.source.length : lineEnd), patternsOf(doc, "period-label"));
|
|
238
|
+
return after !== undefined && offset >= lineStart + after;
|
|
239
|
+
};
|
|
240
|
+
|
|
230
241
|
/** 期間の終わりが始まりより前。範囲の記号と語は言語パッケージの語彙表(range-connector、range-opener、range-closer)から取る。 */
|
|
231
242
|
export const dateRangeReversed: Detector = (doc): Finding[] => {
|
|
232
243
|
if (doc.structure === undefined) return [];
|
|
233
244
|
const dates = datedSpans(doc.structure);
|
|
234
|
-
return reversedRanges(doc.source, dates, rangeWordsOf(doc))
|
|
245
|
+
return reversedRanges(doc.source, dates, rangeWordsOf(doc), labelledAt(doc))
|
|
235
246
|
.filter((issue) => {
|
|
236
247
|
const start = dateAt(dates, issue.offset);
|
|
237
248
|
return !quotedDates(doc, dates[start], dates[start + 1]);
|
|
@@ -3,11 +3,13 @@ import type { StructureIssue } from "../structure/issues.ts";
|
|
|
3
3
|
import { inDocumentOrder } from "../structure/issues.ts";
|
|
4
4
|
import { clockTimes } from "../compare/clock-time.ts";
|
|
5
5
|
import { secondsOf, type DateSpan, type Mark, type TimeSpan, type TimeWords } from "../structure/time-marks.ts";
|
|
6
|
-
import { timeOrderBreaks } from "../structure/time-order.ts";
|
|
7
|
-
import { arrivalsBeforeDeparture } from "../structure/leg-times.ts";
|
|
6
|
+
import { scheduleDays, timeOrderBreaks } from "../structure/time-order.ts";
|
|
7
|
+
import { arrivalOffsets, arrivalsBeforeDeparture } from "../structure/leg-times.ts";
|
|
8
|
+
import { shortConnections, type TravelLength, type TravelWords } from "../structure/connection-times.ts";
|
|
9
|
+
import { timeLengths } from "../derived/time-lengths.ts";
|
|
8
10
|
import { quoteAt } from "./structure-tree.ts";
|
|
9
11
|
|
|
10
|
-
// 一日の予定の時刻の順番(time-order
|
|
12
|
+
// 一日の予定の時刻の順番(time-order)、発より前の着(arrival-before-departure)、移動の時間より短い乗り継ぎ(connection-time-short)。時刻は compare/clock-time.ts が読み、
|
|
11
13
|
// 発着・翌日・時間帯の語は言語パッケージの語彙表(leg-mark、leg-column、day-shift-mark、time-zone、local-time-mark)から取る。
|
|
12
14
|
|
|
13
15
|
const marksOf = (doc: ProseDocument, lexicon: string): Mark[] =>
|
|
@@ -29,13 +31,13 @@ const datesOf = (doc: ProseDocument): DateSpan[] =>
|
|
|
29
31
|
|
|
30
32
|
const timesOf = (text: string): TimeSpan[] => clockTimes(text).map((time) => ({ start: time.start, end: time.end, seconds: secondsOf(time.key) }));
|
|
31
33
|
|
|
32
|
-
type Finder = (source: string, times: readonly TimeSpan[], dates: readonly DateSpan[], words: TimeWords) => StructureIssue[];
|
|
34
|
+
type Finder = (source: string, times: readonly TimeSpan[], dates: readonly DateSpan[], words: TimeWords, doc: ProseDocument) => StructureIssue[];
|
|
33
35
|
|
|
34
36
|
/** 表と箇条書きを行のまま読むので、原文を渡す(doc.prose は表を覆う)。 */
|
|
35
37
|
const detectorOf =
|
|
36
38
|
(rule: string, find: Finder): Detector =>
|
|
37
39
|
(doc): Finding[] =>
|
|
38
|
-
find(doc.source, timesOf(doc.source), datesOf(doc), timeWordsOf(doc)).map((issue) => ({
|
|
40
|
+
find(doc.source, timesOf(doc.source), datesOf(doc), timeWordsOf(doc), doc).map((issue) => ({
|
|
39
41
|
rule,
|
|
40
42
|
severity: "warning",
|
|
41
43
|
line: 0,
|
|
@@ -47,3 +49,36 @@ const detectorOf =
|
|
|
47
49
|
|
|
48
50
|
export const timeOrder: Detector = detectorOf("time-order", timeOrderBreaks);
|
|
49
51
|
export const arrivalBeforeDeparture: Detector = detectorOf("arrival-before-departure", arrivalsBeforeDeparture);
|
|
52
|
+
|
|
53
|
+
const SECONDS_PER_HOUR = 3600;
|
|
54
|
+
const SECONDS_PER_MINUTE = 60;
|
|
55
|
+
|
|
56
|
+
const patternsOf = (doc: ProseDocument, lexicon: string, weight?: number): string[] =>
|
|
57
|
+
(doc.lexicons[lexicon] ?? []).filter((entry) => weight === undefined || entry.weight === weight).map((entry) => entry.pattern);
|
|
58
|
+
|
|
59
|
+
/** 時間と分の長さ(約10分、about 40 minutes)。単位は unit-time の重さ(秒)で時間と分に分ける。 */
|
|
60
|
+
const travelLengthsOf = (doc: ProseDocument): TravelLength[] =>
|
|
61
|
+
timeLengths(doc.source, {
|
|
62
|
+
hourUnits: patternsOf(doc, "unit-time", SECONDS_PER_HOUR),
|
|
63
|
+
minuteUnits: patternsOf(doc, "unit-time", SECONDS_PER_MINUTE),
|
|
64
|
+
halves: patternsOf(doc, "length-half"),
|
|
65
|
+
numberWords: patternsOf(doc, "count-number"),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const travelWordsOf = (doc: ProseDocument): TravelWords => {
|
|
69
|
+
const marks = marksOf(doc, "travel-time-word");
|
|
70
|
+
return { cues: marks.filter((mark) => mark.group === "travel"), bounds: marks.filter((mark) => mark.group === "bound") };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const connectionTimeShort: Detector = detectorOf("connection-time-short", (source, times, dates, words, doc) =>
|
|
74
|
+
shortConnections({
|
|
75
|
+
source,
|
|
76
|
+
times,
|
|
77
|
+
dates,
|
|
78
|
+
words,
|
|
79
|
+
travel: travelWordsOf(doc),
|
|
80
|
+
lengths: travelLengthsOf(doc),
|
|
81
|
+
arrivals: arrivalOffsets(source, times, dates, words),
|
|
82
|
+
days: scheduleDays(source, times, dates, words),
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { definedTerms } from "../structure/definition-use.ts";
|
|
7
7
|
import { prefixGroupsOf, prefixVariants } from "../structure/term-prefix.ts";
|
|
8
8
|
import { quoteAt } from "./structure-tree.ts";
|
|
9
|
+
import { capitalisedTerms, namedWords, readableText, setApartSpans, type Defined } from "./capitalised-term.ts";
|
|
9
10
|
import type { Detector, Finding, ProseDocument, Token } from "../plugin.ts";
|
|
10
11
|
|
|
11
12
|
export type UndefinedTerm = { readonly offset: number; readonly term: string };
|
|
@@ -89,15 +90,31 @@ const findingOf = (doc: ProseDocument, use: UndefinedTerm): Finding => ({
|
|
|
89
90
|
values: { term: use.term, offset: use.offset },
|
|
90
91
|
});
|
|
91
92
|
|
|
93
|
+
const NAME_LISTS = ["proper-name-word", "place-region", "calendar-name", "month-name", "company-form", "party-role"] as const;
|
|
94
|
+
|
|
95
|
+
/** Capitalised terms, for a language whose package lists the words that join one name (name-joiner); Japanese has none. */
|
|
96
|
+
const capitalisedUses = (doc: ProseDocument, sentences: readonly (readonly Token[])[], defined: readonly Defined[]): UndefinedTerm[] => {
|
|
97
|
+
if (doc.lexicons["name-joiner"] === undefined) return [];
|
|
98
|
+
const words = {
|
|
99
|
+
names: NAME_LISTS.flatMap((id) => patternsOf(doc, id)),
|
|
100
|
+
divisions: patternsOf(doc, "numbered-division"),
|
|
101
|
+
joiners: patternsOf(doc, "name-joiner"),
|
|
102
|
+
};
|
|
103
|
+
const marked = [...(doc.markup?.headings ?? []), ...doc.links, ...setApartSpans(doc.source)];
|
|
104
|
+
return capitalisedTerms({ source: doc.source, readable: readableText(doc.source), sentences, defined, named: namedWords(doc.source), marked, words });
|
|
105
|
+
};
|
|
106
|
+
|
|
92
107
|
export const undefinedTerm: Detector = (doc): Finding[] => {
|
|
93
108
|
if (doc.structure === undefined) return [];
|
|
94
109
|
const prefixEntries = doc.lexicons["defined-term-prefix"] ?? [];
|
|
95
110
|
const sentences = doc.sentences.map((sentence) => sentence.tokens ?? []);
|
|
96
|
-
const
|
|
97
|
-
|
|
111
|
+
const definitions = definedTerms(doc.structure);
|
|
112
|
+
const defined = definitions.map((term) => term.term);
|
|
113
|
+
const prefixed = undefinedTerms(doc.source, sentences, defined, {
|
|
98
114
|
prefixes: prefixEntries.map((entry) => entry.pattern),
|
|
99
115
|
prefixGroups: prefixGroupsOf(prefixEntries),
|
|
100
116
|
selfNouns: [...patternsOf(doc, "self-reference-noun"), ...patternsOf(doc, "document-kind")],
|
|
101
117
|
isQuoted: (term) => QUOTE_PAIRS.some(([open, close]) => doc.source.includes(`${open}${term}${close}`)),
|
|
102
|
-
})
|
|
118
|
+
});
|
|
119
|
+
return [...prefixed, ...capitalisedUses(doc, sentences, definitions)].map((use) => findingOf(doc, use));
|
|
103
120
|
};
|
package/src/document.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { cutTextSpans } from "./span-cut.ts";
|
|
|
25
25
|
import { markdownFigures } from "./text-figures.ts";
|
|
26
26
|
import { documentMarkup } from "./markup.ts";
|
|
27
27
|
import { BARE_URL } from "./bare-url.ts";
|
|
28
|
+
import { lazyCells, tableCellsOf } from "./table-cells.ts";
|
|
28
29
|
import type { BulletList, LanguageAdapter, Markup, Paragraph, ProseDocument, Sentence, Span, StructureNode, DocumentProfile } from "./plugin.ts";
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -355,7 +356,7 @@ const documentOf = (path: string, source: string, adapter: LanguageAdapter, team
|
|
|
355
356
|
markdown,
|
|
356
357
|
profile,
|
|
357
358
|
}));
|
|
358
|
-
|
|
359
|
+
const doc: ProseDocument = {
|
|
359
360
|
path,
|
|
360
361
|
source,
|
|
361
362
|
language: adapter.id,
|
|
@@ -381,7 +382,9 @@ const documentOf = (path: string, source: string, adapter: LanguageAdapter, team
|
|
|
381
382
|
get markup(): Markup {
|
|
382
383
|
return documentMarkup(root, source, markdown, [...emailLayout.replyQuotes, ...syntax]);
|
|
383
384
|
},
|
|
385
|
+
tableCells: lazyCells(() => tableCellsOf(doc, (text) => adapter.segment(text))),
|
|
384
386
|
};
|
|
387
|
+
return doc;
|
|
385
388
|
};
|
|
386
389
|
|
|
387
390
|
/**
|
package/src/facts/table-facts.ts
CHANGED
|
@@ -69,3 +69,6 @@ export const tableFacts = (source: string, values: readonly FactValue[]): Fact[]
|
|
|
69
69
|
const headings = cellsOf(table.header).map((cell) => plain(cell.text));
|
|
70
70
|
return table.rows.flatMap((row) => rowFacts(row, headings, values, source));
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
/** 文書の表すべての、本体の行の升(見出しの行を除く)。text は書いたまま。 */
|
|
74
|
+
export const tableBodyCells = (source: string): Cell[] => tablesOf(linesOf(source)).flatMap((table) => table.rows.flatMap(cellsOf));
|
package/src/name-variants.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Token } from "./plugin.ts";
|
|
2
2
|
import { escapeRegExp } from "./orthography.ts";
|
|
3
3
|
import { nameCueAt, type NameCue, type NameCues } from "./name-cue.ts";
|
|
4
|
+
import { isNearSurname } from "./surname-near.ts";
|
|
4
5
|
|
|
5
6
|
// 同じ名前(人・会社・製品)を、文書の中で少しだけ違う形に書いた所。どの形が正しいかは決めず、少ないほうを指す。
|
|
6
7
|
// 三通りで同じ名前と見る。書き方の違いだけ(GitHub と Github、Mac OS と macOS)、読みが同じ(山田太郎 と 山田太朗)、
|
|
@@ -19,7 +20,11 @@ export type NameMention = {
|
|
|
19
20
|
readonly cue?: NameCue;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
export type NameVariant = {
|
|
23
|
+
export type NameVariant = {
|
|
24
|
+
readonly mention: NameMention;
|
|
25
|
+
readonly usual: string;
|
|
26
|
+
readonly kind: "spelling" | "reading" | "near" | "surname" | "character";
|
|
27
|
+
};
|
|
23
28
|
|
|
24
29
|
/** 字体の違う同じ字(斎・斉・齋)。字から、その組の代表の字へ。 */
|
|
25
30
|
export type VariantChars = ReadonlyMap<string, string>;
|
|
@@ -99,6 +104,61 @@ export const mentionsIn = (tokens: readonly Token[], source: string, personSuffi
|
|
|
99
104
|
return [{ surface, offset: first.span.start, reading, words: words.map((token) => token.surface), person }];
|
|
100
105
|
});
|
|
101
106
|
|
|
107
|
+
/** 隣の語を探す範囲(UTF-16 の単位)。 */
|
|
108
|
+
const NEIGHBOUR_LOOKBACK = 40;
|
|
109
|
+
/** 隣の語の外側の記号((Rachel、Floyd.)。名前の側の記号(Rachel, Whitford の ,)は語を切るので残す。 */
|
|
110
|
+
const MARK = /\p{P}/u;
|
|
111
|
+
const withoutLeadingMarks = (word: string): string => {
|
|
112
|
+
const chars = [...word];
|
|
113
|
+
const start = chars.findIndex((char) => !MARK.test(char));
|
|
114
|
+
return start < 0 ? "" : chars.slice(start).join("");
|
|
115
|
+
};
|
|
116
|
+
const withoutTrailingMarks = (word: string): string => {
|
|
117
|
+
const chars = [...word];
|
|
118
|
+
return chars.slice(0, chars.findLastIndex((char) => !MARK.test(char)) + 1).join("");
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const wordBeforeAt = (source: string, offset: number): string | undefined => {
|
|
122
|
+
if (source.charAt(offset - 1) !== " ") return undefined;
|
|
123
|
+
const from = Math.max(0, offset - 1 - NEIGHBOUR_LOOKBACK);
|
|
124
|
+
const parts = source.slice(from, offset - 1).split(/\s/u);
|
|
125
|
+
const word = withoutLeadingMarks(parts.at(-1) ?? "");
|
|
126
|
+
return word === "" || (from > 0 && parts.length === 1) ? undefined : word;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const wordAfterAt = (source: string, end: number): string | undefined => {
|
|
130
|
+
if (source.charAt(end) !== " ") return undefined;
|
|
131
|
+
const word = withoutTrailingMarks(source.slice(end + 1, end + 1 + NEIGHBOUR_LOOKBACK).split(/\s/u)[0] ?? "");
|
|
132
|
+
return word === "" ? undefined : word;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 解析器が名前の一語を名前と読まなかった所(to Rachel Whitford の Rachel を動詞と読む)。空白一つで隣の語と合わせた形を、ほかの所で
|
|
137
|
+
* 名前と読めていれば、その形の現れと見る。隣の語がほかの名前の現れに入るなら合わせない。
|
|
138
|
+
*/
|
|
139
|
+
export const withKnownNeighbours = (mentions: readonly NameMention[], source: string): NameMention[] => {
|
|
140
|
+
const known = new Set(mentions.filter((mention) => mention.words.length >= 2).map((mention) => mention.surface));
|
|
141
|
+
const taken = (start: number, end: number): boolean => mentions.some((mention) => mention.offset < end && start < mention.offset + mention.surface.length);
|
|
142
|
+
return mentions.map((mention) => {
|
|
143
|
+
const end = mention.offset + mention.surface.length;
|
|
144
|
+
const before = wordBeforeAt(source, mention.offset);
|
|
145
|
+
const after = wordAfterAt(source, end);
|
|
146
|
+
if (before !== undefined && known.has(`${before} ${mention.surface}`) && !taken(mention.offset - 1 - before.length, mention.offset - 1)) {
|
|
147
|
+
return {
|
|
148
|
+
...mention,
|
|
149
|
+
surface: `${before} ${mention.surface}`,
|
|
150
|
+
offset: mention.offset - 1 - before.length,
|
|
151
|
+
reading: undefined,
|
|
152
|
+
words: [before, ...mention.words],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (after !== undefined && known.has(`${mention.surface} ${after}`) && !taken(end + 1, end + 1 + after.length)) {
|
|
156
|
+
return { ...mention, surface: `${mention.surface} ${after}`, reading: undefined, words: [...mention.words, after] };
|
|
157
|
+
}
|
|
158
|
+
return mention;
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
|
|
102
162
|
/** 敬称の前の名前の字数(髙橋、髙橋太郎)。長い漢字の連なり(株式会社髙橋)は名前だけを切り出せない。 */
|
|
103
163
|
const MAX_SUFFIXED_NAME = 4;
|
|
104
164
|
const MIN_SUFFIXED_NAME = 2;
|
|
@@ -283,6 +343,20 @@ const isSlipOf = (tally: Tally, other: Tally): boolean => {
|
|
|
283
343
|
|
|
284
344
|
const nearVariants = (tallies: readonly Tally[]): NameVariant[] => variantsIn(groupBy(tallies, maskedKeys), "near", isSlipOf);
|
|
285
345
|
|
|
346
|
+
/**
|
|
347
|
+
* 名が同じで姓だけが二字ほど違う人の名前(Rachel Whitford と Rachel Whitfield)。一字違い(near)より遠いので、名と姓の二語以上
|
|
348
|
+
* で、違うのが最後の語のときに限る。相手が二度以上、こちらが一度だけ。名の違う二人(Sara Whitford と Rachel Whitfield)は比べない。
|
|
349
|
+
*/
|
|
350
|
+
const isSurnameSlipOf = (tally: Tally, other: Tally): boolean => {
|
|
351
|
+
if (tally.count !== 1 || other.count < 2) return false;
|
|
352
|
+
const [words, others] = [wordsOf(tally.surface), wordsOf(other.surface)];
|
|
353
|
+
const last = words.length - 1;
|
|
354
|
+
const sameBefore = words.slice(0, last).every((word, index) => word === others[index]);
|
|
355
|
+
return last >= 1 && words.length === others.length && sameBefore && isNearSurname(words[last] ?? "", others[last] ?? "");
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const surnameVariants = (tallies: readonly Tally[]): NameVariant[] => variantsIn(groupBy(tallies, maskedKeys), "surname", isSurnameSlipOf);
|
|
359
|
+
|
|
286
360
|
/** 字体の違う字(斎と斉)を組の代表の字に寄せた形。 */
|
|
287
361
|
const characterKey = (tally: Tally, chars: VariantChars): string => [...tally.surface].map((letter) => chars.get(letter) ?? letter).join("");
|
|
288
362
|
|
|
@@ -344,6 +418,7 @@ export const nameVariants = (mentions: readonly NameMention[], chars: VariantCha
|
|
|
344
418
|
oneWordApart,
|
|
345
419
|
),
|
|
346
420
|
...nearVariants(tallies),
|
|
421
|
+
...surnameVariants(tallies),
|
|
347
422
|
...characterVariants(tallies, chars),
|
|
348
423
|
...personReadingVariants(tallies),
|
|
349
424
|
];
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { escapeRegExp } from "./orthography.ts";
|
|
2
|
+
|
|
3
|
+
// 語の順を入れ替えて書いた同じ名前(統計学基礎 と 基礎統計学、Foundations of Statistics と Statistical Foundations)。
|
|
4
|
+
// 名前と読める所(括弧や引用符の中、大文字で始まる語の並び、名前を書く欄の後ろ)だけを比べ、同じ語(日本語は形態素、英語は語幹)を
|
|
5
|
+
// 違う順に並べた二つのうち、少ないほうを指す。片方がもう片方を含む名前(統計学 と 統計学基礎)は別の名前として比べない。
|
|
6
|
+
// 大文字の語の並びだけでは名前と言い切れない(Florida Supreme Court と Supreme Court of Florida はふつうの言い換え)ので、
|
|
7
|
+
// 二つのどちらかが、括弧の中か欄の後ろか表の升まるごとに書いた名前(anchored)のときだけ比べる。
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* anchored は、括弧の中、欄の後ろ、表の升まるごとのように、名前として書いたと分かる所。clipped は欄の後ろのように、名前の後ろに
|
|
11
|
+
* 文が続きうる所で、名前は最初の名前の外の語(を、in)の手前まで。
|
|
12
|
+
*/
|
|
13
|
+
export type NameSpan = { readonly start: number; readonly end: number; readonly anchored?: boolean; readonly clipped?: boolean };
|
|
14
|
+
|
|
15
|
+
/** 名前の中の内容語。key は比べるための形(英語は語幹)。 */
|
|
16
|
+
export type OrderWord = { readonly start: number; readonly end: number; readonly key: string };
|
|
17
|
+
|
|
18
|
+
export type OrderName = { readonly offset: number; readonly surface: string; readonly keys: readonly string[]; readonly anchored: boolean };
|
|
19
|
+
|
|
20
|
+
export type OrderVariant = { readonly name: OrderName; readonly usual: string };
|
|
21
|
+
|
|
22
|
+
/** 名前と読む長さの上限(字)。これより長い括弧の中は、名前ではなく引用した文。 */
|
|
23
|
+
export const MAX_NAME_LENGTH = 80;
|
|
24
|
+
|
|
25
|
+
/** 名前の中に来ない文の区切り。 */
|
|
26
|
+
const CLAUSE_MARKS = "\\n。、.,.,;;::!!??";
|
|
27
|
+
|
|
28
|
+
/** 括弧や引用符の組(「」、“”)の中身。開きと閉じの二字で一つの組。 */
|
|
29
|
+
export const quotedSpans = (text: string, pairs: readonly string[]): NameSpan[] =>
|
|
30
|
+
pairs.flatMap((pair) => {
|
|
31
|
+
const [open = "", close = ""] = [...pair];
|
|
32
|
+
if (open === "" || close === "") return [];
|
|
33
|
+
const body = `[^${escapeRegExp(open)}${escapeRegExp(close)}${CLAUSE_MARKS}]{1,${String(MAX_NAME_LENGTH)}}`;
|
|
34
|
+
return [...text.matchAll(new RegExp(`${escapeRegExp(open)}(${body})${escapeRegExp(close)}`, "gu"))].map((found) => {
|
|
35
|
+
const start = (found.index ?? 0) + open.length;
|
|
36
|
+
return { start, end: start + (found[1] ?? "").length, anchored: true };
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const CAPITALISED = "\\p{Lu}[\\p{L}'’-]*";
|
|
41
|
+
|
|
42
|
+
const FIRST_WORD = new RegExp(`^${CAPITALISED}[ \\t]+`, "u");
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 大文字で始まる語が二つ以上並んだ所(Foundations of Statistics)。語のあいだに小さな語(of)を挟んでよい。文の頭の語は、
|
|
46
|
+
* 文の頭だから大文字のことがあるので、外す(Take Data Lab の Take)。
|
|
47
|
+
*/
|
|
48
|
+
export const titleCaseSpans = (text: string, joiners: readonly string[], isSentenceStart: (offset: number) => boolean = () => false): NameSpan[] => {
|
|
49
|
+
const joiner = joiners.length === 0 ? "" : `(?:(?:${joiners.map(escapeRegExp).join("|")})[ \\t]+)*`;
|
|
50
|
+
const pattern = new RegExp(`(?<![\\p{L}'’-])${CAPITALISED}(?:[ \\t]+${joiner}${CAPITALISED})+`, "gu");
|
|
51
|
+
return [...text.matchAll(pattern)].map((found) => {
|
|
52
|
+
const start = found.index ?? 0;
|
|
53
|
+
const lead = isSentenceStart(start) ? (FIRST_WORD.exec(found[0])?.[0].length ?? 0) : 0;
|
|
54
|
+
return { start: start + lead, end: start + found[0].length };
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** 名前を書く欄の語(前提科目:、Course:)の後ろの、句読点か行の終わりまで。 */
|
|
59
|
+
export const labelledSpans = (text: string, labels: readonly string[]): NameSpan[] => {
|
|
60
|
+
if (labels.length === 0) return [];
|
|
61
|
+
const label = labels.map(escapeRegExp).join("|");
|
|
62
|
+
const pattern = new RegExp(`(?<![\\p{L}])(?:${label})[ \\t]*[::][ \\t]*([^${CLAUSE_MARKS}]{1,${String(MAX_NAME_LENGTH)}})`, "giu");
|
|
63
|
+
return [...text.matchAll(pattern)].map((found) => {
|
|
64
|
+
const value = (found[1] ?? "").trimEnd();
|
|
65
|
+
const start = (found.index ?? 0) + found[0].length - (found[1] ?? "").length;
|
|
66
|
+
return { start, end: start + value.length, anchored: true, clipped: true };
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** 語の比べる形: 小文字にして、語尾の語(ical、ics、s)を一つ、長いほうから外す。語幹が短くなりすぎる語尾は外さない。 */
|
|
71
|
+
const MIN_STEM = 4;
|
|
72
|
+
|
|
73
|
+
export const stemOf = (word: string, suffixes: readonly string[]): string => {
|
|
74
|
+
const lower = word.toLowerCase();
|
|
75
|
+
const suffix = suffixes
|
|
76
|
+
.toSorted((left, right) => right.length - left.length)
|
|
77
|
+
.find((candidate) => lower.endsWith(candidate.toLowerCase()) && lower.length - candidate.length >= MIN_STEM);
|
|
78
|
+
return suffix === undefined ? lower : lower.slice(0, lower.length - suffix.length);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** clipped の範囲の終わり: 範囲の中で、内容語の後に来る最初の名前の外の語(stops の位置)の手前。 */
|
|
82
|
+
const clippedEnd = (span: NameSpan, words: readonly OrderWord[], stops: readonly number[]): number => {
|
|
83
|
+
if (span.clipped !== true) return span.end;
|
|
84
|
+
const firstWord = words.find((word) => word.start >= span.start && word.end <= span.end);
|
|
85
|
+
if (firstWord === undefined) return span.end;
|
|
86
|
+
return stops.find((stop) => stop > firstWord.start && stop < span.end) ?? span.end;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** 名前が、もっと長い名前の一部か(“West Indies Hurricanes and other Tropical Cyclones” の West Indies Hurricanes)。 */
|
|
90
|
+
const isPartOf = (name: OrderName, longer: OrderName): boolean =>
|
|
91
|
+
name.offset >= longer.offset && name.offset + name.surface.length <= longer.offset + longer.surface.length;
|
|
92
|
+
|
|
93
|
+
/** 範囲の中の内容語の形。同じ所の同じ名前は一つにまとめ、どれかが anchored ならその名前は anchored。
|
|
94
|
+
* anchored の名前の一部だけを大文字の語の並びとして読んだものは除く。 */
|
|
95
|
+
export const orderNamesOf = (text: string, spans: readonly NameSpan[], words: readonly OrderWord[], stops: readonly number[] = []): OrderName[] => {
|
|
96
|
+
const named = spans.flatMap((span): OrderName[] => {
|
|
97
|
+
const end = clippedEnd(span, words, stops);
|
|
98
|
+
const inside = words.filter((word) => word.start >= span.start && word.end <= end);
|
|
99
|
+
const first = inside[0];
|
|
100
|
+
const last = inside.at(-1);
|
|
101
|
+
if (first === undefined || last === undefined) return [];
|
|
102
|
+
return [{ offset: first.start, surface: text.slice(first.start, last.end), keys: inside.map((word) => word.key), anchored: span.anchored === true }];
|
|
103
|
+
});
|
|
104
|
+
const byPlace = named.reduce<Map<string, OrderName>>((places, name) => {
|
|
105
|
+
const id = `${String(name.offset)}:${name.surface}`;
|
|
106
|
+
const known = places.get(id);
|
|
107
|
+
return places.set(id, known === undefined ? name : { ...known, anchored: known.anchored || name.anchored });
|
|
108
|
+
}, new Map());
|
|
109
|
+
const names = [...byPlace.values()];
|
|
110
|
+
return names
|
|
111
|
+
.filter((name) => name.anchored || !names.some((other) => other.anchored && isPartOf(name, other)))
|
|
112
|
+
.toSorted((left, right) => left.offset - right.offset);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
type Tally = { readonly first: OrderName; readonly count: number; readonly anchored: boolean };
|
|
116
|
+
|
|
117
|
+
const talliesOf = (names: readonly OrderName[]): Tally[] => [
|
|
118
|
+
...names
|
|
119
|
+
.reduce<Map<string, Tally>>((tallies, name) => {
|
|
120
|
+
const known = tallies.get(name.surface);
|
|
121
|
+
return tallies.set(name.surface, { first: known?.first ?? name, count: (known?.count ?? 0) + 1, anchored: (known?.anchored ?? false) || name.anchored });
|
|
122
|
+
}, new Map())
|
|
123
|
+
.values(),
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
/** 多いほうが前。同じ数なら先に書いたほうが前。 */
|
|
127
|
+
const byUse = (left: Tally, right: Tally): number => right.count - left.count || left.first.offset - right.first.offset;
|
|
128
|
+
|
|
129
|
+
const bagOf = (keys: readonly string[]): string => keys.toSorted((left, right) => left.localeCompare(right)).join("\u0000");
|
|
130
|
+
|
|
131
|
+
/** 同じ語を違う順に並べた別の書き方で、どちらも他方を含まないか。 */
|
|
132
|
+
export const isReordered = (left: OrderName, right: OrderName): boolean =>
|
|
133
|
+
left.keys.join("\u0000") !== right.keys.join("\u0000") &&
|
|
134
|
+
bagOf(left.keys) === bagOf(right.keys) &&
|
|
135
|
+
!left.surface.includes(right.surface) &&
|
|
136
|
+
!right.surface.includes(left.surface);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 多く使った書き方と同じ語を違う順に並べた名前。少ないほうを一つずつ指す。二つのどちらかが名前として書いた所(anchored)に
|
|
140
|
+
* あるときだけ。surfaces は表の升まるごとの書き方で、その書き方の名前も anchored と見る。
|
|
141
|
+
*/
|
|
142
|
+
export const wordOrderVariants = (names: readonly OrderName[], surfaces: ReadonlySet<string> = new Set()): OrderVariant[] => {
|
|
143
|
+
const tallies = talliesOf(names).toSorted(byUse);
|
|
144
|
+
const anchored = (tally: Tally): boolean => tally.anchored || surfaces.has(tally.first.surface);
|
|
145
|
+
return tallies.flatMap((tally, index) => {
|
|
146
|
+
const usual = tallies.slice(0, index).find((other) => (anchored(other) || anchored(tally)) && isReordered(other.first, tally.first));
|
|
147
|
+
return usual === undefined ? [] : [{ name: tally.first, usual: usual.first.surface }];
|
|
148
|
+
});
|
|
149
|
+
};
|
package/src/plugin.ts
CHANGED
|
@@ -362,8 +362,13 @@ export type ProseDocument = {
|
|
|
362
362
|
readonly markup?: Markup | undefined;
|
|
363
363
|
/** adapter の綴りの辞書。持たない言語では無い。 */
|
|
364
364
|
readonly dictionary?: (() => ReadonlySet<string>) | undefined;
|
|
365
|
+
/** 表の本体の升と、升ごとに解析した語(位置は source の上)。本文(prose)は表を覆うので、升の語は sentences に入らない。呼んだときに作る。 */
|
|
366
|
+
readonly tableCells?: (() => readonly TableCell[]) | undefined;
|
|
365
367
|
};
|
|
366
368
|
|
|
369
|
+
/** 表の升一つ。text は区切りの | のあいだの字のまま。tokens は升の字を adapter が解析した語で、解析しない adapter では無い。 */
|
|
370
|
+
export type TableCell = Span & { readonly text: string; readonly tokens?: readonly Token[] };
|
|
371
|
+
|
|
367
372
|
/** 見出し 1 つ。text は属性(`{#id}`)を外した言葉。 */
|
|
368
373
|
export type MarkupHeading = { readonly depth: number; readonly text: string; readonly start: number; readonly end: number };
|
|
369
374
|
|