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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readMarkdown } from "../markdown-read.ts";
|
|
2
|
+
import { spanOf, type MarkdownNode } from "../markdown-node.ts";
|
|
3
|
+
import type { Span } from "../plugin.ts";
|
|
4
|
+
|
|
5
|
+
const COMMENT = /<!--[\s\S]*?(?:-->|$)/gu;
|
|
6
|
+
const INLINE_CONTAINERS = new Set(["paragraph", "heading", "tableCell"]);
|
|
7
|
+
const ENTITY = /&(?:#\d+|#x[\da-f]+|[a-z]+\d*);/giu;
|
|
8
|
+
|
|
9
|
+
type TagScan = { readonly kept: string[]; inTag: boolean; quote: string | undefined };
|
|
10
|
+
|
|
11
|
+
/** Reads one character of HTML: text outside a tag is kept. A quoted attribute may hold `>` (`<div title=">">`). */
|
|
12
|
+
const scanned = (scan: TagScan, char: string): TagScan => {
|
|
13
|
+
if (!scan.inTag) {
|
|
14
|
+
if (char === "<") scan.inTag = true;
|
|
15
|
+
else scan.kept.push(char);
|
|
16
|
+
} else if (scan.quote !== undefined) {
|
|
17
|
+
if (char === scan.quote) scan.quote = undefined;
|
|
18
|
+
} else if (char === '"' || char === "'") scan.quote = char;
|
|
19
|
+
else if (char === ">") scan.inTag = false;
|
|
20
|
+
return scan;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const withoutTags = (html: string): string => [...html].reduce<TagScan>(scanned, { kept: [], inTag: false, quote: undefined }).kept.join("");
|
|
24
|
+
|
|
25
|
+
/** What an HTML block shows the reader: its text without tags, comments and character references such as ` `. */
|
|
26
|
+
const showsText = (html: string): boolean => withoutTags(html.replace(COMMENT, "")).replace(ENTITY, "").trim() !== "";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Markdown's HTML blocks that show text. CommonMark runs a block that starts with a tag alone on its line (`<br/>`) to
|
|
30
|
+
* the next blank line, so a `第11条` written right under it is raw HTML: the reader sees it, chaff's structure does not.
|
|
31
|
+
* Only blocks are returned; an inline tag inside a paragraph (`text<br/>`) leaves the text beside it readable.
|
|
32
|
+
*/
|
|
33
|
+
export const htmlTextBlocks = (source: string): Span[] => {
|
|
34
|
+
if (!source.includes("<")) return [];
|
|
35
|
+
const blocks: Span[] = [];
|
|
36
|
+
const visit = (node: MarkdownNode, inParagraph: boolean): void => {
|
|
37
|
+
if (node.type === "html" && !inParagraph && showsText(node.value ?? "")) {
|
|
38
|
+
const span = spanOf(node);
|
|
39
|
+
if (span !== undefined) blocks.push(span);
|
|
40
|
+
}
|
|
41
|
+
(node.children ?? []).forEach((child) => visit(child, inParagraph || INLINE_CONTAINERS.has(node.type)));
|
|
42
|
+
};
|
|
43
|
+
visit(readMarkdown(source).root, false);
|
|
44
|
+
return blocks.toSorted((left, right) => left.start - right.start);
|
|
45
|
+
};
|
package/src/structure/issues.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { StructureNode } from "../plugin.ts";
|
|
2
|
+
import { writtenReading } from "./written-resolve.ts";
|
|
3
|
+
import { namedTargets, pointsToNamedSection } from "./section-names.ts";
|
|
2
4
|
import { listedTags } from "./listed-tags.ts";
|
|
3
5
|
import { namesAbsentUnit } from "./unit-word.ts";
|
|
6
|
+
import { isListedParagraph, listedParagraphsByArticle } from "./listed-paragraph.ts";
|
|
7
|
+
import { referencesInCitedDocuments } from "./cited-document-section.ts";
|
|
4
8
|
|
|
5
9
|
// 木だけで決まる誤り。参照先が無い、番号が飛ぶ・重なる、同じ語を二度定義する。
|
|
6
10
|
// 意味の食い違いは判定しない。書いてあることから機械で決まるものだけを出す。
|
|
@@ -111,7 +115,7 @@ const numbering = (node: StructureNode): unknown => node.attrs["numbering"];
|
|
|
111
115
|
* 参照の番地が木に無い。他の文書の名前が付いた参照(民法第709条、Section 9 of the Master Agreement)は引かない。
|
|
112
116
|
* 条を 1 つも持たない文書も見ない。他の文書を指しているだけかもしれない。
|
|
113
117
|
*/
|
|
114
|
-
export const danglingReferences = (tree: StructureNode, source: string): StructureIssue[] => {
|
|
118
|
+
export const danglingReferences = (tree: StructureNode, source: string, nameNouns: readonly string[] = []): StructureIssue[] => {
|
|
115
119
|
const nodes = inDocumentOrder(tree);
|
|
116
120
|
// 参照は条を指す。条を一つも持たない文書(契約書に付ける承諾書のひな形など)の「契約書第6条」は、別の文書の条。
|
|
117
121
|
if (!nodes.some((node) => node.kind === "article")) return [];
|
|
@@ -127,10 +131,21 @@ export const danglingReferences = (tree: StructureNode, source: string): Structu
|
|
|
127
131
|
const articleLabels = nodes.filter((node) => node.kind === "article").map((node) => textOf(node, "label"));
|
|
128
132
|
const chapters = chaptersOf(nodes);
|
|
129
133
|
const elsewhere = elsewhereReferences(nodes, source);
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
const quoted = referencesInCitedDocuments(tree);
|
|
135
|
+
const references = nodes.filter((node) => node.kind === "reference");
|
|
136
|
+
const named = namedTargets(references, source, nameNouns);
|
|
137
|
+
const unresolved = references
|
|
138
|
+
.filter((node) => !citesOther(node) && !otherNumbering(node) && !pointsToNamedSection(node, named))
|
|
132
139
|
.filter((node) => !namesAbsentUnit(node, articleLabels) && !elsewhere.has(node))
|
|
133
|
-
.filter((node) => !resolves(node, addresses, chapters))
|
|
140
|
+
.filter((node) => !quoted.has(node) && !resolves(node, addresses, chapters));
|
|
141
|
+
// 規程は条の下の項を箇条書き(「1. 」「2.」)で書くことがあり、木はそれを項として読まない。条の本文は、要るときに一度だけ読む。
|
|
142
|
+
const listed = unresolved.length === 0 ? new Map() : listedParagraphsByArticle(nodes, source);
|
|
143
|
+
// 平文の契約は見出しを「9. Indemnification.」と本文の形で書き、木はそれを番地にしない。残った参照があるときだけ読む。
|
|
144
|
+
const written = unresolved.length === 0 ? undefined : writtenReading(nodes, source);
|
|
145
|
+
const writtenAddresses = new Set(written?.addresses);
|
|
146
|
+
return unresolved
|
|
147
|
+
.filter((node) => !isListedParagraph(textOf(node, "target"), listed))
|
|
148
|
+
.filter((node) => !writtenAddresses.has(textOf(node, "target")) && written?.subdivision(node) !== true)
|
|
134
149
|
.map((node) => ({ offset: node.span.start, values: { label: textOf(node, "label"), target: textOf(node, "target") } }));
|
|
135
150
|
};
|
|
136
151
|
|
|
@@ -4,6 +4,7 @@ import type { StructureIssue } from "./issues.ts";
|
|
|
4
4
|
import type { Line } from "./lines.ts";
|
|
5
5
|
import { labelPairsOf, pointsOutOfOrder, type OrderLabel, type PairWords, type PointOf } from "./labelled-order.ts";
|
|
6
6
|
import { TABLE_ROW } from "./runs.ts";
|
|
7
|
+
import type { VersionWords } from "./versioned-label.ts";
|
|
7
8
|
import { isSignedDateLine } from "./signed-date-line.ts";
|
|
8
9
|
import { statedPeriod, type DateMention, type PeriodWords } from "./stated-period.ts";
|
|
9
10
|
|
|
@@ -132,6 +133,8 @@ export type PairedReading = {
|
|
|
132
133
|
readonly spans?: PairSpans | undefined;
|
|
133
134
|
readonly whole?: string | undefined;
|
|
134
135
|
readonly signers?: readonly OrderLabel[] | undefined;
|
|
136
|
+
/** The words that join a version to a label (End of support for 5.0.0, 3.0.0 のサポート終了日). */
|
|
137
|
+
readonly versions?: VersionWords | undefined;
|
|
135
138
|
};
|
|
136
139
|
|
|
137
140
|
const signersOf = (signers: readonly OrderLabel[], group: string): string[] =>
|
|
@@ -166,14 +169,14 @@ export const pairedDatesOutOfOrder = (
|
|
|
166
169
|
labels: readonly OrderLabel[],
|
|
167
170
|
passed: readonly string[],
|
|
168
171
|
maxLineGap: number,
|
|
169
|
-
{ spans, whole = source, signers = [] }: PairedReading = {},
|
|
172
|
+
{ spans, whole = source, signers = [], versions }: PairedReading = {},
|
|
170
173
|
): PairedIssue[] =>
|
|
171
174
|
withoutRepeats(
|
|
172
175
|
labelPairsOf(labels).flatMap((pair) => {
|
|
173
176
|
const context = contextOf(labels, pair.group);
|
|
174
177
|
if (context.length > 0 && !namesDocumentKind(whole, context)) return [];
|
|
175
178
|
const fromStart = spanStartsOf(spans?.entries ?? [], pair.group);
|
|
176
|
-
const words = { ...pair, passed, maxLineGap, spans, fromStart, signers: signersOf(signers, pair.group) };
|
|
179
|
+
const words = { ...pair, passed, maxLineGap, spans, fromStart, signers: signersOf(signers, pair.group), versions };
|
|
177
180
|
return datesOutOfOrder(source, dates, words).map(({ earlier, later }) => ({
|
|
178
181
|
offset: Math.max(later.date.offset, earlier.date.offset),
|
|
179
182
|
values: { later: later.date.value, earlier: earlier.date.value, later_label: later.label, earlier_label: earlier.label },
|
|
@@ -2,6 +2,7 @@ import { linesOf, type Line } from "./lines.ts";
|
|
|
2
2
|
import { tableAround } from "./key-value-row.ts";
|
|
3
3
|
import { signerRoleOf } from "./signed-date-line.ts";
|
|
4
4
|
import { isTotalLabel } from "./total.ts";
|
|
5
|
+
import { withoutLabelVersion, type VersionWords } from "./versioned-label.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Two labelled points that must come in order: a line (or a list item, or a table's first column) starting with an
|
|
@@ -24,6 +25,8 @@ export type PairWords = {
|
|
|
24
25
|
* pointOf decides whether the line is one. A signature closes the whole document, so it is read at any distance.
|
|
25
26
|
*/
|
|
26
27
|
readonly signers?: readonly string[] | undefined;
|
|
28
|
+
/** The words that join a version to a label (End of support for 5.0.0, 3.0.0 のサポート終了日). */
|
|
29
|
+
readonly versions?: VersionWords | undefined;
|
|
27
30
|
};
|
|
28
31
|
|
|
29
32
|
/** A labelled point: the word that labels it, as the lexicon writes it. span is set when the point ends a stated period. */
|
|
@@ -36,7 +39,13 @@ export type PointOf = (line: Line, label: string, tableOf: () => readonly string
|
|
|
36
39
|
|
|
37
40
|
type Read = LabelledPoint & { readonly kind: "earlier" | "later"; readonly signed?: boolean | undefined };
|
|
38
41
|
|
|
39
|
-
const
|
|
42
|
+
const isVersionedLabel = (text: string, word: string, versions: VersionWords | undefined): boolean => {
|
|
43
|
+
const bare = versions === undefined ? undefined : withoutLabelVersion(text, word, versions);
|
|
44
|
+
return bare !== undefined && isTotalLabel(bare, [word]);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const labelOf = (text: string, words: readonly string[], versions?: VersionWords): string | undefined =>
|
|
48
|
+
words.find((word) => isTotalLabel(text, [word]) || isVersionedLabel(text, word, versions));
|
|
40
49
|
|
|
41
50
|
const signerOf = (text: string, words: PairWords): Pick<Read, "kind" | "label" | "signed"> | undefined => {
|
|
42
51
|
const role = signerRoleOf(text, words.signers ?? []);
|
|
@@ -44,9 +53,9 @@ const signerOf = (text: string, words: PairWords): Pick<Read, "kind" | "label" |
|
|
|
44
53
|
};
|
|
45
54
|
|
|
46
55
|
const kindOf = (text: string, words: PairWords): Pick<Read, "kind" | "label" | "signed"> | undefined => {
|
|
47
|
-
const earlier = labelOf(text, words.earlier);
|
|
56
|
+
const earlier = labelOf(text, words.earlier, words.versions);
|
|
48
57
|
if (earlier !== undefined) return { kind: "earlier", label: earlier };
|
|
49
|
-
const later = labelOf(text, words.later);
|
|
58
|
+
const later = labelOf(text, words.later, words.versions);
|
|
50
59
|
return later === undefined ? signerOf(text, words) : { kind: "later", label: later };
|
|
51
60
|
};
|
|
52
61
|
|
|
@@ -4,31 +4,120 @@ import type { Span } from "../plugin.ts";
|
|
|
4
4
|
import type { StructureIssue } from "./issues.ts";
|
|
5
5
|
|
|
6
6
|
const ITEM_NUMBER = /^(?<number>\d{1,9})(?<delimiter>[.)])[ \t]/u;
|
|
7
|
+
const TAB_WIDTH = 4;
|
|
7
8
|
|
|
8
|
-
/** One item's written number: where the item starts, the number and the number as written (「4.」). */
|
|
9
|
-
export type WrittenNumber = {
|
|
9
|
+
/** One item's written number: where the item starts and ends, its column, the number and the number as written (「4.」). */
|
|
10
|
+
export type WrittenNumber = {
|
|
11
|
+
readonly offset: number;
|
|
12
|
+
readonly number: number;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly indent?: number;
|
|
15
|
+
readonly end?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** The column an offset sits at, a tab counted to the next stop of four as CommonMark counts it. */
|
|
19
|
+
const columnAt = (source: string, offset: number): number =>
|
|
20
|
+
Array.from(source.slice(source.lastIndexOf("\n", offset - 1) + 1, offset)).reduce(
|
|
21
|
+
(column, char) => (char === "\t" ? column + TAB_WIDTH - (column % TAB_WIDTH) : column + 1),
|
|
22
|
+
0,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const numberAt = (source: string, span: Span): { number: number; label: string } | undefined => {
|
|
26
|
+
const found = ITEM_NUMBER.exec(source.slice(span.start, span.end))?.groups;
|
|
27
|
+
return found === undefined ? undefined : { number: Number(found["number"]), label: `${found["number"] ?? ""}${found["delimiter"] ?? ""}` };
|
|
28
|
+
};
|
|
10
29
|
|
|
11
30
|
/** The number written at the start of each item, or undefined when an item has none (a bulleted list). */
|
|
12
31
|
export const writtenNumbers = (source: string, itemSpans: readonly Span[]): WrittenNumber[] | undefined => {
|
|
13
32
|
const numbers = itemSpans.map((span) => {
|
|
14
|
-
const found =
|
|
15
|
-
return found === undefined
|
|
16
|
-
? undefined
|
|
17
|
-
: { offset: span.start, number: Number(found["number"]), label: `${found["number"] ?? ""}${found["delimiter"] ?? ""}` };
|
|
33
|
+
const found = numberAt(source, span);
|
|
34
|
+
return found === undefined ? undefined : { offset: span.start, end: span.end, ...found, indent: columnAt(source, span.start) };
|
|
18
35
|
});
|
|
19
36
|
return numbers.every((entry) => entry !== undefined) ? numbers : undefined;
|
|
20
37
|
};
|
|
21
38
|
|
|
39
|
+
/** The last number of an ordered list nested in an item, if there is one. */
|
|
40
|
+
export type NestedLast = (item: WrittenNumber) => number | undefined;
|
|
41
|
+
|
|
42
|
+
type OrderedList = { readonly start: number; readonly end: number; readonly last: number };
|
|
43
|
+
|
|
44
|
+
const orderedListOf = (source: string, spans: readonly Span[]): OrderedList[] => {
|
|
45
|
+
const first = spans[0];
|
|
46
|
+
const last = writtenNumbers(source, spans)?.at(-1);
|
|
47
|
+
return first === undefined || last === undefined ? [] : [{ start: first.start, end: last.end ?? last.offset, last: last.number }];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* For an item, the last number of the ordered list inside it that ends last (the outermost one, when lists nested in
|
|
52
|
+
* each other end together). lists are the item spans of every list in the document; bulleted lists are left out.
|
|
53
|
+
*/
|
|
54
|
+
export const nestedLastFinder = (source: string, lists: readonly (readonly Span[])[]): NestedLast => {
|
|
55
|
+
const ordered = lists.flatMap((spans) => orderedListOf(source, spans));
|
|
56
|
+
return (item) =>
|
|
57
|
+
ordered
|
|
58
|
+
.filter((list) => list.start > item.offset && list.end <= (item.end ?? item.offset))
|
|
59
|
+
.reduce<OrderedList | undefined>(
|
|
60
|
+
(best, list) => (best === undefined || list.end > best.end || (list.end === best.end && list.start < best.start) ? list : best),
|
|
61
|
+
undefined,
|
|
62
|
+
)?.last;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type Compared = { readonly item: WrittenNumber; readonly previous: WrittenNumber | undefined };
|
|
66
|
+
|
|
67
|
+
const indentOf = (item: WrittenNumber): number => item.indent ?? 0;
|
|
68
|
+
|
|
22
69
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
70
|
+
* Which item each item's number follows. An item at the same indentation follows the last one there. An item indented
|
|
71
|
+
* deeper starts a nested count (CommonMark can still put it in the same list, when the indentation falls short of the
|
|
72
|
+
* parent's text), unless it carries on the count above it: the parent's number plus one (a slip of indentation), or
|
|
73
|
+
* one past the last number of the list nested in the parent (that list, carried on at a different indentation).
|
|
74
|
+
*/
|
|
75
|
+
const comparedPairs = (items: readonly WrittenNumber[], nestedLast: NestedLast): Compared[] => {
|
|
76
|
+
const open: WrittenNumber[] = [];
|
|
77
|
+
return items.map((item) => {
|
|
78
|
+
const popped: WrittenNumber[] = [];
|
|
79
|
+
for (let top = open.at(-1); top !== undefined && indentOf(top) > indentOf(item); top = open.at(-1)) {
|
|
80
|
+
popped.push(top);
|
|
81
|
+
open.pop();
|
|
82
|
+
}
|
|
83
|
+
const parent = open.at(-1);
|
|
84
|
+
if (parent !== undefined && indentOf(parent) === indentOf(item)) {
|
|
85
|
+
open[open.length - 1] = item;
|
|
86
|
+
return { item, previous: parent };
|
|
87
|
+
}
|
|
88
|
+
if (parent === undefined) {
|
|
89
|
+
open.push(item);
|
|
90
|
+
return { item, previous: popped.at(-1) };
|
|
91
|
+
}
|
|
92
|
+
if (item.number === parent.number + 1) {
|
|
93
|
+
open[open.length - 1] = { ...item, indent: indentOf(parent) };
|
|
94
|
+
return { item, previous: parent };
|
|
95
|
+
}
|
|
96
|
+
open.push(item);
|
|
97
|
+
return { item, previous: item.number === 1 || item.number === (nestedLast(parent) ?? 0) + 1 ? undefined : parent };
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Each item whose number is not one more than the item it follows. A list that writes one number on every item (「1.」
|
|
103
|
+
* on each, which Markdown counts up) is numbered by the renderer, not by the writer, and is not read. An item numbered 1
|
|
25
104
|
* starts a new count, as the structure rule reads a 1 that follows other items.
|
|
26
105
|
*/
|
|
27
|
-
export const listNumberBreaks = (items: readonly WrittenNumber[]): StructureIssue[] => {
|
|
106
|
+
export const listNumberBreaks = (items: readonly WrittenNumber[], nestedLast: NestedLast = () => undefined): StructureIssue[] => {
|
|
28
107
|
if (new Set(items.map((item) => item.number)).size < 2) return [];
|
|
29
|
-
return items.flatMap((item,
|
|
30
|
-
const previous = items[index - 1];
|
|
108
|
+
return comparedPairs(items, nestedLast).flatMap(({ item, previous }) => {
|
|
31
109
|
if (previous === undefined || item.number === previous.number + 1 || item.number === 1) return [];
|
|
32
110
|
return [{ offset: item.offset, values: { previous: previous.label, label: item.label, expected: previous.number + 1, found: item.number } }];
|
|
33
111
|
});
|
|
34
112
|
};
|
|
113
|
+
|
|
114
|
+
type ListSpans = { readonly span: Span; readonly itemSpans: readonly Span[] };
|
|
115
|
+
|
|
116
|
+
/** The breaks in the document's lists that are checked; every list, checked or not, tells which ordered list sits inside an item. */
|
|
117
|
+
export const listsNumberBreaks = (source: string, lists: readonly ListSpans[], isChecked: (list: ListSpans) => boolean): StructureIssue[] => {
|
|
118
|
+
const nestedLast = nestedLastFinder(
|
|
119
|
+
source,
|
|
120
|
+
lists.map((list) => list.itemSpans),
|
|
121
|
+
);
|
|
122
|
+
return lists.filter(isChecked).flatMap((list) => listNumberBreaks(writtenNumbers(source, list.itemSpans) ?? [], nestedLast));
|
|
123
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { StructureNode } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
// 箇条書きで書いた項。規程は条の下の項を「1. 」「2.」の箇条書きで書くことがあり、木はそれを項として読まない。
|
|
4
|
+
|
|
5
|
+
/** 行頭の箇条書きの番号(「1. 」「2.」)。前の空白(全角も)を飛ばし、全角の数字と点も読む。 */
|
|
6
|
+
const LIST_MARK = /^[ \t\u3000]*(?<n>[0-90-9]{1,3})[..]/u;
|
|
7
|
+
|
|
8
|
+
/** 本文の行頭に箇条書きで書かれた番号。全角は半角の数にする(「2.」は 2)。Pure. */
|
|
9
|
+
export const listedNumbers = (body: string): ReadonlySet<number> =>
|
|
10
|
+
new Set(
|
|
11
|
+
body.split("\n").flatMap((line) => {
|
|
12
|
+
const written = LIST_MARK.exec(line)?.groups?.["n"];
|
|
13
|
+
return written === undefined ? [] : [Number(written.normalize("NFKC"))];
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
/** 「3.3」を条の番地「3」と項の番号 3 に分ける。項の先まで指す(3.3.1)か、項が数でなければ undefined。Pure. */
|
|
18
|
+
export const articleParagraph = (target: string): { readonly article: string; readonly paragraph: number } | undefined => {
|
|
19
|
+
const parts = target.split(".");
|
|
20
|
+
const [article, paragraph] = parts;
|
|
21
|
+
if (parts.length !== 2 || article === undefined || paragraph === undefined || !/^\d{1,3}$/u.test(paragraph)) return undefined;
|
|
22
|
+
return { article, paragraph: Number(paragraph) };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 木が項を一つも読まなかった条ごとの、箇条書きの番号。条の本文は一度だけ読む。同じ番地の条が二つあれば、どちらかに書いてあればよい。
|
|
27
|
+
* nodes は木を平らにしたもの。Pure.
|
|
28
|
+
*/
|
|
29
|
+
export const listedParagraphsByArticle = (nodes: readonly StructureNode[], source: string): ReadonlyMap<string, ReadonlySet<number>> => {
|
|
30
|
+
const byArticle = new Map<string, Set<number>>();
|
|
31
|
+
nodes
|
|
32
|
+
.filter((node) => node.kind === "article" && !node.children.some((child) => child.kind === "item"))
|
|
33
|
+
.forEach((article) => {
|
|
34
|
+
const numbers = byArticle.get(article.address) ?? new Set<number>();
|
|
35
|
+
listedNumbers(source.slice(article.span.start, article.span.end)).forEach((number) => numbers.add(number));
|
|
36
|
+
byArticle.set(article.address, numbers);
|
|
37
|
+
});
|
|
38
|
+
return byArticle;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** 参照(第3条第3項)の項を、その条の箇条書きが書いているか。Pure. */
|
|
42
|
+
export const isListedParagraph = (target: string, listed: ReadonlyMap<string, ReadonlySet<number>>): boolean => {
|
|
43
|
+
const wanted = articleParagraph(target);
|
|
44
|
+
return wanted !== undefined && listed.get(wanted.article)?.has(wanted.paragraph) === true;
|
|
45
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StructureNode } from "../plugin.ts";
|
|
1
|
+
import type { Span, StructureNode } from "../plugin.ts";
|
|
2
2
|
import { inDocumentOrder, type StructureIssue } from "./issues.ts";
|
|
3
3
|
import { numbersQuotedInside } from "./quoted-number.ts";
|
|
4
4
|
import { isVersionHistory } from "./version-order.ts";
|
|
@@ -7,15 +7,21 @@ const labelOf = (node: StructureNode): string => String(node.attrs["label"] ?? n
|
|
|
7
7
|
|
|
8
8
|
type Ordered = { readonly node: StructureNode; readonly ordinal: number; readonly last: number };
|
|
9
9
|
|
|
10
|
+
const OPENING_BRACKET = /^[([{〔【〈《]/u;
|
|
11
|
+
|
|
12
|
+
/** 括弧で囲んだ番号((1)、(a))か。全角と半角は同じに見る。 */
|
|
13
|
+
const isBracketed = (label: string): boolean => OPENING_BRACKET.test(label.normalize("NFKC"));
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
|
-
*
|
|
16
|
+
* 同じ親の子のうち、番号の付いたものを種類と深さと書き方ごとに分ける。条と項は別の並び、項と号も別の並び。
|
|
12
17
|
* 法令の第 1 項は番号が無いので、その号(一、二)と第 2 項(2)は同じ条の直下に並ぶ。深さで分けないと「二の次が2」に見える。
|
|
18
|
+
* 社内規程は号の(1)(2)と第 2 項の「2」を同じ深さに読むことがあるので、括弧で囲んだ番号は囲まない番号と別の並びにする。
|
|
13
19
|
*/
|
|
14
20
|
const sequencesOf = (parent: StructureNode, quoting: ReadonlySet<StructureNode>): Ordered[][] => {
|
|
15
21
|
const groups = new Map<string, Ordered[]>();
|
|
16
22
|
parent.children.forEach((node) => {
|
|
17
23
|
if (node.ordinal === undefined || quoting.has(node)) return;
|
|
18
|
-
const key = `${node.kind}/${String(node.level ?? "")}`;
|
|
24
|
+
const key = `${node.kind}/${String(node.level ?? "")}/${isBracketed(labelOf(node)) ? "bracketed" : "bare"}`;
|
|
19
25
|
// 配列を作り直さずに足す。条が何千もある法令で二乗にしない。
|
|
20
26
|
const group = groups.get(key) ?? [];
|
|
21
27
|
group.push({ node, ordinal: node.ordinal, last: node.ordinalTo ?? node.ordinal });
|
|
@@ -24,20 +30,42 @@ const sequencesOf = (parent: StructureNode, quoting: ReadonlySet<StructureNode>)
|
|
|
24
30
|
return [...groups.values()];
|
|
25
31
|
};
|
|
26
32
|
|
|
27
|
-
/**
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
/** start が after より後ろの最初の範囲。unread は始まりの順に並ぶ。 */
|
|
34
|
+
const firstStartingAfter = (unread: readonly Span[], after: number): number => {
|
|
35
|
+
let low = 0;
|
|
36
|
+
let high = unread.length;
|
|
37
|
+
while (low < high) {
|
|
38
|
+
const middle = Math.floor((low + high) / 2);
|
|
39
|
+
if ((unread[middle]?.start ?? Infinity) > after) high = middle;
|
|
40
|
+
else low = middle + 1;
|
|
41
|
+
}
|
|
42
|
+
return low;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** 二つの番号のあいだに、読めなかった範囲が始まるか。 */
|
|
46
|
+
const unreadBetween = (unread: readonly Span[], previous: Ordered, current: Ordered): boolean =>
|
|
47
|
+
(unread[firstStartingAfter(unread, previous.node.span.start)]?.start ?? Infinity) < current.node.span.start;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 隣り合う二つの番号が、1 つ進んでいなければ指摘する。同じ番号なら重なり、飛べば抜け。
|
|
51
|
+
* あいだに読めなかった範囲(unread)があれば、抜けた番号はそこにあるかもしれないので言わない。重なりは言う。
|
|
52
|
+
*/
|
|
53
|
+
const breaksIn =
|
|
54
|
+
(unread: readonly Span[]) =>
|
|
55
|
+
(sequence: readonly Ordered[]): StructureIssue[] =>
|
|
56
|
+
sequence.flatMap((current, index) => {
|
|
57
|
+
const previous = sequence[index - 1];
|
|
58
|
+
// 1 に戻ったら新しい並び。一つの条に (a)(b) の箇条書きが二つある、附則が第1条から振り直す、のどちらも誤りではない。
|
|
59
|
+
// 範囲をまとめた行(第四十三条から第五十五条まで)の次は、範囲の最後から数える。
|
|
60
|
+
if (previous === undefined || current.ordinal === previous.last + 1 || current.ordinal === 1) return [];
|
|
61
|
+
if (current.ordinal > previous.last + 1 && unreadBetween(unread, previous, current)) return [];
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
offset: current.node.span.start,
|
|
65
|
+
values: { previous: labelOf(previous.node), label: labelOf(current.node), expected: previous.last + 1, found: current.ordinal },
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
});
|
|
41
69
|
|
|
42
70
|
/**
|
|
43
71
|
* 番号の抜けと重なり。比べるのは同じ親の子どうしだけ。英米の法令は章ごとに Section 101、201 と百の位を変えるので、
|
|
@@ -45,12 +73,13 @@ const breaksIn = (sequence: readonly Ordered[]): StructureIssue[] =>
|
|
|
45
73
|
* 1 に戻った番号は新しい並びの始まりとして扱うので、「(a) の後の (a)」の重なりは見逃す。
|
|
46
74
|
* 同じ番号の節を中に持つ節(ほかの文書の条を引いた手本の見出し)は、並びに入れない。
|
|
47
75
|
* 変更履歴の版の見出し(## 3.1.0)は番号の並びではない。版の順番は version-order が見る。
|
|
76
|
+
* unread は、読み手には見えるが木に入らない範囲(Markdown の HTML ブロックの中の文字)を、始まりの順に並べたもの。
|
|
48
77
|
*/
|
|
49
|
-
export const numberingBreaks = (tree: StructureNode): StructureIssue[] => {
|
|
78
|
+
export const numberingBreaks = (tree: StructureNode, unread: readonly Span[] = []): StructureIssue[] => {
|
|
50
79
|
const quoting = numbersQuotedInside(tree);
|
|
51
80
|
return inDocumentOrder(tree)
|
|
52
81
|
.flatMap((parent) => sequencesOf(parent, quoting))
|
|
53
82
|
.filter((sequence) => !isVersionHistory(sequence.map((ordered) => labelOf(ordered.node))))
|
|
54
|
-
.flatMap(breaksIn)
|
|
83
|
+
.flatMap(breaksIn(unread))
|
|
55
84
|
.toSorted((left, right) => left.offset - right.offset);
|
|
56
85
|
};
|
|
@@ -16,6 +16,8 @@ export type ReferenceWords = {
|
|
|
16
16
|
readonly topicLinks: readonly string[];
|
|
17
17
|
/** 話題にならない語(詳細、内容、details)。どの節にも当てはまる。 */
|
|
18
18
|
readonly vagueTopics: readonly string[];
|
|
19
|
+
/** 二つの参照を一つの並びにする語(及び、から、and、to)。並びの後ろの括弧は並び全体の名前。 */
|
|
20
|
+
readonly listJoints: readonly string[];
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
export type ReferenceInput = {
|
|
@@ -70,17 +72,37 @@ export const holdsAside = (name: string, aside: string): boolean => {
|
|
|
70
72
|
return aside.trim() !== "" && words.includes(` ${aside.trim().toLowerCase()} `);
|
|
71
73
|
};
|
|
72
74
|
|
|
75
|
+
/** 句点で終わる括弧や引用符の中は名前でなく文(範囲の断り「…を除く。」、引いた条文)。見出しは句点で終わらない。 */
|
|
76
|
+
const SENTENCE_STOP = /[。.]$/u;
|
|
77
|
+
|
|
78
|
+
export const endsAsSentence = (name: string): boolean => SENTENCE_STOP.test(name);
|
|
79
|
+
|
|
73
80
|
const isName = (name: string, words: ReferenceWords): boolean =>
|
|
74
|
-
LETTER.test(name) &&
|
|
81
|
+
LETTER.test(name) &&
|
|
82
|
+
!DIGIT.test(name) &&
|
|
83
|
+
!ITEM_LABEL.test(name.normalize("NFKC")) &&
|
|
84
|
+
!endsAsSentence(name) &&
|
|
85
|
+
!words.asides.some((aside) => holdsAside(name, aside));
|
|
75
86
|
|
|
76
87
|
const agrees = (name: string, heading: string): boolean => {
|
|
77
88
|
const [a, b] = [normalized(name), normalized(heading)];
|
|
78
89
|
return a.includes(b) || b.includes(a);
|
|
79
90
|
};
|
|
80
91
|
|
|
81
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* The reference ends a list or range of references ("sections 162 and 164", "sections 9 to 11"): only a joint stands
|
|
94
|
+
* between it and the reference before it.
|
|
95
|
+
*/
|
|
96
|
+
export const endsList = (between: string, joints: readonly string[]): boolean => {
|
|
97
|
+
const joint = between.normalize("NFKC").trim().toLowerCase();
|
|
98
|
+
return joint !== "" && joints.some((candidate) => candidate.normalize("NFKC").trim().toLowerCase() === joint);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** 名前を添えた参照で、名前が参照先の見出しと合わない。並びの後ろの括弧は並び全体の名前なので、どの見出しとも比べない。 */
|
|
82
102
|
export const titleMismatches = (input: ReferenceInput): StructureIssue[] =>
|
|
83
|
-
input.references.flatMap((reference) => {
|
|
103
|
+
input.references.flatMap((reference, index) => {
|
|
104
|
+
const previous = input.references[index - 1];
|
|
105
|
+
if (previous !== undefined && endsList(input.source.slice(previous.span.end, reference.span.start), input.words.listJoints)) return [];
|
|
84
106
|
const target = input.targetOf(reference);
|
|
85
107
|
const heading = target === undefined ? "" : headingOf(target);
|
|
86
108
|
const found = nameAfter(input.source, reference.span.end);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { StructureNode } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
// 番号で呼ぶ他の法律の手続き。英国の「section 21 notice」は Housing Act 1988 の section 21 の通知で、この文書の section 21 ではない。
|
|
4
|
+
|
|
5
|
+
/** 参照のすぐ後ろの語。 */
|
|
6
|
+
const NEXT_WORD = /^[ \t]+(?<word>\p{L}+)/u;
|
|
7
|
+
const LOWER_START = /^\p{Ll}/u;
|
|
8
|
+
/** 後ろの語を読む長さ。名前を作る語は短い。 */
|
|
9
|
+
const NEXT_WORD_LENGTH = 24;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 名前の一部として書かれた番号(「section 21 notice」)の行き先。小文字で書いた参照の後ろに、名前を作る語(語彙表)が続くもの。
|
|
13
|
+
* 大文字の「Section 5 notice」は契約が自分の条を指す書き方なので当たらない。Pure.
|
|
14
|
+
*/
|
|
15
|
+
export const namedTargets = (references: readonly StructureNode[], source: string, nouns: readonly string[]): ReadonlySet<string> => {
|
|
16
|
+
if (nouns.length === 0) return new Set();
|
|
17
|
+
const names = new Set(nouns);
|
|
18
|
+
return new Set(
|
|
19
|
+
references.flatMap((reference) => {
|
|
20
|
+
const label = String(reference.attrs["label"] ?? "");
|
|
21
|
+
const word = NEXT_WORD.exec(source.slice(reference.span.end, reference.span.end + NEXT_WORD_LENGTH))?.groups?.["word"];
|
|
22
|
+
return LOWER_START.test(label) && word !== undefined && names.has(word) ? [String(reference.attrs["target"] ?? "")] : [];
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 名前として書かれた番号を指す参照か。小文字で書いた参照(「under section 21」)だけ。同じ文書の大文字の「Section 21」は、
|
|
29
|
+
* 文書が自分の section を指す書き方なので、名前があっても探す。Pure.
|
|
30
|
+
*/
|
|
31
|
+
export const pointsToNamedSection = (reference: StructureNode, named: ReadonlySet<string>): boolean =>
|
|
32
|
+
LOWER_START.test(String(reference.attrs["label"] ?? "")) && named.has(String(reference.attrs["target"] ?? ""));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A label naming the version it is about: the version after the label and a word (End of support for 5.0.0:), or before
|
|
3
|
+
* it with a word (3.0.0 のサポート終了日:). The words come from the language's lexicon; a version is digits joined by dots,
|
|
4
|
+
* with or without a leading v. Pure.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** The words that join a version to a label: after for the label, word, version order; before for version, word, label. */
|
|
8
|
+
export type VersionWords = { readonly after: readonly string[]; readonly before: readonly string[] };
|
|
9
|
+
|
|
10
|
+
/** A list marker, a table cell's bar and emphasis before the label. A numbered marker needs its space: 3.0.0 is a version, not item 3. */
|
|
11
|
+
const LINE_LEAD = /^[ \t]*(?:(?:[-*+]|\d{1,3}[.)])[ \t]+)?(?:\|[ \t]*)?[*_]*/u;
|
|
12
|
+
const VERSION = /^v?\d+(?:\.\d+)+/iu;
|
|
13
|
+
/** After the label's version, only the label's end: a colon or a table cell's bar. */
|
|
14
|
+
const VERSION_END = /^[ \t]*[::|]/u;
|
|
15
|
+
const SPACES = /^[ \t\u3000]*/u;
|
|
16
|
+
|
|
17
|
+
const spacesAt = (text: string): number => SPACES.exec(text)?.[0].length ?? 0;
|
|
18
|
+
|
|
19
|
+
const versionAt = (text: string): number => VERSION.exec(text)?.[0].length ?? 0;
|
|
20
|
+
|
|
21
|
+
/** After the label: one word, spaces on both sides, a version and the label's end. Returns the label's end on. */
|
|
22
|
+
const afterVersion = (rest: string, word: string): string | undefined => {
|
|
23
|
+
const gap = spacesAt(rest);
|
|
24
|
+
if (gap === 0 || !rest.slice(gap).toLowerCase().startsWith(`${word.toLowerCase()} `)) return undefined;
|
|
25
|
+
const from = gap + word.length + spacesAt(rest.slice(gap + word.length));
|
|
26
|
+
const version = versionAt(rest.slice(from));
|
|
27
|
+
const tail = rest.slice(from + version);
|
|
28
|
+
return version > 0 && VERSION_END.test(tail) ? tail : undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Before the label: a version, the word and the label, spaces allowed between. Returns the text from the label on. */
|
|
32
|
+
const beforeVersion = (lead: string, word: string, label: string): string | undefined => {
|
|
33
|
+
const version = versionAt(lead);
|
|
34
|
+
if (version === 0) return undefined;
|
|
35
|
+
const atWord = version + spacesAt(lead.slice(version));
|
|
36
|
+
if (!lead.slice(atWord).startsWith(word)) return undefined;
|
|
37
|
+
const atLabel = atWord + word.length + spacesAt(lead.slice(atWord + word.length));
|
|
38
|
+
return lead.slice(atLabel).toLowerCase().startsWith(label.toLowerCase()) ? lead.slice(atLabel) : undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The line read as the bare label: "End of support for 5.0.0: …" as "End of support: …", "3.0.0 のサポート終了日:…" as
|
|
43
|
+
* "サポート終了日:…". undefined when the line does not start with the label carrying a version that way.
|
|
44
|
+
*/
|
|
45
|
+
export const withoutLabelVersion = (text: string, label: string, words: VersionWords): string | undefined => {
|
|
46
|
+
const lead = text.replace(LINE_LEAD, "");
|
|
47
|
+
const after = lead.toLowerCase().startsWith(label.toLowerCase())
|
|
48
|
+
? words.after.map((word) => afterVersion(lead.slice(label.length), word)).find((tail) => tail !== undefined)
|
|
49
|
+
: undefined;
|
|
50
|
+
if (after !== undefined) return `${lead.slice(0, label.length)}${after}`;
|
|
51
|
+
return words.before.map((word) => beforeVersion(lead, word, label)).find((rest) => rest !== undefined);
|
|
52
|
+
};
|