chaffjs 0.36.0 → 0.37.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/detectors/acronym-expansion.d.ts.map +1 -1
- package/dist/detectors/acronym-expansion.js +10 -3
- package/dist/detectors/acronym-expansion.js.map +1 -1
- package/dist/detectors/acronym-link.d.ts +5 -0
- package/dist/detectors/acronym-link.d.ts.map +1 -1
- package/dist/detectors/acronym-link.js +33 -0
- package/dist/detectors/acronym-link.js.map +1 -1
- package/dist/detectors/acronym-mention.d.ts +5 -0
- package/dist/detectors/acronym-mention.d.ts.map +1 -0
- package/dist/detectors/acronym-mention.js +34 -0
- package/dist/detectors/acronym-mention.js.map +1 -0
- package/dist/detectors/acronym-name.js +2 -2
- package/dist/detectors/acronym-name.js.map +1 -1
- package/dist/detectors/addressee-title-edge.d.ts +21 -0
- package/dist/detectors/addressee-title-edge.d.ts.map +1 -0
- package/dist/detectors/addressee-title-edge.js +22 -0
- package/dist/detectors/addressee-title-edge.js.map +1 -0
- package/dist/detectors/capitalised-term.d.ts +4 -0
- package/dist/detectors/capitalised-term.d.ts.map +1 -1
- package/dist/detectors/capitalised-term.js +18 -1
- package/dist/detectors/capitalised-term.js.map +1 -1
- package/dist/detectors/caps-caption.d.ts +14 -4
- package/dist/detectors/caps-caption.d.ts.map +1 -1
- package/dist/detectors/caps-caption.js +36 -9
- package/dist/detectors/caps-caption.js.map +1 -1
- package/dist/detectors/char-shape.d.ts.map +1 -1
- package/dist/detectors/char-shape.js +37 -14
- package/dist/detectors/char-shape.js.map +1 -1
- package/dist/detectors/citation-sentence.d.ts +8 -0
- package/dist/detectors/citation-sentence.d.ts.map +1 -0
- package/dist/detectors/citation-sentence.js +40 -0
- package/dist/detectors/citation-sentence.js.map +1 -0
- package/dist/detectors/contraction-mention.d.ts +3 -0
- package/dist/detectors/contraction-mention.d.ts.map +1 -0
- package/dist/detectors/contraction-mention.js +22 -0
- package/dist/detectors/contraction-mention.js.map +1 -0
- package/dist/detectors/dash-break.d.ts +7 -0
- package/dist/detectors/dash-break.d.ts.map +1 -0
- package/dist/detectors/dash-break.js +30 -0
- package/dist/detectors/dash-break.js.map +1 -0
- package/dist/detectors/date-any-year.d.ts.map +1 -1
- package/dist/detectors/date-any-year.js +5 -3
- package/dist/detectors/date-any-year.js.map +1 -1
- package/dist/detectors/definition-use.d.ts.map +1 -1
- package/dist/detectors/definition-use.js +9 -4
- package/dist/detectors/definition-use.js.map +1 -1
- package/dist/detectors/document-shape.d.ts +5 -0
- package/dist/detectors/document-shape.d.ts.map +1 -1
- package/dist/detectors/document-shape.js +15 -1
- package/dist/detectors/document-shape.js.map +1 -1
- package/dist/detectors/duplicate-heading.d.ts +1 -0
- package/dist/detectors/duplicate-heading.d.ts.map +1 -1
- package/dist/detectors/duplicate-heading.js +25 -3
- package/dist/detectors/duplicate-heading.js.map +1 -1
- package/dist/detectors/email-letter.d.ts +9 -2
- package/dist/detectors/email-letter.d.ts.map +1 -1
- package/dist/detectors/email-letter.js +39 -8
- package/dist/detectors/email-letter.js.map +1 -1
- package/dist/detectors/fullwidth-alnum.d.ts.map +1 -1
- package/dist/detectors/fullwidth-alnum.js +7 -1
- package/dist/detectors/fullwidth-alnum.js.map +1 -1
- package/dist/detectors/gram-openers.d.ts +4 -3
- package/dist/detectors/gram-openers.d.ts.map +1 -1
- package/dist/detectors/gram-openers.js +26 -2
- package/dist/detectors/gram-openers.js.map +1 -1
- package/dist/detectors/heading-record-rhythm.d.ts +9 -0
- package/dist/detectors/heading-record-rhythm.d.ts.map +1 -0
- package/dist/detectors/heading-record-rhythm.js +57 -0
- package/dist/detectors/heading-record-rhythm.js.map +1 -0
- package/dist/detectors/kutoten-consistency.d.ts.map +1 -1
- package/dist/detectors/kutoten-consistency.js +7 -0
- package/dist/detectors/kutoten-consistency.js.map +1 -1
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +5 -2
- package/dist/detectors/lexicon.js.map +1 -1
- package/dist/detectors/mark-shape.d.ts +3 -1
- package/dist/detectors/mark-shape.d.ts.map +1 -1
- package/dist/detectors/mark-shape.js +20 -5
- package/dist/detectors/mark-shape.js.map +1 -1
- package/dist/detectors/name-variant.d.ts.map +1 -1
- package/dist/detectors/name-variant.js +6 -0
- package/dist/detectors/name-variant.js.map +1 -1
- package/dist/detectors/organisation-run.d.ts +0 -6
- package/dist/detectors/organisation-run.d.ts.map +1 -1
- package/dist/detectors/organisation-run.js +4 -1
- package/dist/detectors/organisation-run.js.map +1 -1
- package/dist/detectors/orthographic-variant.d.ts.map +1 -1
- package/dist/detectors/orthographic-variant.js +33 -4
- package/dist/detectors/orthographic-variant.js.map +1 -1
- package/dist/detectors/orthography.d.ts.map +1 -1
- package/dist/detectors/orthography.js +4 -2
- package/dist/detectors/orthography.js.map +1 -1
- package/dist/detectors/oxford-comma.d.ts.map +1 -1
- package/dist/detectors/oxford-comma.js +33 -3
- package/dist/detectors/oxford-comma.js.map +1 -1
- package/dist/detectors/range-notation.d.ts +2 -2
- package/dist/detectors/range-notation.d.ts.map +1 -1
- package/dist/detectors/range-notation.js +18 -7
- package/dist/detectors/range-notation.js.map +1 -1
- package/dist/detectors/requirement-smell.d.ts.map +1 -1
- package/dist/detectors/requirement-smell.js +10 -6
- package/dist/detectors/requirement-smell.js.map +1 -1
- package/dist/detectors/sentence-counts.d.ts +10 -2
- package/dist/detectors/sentence-counts.d.ts.map +1 -1
- package/dist/detectors/sentence-counts.js +34 -4
- package/dist/detectors/sentence-counts.js.map +1 -1
- package/dist/detectors/sentence-length.d.ts.map +1 -1
- package/dist/detectors/sentence-length.js +13 -4
- package/dist/detectors/sentence-length.js.map +1 -1
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +15 -3
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/superlative-name.d.ts +5 -0
- package/dist/detectors/superlative-name.d.ts.map +1 -1
- package/dist/detectors/superlative-name.js +9 -0
- package/dist/detectors/superlative-name.js.map +1 -1
- package/dist/detectors/token-shape.d.ts.map +1 -1
- package/dist/detectors/token-shape.js +3 -1
- package/dist/detectors/token-shape.js.map +1 -1
- package/dist/detectors/undefined-term.d.ts.map +1 -1
- package/dist/detectors/undefined-term.js +5 -0
- package/dist/detectors/undefined-term.js.map +1 -1
- package/dist/detectors/unknown-word.d.ts +4 -1
- package/dist/detectors/unknown-word.d.ts.map +1 -1
- package/dist/detectors/unknown-word.js +1 -1
- package/dist/detectors/unknown-word.js.map +1 -1
- package/dist/html/html-site-chrome.d.ts.map +1 -1
- package/dist/html/html-site-chrome.js +35 -12
- package/dist/html/html-site-chrome.js.map +1 -1
- package/dist/latin-word-boundary.d.ts +5 -0
- package/dist/latin-word-boundary.d.ts.map +1 -1
- package/dist/latin-word-boundary.js +17 -0
- package/dist/latin-word-boundary.js.map +1 -1
- package/dist/leading-label.d.ts +4 -0
- package/dist/leading-label.d.ts.map +1 -0
- package/dist/leading-label.js +14 -0
- package/dist/leading-label.js.map +1 -0
- package/dist/link-shape.d.ts +3 -2
- package/dist/link-shape.d.ts.map +1 -1
- package/dist/link-shape.js +12 -3
- package/dist/link-shape.js.map +1 -1
- package/dist/name-misspelt-usual.d.ts +5 -0
- package/dist/name-misspelt-usual.d.ts.map +1 -0
- package/dist/name-misspelt-usual.js +19 -0
- package/dist/name-misspelt-usual.js.map +1 -0
- package/dist/notice-header.d.ts +19 -0
- package/dist/notice-header.d.ts.map +1 -0
- package/dist/notice-header.js +31 -0
- package/dist/notice-header.js.map +1 -0
- package/dist/sentence-load.d.ts +2 -1
- package/dist/sentence-load.d.ts.map +1 -1
- package/dist/sentence-load.js +11 -3
- package/dist/sentence-load.js.map +1 -1
- package/dist/structure/attached-parts.d.ts +2 -1
- package/dist/structure/attached-parts.d.ts.map +1 -1
- package/dist/structure/attached-parts.js +22 -9
- package/dist/structure/attached-parts.js.map +1 -1
- package/dist/structure/definition-use.d.ts +5 -2
- package/dist/structure/definition-use.d.ts.map +1 -1
- package/dist/structure/definition-use.js +24 -7
- package/dist/structure/definition-use.js.map +1 -1
- package/dist/structure/gerund-phrase.d.ts +8 -0
- package/dist/structure/gerund-phrase.d.ts.map +1 -0
- package/dist/structure/gerund-phrase.js +30 -0
- package/dist/structure/gerund-phrase.js.map +1 -0
- package/dist/structure/universal.d.ts +7 -3
- package/dist/structure/universal.d.ts.map +1 -1
- package/dist/structure/universal.js +17 -5
- package/dist/structure/universal.js.map +1 -1
- package/dist/title-line.d.ts +2 -0
- package/dist/title-line.d.ts.map +1 -1
- package/dist/title-line.js +2 -1
- package/dist/title-line.js.map +1 -1
- package/dist/topic-clause.d.ts +7 -0
- package/dist/topic-clause.d.ts.map +1 -0
- package/dist/topic-clause.js +87 -0
- package/dist/topic-clause.js.map +1 -0
- package/dist/uncounted-particles.d.ts +18 -0
- package/dist/uncounted-particles.d.ts.map +1 -0
- package/dist/uncounted-particles.js +47 -0
- package/dist/uncounted-particles.js.map +1 -0
- package/package.json +3 -3
- package/rules/adversative-ga-repeat.yaml +1 -0
- package/rules/all-caps-shouting.yaml +2 -2
- package/rules/bare-url-mix.yaml +2 -2
- package/rules/concrete-evidence-density.yaml +1 -0
- package/rules/contraction-consistency.yaml +2 -2
- package/rules/duplicate-definition.yaml +2 -2
- package/rules/duplicate-heading.yaml +2 -2
- package/rules/fullwidth-alnum-consistency.yaml +2 -2
- package/rules/kutoten-consistency.yaml +2 -2
- package/rules/latin-spacing.yaml +2 -2
- package/rules/max-kanji-continuous.yaml +1 -1
- package/rules/max-sentence-length.yaml +2 -2
- package/rules/ngram-repetition.yaml +2 -2
- package/rules/orthographic-variant.yaml +2 -2
- package/rules/oxford-comma-consistency.yaml +2 -2
- package/rules/preamble-length.yaml +3 -2
- package/rules/range-notation-consistency.yaml +2 -2
- package/rules/requirement-smell.yaml +2 -2
- package/rules/title-length.yaml +2 -2
- package/rules/topic-predicate-distance.yaml +2 -2
- package/rules/undefined-acronym.yaml +3 -2
- package/rules/undefined-term.yaml +2 -2
- package/rules/unqualified-superlative.yaml +1 -1
- package/rules/unused-definition.yaml +2 -2
- package/rules/use-before-definition.yaml +2 -2
- package/src/detectors/acronym-expansion.ts +10 -3
- package/src/detectors/acronym-link.ts +38 -0
- package/src/detectors/acronym-mention.ts +44 -0
- package/src/detectors/acronym-name.ts +2 -2
- package/src/detectors/addressee-title-edge.ts +39 -0
- package/src/detectors/capitalised-term.ts +26 -2
- package/src/detectors/caps-caption.ts +43 -9
- package/src/detectors/char-shape.ts +55 -17
- package/src/detectors/citation-sentence.ts +44 -0
- package/src/detectors/contraction-mention.ts +24 -0
- package/src/detectors/dash-break.ts +45 -0
- package/src/detectors/date-any-year.ts +7 -3
- package/src/detectors/definition-use.ts +12 -8
- package/src/detectors/document-shape.ts +20 -1
- package/src/detectors/duplicate-heading.ts +26 -3
- package/src/detectors/email-letter.ts +50 -7
- package/src/detectors/fullwidth-alnum.ts +9 -1
- package/src/detectors/gram-openers.ts +36 -7
- package/src/detectors/heading-record-rhythm.ts +57 -0
- package/src/detectors/kutoten-consistency.ts +9 -0
- package/src/detectors/lexicon.ts +6 -2
- package/src/detectors/mark-shape.ts +30 -5
- package/src/detectors/name-variant.ts +6 -0
- package/src/detectors/organisation-run.ts +6 -1
- package/src/detectors/orthographic-variant.ts +45 -6
- package/src/detectors/orthography.ts +6 -2
- package/src/detectors/oxford-comma.ts +35 -3
- package/src/detectors/range-notation.ts +26 -6
- package/src/detectors/requirement-smell.ts +12 -8
- package/src/detectors/sentence-counts.ts +40 -4
- package/src/detectors/sentence-length.ts +17 -4
- package/src/detectors/signals.ts +18 -3
- package/src/detectors/superlative-name.ts +10 -0
- package/src/detectors/token-shape.ts +3 -1
- package/src/detectors/undefined-term.ts +8 -0
- package/src/detectors/unknown-word.ts +1 -1
- package/src/html/html-site-chrome.ts +70 -13
- package/src/latin-word-boundary.ts +18 -0
- package/src/leading-label.ts +15 -0
- package/src/link-shape.ts +15 -3
- package/src/name-misspelt-usual.ts +25 -0
- package/src/notice-header.ts +57 -0
- package/src/sentence-load.ts +14 -3
- package/src/structure/attached-parts.ts +24 -9
- package/src/structure/definition-use.ts +33 -7
- package/src/structure/gerund-phrase.ts +31 -0
- package/src/structure/universal.ts +22 -7
- package/src/title-line.ts +2 -1
- package/src/topic-clause.ts +109 -0
- package/src/uncounted-particles.ts +72 -0
|
@@ -2,16 +2,38 @@
|
|
|
2
2
|
// any site's wording. A landmark role (banner, search, contentinfo), a <search> element, a <header> that belongs to the
|
|
3
3
|
// page rather than to a section, and a block named by a convention CMSs share: a breadcrumb trail (breadcrumb, pankuzu,
|
|
4
4
|
// topicpath), a search box, the header or footer block (id or class exactly "header" or "footer"), and the U.S.
|
|
5
|
-
// government's ".gov" banner (usa-banner). A block holding the page's <h1> is never chrome
|
|
6
|
-
//
|
|
7
|
-
|
|
5
|
+
// government's ".gov" banner (usa-banner). A block holding the page's <h1> is never chrome, nor a header holding a
|
|
6
|
+
// heading followed by prose, nor a search block holding prose (a results page). Pure; a regular-expression reading,
|
|
7
|
+
// not a parser.
|
|
8
|
+
import {
|
|
9
|
+
ANY_LINK,
|
|
10
|
+
ATTRIBUTES,
|
|
11
|
+
CLOSED_SENTENCE,
|
|
12
|
+
elementRanges,
|
|
13
|
+
headingRanges,
|
|
14
|
+
isInside,
|
|
15
|
+
plainText,
|
|
16
|
+
withoutElementsOpening,
|
|
17
|
+
withoutRanges,
|
|
18
|
+
type ElementRange,
|
|
19
|
+
} from "./html-elements.ts";
|
|
8
20
|
|
|
9
|
-
const CHROME_ROLES = new Set(["banner", "
|
|
21
|
+
const CHROME_ROLES = new Set(["banner", "contentinfo"]);
|
|
10
22
|
|
|
11
|
-
// A name segment, so that "pankuzu_wrap"
|
|
12
|
-
const CHROME_NAME = /(?:^|[-_])(?:breadcrumbs?|pankuzu|topic-?path
|
|
23
|
+
// A name segment, so that "pankuzu_wrap" and "foot_pankuzu" match and "research" does not.
|
|
24
|
+
const CHROME_NAME = /(?:^|[-_])(?:breadcrumbs?|pankuzu|topic-?path)(?:[-_\d]|$)/iu;
|
|
13
25
|
|
|
14
|
-
const
|
|
26
|
+
const SEARCH_NAME = /(?:^|[-_])search(?:[-_\d]|$)/iu;
|
|
27
|
+
|
|
28
|
+
const REGION_NAME = /^(?:footer|usa-banner)$/iu;
|
|
29
|
+
|
|
30
|
+
const HEADER_NAME = /^header$/iu;
|
|
31
|
+
|
|
32
|
+
// A form's own controls, also when the form element itself is gone and only its labels remain.
|
|
33
|
+
const FORM_CONTROL = /<(?:input|select|textarea|button|label|fieldset)\b/iu;
|
|
34
|
+
|
|
35
|
+
/** From this many sentences a block is prose, not a search box: a results page, an article named "search-…". */
|
|
36
|
+
const PROSE_SENTENCES = 2;
|
|
15
37
|
|
|
16
38
|
const NAMED_OPENING = /<([a-z][a-z0-9-]*)\b[^>]*\s(?:class|id|role)\s*=/giu;
|
|
17
39
|
|
|
@@ -24,24 +46,59 @@ const tokens = (value: string): string[] => value.split(/\s+/u).filter((token) =
|
|
|
24
46
|
|
|
25
47
|
const holdsTitle = (range: ElementRange): boolean => /<h1\b/iu.test(range.inner);
|
|
26
48
|
|
|
49
|
+
const SENTENCE_ENDS = new RegExp(CLOSED_SENTENCE.source, "gu");
|
|
50
|
+
|
|
51
|
+
/** Each tag read as a space, as hasSentence does, so that "Done.</li><li>Next" ends one. */
|
|
52
|
+
const sentenceCount = (html: string): number => [...plainText(html.replace(/<\/?[a-z!][^>]*>/giu, " ")).matchAll(SENTENCE_ENDS)].length;
|
|
53
|
+
|
|
54
|
+
/** The block's own running text: what is left without its links (a menu) and its headings (a title, a menu's labels). */
|
|
55
|
+
const ownSentences = (html: string): number => sentenceCount(withoutRanges(html, headingRanges(html)).replace(ANY_LINK, " "));
|
|
56
|
+
|
|
57
|
+
const isProse = (html: string): boolean => ownSentences(html) >= PROSE_SENTENCES;
|
|
58
|
+
|
|
59
|
+
const holdsSentence = (html: string): boolean => ownSentences(html) > 0;
|
|
60
|
+
|
|
61
|
+
/** A heading with a sentence after it: a part of the document, not a site's logo over its menu. */
|
|
62
|
+
const holdsHeadedProse = (range: ElementRange): boolean => headingRanges(range.inner).some((heading) => holdsSentence(range.inner.slice(heading.end)));
|
|
63
|
+
|
|
64
|
+
/** A search box or its menu: a form, controls, or links with no sentence of their own; never prose such as a results page. */
|
|
65
|
+
const isSearchBox = (range: ElementRange): boolean =>
|
|
66
|
+
!isProse(range.inner) && (/^<form\b/iu.test(range.openTag) || FORM_CONTROL.test(range.inner) || !holdsSentence(range.inner));
|
|
67
|
+
|
|
68
|
+
const firstRole = (openTag: string): string => (tokens(attributeValue(openTag, "role"))[0] ?? "").toLowerCase();
|
|
69
|
+
|
|
70
|
+
const names = (openTag: string): string[] => [...tokens(attributeValue(openTag, "class")), ...tokens(attributeValue(openTag, "id"))];
|
|
71
|
+
|
|
27
72
|
/** A role is a list of tokens and the first one counts, as for role="main". */
|
|
28
|
-
const hasChromeRole = (openTag: string): boolean => CHROME_ROLES.has((
|
|
73
|
+
const hasChromeRole = (openTag: string): boolean => CHROME_ROLES.has(firstRole(openTag));
|
|
74
|
+
|
|
75
|
+
const hasChromeName = (openTag: string): boolean => names(openTag).some((name) => CHROME_NAME.test(name) || REGION_NAME.test(name));
|
|
76
|
+
|
|
77
|
+
/** The search landmark is chrome as marked; a name is only a hint, so the block must also look like a search box. */
|
|
78
|
+
const isSearchBlock = (range: ElementRange): boolean =>
|
|
79
|
+
firstRole(range.openTag) === "search" ? !isProse(range.inner) : names(range.openTag).some((name) => SEARCH_NAME.test(name)) && isSearchBox(range);
|
|
29
80
|
|
|
30
|
-
const
|
|
31
|
-
[...tokens(attributeValue(openTag, "class")), ...tokens(attributeValue(openTag, "id"))].some((name) => CHROME_NAME.test(name) || REGION_NAME.test(name));
|
|
81
|
+
const isHeaderBlock = (range: ElementRange): boolean => names(range.openTag).some((name) => HEADER_NAME.test(name)) && !holdsHeadedProse(range);
|
|
32
82
|
|
|
33
83
|
/** A block the markup names or marks as the site's chrome, unless it holds the page's title. */
|
|
34
|
-
export const isSiteChrome = (range: ElementRange): boolean =>
|
|
84
|
+
export const isSiteChrome = (range: ElementRange): boolean =>
|
|
85
|
+
!holdsTitle(range) && (hasChromeRole(range.openTag) || hasChromeName(range.openTag) || isSearchBlock(range) || isHeaderBlock(range));
|
|
35
86
|
|
|
36
87
|
const SECTIONING = ["article", "section", "main", "aside", "nav"];
|
|
37
88
|
|
|
38
89
|
/** A <header> inside an article or a section heads that part; one outside them is the page's banner. */
|
|
39
90
|
const withoutPageHeaders = (html: string): string => {
|
|
40
91
|
const sections = SECTIONING.flatMap((tag) => elementRanges(html, tag));
|
|
41
|
-
const banners = elementRanges(html, "header").filter(
|
|
92
|
+
const banners = elementRanges(html, "header").filter(
|
|
93
|
+
(header) => !holdsTitle(header) && !holdsHeadedProse(header) && !sections.some((section) => isInside(section, header)),
|
|
94
|
+
);
|
|
42
95
|
return withoutRanges(html, banners);
|
|
43
96
|
};
|
|
44
97
|
|
|
45
|
-
const withoutSearchElements = (html: string): string =>
|
|
98
|
+
const withoutSearchElements = (html: string): string =>
|
|
99
|
+
withoutRanges(
|
|
100
|
+
html,
|
|
101
|
+
elementRanges(html, "search").filter((search) => !isProse(search.inner)),
|
|
102
|
+
);
|
|
46
103
|
|
|
47
104
|
export const withoutSiteChrome = (html: string): string => withoutPageHeaders(withoutSearchElements(withoutElementsOpening(html, NAMED_OPENING, isSiteChrome)));
|
|
@@ -33,6 +33,24 @@ export const isJoinerSpace = (text: string, boundary: Boundary, joiners: Readonl
|
|
|
33
33
|
return joiners.has(text.slice(word.start, word.end).toLowerCase()) && standsBetweenJapanese(text, word);
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
/** チャンネルや宛先の名前(#pj_oss-team、@sato)の頭の印と、名前を作る字。名前は英字で始まる(#1件、@2名は番号と数)。 */
|
|
37
|
+
const HANDLE_MARK = /[#@]/u;
|
|
38
|
+
const HANDLE_CHAR = /[A-Za-z0-9_.-]/u;
|
|
39
|
+
const HANDLE_HEAD = /[A-Za-z]/u;
|
|
40
|
+
/** 印の前の英数字。あれば印は語やメールアドレスの中のもの(C#、sato@example)。 */
|
|
41
|
+
const WORD_CHAR = /\w/u;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* チャンネルや宛先の名前(「#pj_oss-team チャンネル」「@sato さん」)の後ろの境目。名前は空白で閉じ、詰めると後ろの字まで名前に
|
|
45
|
+
* 読まれるので、空けるのは名前を閉じるためで空け方の好みではない。詰めて書いた所(#generalで)も書き手の空け方の票にしない。
|
|
46
|
+
*/
|
|
47
|
+
export const followsHandle = (text: string, boundary: Boundary): boolean => {
|
|
48
|
+
const before = Array.from(text.slice(0, boundary.offset));
|
|
49
|
+
const mark = before.findLastIndex((char) => !HANDLE_CHAR.test(char));
|
|
50
|
+
if (mark === -1 || !HANDLE_MARK.test(before[mark] ?? "") || !HANDLE_HEAD.test(before[mark + 1] ?? "")) return false;
|
|
51
|
+
return !WORD_CHAR.test(before[mark - 1] ?? "");
|
|
52
|
+
};
|
|
53
|
+
|
|
36
54
|
/**
|
|
37
55
|
* 英字の語に付けて一つの語にする日本語の接尾の語(PDF版、IT化、Apple製)の前の境目。空けて書くことはほとんど無く、
|
|
38
56
|
* 詰めたのは空け方の好みではない。語の頭の字が同じでも別の語(製品、型番)なら数える。suffixes は語彙表 latin-bound-suffix の語。
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Span } from "./plugin.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 文の頭で、丸括弧に語を一つ入れた印(問答の(問)(答)、(注)(例))。印は決まった略した形で、本文のその語の書き方
|
|
5
|
+
* (答え、注意)とは比べない。括弧の中は字と数字だけで、句読点や空白、ほかの括弧があれば印と読まない。
|
|
6
|
+
*/
|
|
7
|
+
const LEADING_LABEL = /^([ \t\u3000]*)([((][\p{L}\p{N}]{1,4}[))])/u;
|
|
8
|
+
|
|
9
|
+
/** text の頭の印の範囲。印が無ければ空。 */
|
|
10
|
+
export const leadingLabelSpans = (text: string): Span[] => {
|
|
11
|
+
const match = LEADING_LABEL.exec(text);
|
|
12
|
+
const [lead, label] = [match?.[1], match?.[2]];
|
|
13
|
+
if (lead === undefined || label === undefined) return [];
|
|
14
|
+
return [{ start: lead.length, end: lead.length + label.length }];
|
|
15
|
+
};
|
package/src/link-shape.ts
CHANGED
|
@@ -71,11 +71,23 @@ const BOLD_LABEL = /^(?:\*\*[^*\n.!?。!?,,、::]{1,40}\*\*|__[^_\n.!?
|
|
|
71
71
|
const COLON_LABEL = /^[^.!?。!?,,、::]{1,40}[::]$/u;
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
74
|
+
* A member of a data object written in the text (`"company": "https://…",`), its quotes escaped or not: the line opens with
|
|
75
|
+
* a quoted key (after the object's brace, if any) and a colon, then the URL inside quotes, then at most a comma, a semicolon
|
|
76
|
+
* or the object's close. A sentence before the key makes it prose.
|
|
76
77
|
*/
|
|
77
|
-
|
|
78
|
+
const MEMBER_KEY = /^[\s{]*\\?"[^"\\\n]{1,40}\\?"\s*:\s*\\?"$/u;
|
|
79
|
+
const MEMBER_END = /^\\?"[,;}\]]?$/u;
|
|
80
|
+
|
|
81
|
+
const isLabelledValue = (lineBefore: string, lineAfter: string): boolean => {
|
|
78
82
|
if (lineAfter.trim() !== "") return false;
|
|
79
83
|
const label = lineBefore.replace(LIST_MARKER, "").trim();
|
|
80
84
|
return BOLD_LABEL.test(label) || COLON_LABEL.test(label);
|
|
81
85
|
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Whether a URL is the value of a field (`**Licence text** https://…`, `Website: https://…`, `"home": "https://…"`): on its
|
|
89
|
+
* line only a label comes before it and nothing after, or it is a quoted member of a data object. There the address is the
|
|
90
|
+
* content, as in a table cell, so writing it bare is right.
|
|
91
|
+
*/
|
|
92
|
+
export const isFieldValueUrl = (lineBefore: string, lineAfter: string): boolean =>
|
|
93
|
+
isLabelledValue(lineBefore, lineAfter) || (MEMBER_KEY.test(lineBefore) && MEMBER_END.test(lineAfter.replaceAll(/\s/gu, "")));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// name-variant points at the less frequent form and names the more frequent one as the usual. When the usual form holds a
|
|
2
|
+
// word the dictionary lacks, one letter from the dictionary word the pointed form has in its place ("Release Sechedule" ×3
|
|
3
|
+
// beside "Release Schedule"), the frequent form is a repeated typo, and the pointed form is the right spelling. Pure.
|
|
4
|
+
import { editsOf } from "./spelling.ts";
|
|
5
|
+
|
|
6
|
+
/** Whether a lower-case word is a dictionary word, with its endings (lords, schedules). */
|
|
7
|
+
export type IsKnown = (word: string) => boolean;
|
|
8
|
+
|
|
9
|
+
/** A word the edits are made on: ASCII letters only, as editsOf makes them. */
|
|
10
|
+
const PLAIN_WORD = /^[a-z]+$/u;
|
|
11
|
+
|
|
12
|
+
const wordsOf = (name: string): string[] => name.toLowerCase().trim().split(/\s+/u);
|
|
13
|
+
|
|
14
|
+
/** The usual form's word is a one-letter slip of the pointed form's dictionary word in the same place. */
|
|
15
|
+
const isSlipOf = (usualWord: string, nameWord: string, isKnown: IsKnown): boolean =>
|
|
16
|
+
PLAIN_WORD.test(usualWord) && PLAIN_WORD.test(nameWord) && isKnown(nameWord) && !isKnown(usualWord) && editsOf(usualWord).includes(nameWord);
|
|
17
|
+
|
|
18
|
+
/** Whether the two forms have the same words in the same places but where the usual one has a one-letter slip of a dictionary word. */
|
|
19
|
+
export const usualIsMisspelt = (name: string, usual: string, isKnown: IsKnown): boolean => {
|
|
20
|
+
const nameWords = wordsOf(name);
|
|
21
|
+
const usualWords = wordsOf(usual);
|
|
22
|
+
if (nameWords.length !== usualWords.length) return false;
|
|
23
|
+
const differing = usualWords.flatMap((word, index) => (word === nameWords[index] ? [] : [index]));
|
|
24
|
+
return differing.length > 0 && differing.every((index) => isSlipOf(usualWords[index] ?? "", nameWords[index] ?? "", isKnown));
|
|
25
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// An official notice's head (通達: 課資2-7 / 各国税局長 殿): a document number and an addressee with an honorific of
|
|
2
|
+
// office. Pure. The words come from the language's notice-header lexicon; a notice takes no greeting and no closing.
|
|
3
|
+
import type { Lexicon } from "./plugin.ts";
|
|
4
|
+
|
|
5
|
+
/** The words of a notice's head, by their group in the notice-header lexicon. */
|
|
6
|
+
export type NoticeWords = {
|
|
7
|
+
/** What a document number starts with (課資2-7: 課). */
|
|
8
|
+
readonly numberPrefixes: readonly string[];
|
|
9
|
+
/** The word before and the word after the number of a document number written 〜第123号. */
|
|
10
|
+
readonly numberHeads: readonly string[];
|
|
11
|
+
readonly numberTails: readonly string[];
|
|
12
|
+
/** The honorific an official notice puts after its addressee (各国税局長 殿). */
|
|
13
|
+
readonly addressees: readonly string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const patternsIn = (lexicon: Lexicon, group: string): string[] => lexicon.filter((entry) => entry.group === group).map((entry) => entry.pattern);
|
|
17
|
+
|
|
18
|
+
export const noticeWordsOf = (lexicon: Lexicon): NoticeWords => ({
|
|
19
|
+
numberPrefixes: patternsIn(lexicon, "number-prefix"),
|
|
20
|
+
numberHeads: patternsIn(lexicon, "number-head"),
|
|
21
|
+
numberTails: patternsIn(lexicon, "number-tail"),
|
|
22
|
+
addressees: patternsIn(lexicon, "addressee"),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** A document number is one short token of a line: 課資2-7, 総務省発第123号. */
|
|
26
|
+
const MAX_NUMBER_LENGTH = 20;
|
|
27
|
+
const DIGIT = /\p{Nd}/u;
|
|
28
|
+
const BLANK = /\s/u;
|
|
29
|
+
/** A document number is written in kanji, digits and dashes; hiragana makes the line words (課題は3つ). */
|
|
30
|
+
const HIRAGANA = /\p{Script=Hiragana}/u;
|
|
31
|
+
|
|
32
|
+
/** What follows the prefix: the office's kanji, then numbers joined by dashes (課資3-7: 資, 3-7). 課題3 has no dash. */
|
|
33
|
+
const PREFIXED_NUMBER_BODY = /^\p{Script=Han}*\p{Nd}+(?:[--‐−–―]\p{Nd}+)+$/u;
|
|
34
|
+
|
|
35
|
+
const isNumberedForm = (line: string, words: NoticeWords): boolean =>
|
|
36
|
+
words.numberPrefixes.some((prefix) => prefix !== "" && line.startsWith(prefix) && PREFIXED_NUMBER_BODY.test(line.slice(prefix.length))) ||
|
|
37
|
+
words.numberTails.some(
|
|
38
|
+
(tail) => tail !== "" && line.endsWith(tail) && words.numberHeads.some((head) => head !== "" && line.slice(0, -tail.length).includes(head)),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const isNumberShaped = (line: string): boolean => line.length <= MAX_NUMBER_LENGTH && DIGIT.test(line) && !BLANK.test(line) && !HIRAGANA.test(line);
|
|
42
|
+
|
|
43
|
+
/** Whether a line is a document number alone: short, one run of characters with a digit and no hiragana, in one of the lexicon's forms. */
|
|
44
|
+
export const isDocumentNumber = (line: string, words: NoticeWords): boolean => {
|
|
45
|
+
const trimmed = line.trim();
|
|
46
|
+
return isNumberShaped(trimmed) && isNumberedForm(trimmed, words);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Whether a line is an addressee: a name, then the notice's honorific at the line's end (各国税局長 殿). */
|
|
50
|
+
export const isNoticeAddressee = (line: string, words: NoticeWords): boolean => {
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
return words.addressees.some((word) => trimmed.endsWith(word) && trimmed.length > word.length);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Whether head lines (before the first sentence) hold both a document number and an addressee of a notice. */
|
|
56
|
+
export const isNoticeHead = (headLines: readonly string[], words: NoticeWords): boolean =>
|
|
57
|
+
headLines.some((line) => isDocumentNumber(line, words)) && headLines.some((line) => isNoticeAddressee(line, words));
|
package/src/sentence-load.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// How much one sentence asks the reader to hold at once: brackets inside brackets, clauses chained one after another, and how
|
|
2
2
|
// far the topic stands from the end where its predicate is. Pure: a sentence's text and tokens come in, numbers go out.
|
|
3
3
|
import type { Token } from "./plugin.ts";
|
|
4
|
+
import { topicClauseEnd } from "./topic-clause.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Opening brackets and the mark that closes each. Quotation marks that open and close with the same character are left out, and
|
|
@@ -26,15 +27,24 @@ const OTHER_WIDTH: Readonly<Record<string, string>> = { ")": ")", ")": ")",
|
|
|
26
27
|
/** A formula written in TeX ($d^k = -\nabla f(x^k)$): its brackets are the formula's, not the sentence's. */
|
|
27
28
|
const FORMULA = /\$\$[^$]*\$\$|\$[^$\n]*\$/gu;
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* A doubled parenthesis around text with no parenthesis inside (((居住の用に供したとき等)), ((注))): one mark written with two
|
|
32
|
+
* characters, as a directive sets an article's caption, not a bracket inside a bracket.
|
|
33
|
+
*/
|
|
34
|
+
const DOUBLED_PAREN = /\(\(([^()()]*)\)\)|((([^()()]*)))/gu;
|
|
35
|
+
|
|
36
|
+
const singled = (text: string): string =>
|
|
37
|
+
text.replace(DOUBLED_PAREN, (_whole: string, half?: string, full?: string) => (half === undefined ? `(${full ?? ""})` : `(${half})`));
|
|
38
|
+
|
|
29
39
|
/**
|
|
30
40
|
* The deepest the brackets of a text nest: 「計画(案)」 is one, 「(注(a)を参照)」 two, plain text none. A closer with no
|
|
31
41
|
* opener (the 1) of a list) is ignored, an opener never closed counts as open until the end, a closer of the other width closes
|
|
32
|
-
* its opener ((1) is one), and formulas in $…$ are skipped.
|
|
42
|
+
* its opener ((1) is one), a doubled parenthesis ((題)) is one, and formulas in $…$ are skipped.
|
|
33
43
|
*/
|
|
34
44
|
export const bracketDepth = (text: string): number => {
|
|
35
45
|
// The closers still awaited, innermost last. Pushed and popped in place: copying it per character is quadratic on deep nesting.
|
|
36
46
|
const awaited: string[] = [];
|
|
37
|
-
return [...text.replace(FORMULA, " ")].reduce((deepest, char) => {
|
|
47
|
+
return [...singled(text.replace(FORMULA, " "))].reduce((deepest, char) => {
|
|
38
48
|
const innermost = awaited.at(-1);
|
|
39
49
|
if (innermost !== undefined && (innermost === char || innermost === OTHER_WIDTH[char])) awaited.pop();
|
|
40
50
|
else if (BRACKETS[char] !== undefined) awaited.push(BRACKETS[char]);
|
|
@@ -71,6 +81,7 @@ const TOPIC_BEFORE = new Set(["NOUN", "PRON", "PROPN"]);
|
|
|
71
81
|
/**
|
|
72
82
|
* How far the sentence's first topic (会議は, これは) stands from the end of the sentence, where Japanese puts the predicate the
|
|
73
83
|
* topic belongs to, in characters of the words between them (spaces, and a link's address the document hides, are not read).
|
|
84
|
+
* When the topic gets its own predicate in an earlier clause (topicClauseEnd), the distance is to that clause's end.
|
|
74
85
|
* undefined when the sentence has no topic. The distance is the reader's wait: everything between has to be held before the
|
|
75
86
|
* topic is said to do anything.
|
|
76
87
|
*/
|
|
@@ -78,5 +89,5 @@ export const topicDistance = (tokens: readonly Token[]): number | undefined => {
|
|
|
78
89
|
const topic = tokens.findIndex((token, at) => token.surface === "は" && token.pos === "ADP" && TOPIC_BEFORE.has(tokens[at - 1]?.pos ?? ""));
|
|
79
90
|
const last = tokens.findLastIndex((token) => !TRAILING.has(token.pos));
|
|
80
91
|
if (topic === -1 || last <= topic) return undefined;
|
|
81
|
-
return tokens.slice(topic + 1, last).reduce((sum, token) => sum + [...token.surface.replace(/\s/gu, "")].length, 0);
|
|
92
|
+
return tokens.slice(topic + 1, topicClauseEnd(tokens, topic, last) ?? last).reduce((sum, token) => sum + [...token.surface.replace(/\s/gu, "")].length, 0);
|
|
82
93
|
};
|
|
@@ -1,31 +1,46 @@
|
|
|
1
1
|
// Where an instrument attached to the document starts: a title line naming it (Exhibit A to Merger Agreement, Annex 1).
|
|
2
2
|
// What is attached is a separate document (a restated charter, a form of note), so its definitions are its own. Pure.
|
|
3
3
|
import { escapeRegExp } from "../orthography.ts";
|
|
4
|
-
import { isTitleLine, type TitleNotion } from "../title-line.ts";
|
|
4
|
+
import { isShortTitle, isTitleLine, type TitleNotion } from "../title-line.ts";
|
|
5
5
|
|
|
6
6
|
/** An instrument's title: any case (EXHIBIT A to the merger agreement), but no `: ; ,` after it, and only blank lines around. */
|
|
7
7
|
export const INSTRUMENT_TITLE: TitleNotion = { sentenceEnd: /[.!?。.?!:;,]$/u, capitalised: false, edge: "blank-line" };
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/** Bold around a title (## **SCHEDULE A**) is only how it is shown. */
|
|
10
|
+
const BOLD = /\*\*/gu;
|
|
11
|
+
/** A Markdown heading is a block of its own, so it needs no blank line around it (a closing fence may sit just above). */
|
|
12
|
+
const MARKDOWN_HEADING = /^ {0,3}#{1,6}[ \t]/u;
|
|
13
|
+
|
|
14
|
+
type Line = { readonly start: number; readonly text: string };
|
|
15
|
+
|
|
16
|
+
const linesOf = (source: string): Line[] => {
|
|
10
17
|
const starts = [0, ...[...source.matchAll(/\n/gu)].map((match) => match.index + 1)];
|
|
11
18
|
return starts.map((start, index) => ({ start, text: source.slice(start, (starts[index + 1] ?? source.length + 1) - 1) }));
|
|
12
19
|
};
|
|
13
20
|
|
|
21
|
+
const opensInstrument = (source: string, lines: readonly Line[], index: number, title: RegExp): boolean => {
|
|
22
|
+
const line = lines[index];
|
|
23
|
+
if (line === undefined) return false;
|
|
24
|
+
const text = line.text.replace(BOLD, "");
|
|
25
|
+
const label = title.exec(text);
|
|
26
|
+
if (label === null) return false;
|
|
27
|
+
const rest = text.slice(label[0].length);
|
|
28
|
+
if (MARKDOWN_HEADING.test(text)) return isShortTitle(rest, INSTRUMENT_TITLE);
|
|
29
|
+
const lineEnd = index === lines.length - 1 ? -1 : line.start + line.text.length;
|
|
30
|
+
return isTitleLine(source, line.start, lineEnd, rest, INSTRUMENT_TITLE);
|
|
31
|
+
};
|
|
32
|
+
|
|
14
33
|
/**
|
|
15
34
|
* The offsets of the lines that open an attached instrument: the line holds a label (Exhibit, Annex), its letter or
|
|
16
|
-
* number and at most a short title with no full stop, and a
|
|
35
|
+
* number and at most a short title with no full stop, and it is a Markdown heading or has a blank line or the
|
|
36
|
+
* document's edge on both sides. Bold around the title is ignored.
|
|
17
37
|
*/
|
|
18
38
|
export const attachedPartStarts = (source: string, labels: readonly string[]): number[] => {
|
|
19
39
|
const words = labels.filter((label) => label !== "").map(escapeRegExp);
|
|
20
40
|
if (words.length === 0) return [];
|
|
21
41
|
const title = new RegExp(String.raw`^[ \t]*(?:#{1,6}[ \t]+)?(?:${words.join("|")})[ \t]+[A-Z0-9](?:[A-Z0-9\-]|\.(?=[A-Z0-9]))*(?=[ \t\r]|$)`, "u");
|
|
22
42
|
const lines = linesOf(source);
|
|
23
|
-
return lines.flatMap((line, index) =>
|
|
24
|
-
const label = title.exec(line.text);
|
|
25
|
-
if (label === null) return [];
|
|
26
|
-
const lineEnd = index === lines.length - 1 ? -1 : line.start + line.text.length;
|
|
27
|
-
return isTitleLine(source, line.start, lineEnd, line.text.slice(label[0].length), INSTRUMENT_TITLE) ? [line.start] : [];
|
|
28
|
-
});
|
|
43
|
+
return lines.flatMap((line, index) => (opensInstrument(source, lines, index, title) ? [line.start] : []));
|
|
29
44
|
};
|
|
30
45
|
|
|
31
46
|
/** Which part an offset falls in: 0 for the document itself, n for the n-th attached instrument. */
|
|
@@ -2,6 +2,7 @@ import type { Span, StructureNode } from "../plugin.ts";
|
|
|
2
2
|
import { escapeRegExp } from "../orthography.ts";
|
|
3
3
|
import { inDocumentOrder } from "./issues.ts";
|
|
4
4
|
import { isTitleWords } from "./contents-lines.ts";
|
|
5
|
+
import { verbFormsOf } from "./gerund-phrase.ts";
|
|
5
6
|
import {
|
|
6
7
|
definingNameOf,
|
|
7
8
|
echoesName,
|
|
@@ -109,13 +110,23 @@ const groupsOf = (terms: readonly DefinedTerm[]): TermGroup[] => {
|
|
|
109
110
|
return [...groups.values()];
|
|
110
111
|
};
|
|
111
112
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
113
|
+
/** 語か、動名詞で始まる句の動詞の形(Exercising your options の exercise your options)が、定義の外に一度でも現れるか。 */
|
|
114
|
+
const isUsed = (group: TermGroup, texts: readonly BodyText[], determiners: ReadonlySet<string>): boolean =>
|
|
115
|
+
[group.first.term, ...verbFormsOf(group.first.term, determiners)].some((form) => usesOf(form, texts, group.spans).length > 0);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 定義したのに、定義の外で一度も使っていない語。同じ語の定義が二つあれば、最初の一つだけを言う(二つ目は duplicate-definition)。
|
|
119
|
+
* determiners: 動名詞の後ろに来て、それが目的語を取る動詞だと分かる語(英語の a, the, your)。
|
|
120
|
+
*/
|
|
121
|
+
export const unusedDefinitions = (
|
|
122
|
+
terms: readonly DefinedTerm[],
|
|
123
|
+
texts: readonly BodyText[],
|
|
124
|
+
mentioned: Mentioned = NEVER_MENTIONED,
|
|
125
|
+
determiners: ReadonlySet<string> = new Set(),
|
|
126
|
+
): DefinedTerm[] => {
|
|
114
127
|
const groups = groupsOf(terms);
|
|
115
128
|
const examples = quotesExamples(groups, texts, mentioned);
|
|
116
|
-
return groups
|
|
117
|
-
.filter((group) => !(examples && group.first.bare === true) && usesOf(group.first.term, texts, group.spans).length === 0)
|
|
118
|
-
.map((group) => group.first);
|
|
129
|
+
return groups.filter((group) => !(examples && group.first.bare === true) && !isUsed(group, texts, determiners)).map((group) => group.first);
|
|
119
130
|
};
|
|
120
131
|
|
|
121
132
|
/**
|
|
@@ -144,9 +155,24 @@ const LETTER = /\p{L}/gu;
|
|
|
144
155
|
/** 札の文が語のほかに持ってよい字の数("The Agreement." の The)。 */
|
|
145
156
|
const LABEL_SLACK = 3;
|
|
146
157
|
|
|
147
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* 札の終わりに添えた条の番地の注((第7条関係)、(Section 4))。同じ形の括弧の一組で、入れ子が無く、中身は番号を含む 20 字まで。
|
|
160
|
+
* 長い括弧、番号の無い括弧((if any))、文の途中の括弧は、文の一部として読む。
|
|
161
|
+
*/
|
|
162
|
+
const SECTION_NOTE = /(?:([^()()]{0,20})|\([^()()]{0,20}\))[.。.]?\s*$/u;
|
|
163
|
+
const NUMBER = /[\p{N}〇一二三四五六七八九十]/u;
|
|
164
|
+
|
|
165
|
+
const withoutSectionNote = (text: string): string => {
|
|
166
|
+
const note = SECTION_NOTE.exec(text);
|
|
167
|
+
return note === null || !NUMBER.test(note[0]) ? text : text.slice(0, note.index);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 語だけの札の文(**12.2.** Taxes.、1. The Agreement.、(1)文化芸術推進基本計画(第7条関係))。条や節の頭に語を掲げただけで、
|
|
172
|
+
* 使用ではない。終わりに添えた条の番地の注は札の字に数えない。
|
|
173
|
+
*/
|
|
148
174
|
const isLabel = (body: BodyText | undefined, term: string): boolean =>
|
|
149
|
-
body !== undefined && (body.text.replaceAll(term, "").match(LETTER) ?? []).length <= LABEL_SLACK;
|
|
175
|
+
body !== undefined && (withoutSectionNote(body.text.trimEnd()).replaceAll(term, "").match(LETTER) ?? []).length <= LABEL_SLACK;
|
|
150
176
|
|
|
151
177
|
/** 番地の後ろの、条の題の始まり(Section 1.1 Closing. の Closing)。 */
|
|
152
178
|
const AFTER_ADDRESS = /\d\.?\s+(?=\p{Lu})/gu;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// A defined phrase led by an English gerund and its object ("Exercising your options" means ...) names an act, and the
|
|
2
|
+
// document then writes the act as a verb: exercise your options, exercised their options. Pure.
|
|
3
|
+
|
|
4
|
+
const GERUND = /^(\p{Script=Latin}{2,})ing$/u;
|
|
5
|
+
/** A consonant written twice before -ing (running, setting): the verb has it once. */
|
|
6
|
+
const DOUBLED = /(\p{Script=Latin})\1$/u;
|
|
7
|
+
|
|
8
|
+
/** The verb forms a stem takes (file, files, filed, filing for "Filing"; exercise for "Exercising"). */
|
|
9
|
+
const formsOf = (stem: string): string[] => {
|
|
10
|
+
const single = DOUBLED.test(stem) ? [stem.slice(0, -1)] : [];
|
|
11
|
+
return [...new Set([stem, `${stem}e`, `${stem}es`, `${stem}s`, `${stem}ed`, `${stem}ing`, ...single, ...single.map((verb) => `${verb}s`)])];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const capitalised = (word: string): boolean => word.charAt(0) !== word.charAt(0).toLowerCase();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The same phrase with its leading gerund written as the other forms of its verb ("exercise your options",
|
|
18
|
+
* "exercised your options" for "Exercising your options"). Only when a determiner follows the gerund (a, the, your):
|
|
19
|
+
* then it takes an object and is a verb. "Building materials" and "Pricing information" name things, and "Licensing"
|
|
20
|
+
* alone may be a noun. Empty when the phrase does not take that shape.
|
|
21
|
+
*/
|
|
22
|
+
export const verbFormsOf = (term: string, determiners: ReadonlySet<string>): string[] => {
|
|
23
|
+
const [first, second, ...rest] = term.split(" ");
|
|
24
|
+
if (first === undefined || second === undefined || !determiners.has(second.toLowerCase())) return [];
|
|
25
|
+
const stem = GERUND.exec(first.toLowerCase())?.[1];
|
|
26
|
+
if (stem === undefined) return [];
|
|
27
|
+
return formsOf(stem)
|
|
28
|
+
.map((form) => (capitalised(first) ? `${form.charAt(0).toUpperCase()}${form.slice(1)}` : form))
|
|
29
|
+
.map((form) => [form, second, ...rest].join(" "))
|
|
30
|
+
.filter((form) => form !== term);
|
|
31
|
+
};
|
|
@@ -5,7 +5,7 @@ import { numberInSentence } from "./number-in-sentence.ts";
|
|
|
5
5
|
* 「4.2 設定」「3.1.4 Scope」のような通し番号。数字と点だけで書くので言語を問わない。
|
|
6
6
|
* 各部分を 3 桁・深さを 6 までに抑え、後戻りが爆発しない形にしてある。
|
|
7
7
|
*/
|
|
8
|
-
const DOTTED = /^[ \t]{0,3}(?<number>\d{1,3}(?:\.\d{1,3}){0,5})(?<closing>[..])?[ \t\u3000]+(?<rest>\S.*)$/u;
|
|
8
|
+
const DOTTED = /^[ \t]{0,3}(?<number>\d{1,3}(?:\.\d{1,3}){0,5})(?<closing>[..])?(?<gap>[ \t\u3000]+)(?<rest>\S.*)$/u;
|
|
9
9
|
/**
|
|
10
10
|
* 見出しで、点で閉じた番号に題を詰めて書いたもの(「7.委託」「7.委託」「2.Overview」)。後ろが数字なら小数(「1.5万人」)、
|
|
11
11
|
* 小文字なら版(「2.x 系」)なので読まない。
|
|
@@ -26,8 +26,11 @@ const isTopSection = (line: string, rest: string): boolean => TOP_SECTION.test(l
|
|
|
26
26
|
const dottedGroups = (line: string, context: NumberingContext): Readonly<Record<string, string | undefined>> | undefined =>
|
|
27
27
|
DOTTED.exec(line)?.groups ?? (context.isHeading ? TIGHT_HEADING.exec(line)?.groups : undefined);
|
|
28
28
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* closedByDot: 番号を点で閉じたか(「5. 」)。「1.5 万人」の 1.5 は閉じていない。wideGap: 番号と題の間に全角の空白を置いたか
|
|
31
|
+
* (「9(全角空白)年間収入階級」)。どちらも番号の札の書き方で、数量はそう書かない。
|
|
32
|
+
*/
|
|
33
|
+
type DottedLine = NumberedLine & { readonly closedByDot: boolean; readonly wideGap: boolean };
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
36
|
* 本文の「1. 」は箇条書きで、見出しの「1. 」は章番号。点を含まない番号は、見出しの行か、テキストの章見出しの形でだけ読む。
|
|
@@ -41,15 +44,27 @@ export const dottedNumber = (line: string, context: NumberingContext, plainText
|
|
|
41
44
|
const rest = groups?.["rest"]?.trim() ?? "";
|
|
42
45
|
if (depth < 2 && !context.isHeading && !(plainText && isTopSection(line, rest))) return undefined;
|
|
43
46
|
const closedByDot = groups?.["closing"] !== undefined;
|
|
44
|
-
|
|
47
|
+
const wideGap = groups?.["gap"]?.includes("\u3000") === true;
|
|
48
|
+
return {
|
|
49
|
+
kind: "article",
|
|
50
|
+
depth,
|
|
51
|
+
number,
|
|
52
|
+
absolute: true,
|
|
53
|
+
label: number,
|
|
54
|
+
heading: rest,
|
|
55
|
+
rest,
|
|
56
|
+
ordinal: Number(number.split(".").at(-1)),
|
|
57
|
+
closedByDot,
|
|
58
|
+
wideGap,
|
|
59
|
+
};
|
|
45
60
|
};
|
|
46
61
|
|
|
47
62
|
/**
|
|
48
|
-
* 番号の後ろが単位なら数量(「1.5 万人」「2.5 days」)。点で閉じた番号(「5. ページ自身の通信」「2. Days off
|
|
49
|
-
*
|
|
63
|
+
* 番号の後ろが単位なら数量(「1.5 万人」「2.5 days」)。点で閉じた番号(「5. ページ自身の通信」「2. Days off」)と、全角の空白を
|
|
64
|
+
* 置いた番号(「9(全角空白)年間収入階級」)は札で、数量はそう書かないので、後ろに単位の語が来ても数量にしない。
|
|
50
65
|
*/
|
|
51
66
|
export const isAmount = (dotted: DottedLine, countedAfter: StructurePatterns["countedAfter"]): boolean =>
|
|
52
|
-
!dotted.closedByDot && countedAfter?.(dotted.number, dotted.rest) === true;
|
|
67
|
+
!dotted.closedByDot && !dotted.wideGap && countedAfter?.(dotted.number, dotted.rest) === true;
|
|
53
68
|
|
|
54
69
|
/** 年まで書いた日付の値(2022-12-08)。年の無い「2 May」は、番号 2 の題「May」とも読める。 */
|
|
55
70
|
const WITH_YEAR = /^\d{4}-/u;
|
package/src/title-line.ts
CHANGED
|
@@ -26,7 +26,8 @@ const ENDS_LINE = /^[ \t\r]*\n/u;
|
|
|
26
26
|
const BLANK = /^[ \t\r]*$/u;
|
|
27
27
|
const LOWER_START = /^\p{Ll}/u;
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
/** rest, the words after a label and its number, are a short title by notion's measure. */
|
|
30
|
+
export const isShortTitle = (rest: string, notion: TitleNotion): boolean => {
|
|
30
31
|
const title = rest.trim();
|
|
31
32
|
const words = title.split(/\s+/u).filter((word) => word !== "");
|
|
32
33
|
return (
|