chaffjs 0.34.0 → 0.35.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/capitals-passage.d.ts +3 -0
- package/dist/capitals-passage.d.ts.map +1 -0
- package/dist/capitals-passage.js +26 -0
- package/dist/capitals-passage.js.map +1 -0
- package/dist/coined-hyphen-name.d.ts +13 -0
- package/dist/coined-hyphen-name.d.ts.map +1 -0
- package/dist/coined-hyphen-name.js +21 -0
- package/dist/coined-hyphen-name.js.map +1 -0
- package/dist/company-names.d.ts +6 -1
- package/dist/company-names.d.ts.map +1 -1
- package/dist/company-names.js +22 -3
- package/dist/company-names.js.map +1 -1
- package/dist/detectors/acronym-context.d.ts +1 -1
- package/dist/detectors/acronym-context.d.ts.map +1 -1
- package/dist/detectors/acronym-context.js +4 -4
- package/dist/detectors/acronym-context.js.map +1 -1
- package/dist/detectors/acronym-definition.d.ts +13 -0
- package/dist/detectors/acronym-definition.d.ts.map +1 -0
- package/dist/detectors/acronym-definition.js +90 -0
- package/dist/detectors/acronym-definition.js.map +1 -0
- package/dist/detectors/acronym-lone.d.ts +13 -0
- package/dist/detectors/acronym-lone.d.ts.map +1 -0
- package/dist/detectors/acronym-lone.js +22 -0
- package/dist/detectors/acronym-lone.js.map +1 -0
- package/dist/detectors/acronym-name.d.ts +55 -0
- package/dist/detectors/acronym-name.d.ts.map +1 -0
- package/dist/detectors/acronym-name.js +75 -0
- package/dist/detectors/acronym-name.js.map +1 -0
- package/dist/detectors/capitalised-term.d.ts +14 -2
- package/dist/detectors/capitalised-term.d.ts.map +1 -1
- package/dist/detectors/capitalised-term.js +115 -4
- package/dist/detectors/capitalised-term.js.map +1 -1
- package/dist/detectors/caps-caption.d.ts +11 -0
- package/dist/detectors/caps-caption.d.ts.map +1 -0
- package/dist/detectors/caps-caption.js +51 -0
- package/dist/detectors/caps-caption.js.map +1 -0
- package/dist/detectors/char-shape.d.ts.map +1 -1
- package/dist/detectors/char-shape.js +47 -18
- package/dist/detectors/char-shape.js.map +1 -1
- package/dist/detectors/clause-shape.d.ts +1 -1
- package/dist/detectors/clause-shape.d.ts.map +1 -1
- package/dist/detectors/clause-shape.js +9 -4
- package/dist/detectors/clause-shape.js.map +1 -1
- package/dist/detectors/concrete-evidence.d.ts +13 -1
- package/dist/detectors/concrete-evidence.d.ts.map +1 -1
- package/dist/detectors/concrete-evidence.js +59 -0
- package/dist/detectors/concrete-evidence.js.map +1 -1
- package/dist/detectors/contraction-minority.d.ts +11 -0
- package/dist/detectors/contraction-minority.d.ts.map +1 -0
- package/dist/detectors/contraction-minority.js +10 -0
- package/dist/detectors/contraction-minority.js.map +1 -0
- package/dist/detectors/counter-edge.d.ts +5 -0
- package/dist/detectors/counter-edge.d.ts.map +1 -1
- package/dist/detectors/counter-edge.js +23 -0
- package/dist/detectors/counter-edge.js.map +1 -1
- package/dist/detectors/date-any-year.d.ts +6 -3
- package/dist/detectors/date-any-year.d.ts.map +1 -1
- package/dist/detectors/date-any-year.js +47 -10
- package/dist/detectors/date-any-year.js.map +1 -1
- package/dist/detectors/date-heading-year.d.ts +18 -0
- package/dist/detectors/date-heading-year.d.ts.map +1 -0
- package/dist/detectors/date-heading-year.js +38 -0
- package/dist/detectors/date-heading-year.js.map +1 -0
- package/dist/detectors/date-year.d.ts +12 -2
- package/dist/detectors/date-year.d.ts.map +1 -1
- package/dist/detectors/date-year.js +31 -6
- package/dist/detectors/date-year.js.map +1 -1
- package/dist/detectors/defined-term-form.d.ts.map +1 -1
- package/dist/detectors/defined-term-form.js +26 -5
- package/dist/detectors/defined-term-form.js.map +1 -1
- package/dist/detectors/due-date.d.ts.map +1 -1
- package/dist/detectors/due-date.js +6 -0
- package/dist/detectors/due-date.js.map +1 -1
- package/dist/detectors/en-shape.d.ts.map +1 -1
- package/dist/detectors/en-shape.js +5 -1
- package/dist/detectors/en-shape.js.map +1 -1
- package/dist/detectors/fact-consistency.d.ts.map +1 -1
- package/dist/detectors/fact-consistency.js +3 -2
- package/dist/detectors/fact-consistency.js.map +1 -1
- package/dist/detectors/gram-passages.d.ts +11 -0
- package/dist/detectors/gram-passages.d.ts.map +1 -0
- package/dist/detectors/gram-passages.js +26 -0
- package/dist/detectors/gram-passages.js.map +1 -0
- package/dist/detectors/heading-end-mark.d.ts +8 -3
- package/dist/detectors/heading-end-mark.d.ts.map +1 -1
- package/dist/detectors/heading-end-mark.js +18 -8
- package/dist/detectors/heading-end-mark.js.map +1 -1
- package/dist/detectors/heading-names.d.ts +7 -0
- package/dist/detectors/heading-names.d.ts.map +1 -0
- package/dist/detectors/heading-names.js +24 -0
- package/dist/detectors/heading-names.js.map +1 -0
- package/dist/detectors/heading-numbering.d.ts.map +1 -1
- package/dist/detectors/heading-numbering.js +7 -2
- package/dist/detectors/heading-numbering.js.map +1 -1
- package/dist/detectors/inch-mark.d.ts +10 -0
- package/dist/detectors/inch-mark.d.ts.map +1 -0
- package/dist/detectors/inch-mark.js +48 -0
- package/dist/detectors/inch-mark.js.map +1 -0
- package/dist/detectors/lexicon.d.ts +0 -1
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +29 -23
- package/dist/detectors/lexicon.js.map +1 -1
- package/dist/detectors/mark-shape.d.ts +6 -3
- package/dist/detectors/mark-shape.d.ts.map +1 -1
- package/dist/detectors/mark-shape.js +12 -6
- package/dist/detectors/mark-shape.js.map +1 -1
- package/dist/detectors/name-variant-apart.d.ts +27 -0
- package/dist/detectors/name-variant-apart.d.ts.map +1 -0
- package/dist/detectors/name-variant-apart.js +51 -0
- package/dist/detectors/name-variant-apart.js.map +1 -0
- package/dist/detectors/name-variant.d.ts.map +1 -1
- package/dist/detectors/name-variant.js +18 -11
- package/dist/detectors/name-variant.js.map +1 -1
- package/dist/detectors/numeral-word.d.ts +15 -0
- package/dist/detectors/numeral-word.d.ts.map +1 -0
- package/dist/detectors/numeral-word.js +26 -0
- package/dist/detectors/numeral-word.js.map +1 -0
- package/dist/detectors/organisation-run.d.ts +14 -0
- package/dist/detectors/organisation-run.d.ts.map +1 -0
- package/dist/detectors/organisation-run.js +73 -0
- package/dist/detectors/organisation-run.js.map +1 -0
- package/dist/detectors/orthographic-variant.d.ts.map +1 -1
- package/dist/detectors/orthographic-variant.js +13 -5
- package/dist/detectors/orthographic-variant.js.map +1 -1
- package/dist/detectors/outline-label.d.ts +14 -0
- package/dist/detectors/outline-label.d.ts.map +1 -0
- package/dist/detectors/outline-label.js +32 -0
- package/dist/detectors/outline-label.js.map +1 -0
- package/dist/detectors/page-marker.d.ts +6 -0
- package/dist/detectors/page-marker.d.ts.map +1 -0
- package/dist/detectors/page-marker.js +19 -0
- package/dist/detectors/page-marker.js.map +1 -0
- package/dist/detectors/parallel-definition.d.ts +16 -0
- package/dist/detectors/parallel-definition.d.ts.map +1 -0
- package/dist/detectors/parallel-definition.js +51 -0
- package/dist/detectors/parallel-definition.js.map +1 -0
- package/dist/detectors/reference-text.d.ts.map +1 -1
- package/dist/detectors/reference-text.js +1 -0
- package/dist/detectors/reference-text.js.map +1 -1
- package/dist/detectors/registry/unfilled-placeholder.d.ts.map +1 -1
- package/dist/detectors/registry/unfilled-placeholder.js +2 -2
- package/dist/detectors/registry/unfilled-placeholder.js.map +1 -1
- package/dist/detectors/registry/vague-density.js +1 -1
- package/dist/detectors/registry/vague-density.js.map +1 -1
- package/dist/detectors/request-owner.d.ts +6 -2
- package/dist/detectors/request-owner.d.ts.map +1 -1
- package/dist/detectors/request-owner.js +57 -7
- package/dist/detectors/request-owner.js.map +1 -1
- package/dist/detectors/requirement-smell.d.ts.map +1 -1
- package/dist/detectors/requirement-smell.js +10 -2
- package/dist/detectors/requirement-smell.js.map +1 -1
- package/dist/detectors/sentence-length.d.ts.map +1 -1
- package/dist/detectors/sentence-length.js +24 -16
- package/dist/detectors/sentence-length.js.map +1 -1
- package/dist/detectors/sentence-rhythm.d.ts.map +1 -1
- package/dist/detectors/sentence-rhythm.js +6 -3
- package/dist/detectors/sentence-rhythm.js.map +1 -1
- package/dist/detectors/series-label.d.ts +2 -1
- package/dist/detectors/series-label.d.ts.map +1 -1
- package/dist/detectors/series-label.js +10 -4
- package/dist/detectors/series-label.js.map +1 -1
- package/dist/detectors/signals.d.ts +4 -0
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +95 -36
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +8 -7
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/template-sections.d.ts +14 -0
- package/dist/detectors/template-sections.d.ts.map +1 -0
- package/dist/detectors/template-sections.js +25 -0
- package/dist/detectors/template-sections.js.map +1 -0
- package/dist/detectors/titled-run.d.ts +22 -0
- package/dist/detectors/titled-run.d.ts.map +1 -0
- package/dist/detectors/titled-run.js +34 -0
- package/dist/detectors/titled-run.js.map +1 -0
- package/dist/detectors/unbalanced-bracket.d.ts.map +1 -1
- package/dist/detectors/unbalanced-bracket.js +4 -0
- package/dist/detectors/unbalanced-bracket.js.map +1 -1
- package/dist/detectors/undefined-term.d.ts.map +1 -1
- package/dist/detectors/undefined-term.js +8 -0
- package/dist/detectors/undefined-term.js.map +1 -1
- package/dist/detectors/unfilled-placeholder.d.ts +4 -0
- package/dist/detectors/unfilled-placeholder.d.ts.map +1 -0
- package/dist/detectors/unfilled-placeholder.js +9 -0
- package/dist/detectors/unfilled-placeholder.js.map +1 -0
- package/dist/detectors/vague-density.d.ts +5 -0
- package/dist/detectors/vague-density.d.ts.map +1 -0
- package/dist/detectors/vague-density.js +52 -0
- package/dist/detectors/vague-density.js.map +1 -0
- package/dist/facts/fact-scope.d.ts +10 -1
- package/dist/facts/fact-scope.d.ts.map +1 -1
- package/dist/facts/fact-scope.js +34 -3
- package/dist/facts/fact-scope.js.map +1 -1
- package/dist/facts/table-facts.d.ts +12 -2
- package/dist/facts/table-facts.d.ts.map +1 -1
- package/dist/facts/table-facts.js +22 -4
- package/dist/facts/table-facts.js.map +1 -1
- package/dist/grade-api.d.ts +9 -2
- package/dist/grade-api.d.ts.map +1 -1
- package/dist/grade-api.js +31 -15
- package/dist/grade-api.js.map +1 -1
- package/dist/heading-numbering.d.ts +5 -2
- package/dist/heading-numbering.d.ts.map +1 -1
- package/dist/heading-numbering.js +27 -6
- package/dist/heading-numbering.js.map +1 -1
- package/dist/name-end-letter.d.ts +5 -0
- package/dist/name-end-letter.d.ts.map +1 -1
- package/dist/name-end-letter.js +8 -0
- package/dist/name-end-letter.js.map +1 -1
- package/dist/name-given-before.d.ts +15 -0
- package/dist/name-given-before.d.ts.map +1 -0
- package/dist/name-given-before.js +30 -0
- package/dist/name-given-before.js.map +1 -0
- package/dist/name-separator.d.ts +2 -2
- package/dist/name-separator.d.ts.map +1 -1
- package/dist/name-separator.js +56 -5
- package/dist/name-separator.js.map +1 -1
- package/dist/name-variants.d.ts +3 -1
- package/dist/name-variants.d.ts.map +1 -1
- package/dist/name-variants.js +8 -7
- package/dist/name-variants.js.map +1 -1
- package/dist/name-word-order.d.ts +9 -5
- package/dist/name-word-order.d.ts.map +1 -1
- package/dist/name-word-order.js +47 -15
- package/dist/name-word-order.js.map +1 -1
- package/dist/nominal-stem.d.ts +13 -0
- package/dist/nominal-stem.d.ts.map +1 -0
- package/dist/nominal-stem.js +32 -0
- package/dist/nominal-stem.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 +6 -2
- package/dist/sentence-load.js.map +1 -1
- package/dist/structure/cited-document-section.d.ts +7 -0
- package/dist/structure/cited-document-section.d.ts.map +1 -0
- package/dist/structure/cited-document-section.js +33 -0
- package/dist/structure/cited-document-section.js.map +1 -0
- package/dist/structure/html-text-blocks.d.ts +8 -0
- package/dist/structure/html-text-blocks.d.ts.map +1 -0
- package/dist/structure/html-text-blocks.js +47 -0
- package/dist/structure/html-text-blocks.js.map +1 -0
- package/dist/structure/issues.d.ts +1 -1
- package/dist/structure/issues.d.ts.map +1 -1
- package/dist/structure/issues.js +19 -4
- package/dist/structure/issues.js.map +1 -1
- package/dist/structure/labelled-date-order.d.ts +4 -1
- package/dist/structure/labelled-date-order.d.ts.map +1 -1
- package/dist/structure/labelled-date-order.js +2 -2
- package/dist/structure/labelled-date-order.js.map +1 -1
- package/dist/structure/labelled-order.d.ts +3 -0
- package/dist/structure/labelled-order.d.ts.map +1 -1
- package/dist/structure/labelled-order.js +8 -3
- package/dist/structure/labelled-order.js.map +1 -1
- package/dist/structure/list-numbering.d.ts +20 -4
- package/dist/structure/list-numbering.d.ts.map +1 -1
- package/dist/structure/list-numbering.js +65 -9
- package/dist/structure/list-numbering.js.map +1 -1
- package/dist/structure/listed-paragraph.d.ts +16 -0
- package/dist/structure/listed-paragraph.d.ts.map +1 -0
- package/dist/structure/listed-paragraph.js +37 -0
- package/dist/structure/listed-paragraph.js.map +1 -0
- package/dist/structure/numbering.d.ts +3 -2
- package/dist/structure/numbering.d.ts.map +1 -1
- package/dist/structure/numbering.js +31 -6
- package/dist/structure/numbering.js.map +1 -1
- package/dist/structure/reference-text.d.ts +9 -1
- package/dist/structure/reference-text.d.ts.map +1 -1
- package/dist/structure/reference-text.js +21 -3
- package/dist/structure/reference-text.js.map +1 -1
- package/dist/structure/section-names.d.ts +12 -0
- package/dist/structure/section-names.d.ts.map +1 -0
- package/dist/structure/section-names.js +26 -0
- package/dist/structure/section-names.js.map +1 -0
- package/dist/structure/versioned-label.d.ts +16 -0
- package/dist/structure/versioned-label.d.ts.map +1 -0
- package/dist/structure/versioned-label.js +48 -0
- package/dist/structure/versioned-label.js.map +1 -0
- package/dist/structure/written-headings.d.ts +31 -0
- package/dist/structure/written-headings.d.ts.map +1 -0
- package/dist/structure/written-headings.js +119 -0
- package/dist/structure/written-headings.js.map +1 -0
- package/dist/structure/written-resolve.d.ts +12 -0
- package/dist/structure/written-resolve.d.ts.map +1 -0
- package/dist/structure/written-resolve.js +49 -0
- package/dist/structure/written-resolve.js.map +1 -0
- package/package.json +3 -3
- package/rules/all-caps-shouting.yaml +2 -2
- package/rules/concrete-evidence-density.yaml +2 -2
- package/rules/contraction-consistency.yaml +13 -13
- package/rules/dangling-reference.yaml +2 -2
- package/rules/date-without-year.yaml +2 -2
- package/rules/defined-name-repeated.yaml +2 -2
- package/rules/fact-conflict.yaml +2 -2
- package/rules/heading-end-punctuation-mix.yaml +2 -2
- package/rules/heading-numbering-mix.yaml +2 -2
- package/rules/latin-spacing.yaml +2 -2
- package/rules/max-kanji-continuous.yaml +5 -3
- package/rules/max-sentence-length.yaml +3 -2
- package/rules/name-variant.yaml +2 -2
- package/rules/ngram-repetition.yaml +2 -2
- package/rules/numbering-gap.yaml +2 -2
- package/rules/orthographic-variant.yaml +2 -2
- package/rules/reference-title-mismatch.yaml +2 -2
- package/rules/repeated-sentence-ending.yaml +3 -2
- package/rules/request-without-deadline.yaml +2 -2
- package/rules/requirement-smell.yaml +2 -2
- package/rules/sentence-rhythm.yaml +2 -2
- package/rules/title-case-consistency.yaml +2 -2
- package/rules/unbalanced-bracket.yaml +2 -2
- package/rules/undefined-acronym.yaml +11 -2
- package/rules/undefined-term.yaml +2 -2
- package/rules/unfilled-placeholder.yaml +2 -2
- package/rules/vague-word-density.yaml +2 -2
- package/src/capitals-passage.ts +31 -0
- package/src/coined-hyphen-name.ts +27 -0
- package/src/company-names.ts +24 -3
- package/src/detectors/acronym-context.ts +5 -6
- package/src/detectors/acronym-definition.ts +123 -0
- package/src/detectors/acronym-lone.ts +26 -0
- package/src/detectors/acronym-name.ts +117 -0
- package/src/detectors/capitalised-term.ts +154 -4
- package/src/detectors/caps-caption.ts +61 -0
- package/src/detectors/char-shape.ts +74 -22
- package/src/detectors/clause-shape.ts +14 -4
- package/src/detectors/concrete-evidence.ts +77 -1
- package/src/detectors/contraction-minority.ts +11 -0
- package/src/detectors/counter-edge.ts +23 -0
- package/src/detectors/date-any-year.ts +56 -10
- package/src/detectors/date-heading-year.ts +55 -0
- package/src/detectors/date-year.ts +54 -9
- package/src/detectors/defined-term-form.ts +26 -5
- package/src/detectors/due-date.ts +10 -1
- package/src/detectors/en-shape.ts +5 -1
- package/src/detectors/fact-consistency.ts +3 -2
- package/src/detectors/gram-passages.ts +36 -0
- package/src/detectors/heading-end-mark.ts +23 -10
- package/src/detectors/heading-names.ts +31 -0
- package/src/detectors/heading-numbering.ts +7 -2
- package/src/detectors/inch-mark.ts +49 -0
- package/src/detectors/lexicon.ts +32 -21
- package/src/detectors/mark-shape.ts +12 -6
- package/src/detectors/name-variant-apart.ts +63 -0
- package/src/detectors/name-variant.ts +30 -12
- package/src/detectors/numeral-word.ts +33 -0
- package/src/detectors/organisation-run.ts +86 -0
- package/src/detectors/orthographic-variant.ts +22 -6
- package/src/detectors/outline-label.ts +48 -0
- package/src/detectors/page-marker.ts +26 -0
- package/src/detectors/parallel-definition.ts +64 -0
- package/src/detectors/reference-text.ts +1 -0
- package/src/detectors/registry/unfilled-placeholder.ts +2 -2
- package/src/detectors/registry/vague-density.ts +1 -1
- package/src/detectors/request-owner.ts +71 -8
- package/src/detectors/requirement-smell.ts +15 -2
- package/src/detectors/sentence-length.ts +14 -6
- package/src/detectors/sentence-rhythm.ts +8 -4
- package/src/detectors/series-label.ts +13 -4
- package/src/detectors/signals.ts +94 -26
- package/src/detectors/structure-tree.ts +10 -8
- package/src/detectors/template-sections.ts +37 -0
- package/src/detectors/titled-run.ts +43 -0
- package/src/detectors/unbalanced-bracket.ts +3 -0
- package/src/detectors/undefined-term.ts +8 -0
- package/src/detectors/unfilled-placeholder.ts +10 -0
- package/src/detectors/vague-density.ts +68 -0
- package/src/facts/fact-scope.ts +53 -4
- package/src/facts/table-facts.ts +43 -6
- package/src/grade-api.ts +38 -16
- package/src/heading-numbering.ts +35 -8
- package/src/name-end-letter.ts +9 -0
- package/src/name-given-before.ts +40 -0
- package/src/name-separator.ts +66 -5
- package/src/name-variants.ts +21 -11
- package/src/name-word-order.ts +62 -17
- package/src/nominal-stem.ts +36 -0
- package/src/sentence-load.ts +7 -2
- package/src/structure/cited-document-section.ts +42 -0
- package/src/structure/html-text-blocks.ts +45 -0
- package/src/structure/issues.ts +19 -4
- package/src/structure/labelled-date-order.ts +5 -2
- package/src/structure/labelled-order.ts +12 -3
- package/src/structure/list-numbering.ts +100 -11
- package/src/structure/listed-paragraph.ts +45 -0
- package/src/structure/numbering.ts +48 -19
- package/src/structure/reference-text.ts +25 -3
- package/src/structure/section-names.ts +32 -0
- package/src/structure/versioned-label.ts +52 -0
- package/src/structure/written-headings.ts +137 -0
- package/src/structure/written-resolve.ts +57 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { Detector, DocumentProfile, Finding, Sentence, Span, Token } from "../plugin.ts";
|
|
1
|
+
import type { Detector, DocumentProfile, Finding, ProseDocument, Sentence, Span, Token } from "../plugin.ts";
|
|
2
2
|
import { compacted } from "./gram-place.ts";
|
|
3
|
-
import { leadingCounter } from "./counter-edge.ts";
|
|
3
|
+
import { leadingCounter, trailingPrefix } from "./counter-edge.ts";
|
|
4
4
|
import { maskAddresses } from "../address-chain.ts";
|
|
5
5
|
import { isAddressRun } from "./place-run.ts";
|
|
6
6
|
import { isOneName } from "./name-run.ts";
|
|
7
|
+
import { isOrganisationRun, type OrganisationWords } from "./organisation-run.ts";
|
|
8
|
+
import { endsInTitle, isTitlePosition, isTitledName, type TitleWords } from "./titled-run.ts";
|
|
7
9
|
import { proseText } from "../measure.ts";
|
|
8
10
|
import { parallelDotCount } from "./middle-dot.ts";
|
|
9
11
|
import { maskSpans } from "../mask.ts";
|
|
@@ -16,11 +18,17 @@ import { nameSpans } from "../team-names.ts";
|
|
|
16
18
|
*/
|
|
17
19
|
const KANJI_RUN = /[一-鿿]+/gu;
|
|
18
20
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* 漢字の連なりと、その文書全体の座標での範囲。quoted は括弧の中身がまるごとこの連なりであること。
|
|
23
|
+
* placed は、題の名前の置き方(括弧を閉じる、補いの括弧が続く、文がこれだけ)をしていること。
|
|
24
|
+
*/
|
|
25
|
+
type KanjiRun = { readonly text: string; readonly span: Span | undefined; readonly quoted: boolean; readonly placed: boolean };
|
|
21
26
|
|
|
22
27
|
const SPACE = /\s/gu;
|
|
23
28
|
|
|
29
|
+
/** 名前を囲む括弧。鉤括弧と、見出しや分類の名前を囲む隅付き括弧(【浪速区役所公募型比較見積】)。 */
|
|
30
|
+
const NAME_MARKS = { "「": "」", "『": "』", "【": "】" };
|
|
31
|
+
|
|
24
32
|
/**
|
|
25
33
|
* 同じ連なりが文に二度出ても、それぞれの位置で読む。proseText は空白を詰めたり除いたりするだけなので、
|
|
26
34
|
* 空白でない k 文字目は元の文でも空白でない k 文字目。番地の覆いは長さを保つので、proseText の位置がそのまま使える。
|
|
@@ -29,9 +37,9 @@ const SPACE = /\s/gu;
|
|
|
29
37
|
const maskedProse = (prose: string, profile: DocumentProfile | undefined, names: readonly string[]): string =>
|
|
30
38
|
maskSpans(maskAddresses(prose, profile), nameSpans(prose, names));
|
|
31
39
|
|
|
32
|
-
/**
|
|
40
|
+
/** 鉤括弧と隅付き括弧の中身が、ちょうどこの範囲(文の先頭からの位置)か。括弧でくくった漢字だけの語は、書き手が 1 つの名前として示したもの。 */
|
|
33
41
|
const fillsQuote = (sentence: Sentence, start: number, end: number): boolean =>
|
|
34
|
-
quotedSpans(sentence.text).some((span) => span.start === start && span.end === end);
|
|
42
|
+
quotedSpans(sentence.text, NAME_MARKS).some((span) => span.start === start && span.end === end);
|
|
35
43
|
|
|
36
44
|
const runsOf = (sentence: Sentence, profile: DocumentProfile | undefined, names: readonly string[]): KanjiRun[] => {
|
|
37
45
|
const prose = proseText(sentence);
|
|
@@ -39,42 +47,86 @@ const runsOf = (sentence: Sentence, profile: DocumentProfile | undefined, names:
|
|
|
39
47
|
return [...maskedProse(prose, profile, names).matchAll(KANJI_RUN)].map((match) => {
|
|
40
48
|
const before = prose.slice(0, match.index).replace(SPACE, "").length;
|
|
41
49
|
const [start, last] = [printed[before], printed[before + match[0].length - 1]];
|
|
42
|
-
if (start === undefined || last === undefined) return { text: match[0], span: undefined, quoted: false };
|
|
43
|
-
|
|
50
|
+
if (start === undefined || last === undefined) return { text: match[0], span: undefined, quoted: false, placed: false };
|
|
51
|
+
const span = { start: sentence.span.start + start, end: sentence.span.start + last + 1 };
|
|
52
|
+
return { text: match[0], span, quoted: fillsQuote(sentence, start, last + 1), placed: isTitlePosition(sentence.text, start, last + 1) };
|
|
44
53
|
});
|
|
45
54
|
};
|
|
46
55
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
/** 連なりを数えないと決める語彙: 住所の先頭にしか来ない単位(都道府県)と、組織の名前の形と単位と、題の名前を閉じる語。 */
|
|
57
|
+
type NameWords = { readonly topUnits: ReadonlySet<string>; readonly organisation: OrganisationWords; readonly title: TitleWords };
|
|
58
|
+
|
|
59
|
+
/** 1 つの文書で連なりを読むのに要るもの。titled は、文書の中で題として置かれた名前(titledNames)。 */
|
|
60
|
+
type RunContext = {
|
|
61
|
+
readonly profile: DocumentProfile | undefined;
|
|
62
|
+
readonly names: readonly string[];
|
|
63
|
+
readonly words: NameWords;
|
|
64
|
+
readonly titled: readonly string[];
|
|
51
65
|
};
|
|
52
66
|
|
|
67
|
+
const coveringOf = (tokens: readonly Token[], span: Span): number[] =>
|
|
68
|
+
tokens.flatMap((token, index) => (token.span.start < span.end && span.start < token.span.end ? [index] : []));
|
|
69
|
+
|
|
53
70
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
71
|
+
* 漢字の連なりが住所(東京都港区新橋二丁目)か 1 つの名前(日本銀行)か 1 つの組織の名前(国立研究開発法人産業技術総合研究所)か。
|
|
72
|
+
* 形態素解析の地名・人名・組織名と数、組織の語彙で決める。
|
|
56
73
|
*/
|
|
57
|
-
const
|
|
74
|
+
const isPlaceName = (tokens: readonly Token[], span: Span, words: NameWords): boolean => {
|
|
75
|
+
const covering = coveringOf(tokens, span);
|
|
76
|
+
return isAddressRun(tokens, covering, words.topUnits) || isOneName(tokens, covering) || isOrganisationRun(tokens, covering, words.organisation);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** 題の名前(大阪市鶴見区役所公募型比較見積実施要綱)か。題を閉じる語で終わり、ここか文書のほかの所で題として置かれている。 */
|
|
80
|
+
const isTitle = (tokens: readonly Token[], span: Span, text: string, placed: boolean, context: RunContext): boolean =>
|
|
81
|
+
endsInTitle(
|
|
82
|
+
coveringOf(tokens, span).flatMap((index) => tokens.slice(index, index + 1)),
|
|
83
|
+
context.words.title,
|
|
84
|
+
) &&
|
|
85
|
+
(placed || isTitledName(text, context.titled));
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 数えない連なりは空。頭の、数に付いた助数詞(2日日本弁護士連合会の「日」)と、尻の、数に付く接頭の語(施行令第172条の「第」)は数えない。
|
|
89
|
+
* 住所か名前かは、それを外した残りで決める(1日日本銀行の「日本銀行」)。品詞が無ければ判定しない。
|
|
90
|
+
*/
|
|
91
|
+
const measuredRun = (tokens: readonly Token[] | undefined, run: KanjiRun, context: RunContext): string => {
|
|
58
92
|
if (run.quoted) return "";
|
|
59
93
|
if (tokens === undefined || run.span === undefined) return run.text;
|
|
60
94
|
const counter = leadingCounter(tokens, run.span.start, run.text);
|
|
61
|
-
const
|
|
62
|
-
|
|
95
|
+
const prefix = trailingPrefix(tokens, run.span.end, run.text);
|
|
96
|
+
const rest = { start: counter?.span.end ?? run.span.start, end: prefix?.span.start ?? run.span.end };
|
|
97
|
+
const text = run.text.slice(counter?.surface.length ?? 0, run.text.length - (prefix?.surface.length ?? 0));
|
|
98
|
+
return isPlaceName(tokens, rest, context.words) || isTitle(tokens, rest, text, run.placed, context) ? "" : text;
|
|
63
99
|
};
|
|
64
100
|
|
|
65
|
-
const longestKanji = (sentence: Sentence,
|
|
66
|
-
runsOf(sentence, profile, names)
|
|
67
|
-
.map((run) => measuredRun(sentence.tokens, run,
|
|
101
|
+
const longestKanji = (sentence: Sentence, context: RunContext): string =>
|
|
102
|
+
runsOf(sentence, context.profile, context.names)
|
|
103
|
+
.map((run) => measuredRun(sentence.tokens, run, context))
|
|
68
104
|
.reduce((longest, run) => (run.length > longest.length ? run : longest), "");
|
|
69
105
|
|
|
106
|
+
/**
|
|
107
|
+
* 文書の中で題として置かれた漢字の連なり: 題の置き方をした文の中の連なりと、見出しの中の連なり。
|
|
108
|
+
* 同じ名前を文の中で使えば(認定職業訓練実施奨励金の支給額)、それも題の名前と読む。
|
|
109
|
+
*/
|
|
110
|
+
const titledNames = (doc: ProseDocument, profile: DocumentProfile | undefined, names: readonly string[]): string[] => [
|
|
111
|
+
...doc.sentences.flatMap((sentence) => runsOf(sentence, profile, names).flatMap((run) => (run.placed ? [run.text] : []))),
|
|
112
|
+
...(doc.markup?.headings ?? []).flatMap((heading) => [...heading.text.matchAll(KANJI_RUN)].map((match) => match[0])),
|
|
113
|
+
];
|
|
114
|
+
|
|
70
115
|
/**
|
|
71
116
|
* 漢字が続くと、どこで語が切れるのか読み手が探すことになる。
|
|
72
117
|
* 「情報処理推進機構認定試験」は 12 字。ひらがなを 1 つ挟むだけで読める。
|
|
73
118
|
*/
|
|
74
119
|
export const kanjiRun: Detector = (doc, options): Finding[] => {
|
|
75
|
-
const
|
|
120
|
+
const patterns = (id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
|
|
121
|
+
const words: NameWords = {
|
|
122
|
+
topUnits: new Set(patterns("prefecture-unit")),
|
|
123
|
+
organisation: { forms: patterns("organisation-form"), units: patterns("organisation-unit") },
|
|
124
|
+
title: { endings: patterns("title-ending") },
|
|
125
|
+
};
|
|
126
|
+
const names = doc.names ?? [];
|
|
127
|
+
const context: RunContext = { profile: doc.profile, names, words, titled: titledNames(doc, doc.profile, names) };
|
|
76
128
|
return doc.sentences
|
|
77
|
-
.map((sentence) => ({ sentence, run: longestKanji(sentence,
|
|
129
|
+
.map((sentence) => ({ sentence, run: longestKanji(sentence, context) }))
|
|
78
130
|
.filter(({ run }) => run.length > options.limit)
|
|
79
131
|
.map(({ sentence, run }) => ({
|
|
80
132
|
rule: "max-kanji-continuous",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Detector, DetectorOptions, Finding, Lexicon, ProseDocument, Sentence, Token } from "../plugin.ts";
|
|
2
2
|
import { isWithinAny, quotedIn } from "../quoted-span.ts";
|
|
3
|
+
import { definitionWordsOf, isParallelDefinition } from "./parallel-definition.ts";
|
|
3
4
|
|
|
4
5
|
// The shape of a clause read from its words: how it ends, a paired word left alone, an adverb that needs a
|
|
5
6
|
// negation, a particle used where another one belongs. Every word comes from a lexicon; this file knows parts of speech.
|
|
@@ -74,13 +75,22 @@ const runFinding = (run: readonly Ended[], limit: number): Finding => {
|
|
|
74
75
|
};
|
|
75
76
|
};
|
|
76
77
|
|
|
77
|
-
/** More than limit sentences in a row within one paragraph that end with the same words. */
|
|
78
|
-
export const endingRun: Detector = (doc, options): Finding[] =>
|
|
79
|
-
|
|
78
|
+
/** More than limit sentences in a row within one paragraph that end with the same words, unless they define different terms in one frame. */
|
|
79
|
+
export const endingRun: Detector = (doc, options): Finding[] => {
|
|
80
|
+
const words = definitionWordsOf(options.lexicon);
|
|
81
|
+
return doc.paragraphs.flatMap((paragraph) =>
|
|
80
82
|
endingRuns(paragraph.sentences)
|
|
81
|
-
.filter(
|
|
83
|
+
.filter(
|
|
84
|
+
(run) =>
|
|
85
|
+
run.length > options.limit &&
|
|
86
|
+
!isParallelDefinition(
|
|
87
|
+
run.map(({ sentence }) => sentence),
|
|
88
|
+
words,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
82
91
|
.map((run) => runFinding(run, options.limit)),
|
|
83
92
|
);
|
|
93
|
+
};
|
|
84
94
|
|
|
85
95
|
// --- unpaired-parallel: a word that lists by pairs (〜たり) used once, followed by another clause -----------------
|
|
86
96
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { Span, StructureKind, StructureNode, Token } from "../plugin.ts";
|
|
1
|
+
import type { Sentence, Span, StructureKind, StructureNode, Token } from "../plugin.ts";
|
|
2
2
|
import { preOrder } from "../tree-walk.ts";
|
|
3
|
+
import { isStatement } from "./technical-docs.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* 形態素解析が数と読んだ語(UD の NumType=Card)。漢数字の「二割」「十五分」も、算用数字と同じ具体的な数。
|
|
@@ -30,3 +31,78 @@ const EVIDENCE: ReadonlySet<StructureKind> = new Set(["reference", "quantity", "
|
|
|
30
31
|
|
|
31
32
|
export const evidenceSpans = (root: StructureNode | undefined): Span[] =>
|
|
32
33
|
root === undefined ? [] : preOrder(root).flatMap((node) => (EVIDENCE.has(node.kind) ? [node.span] : []));
|
|
34
|
+
|
|
35
|
+
/** 読み手にすることを伝える言い方。英語は文の書き出し(Never use, Please submit)、日本語は文末(〜ください、〜しましょう)。 */
|
|
36
|
+
export type AdviceWords = { readonly openers: readonly string[]; readonly endings: readonly string[] };
|
|
37
|
+
|
|
38
|
+
const CLOSERS = new Set(Array.from("\"'”’」』))】〕〉》]]"));
|
|
39
|
+
const TERMINALS = new Set(Array.from("。..!!"));
|
|
40
|
+
const QUESTION_MARKS = new Set(["?", "?"]);
|
|
41
|
+
const SKIPPED = new Set(["PUNCT", "SYM", "X", "SPACE"]);
|
|
42
|
+
|
|
43
|
+
/** 文末から、閉じ括弧・引用符(と、渡されれば句点)を外した残り。 */
|
|
44
|
+
const withoutTail = (text: string, tail: ReadonlySet<string>): string => {
|
|
45
|
+
const chars = Array.from(text.trimEnd());
|
|
46
|
+
const last = chars.findLastIndex((char) => !tail.has(char) && char.trim() !== "");
|
|
47
|
+
return chars.slice(0, last + 1).join("");
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const isQuestion = (text: string): boolean => QUESTION_MARKS.has(Array.from(withoutTail(text, CLOSERS)).at(-1) ?? "");
|
|
51
|
+
|
|
52
|
+
const opensWith = (text: string, openers: readonly string[]): boolean => {
|
|
53
|
+
const lower = text.trimStart().toLowerCase();
|
|
54
|
+
return openers.some((opener) => lower.startsWith(`${opener} `));
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const endsWith = (text: string, endings: readonly string[]): boolean => {
|
|
58
|
+
const body = withoutTail(text, new Set([...CLOSERS, ...TERMINALS]));
|
|
59
|
+
return endings.some((ending) => body.endsWith(ending));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** 項目の番号(a, 12, iv)。 */
|
|
63
|
+
const ENUMERATOR = /^(?:\p{L}|\p{Nd}{1,3}|[ivx]{1,4})$/iu;
|
|
64
|
+
const BRACKETED = /^[((](.+)[))]$/u;
|
|
65
|
+
const MARKED = /^(.+)[.)).]$/u;
|
|
66
|
+
|
|
67
|
+
/** 項目の番号だけが文として切り出されたもの(a. や (1))。述べることも問うことも無い。 */
|
|
68
|
+
export const isLabelOnly = (sentence: Pick<Sentence, "text">): boolean => {
|
|
69
|
+
const text = sentence.text.trim();
|
|
70
|
+
const core = BRACKETED.exec(text)?.[1] ?? MARKED.exec(text)?.[1];
|
|
71
|
+
return core !== undefined && ENUMERATOR.test(core);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** 動詞の目的語の書き出し(Use a generator, Review our benefits)。名詞と読まれた語の後ろにこれが来れば、その語は動詞。 */
|
|
75
|
+
const OBJECT_OPENER = new Set(["DET", "PRON"]);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 原形の動詞(Keep, Take)。解析器が名詞と読んだ、動詞にもなる語も、目的語が続けば同じ(Use a generator)。
|
|
79
|
+
* 後ろが名詞や前置詞なら名詞句の頭(evidence of complexity, trust development plans)。
|
|
80
|
+
*/
|
|
81
|
+
const isBaseVerb = (token: Token, next: Token | undefined): boolean =>
|
|
82
|
+
(token.pos === "VERB" && token.lemma?.toLowerCase() === token.surface.toLowerCase() && token.features?.["VerbForm"] === undefined) ||
|
|
83
|
+
(token.pos === "NOUN" && token.features?.["AlsoVerb"] === "Yes" && token.features["Number"] !== "Plur" && OBJECT_OPENER.has(next?.pos ?? ""));
|
|
84
|
+
|
|
85
|
+
/** 頭の語が一語として立っている。空白を置かずに続けば、語の一部(Cloud.gov の Cloud)。 */
|
|
86
|
+
const standsAlone = (tokens: readonly Token[], head: Token): boolean => {
|
|
87
|
+
const after = tokens[tokens.indexOf(head) + 1];
|
|
88
|
+
return after === undefined || after.span.start > head.span.end || after === tokens.at(-1) || after.surface === ",";
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 動詞で始まる英語の文(Keep freezers closed)は指示。すぐ後ろがコロンなら見出し語(Release: none)で、主語のある文は述べる文。
|
|
93
|
+
* 日本語は動詞が文末に来るので、品詞では読まない。
|
|
94
|
+
*/
|
|
95
|
+
const opensWithVerb = (tokens: readonly Token[], language: string): boolean => {
|
|
96
|
+
const [head, next] = tokens.filter((token) => !SKIPPED.has(token.pos) || token.surface === ":");
|
|
97
|
+
return language === "en" && head !== undefined && isBaseVerb(head, next) && standsAlone(tokens, head) && next?.surface !== ":" && !isStatement(tokens);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 読み手が裏づけを求められることを述べた文。問い(情報提供の依頼に並ぶ質問)と指示(安全の手引き)は主張ではなく、
|
|
102
|
+
* 数字が欠けているとは言えない。
|
|
103
|
+
*/
|
|
104
|
+
export const isClaim = (sentence: Pick<Sentence, "text" | "tokens">, language: string, words: AdviceWords): boolean =>
|
|
105
|
+
!isQuestion(sentence.text) &&
|
|
106
|
+
!opensWith(sentence.text, words.openers) &&
|
|
107
|
+
!endsWith(sentence.text, words.endings) &&
|
|
108
|
+
!opensWithVerb(sentence.tokens ?? [], language);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 1 つの対(「don't」と「do not」)が、文書の中でそれぞれ何回出てくるか。 */
|
|
2
|
+
export type PairUses = { readonly short: number; readonly long: number };
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* その対で少ないほうの書きかた。"short" なら短縮形、"long" なら長い形、少数派が無ければ undefined。
|
|
6
|
+
* 片方しか出てこない対は揃っている。両方が同じ回数なら、どちらを指しても当て推量になる。
|
|
7
|
+
*/
|
|
8
|
+
export const pairMinority = (uses: PairUses): "short" | "long" | undefined => {
|
|
9
|
+
if (uses.short === 0 || uses.long === 0 || uses.short === uses.long) return undefined;
|
|
10
|
+
return uses.short < uses.long ? "short" : "long";
|
|
11
|
+
};
|
|
@@ -27,3 +27,26 @@ export const leadingCounter = (tokens: readonly Token[], start: number, run: str
|
|
|
27
27
|
if (counter === undefined || !isCounter(counter) || !isNumber(wordBefore(tokens, at))) return undefined;
|
|
28
28
|
return run.startsWith(counter.surface) ? counter : undefined;
|
|
29
29
|
};
|
|
30
|
+
|
|
31
|
+
const isPrefix = (token: Token | undefined): boolean => token?.pos === "ADJ" && token.features?.["Bound"] === "Yes";
|
|
32
|
+
|
|
33
|
+
/** at の語のすぐ後ろの語。空白一つ(改行を含む)を挟んでもよい(「第\n172条」)。 */
|
|
34
|
+
const wordAfter = (tokens: readonly Token[], at: number): Token | undefined => {
|
|
35
|
+
const [token, next] = [tokens[at], tokens[at + 1]];
|
|
36
|
+
if (token === undefined || next === undefined || !touches(token, next)) return undefined;
|
|
37
|
+
if (!BLANK.test(next.surface)) return next;
|
|
38
|
+
const beyond = tokens[at + 2];
|
|
39
|
+
return beyond !== undefined && touches(next, beyond) ? beyond : undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 漢字の連なり run(文書全体の座標で end に終わる)の尻が、すぐ後ろの数に付く接頭の語(地方自治法施行令第172条の「第」)なら、その語。
|
|
44
|
+
* 接頭の語は後ろの数の一部で、前の複合語の一部ではない。そうでなければ undefined。
|
|
45
|
+
*/
|
|
46
|
+
export const trailingPrefix = (tokens: readonly Token[], end: number, run: string): Token | undefined => {
|
|
47
|
+
// 折り返しをまたぐ語は span が surface より長いので、end で終わる語ではなく end の直前の字を覆う語を探す。
|
|
48
|
+
const at = tokens.findIndex((token) => token.span.start < end && end <= token.span.end);
|
|
49
|
+
const prefix = tokens[at];
|
|
50
|
+
if (prefix === undefined || !isPrefix(prefix) || !isNumber(wordAfter(tokens, at))) return undefined;
|
|
51
|
+
return run.endsWith(prefix.surface) ? prefix : undefined;
|
|
52
|
+
};
|
|
@@ -9,10 +9,14 @@ export const YEAR_SPAN_JOINER_LEXICON = "year-span-joiner";
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Where a marker reaches. `sentence`: earlier in the date's sentence, not closed off by a bracket (例えば…4月1日), or the
|
|
12
|
-
* line a list of dates hangs from ("for example:"). `
|
|
13
|
-
*
|
|
12
|
+
* line a list of dates hangs from ("for example:"). `example`: as `sentence`, and on to the end of the list item it is in.
|
|
13
|
+
* `adjacent`: just before the date ("each December 31"). `near`: ending a few characters before it (決算日は、12月31日).
|
|
14
|
+
* `label`: before the bracket the date is in (年末年始閉庁日(12月29日~1月3日)). `continues`: opening a list item just
|
|
15
|
+
* after an example's item, it carries the example on ("- For example, … June 22." then "- If instead … June 25").
|
|
14
16
|
*/
|
|
15
|
-
export type MarkerReach = "sentence" | "adjacent" | "label";
|
|
17
|
+
export type MarkerReach = "sentence" | "example" | "adjacent" | "near" | "label" | "continues";
|
|
18
|
+
|
|
19
|
+
export const MARKER_REACHES: readonly MarkerReach[] = ["sentence", "example", "adjacent", "near", "label", "continues"];
|
|
16
20
|
|
|
17
21
|
export type AnyYearMarker = { readonly word: string; readonly reach: MarkerReach };
|
|
18
22
|
|
|
@@ -25,6 +29,9 @@ const SPAN_GAP_REACH = 12;
|
|
|
25
29
|
/** How many lines above a list item its introducing line is looked for: a longer list is read as no example. */
|
|
26
30
|
const LIST_REACH = 64;
|
|
27
31
|
|
|
32
|
+
/** How many characters a `near` marker may end before the date: 「は、」 of 「決算日は、」, " is " of "fiscal year end is". */
|
|
33
|
+
const NEAR_REACH = 4;
|
|
34
|
+
|
|
28
35
|
/** The end of a sentence: 。, a line break, or a full stop before a capital or a digit ("Mar 3. The", "so. 10 November"). */
|
|
29
36
|
const SENTENCE_END = /[。!?\n]|[.!?]["'”’)\]]*\s+(?=[A-Z0-9])/u;
|
|
30
37
|
const OPENERS = "((「『[【〔";
|
|
@@ -109,17 +116,56 @@ const introOf = (source: string, before: string, offset: number): string | undef
|
|
|
109
116
|
/** The line ends with the marker, before or after its colon: 「例:」 and "for example:" alike. */
|
|
110
117
|
const introEndsWith = (intro: string, word: string): boolean => isAdjacent(intro, word) || isAdjacent(intro.replace(COLON_END, ""), word);
|
|
111
118
|
|
|
119
|
+
const wordsOf = (markers: readonly AnyYearMarker[], reach: MarkerReach): string[] =>
|
|
120
|
+
markers.flatMap((marker) => (marker.reach === reach && marker.word !== "" ? [latinLower(marker.word)] : []));
|
|
121
|
+
|
|
122
|
+
/** A list item's text after its bullet: "If instead …" of " - If instead …". */
|
|
123
|
+
const itemText = (line: string): string => line.replace(LIST_ITEM, "");
|
|
124
|
+
|
|
125
|
+
const indentOf = (line: string): number => line.length - line.trimStart().length;
|
|
126
|
+
|
|
127
|
+
/** The line just above the one at `offset`, when it is a list item at the same depth (no blank line between). */
|
|
128
|
+
const itemAbove = (source: string, offset: number): string | undefined => {
|
|
129
|
+
const start = lineStart(source, offset);
|
|
130
|
+
if (start === 0) return undefined;
|
|
131
|
+
const above = source.slice(lineStart(source, start - 1), start - 1);
|
|
132
|
+
return LIST_ITEM.test(above) && indentOf(above) === indentOf(source.slice(start, offset)) ? above : undefined;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const opensWith = (text: string, word: string): boolean => text.startsWith(word) && !runsOn(text, word);
|
|
136
|
+
|
|
137
|
+
const opensWithAny = (line: string, words: readonly string[]): boolean => words.some((word) => opensWith(latinLower(itemText(line)), word));
|
|
138
|
+
|
|
139
|
+
/** `item` holds an example, or carries one on from the item above it; `steps` bounds how far up the chain is followed. */
|
|
140
|
+
const exampleItem = (source: string, item: string, itemEnd: number, markers: readonly AnyYearMarker[], steps: number): boolean => {
|
|
141
|
+
if (wordsOf(markers, "example").some((word) => lastWordAt(latinLower(item), word) !== -1)) return true;
|
|
142
|
+
if (steps === 0 || !opensWithAny(item, wordsOf(markers, "continues"))) return false;
|
|
143
|
+
const above = itemAbove(source, itemEnd);
|
|
144
|
+
return above !== undefined && exampleItem(source, above, lineStart(source, itemEnd) - 1, markers, steps - 1);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** The date's list item opens with a word that carries on the example in the list item just above it. */
|
|
148
|
+
const continuesExample = (date: DateSpan, before: string, source: string, markers: readonly AnyYearMarker[]): boolean => {
|
|
149
|
+
if (!LIST_ITEM.test(before) || !opensWithAny(before, wordsOf(markers, "continues"))) return false;
|
|
150
|
+
const above = itemAbove(source, date.offset);
|
|
151
|
+
return above !== undefined && exampleItem(source, above, lineStart(source, date.offset) - 1, markers, LIST_REACH);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const coversDate = (before: string, dateStart: string, intro: string | undefined, word: string, reach: MarkerReach): boolean => {
|
|
155
|
+
if (reach === "adjacent") return isAdjacent(latinLower(before), word);
|
|
156
|
+
if (reach === "near") return lastWordAt(latinLower(before.slice(-(word.length + NEAR_REACH))), word) !== -1;
|
|
157
|
+
if (reach === "label") return latinLower(labelOf(before) ?? "").includes(word);
|
|
158
|
+
if (reach === "continues") return false;
|
|
159
|
+
const inItem = reach === "example" && LIST_ITEM.test(before) && lastWordAt(latinLower(before), word) !== -1;
|
|
160
|
+
return inItem || inSentence(before, dateStart, word) || (intro !== undefined && introEndsWith(latinLower(intro), word));
|
|
161
|
+
};
|
|
162
|
+
|
|
112
163
|
/** The date is a recurring day or a worked example: a marker stands where its reach covers the date. */
|
|
113
164
|
export const anyYearDate = (date: DateSpan, source: string, markers: readonly AnyYearMarker[]): boolean => {
|
|
114
165
|
const before = source.slice(lineStart(source, date.offset), date.offset);
|
|
115
166
|
const intro = introOf(source, before, date.offset);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (word === "") return false;
|
|
119
|
-
if (reach === "adjacent") return isAdjacent(latinLower(before), word);
|
|
120
|
-
if (reach === "label") return latinLower(labelOf(before) ?? "").includes(word);
|
|
121
|
-
return inSentence(before, source.charAt(date.offset), word) || (intro !== undefined && introEndsWith(latinLower(intro), word));
|
|
122
|
-
});
|
|
167
|
+
const covered = markers.some(({ word, reach }) => word !== "" && coversDate(before, source.charAt(date.offset), intro, latinLower(word), reach));
|
|
168
|
+
return covered || continuesExample(date, before, source, markers);
|
|
123
169
|
};
|
|
124
170
|
|
|
125
171
|
/** The date opens a period whose other end, just after it, follows a joiner (「4月1日から翌年3月31日」). */
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// The year a section's heading gives the dates under it: "FY27-Q2 (May 1)", 「2026年度の予定」, 「令和8年の行事」. A yearless
|
|
2
|
+
// date in such a section has its year. Pure.
|
|
3
|
+
import type { StructureNode } from "../plugin.ts";
|
|
4
|
+
|
|
5
|
+
/** A fiscal-year name with its year as a number: "FY{y}" of the language's reporting-period-frame lexicon. */
|
|
6
|
+
export type YearLabel = { readonly before: string; readonly after: string };
|
|
7
|
+
|
|
8
|
+
/** The words that name a year in a heading: era names (令和) and fiscal-year labels ("FY{y}"). */
|
|
9
|
+
export type HeadingYearWords = { readonly eras: readonly string[]; readonly labels: readonly YearLabel[] };
|
|
10
|
+
|
|
11
|
+
/** A year written as figures: 2024年度, "2024 events". */
|
|
12
|
+
const YEAR_FIGURES = /(?<![0-90-9])(?:1[89]|20)\d{2}(?![0-90-9])/u;
|
|
13
|
+
/** An era's year after its name: 8年 of 令和8年, 元年 of 令和元年度 (not 8月). */
|
|
14
|
+
const ERA_YEAR_NUMBER = /^[ \t]?(?:[0-90-9]{1,2}|元)[ \t]?年/u;
|
|
15
|
+
/** A fiscal year's number: two figures (FY27) or four (FY2027). */
|
|
16
|
+
const LABEL_YEAR = /^(?:\d{2}|\d{4})(?!\d)/u;
|
|
17
|
+
const LETTER = /\p{L}/u;
|
|
18
|
+
|
|
19
|
+
/** "FY{y}" as the lexicon writes it: the words before and after the year. A name with no {y} names no year. */
|
|
20
|
+
export const yearLabelOf = (pattern: string): YearLabel | undefined => {
|
|
21
|
+
const at = pattern.indexOf("{y}");
|
|
22
|
+
return at === -1 || pattern.includes("{", at + 1) ? undefined : { before: pattern.slice(0, at).toLowerCase(), after: pattern.slice(at + 3).toLowerCase() };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const startsAt = (text: string, index: number): boolean => index === 0 || !LETTER.test(text.charAt(index - 1));
|
|
26
|
+
|
|
27
|
+
const indicesOf = (text: string, word: string, from = 0): number[] => {
|
|
28
|
+
const at = word === "" ? -1 : text.indexOf(word, from);
|
|
29
|
+
return at === -1 ? [] : [at, ...indicesOf(text, word, at + 1)];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const namesLabelYear = (heading: string, label: YearLabel): boolean => {
|
|
33
|
+
const text = heading.toLowerCase();
|
|
34
|
+
return indicesOf(text, label.before).some((at) => {
|
|
35
|
+
if (!startsAt(text, at)) return false;
|
|
36
|
+
const year = LABEL_YEAR.exec(text.slice(at + label.before.length))?.[0];
|
|
37
|
+
return year !== undefined && text.startsWith(label.after, at + label.before.length + year.length);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const namesEraYear = (heading: string, era: string): boolean => indicesOf(heading, era).some((at) => ERA_YEAR_NUMBER.test(heading.slice(at + era.length)));
|
|
42
|
+
|
|
43
|
+
/** The heading names a year: figures (2026年度), an era's year (令和8年), or a fiscal year (FY27). */
|
|
44
|
+
export const headingNamesYear = (heading: string, words: HeadingYearWords): boolean =>
|
|
45
|
+
YEAR_FIGURES.test(heading) || words.eras.some((era) => namesEraYear(heading, era)) || words.labels.some((label) => namesLabelYear(heading, label));
|
|
46
|
+
|
|
47
|
+
const sectionHeading = (node: StructureNode, current: string): string =>
|
|
48
|
+
node.kind === "section" && node.attrs["heading"] !== undefined ? String(node.attrs["heading"]) : current;
|
|
49
|
+
|
|
50
|
+
/** The offsets of the dates whose own section, the innermost one they are in, has a heading that names a year. */
|
|
51
|
+
export const datesUnderYearHeadings = (tree: StructureNode, words: HeadingYearWords, heading = ""): number[] => {
|
|
52
|
+
const own = sectionHeading(tree, heading);
|
|
53
|
+
if (tree.kind === "date") return headingNamesYear(own, words) ? [tree.span.start] : [];
|
|
54
|
+
return tree.children.flatMap((child) => datesUnderYearHeadings(child, words, own));
|
|
55
|
+
};
|
|
@@ -3,15 +3,27 @@
|
|
|
3
3
|
import type { Detector, Finding, LexiconEntry, ProseDocument, StructureNode } from "../plugin.ts";
|
|
4
4
|
import { inDocumentOrder } from "../structure/issues.ts";
|
|
5
5
|
import { quoteAt } from "./structure-tree.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ANY_YEAR_MARKER_LEXICON,
|
|
8
|
+
MARKER_REACHES,
|
|
9
|
+
YEAR_SPAN_JOINER_LEXICON,
|
|
10
|
+
anyYearDate,
|
|
11
|
+
opensSpan,
|
|
12
|
+
type AnyYearMarker,
|
|
13
|
+
type MarkerReach,
|
|
14
|
+
} from "./date-any-year.ts";
|
|
15
|
+
import { datesUnderYearHeadings, yearLabelOf, type HeadingYearWords } from "./date-heading-year.ts";
|
|
7
16
|
|
|
8
17
|
/** The words that make a yearless date any year's (毎年, 例えば, "each") and that join a period's two ends (から, "to"). */
|
|
9
18
|
export type AnyYearWords = { readonly markers: readonly AnyYearMarker[]; readonly joiners: readonly string[] };
|
|
10
19
|
|
|
11
20
|
const NO_ANY_YEAR_WORDS: AnyYearWords = { markers: [], joiners: [] };
|
|
12
21
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
22
|
+
/**
|
|
23
|
+
* A date as the tree reads it: "2026-10-14" with its year, "10-14" without. `yearHeaded`: the heading of its section
|
|
24
|
+
* names a year ("FY27-Q2 (May 1)", 「2026年度の予定」), which the date takes.
|
|
25
|
+
*/
|
|
26
|
+
export type DatePoint = { readonly offset: number; readonly end: number; readonly value: string; readonly yearHeaded?: boolean };
|
|
15
27
|
|
|
16
28
|
/** The first and the last year of the document's dated dates. */
|
|
17
29
|
export type YearSpan = { readonly from: number; readonly to: number };
|
|
@@ -106,9 +118,23 @@ const hasYearWord = (point: DatePoint, source: string, yearWords: readonly YearW
|
|
|
106
118
|
*/
|
|
107
119
|
const yearGiven = (point: DatePoint, anchors: readonly DatePoint[], source: string, yearWords: readonly YearWord[]): boolean => {
|
|
108
120
|
const start = lineStart(source, point.offset);
|
|
109
|
-
return hasYearWord(point, source, yearWords) || anchors.some((other) => other.offset >= start && other.offset < point.offset);
|
|
121
|
+
return point.yearHeaded === true || hasYearWord(point, source, yearWords) || anchors.some((other) => other.offset >= start && other.offset < point.offset);
|
|
110
122
|
};
|
|
111
123
|
|
|
124
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
125
|
+
const COMMON_YEAR_MS = 365 * DAY_MS;
|
|
126
|
+
/** A year with no 29 February, so that "03-01" follows "02-28" as it does in most years. */
|
|
127
|
+
const COMMON_YEAR = 2025;
|
|
128
|
+
|
|
129
|
+
const dayOf = (value: string): number => Date.UTC(COMMON_YEAR, Number(value.slice(0, 2)) - 1, Number(value.slice(3, 5)));
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A period of one whole year: its end is the day before its start (January 1 to December 31, 4月1日から3月31日). Without
|
|
133
|
+
* a year it is every year's, like a calendar or a fiscal year.
|
|
134
|
+
*/
|
|
135
|
+
export const wholeYear = (start: string, end: string): boolean =>
|
|
136
|
+
MONTH_DAY.test(start) && MONTH_DAY.test(end) && (dayOf(end) + DAY_MS - dayOf(start)) % COMMON_YEAR_MS === 0;
|
|
137
|
+
|
|
112
138
|
/**
|
|
113
139
|
* The yearless dates whose year the reader cannot tell. Where most dates carry a year (at least two, and more than the
|
|
114
140
|
* yearless ones) and those years differ, every yearless one is reported: in a one-year document the year is plain. Where
|
|
@@ -124,9 +150,13 @@ export const yearGaps = (
|
|
|
124
150
|
const dated = points.filter((point) => FULL.test(point.value));
|
|
125
151
|
const anchors = [...dated, ...points.filter((point) => hasYearWord(point, source, yearWords))];
|
|
126
152
|
const nextOf = new Map(points.map((point, index) => [point, points[index + 1]]));
|
|
153
|
+
const previousOf = new Map(points.map((point, index) => [point, points[index - 1]]));
|
|
154
|
+
const yearlySpan = (start: DatePoint | undefined, end: DatePoint | undefined): boolean =>
|
|
155
|
+
start !== undefined && end !== undefined && wholeYear(start.value, end.value) && opensSpan(start, end, source, anyYear.joiners);
|
|
127
156
|
const yearOfAny = (point: DatePoint): boolean => {
|
|
128
157
|
const next = nextOf.get(point);
|
|
129
158
|
if (anyYearDate(point, source, anyYear.markers)) return true;
|
|
159
|
+
if (yearlySpan(point, next) || yearlySpan(previousOf.get(point), point)) return true;
|
|
130
160
|
return next !== undefined && opensSpan(point, next, source, anyYear.joiners) && hasYearWord(next, source, yearWords);
|
|
131
161
|
};
|
|
132
162
|
// A recurring day or an example's date has no year to write, and is not the document's way of writing dates either.
|
|
@@ -134,20 +164,35 @@ export const yearGaps = (
|
|
|
134
164
|
const span = spanOf(dated);
|
|
135
165
|
const unclear = dated.length >= 2 && yearless.length < dated.length && span !== undefined && span.to > span.from;
|
|
136
166
|
const datedSides = new Set(dated.flatMap((point) => sideOf(monthOf(FULL, point.value)) ?? []));
|
|
137
|
-
const previousOf = new Map(points.map((point, index) => [point, points[index - 1]]));
|
|
138
167
|
return yearless.flatMap((point): YearGap[] => {
|
|
139
168
|
if (unclear) return [{ point, kind: "years", span }];
|
|
140
169
|
return crossesYear(point, previousOf.get(point), datedSides) ? [{ point, kind: "boundary", span }] : [];
|
|
141
170
|
});
|
|
142
171
|
};
|
|
143
172
|
|
|
144
|
-
|
|
145
|
-
|
|
173
|
+
const NO_HEADING_YEAR_WORDS: HeadingYearWords = { eras: [], labels: [] };
|
|
174
|
+
|
|
175
|
+
export const datePoints = (tree: StructureNode, headingWords: HeadingYearWords = NO_HEADING_YEAR_WORDS): DatePoint[] => {
|
|
176
|
+
const yearHeaded = new Set(datesUnderYearHeadings(tree, headingWords));
|
|
177
|
+
return inDocumentOrder(tree).flatMap((node) =>
|
|
178
|
+
node.kind === "date"
|
|
179
|
+
? [{ offset: node.span.start, end: node.span.end, value: String(node.attrs["value"]), yearHeaded: yearHeaded.has(node.span.start) }]
|
|
180
|
+
: [],
|
|
181
|
+
);
|
|
182
|
+
};
|
|
146
183
|
|
|
147
184
|
const yearWordsOf = (doc: ProseDocument): YearWord[] =>
|
|
148
185
|
(doc.lexicons[YEAR_REFERENCE_LEXICON] ?? []).map((entry: LexiconEntry) => ({ word: entry.pattern, position: entry.position }));
|
|
149
186
|
|
|
150
|
-
|
|
187
|
+
/** The era names (calendar-era) and the fiscal-year names ("FY{y}" of reporting-period-frame) a heading may give a year with. */
|
|
188
|
+
const headingYearWordsOf = (doc: ProseDocument): HeadingYearWords => ({
|
|
189
|
+
eras: (doc.lexicons["calendar-era"] ?? []).map((entry: LexiconEntry) => entry.pattern),
|
|
190
|
+
labels: (doc.lexicons["reporting-period-frame"] ?? []).flatMap((entry: LexiconEntry) =>
|
|
191
|
+
entry.group === "fiscal-year" ? (yearLabelOf(entry.pattern) ?? []) : [],
|
|
192
|
+
),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const reachOf = (group: string | undefined): MarkerReach => MARKER_REACHES.find((reach) => reach === group) ?? "sentence";
|
|
151
196
|
|
|
152
197
|
const anyYearWordsOf = (doc: ProseDocument): AnyYearWords => ({
|
|
153
198
|
markers: (doc.lexicons[ANY_YEAR_MARKER_LEXICON] ?? []).map((entry: LexiconEntry) => ({ word: entry.pattern, reach: reachOf(entry.group) })),
|
|
@@ -157,7 +202,7 @@ const anyYearWordsOf = (doc: ProseDocument): AnyYearWords => ({
|
|
|
157
202
|
export const dateWithoutYear: Detector = (doc: ProseDocument): Finding[] =>
|
|
158
203
|
doc.structure === undefined
|
|
159
204
|
? []
|
|
160
|
-
: yearGaps(datePoints(doc.structure), doc.source, yearWordsOf(doc), anyYearWordsOf(doc)).map((gap) => ({
|
|
205
|
+
: yearGaps(datePoints(doc.structure, headingYearWordsOf(doc)), doc.source, yearWordsOf(doc), anyYearWordsOf(doc)).map((gap) => ({
|
|
161
206
|
rule: "date-without-year",
|
|
162
207
|
severity: "warning",
|
|
163
208
|
line: 0,
|
|
@@ -199,11 +199,31 @@ const MIN_NAME_CHARS = 4;
|
|
|
199
199
|
/** In English a long name has two words or more (Acme Corporation); one capitalised word is the term's own kind of word. */
|
|
200
200
|
const MIN_NAME_WORDS = 2;
|
|
201
201
|
const LATIN = /\p{Script=Latin}/u;
|
|
202
|
+
const LETTER = /\p{L}/u;
|
|
203
|
+
const JAPANESE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u;
|
|
202
204
|
|
|
203
205
|
const isNamePart = (token: Token): boolean => NAME_PARTS.has(token.pos) || NAME_MARKS.has(token.surface);
|
|
204
206
|
|
|
207
|
+
/**
|
|
208
|
+
* A label for each of several things or for all of them together ((each, a "Certificate"), (collectively, the "Plans"))
|
|
209
|
+
* names the head of the clause before it (a certificate that represented any share of Common Stock), not its last words.
|
|
210
|
+
*/
|
|
211
|
+
const LABELS_THE_CLAUSE = /^[((](?:each|collectively)\b/iu;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Whether the words make a long name. In English, two words with letters, one of them capitalised: a figure is not a word
|
|
215
|
+
* of a name (as amended on January 26, 2015 (the "Indenture"), a par value of $1.00 (the "Preference Stock")), and lower-case
|
|
216
|
+
* words (with respect thereto) are used in their own sense. A name with Japanese in it (株式会社ABC), four characters or more.
|
|
217
|
+
*/
|
|
218
|
+
const isLongName = (name: string): boolean => {
|
|
219
|
+
const words = name.split(/\s+/u).filter((word) => LETTER.test(word));
|
|
220
|
+
if (words.length === 0 || [...name].length < MIN_NAME_CHARS) return false;
|
|
221
|
+
return !LATIN.test(name) || JAPANESE.test(name) || (words.length >= MIN_NAME_WORDS && words.some((word) => UPPER_START.test(word)));
|
|
222
|
+
};
|
|
223
|
+
|
|
205
224
|
/** The long name written right before an inline definition: the run of name words that ends at its opening bracket. */
|
|
206
225
|
export const nameBefore = (sentence: Sentence, bracket: number): string | undefined => {
|
|
226
|
+
if (LABELS_THE_CLAUSE.test(sentence.text.slice(bracket - sentence.span.start))) return undefined;
|
|
207
227
|
const tokens = (sentence.tokens ?? []).filter((token) => token.span.end <= bracket);
|
|
208
228
|
const firstOfRun = tokens.findLastIndex((token) => !isNamePart(token)) + 1;
|
|
209
229
|
if (isModifier(tokens[firstOfRun - 1])) return undefined;
|
|
@@ -212,16 +232,17 @@ export const nameBefore = (sentence: Sentence, bracket: number): string | undefi
|
|
|
212
232
|
const end = run.at(-1);
|
|
213
233
|
if (first === undefined || end === undefined || bracket - end.span.end > 1) return undefined;
|
|
214
234
|
const name = withoutTrailingMarks(sentence.text.slice(first.span.start - sentence.span.start, end.span.end - sentence.span.start));
|
|
215
|
-
|
|
216
|
-
return [...name].length < MIN_NAME_CHARS || (LATIN.test(name) && words < MIN_NAME_WORDS) ? undefined : name;
|
|
235
|
+
return isLongName(name) ? name : undefined;
|
|
217
236
|
};
|
|
218
237
|
|
|
219
238
|
/**
|
|
220
|
-
* A word that narrows the name before it: an article, an adjective, a verb, or
|
|
221
|
-
* The words then name a kind of thing the definition picks out, not one
|
|
239
|
+
* A word that narrows the name before it: an article, an adjective, a verb, or a possessive (妊娠中の女性従業員, the Beta
|
|
240
|
+
* Preview, 本利用規約, users of Customer's Web site). The words then name a kind of thing the definition picks out, not one
|
|
241
|
+
* party, and are used in their own sense again.
|
|
222
242
|
*/
|
|
223
243
|
const MODIFIERS = new Set(["DET", "ADJ", "VERB", "AUX"]);
|
|
224
|
-
const
|
|
244
|
+
const POSSESSIVES = new Set(["の", "'s", "’s", "'", "’"]);
|
|
245
|
+
const isModifier = (token: Token | undefined): boolean => token !== undefined && (MODIFIERS.has(token.pos) || POSSESSIVES.has(token.surface));
|
|
225
246
|
|
|
226
247
|
const TRAILING_MARKS = new Set([",", ".", " "]);
|
|
227
248
|
|