chaffjs 0.20.0 → 0.22.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/README.md +7 -5
- package/dist/api.d.ts +16 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js.map +1 -1
- package/dist/cli-text.d.ts +2 -0
- package/dist/cli-text.d.ts.map +1 -1
- package/dist/cli-text.js +18 -5
- package/dist/cli-text.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -11
- package/dist/cli.js.map +1 -1
- package/dist/command-help.d.ts +7 -0
- package/dist/command-help.d.ts.map +1 -0
- package/dist/command-help.js +18 -0
- package/dist/command-help.js.map +1 -0
- package/dist/commands/eval.js +2 -2
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/fix-plan.d.ts.map +1 -1
- package/dist/commands/fix-plan.js +11 -1
- package/dist/commands/fix-plan.js.map +1 -1
- package/dist/commands/help.d.ts +4 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +7 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/init-command.d.ts +4 -0
- package/dist/commands/init-command.d.ts.map +1 -0
- package/dist/commands/init-command.js +15 -0
- package/dist/commands/init-command.js.map +1 -0
- package/dist/commands/plugin-init.d.ts +20 -0
- package/dist/commands/plugin-init.d.ts.map +1 -0
- package/dist/commands/plugin-init.js +129 -0
- package/dist/commands/plugin-init.js.map +1 -0
- package/dist/commands/plugin-test.d.ts +11 -0
- package/dist/commands/plugin-test.d.ts.map +1 -0
- package/dist/commands/plugin-test.js +81 -0
- package/dist/commands/plugin-test.js.map +1 -0
- package/dist/config/load.d.ts +2 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +1 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/option-problems.js +2 -2
- package/dist/config/option-problems.js.map +1 -1
- package/dist/config/rule-problems.js +2 -2
- package/dist/config/rule-problems.js.map +1 -1
- package/dist/custom/checked.d.ts +9 -0
- package/dist/custom/checked.d.ts.map +1 -0
- package/dist/custom/checked.js +3 -0
- package/dist/custom/checked.js.map +1 -0
- package/dist/custom/dsl-fields.d.ts +27 -0
- package/dist/custom/dsl-fields.d.ts.map +1 -0
- package/dist/custom/dsl-fields.js +86 -0
- package/dist/custom/dsl-fields.js.map +1 -0
- package/dist/custom/load.d.ts.map +1 -1
- package/dist/custom/load.js +2 -0
- package/dist/custom/load.js.map +1 -1
- package/dist/custom/parse.d.ts +14 -3
- package/dist/custom/parse.d.ts.map +1 -1
- package/dist/custom/parse.js +44 -40
- package/dist/custom/parse.js.map +1 -1
- package/dist/custom/problems.d.ts.map +1 -1
- package/dist/custom/problems.js +10 -1
- package/dist/custom/problems.js.map +1 -1
- package/dist/detectors/amount-for-count.d.ts +21 -0
- package/dist/detectors/amount-for-count.d.ts.map +1 -0
- package/dist/detectors/amount-for-count.js +55 -0
- package/dist/detectors/amount-for-count.js.map +1 -0
- package/dist/detectors/blank-structure.d.ts.map +1 -1
- package/dist/detectors/blank-structure.js +6 -4
- package/dist/detectors/blank-structure.js.map +1 -1
- package/dist/detectors/clause-shape.d.ts +22 -0
- package/dist/detectors/clause-shape.d.ts.map +1 -0
- package/dist/detectors/clause-shape.js +149 -0
- package/dist/detectors/clause-shape.js.map +1 -0
- package/dist/detectors/code-fences.d.ts +34 -0
- package/dist/detectors/code-fences.d.ts.map +1 -0
- package/dist/detectors/code-fences.js +100 -0
- package/dist/detectors/code-fences.js.map +1 -0
- package/dist/detectors/cross-definitions.d.ts +4 -0
- package/dist/detectors/cross-definitions.d.ts.map +1 -0
- package/dist/detectors/cross-definitions.js +43 -0
- package/dist/detectors/cross-definitions.js.map +1 -0
- package/dist/detectors/cross-facts.d.ts +4 -0
- package/dist/detectors/cross-facts.d.ts.map +1 -0
- package/dist/detectors/cross-facts.js +53 -0
- package/dist/detectors/cross-facts.js.map +1 -0
- package/dist/detectors/cross-registry/cross-doc-duplicate-definition.d.ts +3 -0
- package/dist/detectors/cross-registry/cross-doc-duplicate-definition.d.ts.map +1 -0
- package/dist/detectors/cross-registry/cross-doc-duplicate-definition.js +3 -0
- package/dist/detectors/cross-registry/cross-doc-duplicate-definition.js.map +1 -0
- package/dist/detectors/cross-registry/cross-doc-fact-conflict.d.ts +3 -0
- package/dist/detectors/cross-registry/cross-doc-fact-conflict.d.ts.map +1 -0
- package/dist/detectors/cross-registry/cross-doc-fact-conflict.js +3 -0
- package/dist/detectors/cross-registry/cross-doc-fact-conflict.js.map +1 -0
- package/dist/detectors/currency-notation.d.ts +29 -0
- package/dist/detectors/currency-notation.d.ts.map +1 -0
- package/dist/detectors/currency-notation.js +58 -0
- package/dist/detectors/currency-notation.js.map +1 -0
- package/dist/detectors/custom.d.ts.map +1 -1
- package/dist/detectors/custom.js +4 -1
- package/dist/detectors/custom.js.map +1 -1
- package/dist/detectors/dangling-opener.d.ts +14 -0
- package/dist/detectors/dangling-opener.d.ts.map +1 -0
- package/dist/detectors/dangling-opener.js +55 -0
- package/dist/detectors/dangling-opener.js.map +1 -0
- package/dist/detectors/defined-term-form.d.ts +25 -0
- package/dist/detectors/defined-term-form.d.ts.map +1 -0
- package/dist/detectors/defined-term-form.js +170 -0
- package/dist/detectors/defined-term-form.js.map +1 -0
- package/dist/detectors/email-letter.d.ts +46 -0
- package/dist/detectors/email-letter.d.ts.map +1 -0
- package/dist/detectors/email-letter.js +234 -0
- package/dist/detectors/email-letter.js.map +1 -0
- package/dist/detectors/footnote-mismatch.d.ts +11 -0
- package/dist/detectors/footnote-mismatch.d.ts.map +1 -0
- package/dist/detectors/footnote-mismatch.js +55 -0
- package/dist/detectors/footnote-mismatch.js.map +1 -0
- package/dist/detectors/form-minority.d.ts +13 -0
- package/dist/detectors/form-minority.d.ts.map +1 -0
- package/dist/detectors/form-minority.js +19 -0
- package/dist/detectors/form-minority.js.map +1 -0
- package/dist/detectors/heading-end-mark.d.ts +10 -0
- package/dist/detectors/heading-end-mark.d.ts.map +1 -0
- package/dist/detectors/heading-end-mark.js +59 -0
- package/dist/detectors/heading-end-mark.js.map +1 -0
- package/dist/detectors/impossible-date.d.ts +20 -0
- package/dist/detectors/impossible-date.d.ts.map +1 -0
- package/dist/detectors/impossible-date.js +105 -0
- package/dist/detectors/impossible-date.js.map +1 -0
- package/dist/detectors/impossible-time.d.ts +18 -0
- package/dist/detectors/impossible-time.d.ts.map +1 -0
- package/dist/detectors/impossible-time.js +116 -0
- package/dist/detectors/impossible-time.js.map +1 -0
- package/dist/detectors/link-shape.d.ts +11 -0
- package/dist/detectors/link-shape.d.ts.map +1 -0
- package/dist/detectors/link-shape.js +56 -0
- package/dist/detectors/link-shape.js.map +1 -0
- package/dist/detectors/list-marker.d.ts +7 -0
- package/dist/detectors/list-marker.d.ts.map +1 -0
- package/dist/detectors/list-marker.js +50 -0
- package/dist/detectors/list-marker.js.map +1 -0
- package/dist/detectors/mark-shape.d.ts +53 -0
- package/dist/detectors/mark-shape.d.ts.map +1 -0
- package/dist/detectors/mark-shape.js +160 -0
- package/dist/detectors/mark-shape.js.map +1 -0
- package/dist/detectors/markdown-slip.d.ts +19 -0
- package/dist/detectors/markdown-slip.d.ts.map +1 -0
- package/dist/detectors/markdown-slip.js +122 -0
- package/dist/detectors/markdown-slip.js.map +1 -0
- package/dist/detectors/numeral-opener.d.ts +10 -0
- package/dist/detectors/numeral-opener.d.ts.map +1 -0
- package/dist/detectors/numeral-opener.js +65 -0
- package/dist/detectors/numeral-opener.js.map +1 -0
- package/dist/detectors/ordinal-suffix.d.ts +12 -0
- package/dist/detectors/ordinal-suffix.d.ts.map +1 -0
- package/dist/detectors/ordinal-suffix.js +33 -0
- package/dist/detectors/ordinal-suffix.js.map +1 -0
- package/dist/detectors/range-notation.d.ts +15 -0
- package/dist/detectors/range-notation.d.ts.map +1 -0
- package/dist/detectors/range-notation.js +58 -0
- package/dist/detectors/range-notation.js.map +1 -0
- package/dist/detectors/registry/adverb-polarity.d.ts +3 -0
- package/dist/detectors/registry/adverb-polarity.d.ts.map +1 -0
- package/dist/detectors/registry/adverb-polarity.js +3 -0
- package/dist/detectors/registry/adverb-polarity.js.map +1 -0
- package/dist/detectors/registry/amount-for-count.d.ts +3 -0
- package/dist/detectors/registry/amount-for-count.d.ts.map +1 -0
- package/dist/detectors/registry/amount-for-count.js +3 -0
- package/dist/detectors/registry/amount-for-count.js.map +1 -0
- package/dist/detectors/registry/attachment-missing.d.ts +3 -0
- package/dist/detectors/registry/attachment-missing.d.ts.map +1 -0
- package/dist/detectors/registry/attachment-missing.js +3 -0
- package/dist/detectors/registry/attachment-missing.js.map +1 -0
- package/dist/detectors/registry/bare-url-mix.d.ts +3 -0
- package/dist/detectors/registry/bare-url-mix.d.ts.map +1 -0
- package/dist/detectors/registry/bare-url-mix.js +3 -0
- package/dist/detectors/registry/bare-url-mix.js.map +1 -0
- package/dist/detectors/registry/caps-shouting.d.ts +3 -0
- package/dist/detectors/registry/caps-shouting.d.ts.map +1 -0
- package/dist/detectors/registry/caps-shouting.js +3 -0
- package/dist/detectors/registry/caps-shouting.js.map +1 -0
- package/dist/detectors/registry/code-fence-language.d.ts +3 -0
- package/dist/detectors/registry/code-fence-language.d.ts.map +1 -0
- package/dist/detectors/registry/code-fence-language.js +3 -0
- package/dist/detectors/registry/code-fence-language.js.map +1 -0
- package/dist/detectors/registry/currency-notation.d.ts +3 -0
- package/dist/detectors/registry/currency-notation.d.ts.map +1 -0
- package/dist/detectors/registry/currency-notation.js +3 -0
- package/dist/detectors/registry/currency-notation.js.map +1 -0
- package/dist/detectors/registry/dangling-opener.d.ts +3 -0
- package/dist/detectors/registry/dangling-opener.d.ts.map +1 -0
- package/dist/detectors/registry/dangling-opener.js +3 -0
- package/dist/detectors/registry/dangling-opener.js.map +1 -0
- package/dist/detectors/registry/defined-name-repeated.d.ts +3 -0
- package/dist/detectors/registry/defined-name-repeated.d.ts.map +1 -0
- package/dist/detectors/registry/defined-name-repeated.js +3 -0
- package/dist/detectors/registry/defined-name-repeated.js.map +1 -0
- package/dist/detectors/registry/defined-term-form.d.ts +3 -0
- package/dist/detectors/registry/defined-term-form.d.ts.map +1 -0
- package/dist/detectors/registry/defined-term-form.js +3 -0
- package/dist/detectors/registry/defined-term-form.js.map +1 -0
- package/dist/detectors/registry/email-frame.d.ts +3 -0
- package/dist/detectors/registry/email-frame.d.ts.map +1 -0
- package/dist/detectors/registry/email-frame.js +3 -0
- package/dist/detectors/registry/email-frame.js.map +1 -0
- package/dist/detectors/registry/email-subject.d.ts +3 -0
- package/dist/detectors/registry/email-subject.d.ts.map +1 -0
- package/dist/detectors/registry/email-subject.js +3 -0
- package/dist/detectors/registry/email-subject.js.map +1 -0
- package/dist/detectors/registry/ending-run.d.ts +3 -0
- package/dist/detectors/registry/ending-run.d.ts.map +1 -0
- package/dist/detectors/registry/ending-run.js +3 -0
- package/dist/detectors/registry/ending-run.js.map +1 -0
- package/dist/detectors/registry/footnote-mismatch.d.ts +3 -0
- package/dist/detectors/registry/footnote-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/footnote-mismatch.js +3 -0
- package/dist/detectors/registry/footnote-mismatch.js.map +1 -0
- package/dist/detectors/registry/heading-command.d.ts +3 -0
- package/dist/detectors/registry/heading-command.d.ts.map +1 -0
- package/dist/detectors/registry/heading-command.js +3 -0
- package/dist/detectors/registry/heading-command.js.map +1 -0
- package/dist/detectors/registry/heading-end-punctuation.d.ts +3 -0
- package/dist/detectors/registry/heading-end-punctuation.d.ts.map +1 -0
- package/dist/detectors/registry/heading-end-punctuation.js +3 -0
- package/dist/detectors/registry/heading-end-punctuation.js.map +1 -0
- package/dist/detectors/registry/hyphen-dash.d.ts +3 -0
- package/dist/detectors/registry/hyphen-dash.d.ts.map +1 -0
- package/dist/detectors/registry/hyphen-dash.js +3 -0
- package/dist/detectors/registry/hyphen-dash.js.map +1 -0
- package/dist/detectors/registry/image-file-name-alt.d.ts +3 -0
- package/dist/detectors/registry/image-file-name-alt.d.ts.map +1 -0
- package/dist/detectors/registry/image-file-name-alt.js +3 -0
- package/dist/detectors/registry/image-file-name-alt.js.map +1 -0
- package/dist/detectors/registry/impossible-date.d.ts +3 -0
- package/dist/detectors/registry/impossible-date.d.ts.map +1 -0
- package/dist/detectors/registry/impossible-date.js +3 -0
- package/dist/detectors/registry/impossible-date.js.map +1 -0
- package/dist/detectors/registry/impossible-time.d.ts +3 -0
- package/dist/detectors/registry/impossible-time.d.ts.map +1 -0
- package/dist/detectors/registry/impossible-time.js +3 -0
- package/dist/detectors/registry/impossible-time.js.map +1 -0
- package/dist/detectors/registry/latin-abbreviation.d.ts +3 -0
- package/dist/detectors/registry/latin-abbreviation.d.ts.map +1 -0
- package/dist/detectors/registry/latin-abbreviation.js +3 -0
- package/dist/detectors/registry/latin-abbreviation.js.map +1 -0
- package/dist/detectors/registry/link-text-url-mismatch.d.ts +3 -0
- package/dist/detectors/registry/link-text-url-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/link-text-url-mismatch.js +3 -0
- package/dist/detectors/registry/link-text-url-mismatch.js.map +1 -0
- package/dist/detectors/registry/list-marker.d.ts +3 -0
- package/dist/detectors/registry/list-marker.d.ts.map +1 -0
- package/dist/detectors/registry/list-marker.js +3 -0
- package/dist/detectors/registry/list-marker.js.map +1 -0
- package/dist/detectors/registry/mark-consistency.d.ts +3 -0
- package/dist/detectors/registry/mark-consistency.d.ts.map +1 -0
- package/dist/detectors/registry/mark-consistency.js +3 -0
- package/dist/detectors/registry/mark-consistency.js.map +1 -0
- package/dist/detectors/registry/numeral-opener.d.ts +3 -0
- package/dist/detectors/registry/numeral-opener.d.ts.map +1 -0
- package/dist/detectors/registry/numeral-opener.js +3 -0
- package/dist/detectors/registry/numeral-opener.js.map +1 -0
- package/dist/detectors/registry/ordinal-suffix.d.ts +3 -0
- package/dist/detectors/registry/ordinal-suffix.d.ts.map +1 -0
- package/dist/detectors/registry/ordinal-suffix.js +3 -0
- package/dist/detectors/registry/ordinal-suffix.js.map +1 -0
- package/dist/detectors/registry/origin-particle.d.ts +3 -0
- package/dist/detectors/registry/origin-particle.d.ts.map +1 -0
- package/dist/detectors/registry/origin-particle.js +3 -0
- package/dist/detectors/registry/origin-particle.js.map +1 -0
- package/dist/detectors/registry/range-notation.d.ts +3 -0
- package/dist/detectors/registry/range-notation.d.ts.map +1 -0
- package/dist/detectors/registry/range-notation.js +3 -0
- package/dist/detectors/registry/range-notation.js.map +1 -0
- package/dist/detectors/registry/relative-pronoun-mix.d.ts +3 -0
- package/dist/detectors/registry/relative-pronoun-mix.d.ts.map +1 -0
- package/dist/detectors/registry/relative-pronoun-mix.js +3 -0
- package/dist/detectors/registry/relative-pronoun-mix.js.map +1 -0
- package/dist/detectors/registry/request-without-deadline.d.ts +3 -0
- package/dist/detectors/registry/request-without-deadline.d.ts.map +1 -0
- package/dist/detectors/registry/request-without-deadline.js +3 -0
- package/dist/detectors/registry/request-without-deadline.js.map +1 -0
- package/dist/detectors/registry/sentence-spacing.d.ts +3 -0
- package/dist/detectors/registry/sentence-spacing.d.ts.map +1 -0
- package/dist/detectors/registry/sentence-spacing.js +3 -0
- package/dist/detectors/registry/sentence-spacing.js.map +1 -0
- package/dist/detectors/registry/share-over-hundred.d.ts +3 -0
- package/dist/detectors/registry/share-over-hundred.d.ts.map +1 -0
- package/dist/detectors/registry/share-over-hundred.js +3 -0
- package/dist/detectors/registry/share-over-hundred.js.map +1 -0
- package/dist/detectors/registry/shell-prompt.d.ts +3 -0
- package/dist/detectors/registry/shell-prompt.d.ts.map +1 -0
- package/dist/detectors/registry/shell-prompt.js +3 -0
- package/dist/detectors/registry/shell-prompt.js.map +1 -0
- package/dist/detectors/registry/step-reference.d.ts +3 -0
- package/dist/detectors/registry/step-reference.d.ts.map +1 -0
- package/dist/detectors/registry/step-reference.js +3 -0
- package/dist/detectors/registry/step-reference.js.map +1 -0
- package/dist/detectors/registry/step-statement.d.ts +3 -0
- package/dist/detectors/registry/step-statement.d.ts.map +1 -0
- package/dist/detectors/registry/step-statement.js +3 -0
- package/dist/detectors/registry/step-statement.js.map +1 -0
- package/dist/detectors/registry/table-row-overflow.d.ts +3 -0
- package/dist/detectors/registry/table-row-overflow.d.ts.map +1 -0
- package/dist/detectors/registry/table-row-overflow.js +3 -0
- package/dist/detectors/registry/table-row-overflow.js.map +1 -0
- package/dist/detectors/registry/toc-heading.d.ts +3 -0
- package/dist/detectors/registry/toc-heading.d.ts.map +1 -0
- package/dist/detectors/registry/toc-heading.js +3 -0
- package/dist/detectors/registry/toc-heading.js.map +1 -0
- package/dist/detectors/registry/unclosed-code-fence.d.ts +3 -0
- package/dist/detectors/registry/unclosed-code-fence.d.ts.map +1 -0
- package/dist/detectors/registry/unclosed-code-fence.js +3 -0
- package/dist/detectors/registry/unclosed-code-fence.js.map +1 -0
- package/dist/detectors/registry/unpaired-parallel.d.ts +3 -0
- package/dist/detectors/registry/unpaired-parallel.d.ts.map +1 -0
- package/dist/detectors/registry/unpaired-parallel.js +3 -0
- package/dist/detectors/registry/unpaired-parallel.js.map +1 -0
- package/dist/detectors/registry/unrendered-emphasis.d.ts +3 -0
- package/dist/detectors/registry/unrendered-emphasis.d.ts.map +1 -0
- package/dist/detectors/registry/unrendered-emphasis.js +3 -0
- package/dist/detectors/registry/unrendered-emphasis.js.map +1 -0
- package/dist/detectors/relative-pronoun-mix.d.ts +19 -0
- package/dist/detectors/relative-pronoun-mix.d.ts.map +1 -0
- package/dist/detectors/relative-pronoun-mix.js +60 -0
- package/dist/detectors/relative-pronoun-mix.js.map +1 -0
- package/dist/detectors/request-owner.d.ts +23 -0
- package/dist/detectors/request-owner.d.ts.map +1 -0
- package/dist/detectors/request-owner.js +110 -0
- package/dist/detectors/request-owner.js.map +1 -0
- package/dist/detectors/share-over-hundred.d.ts +20 -0
- package/dist/detectors/share-over-hundred.d.ts.map +1 -0
- package/dist/detectors/share-over-hundred.js +55 -0
- package/dist/detectors/share-over-hundred.js.map +1 -0
- package/dist/detectors/step-reference.d.ts +20 -0
- package/dist/detectors/step-reference.d.ts.map +1 -0
- package/dist/detectors/step-reference.js +57 -0
- package/dist/detectors/step-reference.js.map +1 -0
- package/dist/detectors/technical-docs.d.ts +32 -0
- package/dist/detectors/technical-docs.d.ts.map +1 -0
- package/dist/detectors/technical-docs.js +199 -0
- package/dist/detectors/technical-docs.js.map +1 -0
- package/dist/detectors/toc-heading.d.ts +10 -0
- package/dist/detectors/toc-heading.d.ts.map +1 -0
- package/dist/detectors/toc-heading.js +99 -0
- package/dist/detectors/toc-heading.js.map +1 -0
- package/dist/extension/pack-read.d.ts +21 -0
- package/dist/extension/pack-read.d.ts.map +1 -0
- package/dist/extension/pack-read.js +117 -0
- package/dist/extension/pack-read.js.map +1 -0
- package/dist/extension/plugin-load.d.ts.map +1 -1
- package/dist/extension/plugin-load.js +44 -10
- package/dist/extension/plugin-load.js.map +1 -1
- package/dist/extension/plugin-parse.d.ts.map +1 -1
- package/dist/extension/plugin-parse.js +10 -3
- package/dist/extension/plugin-parse.js.map +1 -1
- package/dist/facts/address-values.d.ts +5 -0
- package/dist/facts/address-values.d.ts.map +1 -0
- package/dist/facts/address-values.js +64 -0
- package/dist/facts/address-values.js.map +1 -0
- package/dist/facts/cross-facts.d.ts +20 -0
- package/dist/facts/cross-facts.d.ts.map +1 -0
- package/dist/facts/cross-facts.js +41 -0
- package/dist/facts/cross-facts.js.map +1 -0
- package/dist/fix-plan/chosen-depth.d.ts +10 -0
- package/dist/fix-plan/chosen-depth.d.ts.map +1 -0
- package/dist/fix-plan/chosen-depth.js +24 -0
- package/dist/fix-plan/chosen-depth.js.map +1 -0
- package/dist/fix-plan/mode.d.ts +13 -4
- package/dist/fix-plan/mode.d.ts.map +1 -1
- package/dist/fix-plan/mode.js +38 -7
- package/dist/fix-plan/mode.js.map +1 -1
- package/dist/fix-plan/plan.d.ts +20 -0
- package/dist/fix-plan/plan.d.ts.map +1 -1
- package/dist/fix-plan/plan.js +23 -5
- package/dist/fix-plan/plan.js.map +1 -1
- package/dist/fix-plan/render.d.ts.map +1 -1
- package/dist/fix-plan/render.js +24 -0
- package/dist/fix-plan/render.js.map +1 -1
- package/dist/fix-plan/text.d.ts +8 -0
- package/dist/fix-plan/text.d.ts.map +1 -1
- package/dist/fix-plan/text.js +36 -10
- package/dist/fix-plan/text.js.map +1 -1
- package/dist/genre-load.d.ts +1 -1
- package/dist/genre-load.d.ts.map +1 -1
- package/dist/genre-load.js +7 -4
- package/dist/genre-load.js.map +1 -1
- package/dist/genre-parse.d.ts +6 -0
- package/dist/genre-parse.d.ts.map +1 -1
- package/dist/genre-parse.js +18 -0
- package/dist/genre-parse.js.map +1 -1
- package/dist/init.js +10 -10
- package/dist/init.js.map +1 -1
- package/dist/known-genres.d.ts +8 -0
- package/dist/known-genres.d.ts.map +1 -0
- package/dist/known-genres.js +17 -0
- package/dist/known-genres.js.map +1 -0
- package/dist/link-shape.d.ts +20 -0
- package/dist/link-shape.d.ts.map +1 -0
- package/dist/link-shape.js +58 -0
- package/dist/link-shape.js.map +1 -0
- package/dist/list-marks.d.ts +21 -0
- package/dist/list-marks.d.ts.map +1 -0
- package/dist/list-marks.js +82 -0
- package/dist/list-marks.js.map +1 -0
- package/dist/reasons.d.ts.map +1 -1
- package/dist/reasons.js +15 -4
- package/dist/reasons.js.map +1 -1
- package/dist/render/explain.d.ts.map +1 -1
- package/dist/render/explain.js +11 -2
- package/dist/render/explain.js.map +1 -1
- package/dist/render/friendly.js +1 -1
- package/dist/render/friendly.js.map +1 -1
- package/dist/render/rules-json.d.ts.map +1 -1
- package/dist/render/rules-json.js +12 -3
- package/dist/render/rules-json.js.map +1 -1
- package/dist/render/rules-table.js +6 -6
- package/dist/render/rules-table.js.map +1 -1
- package/dist/render/semantic.js +2 -2
- package/dist/render/semantic.js.map +1 -1
- package/dist/render/suppressions.js +2 -2
- package/dist/render/suppressions.js.map +1 -1
- package/dist/rewrite-depth.d.ts +22 -0
- package/dist/rewrite-depth.d.ts.map +1 -0
- package/dist/rewrite-depth.js +42 -0
- package/dist/rewrite-depth.js.map +1 -0
- package/dist/rule-fields.d.ts +28 -0
- package/dist/rule-fields.d.ts.map +1 -0
- package/dist/rule-fields.js +85 -0
- package/dist/rule-fields.js.map +1 -0
- package/dist/rule-guide.d.ts +3 -0
- package/dist/rule-guide.d.ts.map +1 -1
- package/dist/rule-guide.js +6 -0
- package/dist/rule-guide.js.map +1 -1
- package/dist/rule-load.d.ts.map +1 -1
- package/dist/rule-load.js +13 -5
- package/dist/rule-load.js.map +1 -1
- package/dist/rule-offs.d.ts +14 -0
- package/dist/rule-offs.d.ts.map +1 -0
- package/dist/rule-offs.js +45 -0
- package/dist/rule-offs.js.map +1 -0
- package/dist/structure/build.d.ts.map +1 -1
- package/dist/structure/build.js +14 -1
- package/dist/structure/build.js.map +1 -1
- package/dist/structure/cross-definitions.d.ts +23 -0
- package/dist/structure/cross-definitions.d.ts.map +1 -0
- package/dist/structure/cross-definitions.js +34 -0
- package/dist/structure/cross-definitions.js.map +1 -0
- package/genres.yaml +14 -313
- package/package.json +3 -3
- package/rules/absolute-exception.yaml +3 -1
- package/rules/abstract-length.yaml +7 -1
- package/rules/acronym-expansion-conflict.yaml +2 -1
- package/rules/adverb-overuse.yaml +7 -1
- package/rules/adverb-without-negation.yaml +60 -0
- package/rules/adversative-ga-repeat.yaml +2 -1
- package/rules/agentless-passive.yaml +10 -1
- package/rules/agreement-slip.yaml +4 -1
- package/rules/ai-generated-composite.yaml +6 -1
- package/rules/ai-structure.yaml +8 -2
- package/rules/ai-tell.yaml +7 -1
- package/rules/all-caps-shouting.yaml +61 -0
- package/rules/announced-count-mismatch.yaml +3 -1
- package/rules/announcing-opener.yaml +7 -1
- package/rules/article-sound.yaml +6 -2
- package/rules/assistant-residue.yaml +4 -1
- package/rules/attachment-not-attached.yaml +112 -0
- package/rules/baito-keigo.yaml +64 -0
- package/rules/bare-url-mix.yaml +86 -0
- package/rules/bold-density.yaml +1 -0
- package/rules/bold-label-list.yaml +9 -1
- package/rules/bracket-nesting.yaml +2 -1
- package/rules/bracket-width-consistency.yaml +64 -0
- package/rules/broken-link.yaml +1 -1
- package/rules/chat-citation-residue.yaml +2 -1
- package/rules/citation-reference-mismatch.yaml +7 -1
- package/rules/citation-style-mix.yaml +7 -1
- package/rules/clause-chain.yaml +6 -1
- package/rules/cliche.yaml +67 -0
- package/rules/closing-cliche.yaml +7 -0
- package/rules/code-fence-language-mix.yaml +176 -0
- package/rules/colloquial-opener.yaml +62 -0
- package/rules/colon-lead-in.yaml +7 -1
- package/rules/concrete-evidence-density.yaml +6 -0
- package/rules/contraction-consistency.yaml +4 -1
- package/rules/contrast-framing.yaml +7 -1
- package/rules/cross-doc-duplicate-definition.yaml +70 -0
- package/rules/cross-doc-fact-conflict.yaml +64 -0
- package/rules/currency-notation-consistency.yaml +77 -0
- package/rules/cushion-phrase-density.yaml +8 -1
- package/rules/dangling-figure-reference.yaml +3 -1
- package/rules/dangling-participle.yaml +58 -0
- package/rules/dangling-reference.yaml +3 -1
- package/rules/date-format-consistency.yaml +3 -2
- package/rules/date-order.yaml +3 -1
- package/rules/date-range-reversed.yaml +3 -1
- package/rules/date-weekday-mismatch.yaml +3 -1
- package/rules/date-without-year.yaml +4 -3
- package/rules/defined-name-repeated.yaml +97 -0
- package/rules/defined-term-form.yaml +113 -0
- package/rules/demonstrative-opener-run.yaml +2 -1
- package/rules/double-keigo.yaml +6 -1
- package/rules/double-negative.yaml +5 -1
- package/rules/doubled-nado.yaml +2 -1
- package/rules/doubled-punctuation.yaml +1 -1
- package/rules/doubled-word.yaml +4 -1
- package/rules/duplicate-definition.yaml +3 -1
- package/rules/duplicate-heading.yaml +1 -1
- package/rules/duration-mismatch.yaml +2 -1
- package/rules/elapsed-years-mismatch.yaml +4 -1
- package/rules/email-greeting-closing.yaml +122 -0
- package/rules/email-subject-length.yaml +112 -0
- package/rules/emoji-density.yaml +3 -1
- package/rules/emoji-heading.yaml +7 -1
- package/rules/empty-intensifier.yaml +4 -0
- package/rules/empty-list-item.yaml +2 -1
- package/rules/empty-section.yaml +1 -1
- package/rules/empty-table-cell.yaml +2 -1
- package/rules/excessive-hedging.yaml +7 -1
- package/rules/exclamation-width-consistency.yaml +64 -0
- package/rules/expletive-construction.yaml +7 -1
- package/rules/fact-conflict.yaml +4 -1
- package/rules/figure-reference-order.yaml +7 -1
- package/rules/footnote-mismatch.yaml +91 -0
- package/rules/fullwidth-alnum-consistency.yaml +2 -2
- package/rules/hankaku-kana.yaml +1 -1
- package/rules/heading-command-missing.yaml +109 -0
- package/rules/heading-echo.yaml +6 -1
- package/rules/heading-end-punctuation-mix.yaml +158 -0
- package/rules/heading-level-skip.yaml +3 -3
- package/rules/heading-numbering-mix.yaml +4 -3
- package/rules/hiragana-fukushi.yaml +4 -1
- package/rules/homophone-slip.yaml +5 -1
- package/rules/humble-for-others.yaml +63 -0
- package/rules/hyphen-as-dash.yaml +59 -0
- package/rules/idiom-misuse.yaml +4 -1
- package/rules/image-file-name-alt.yaml +83 -0
- package/rules/impossible-date.yaml +79 -0
- package/rules/impossible-time.yaml +76 -0
- package/rules/internal-jargon.yaml +1 -1
- package/rules/invisible-character.yaml +1 -1
- package/rules/katakana-density.yaml +2 -1
- package/rules/katakana-long-vowel.yaml +1 -1
- package/rules/known-misspelling.yaml +2 -1
- package/rules/kutoten-consistency.yaml +1 -1
- package/rules/latin-abbreviation-form.yaml +58 -0
- package/rules/latin-spacing.yaml +6 -2
- package/rules/less-for-fewer.yaml +59 -0
- package/rules/link-text-url-mismatch.yaml +72 -0
- package/rules/list-item-form-mix.yaml +7 -1
- package/rules/list-marker-mix.yaml +111 -0
- package/rules/max-kanji-continuous.yaml +10 -2
- package/rules/max-paragraph-length.yaml +4 -1
- package/rules/max-sentence-length.yaml +29 -21
- package/rules/max-ten.yaml +4 -1
- package/rules/misconversion.yaml +2 -1
- package/rules/modal-conflict.yaml +2 -1
- package/rules/name-variant.yaml +4 -3
- package/rules/ngram-repetition.yaml +5 -0
- package/rules/no-doubled-joshi.yaml +5 -1
- package/rules/no-em-dash.yaml +4 -1
- package/rules/no-lead.yaml +6 -1
- package/rules/no-mixed-desumasu.yaml +5 -2
- package/rules/no-nakaguro-parallel.yaml +3 -0
- package/rules/nominalization.yaml +5 -1
- package/rules/nonstandard-word.yaml +80 -0
- package/rules/number-style-consistency.yaml +6 -1
- package/rules/number-without-unit.yaml +2 -1
- package/rules/numbering-gap.yaml +1 -1
- package/rules/numeral-sentence-start.yaml +56 -0
- package/rules/one-sentence-paragraph-run.yaml +5 -1
- package/rules/ordinal-suffix-mismatch.yaml +65 -0
- package/rules/orthographic-variant.yaml +4 -1
- package/rules/outside-announced-list.yaml +2 -1
- package/rules/oxford-comma-consistency.yaml +3 -1
- package/rules/padded-intro.yaml +7 -0
- package/rules/paragraph-length-variance.yaml +7 -1
- package/rules/paragraph-restatement.yaml +6 -1
- package/rules/percent-sum-mismatch.yaml +3 -1
- package/rules/person-consistency.yaml +4 -1
- package/rules/preamble-length.yaml +6 -0
- package/rules/preferred-term.yaml +1 -1
- package/rules/proper-noun-density.yaml +8 -1
- package/rules/quote-style-consistency.yaml +66 -0
- package/rules/ra-nuki.yaml +4 -1
- package/rules/range-notation-consistency.yaml +73 -0
- package/rules/redundant-expression.yaml +4 -1
- package/rules/reference-title-mismatch.yaml +4 -3
- package/rules/reference-topic-missing.yaml +2 -1
- package/rules/relative-date-mismatch.yaml +2 -1
- package/rules/repeated-conjunction.yaml +4 -1
- package/rules/repeated-sentence-ending.yaml +62 -0
- package/rules/repeated-sentence-head.yaml +4 -0
- package/rules/request-without-deadline.yaml +92 -0
- package/rules/required-sections.yaml +1 -1
- package/rules/requirement-modal.yaml +1 -1
- package/rules/requirement-smell.yaml +12 -4
- package/rules/rule-of-three.yaml +8 -1
- package/rules/sasete-itadaku.yaml +7 -1
- package/rules/section-length-uniformity.yaml +7 -1
- package/rules/sentence-initial-conjunction-run.yaml +5 -1
- package/rules/sentence-initial-so.yaml +6 -1
- package/rules/sentence-rhythm.yaml +8 -1
- package/rules/sentence-spacing-consistency.yaml +63 -0
- package/rules/share-over-hundred.yaml +65 -0
- package/rules/shell-prompt-mix.yaml +128 -0
- package/rules/space-before-punctuation.yaml +1 -1
- package/rules/spelling-consistency.yaml +3 -1
- package/rules/step-reference-missing.yaml +85 -0
- package/rules/step-statement-mix.yaml +76 -0
- package/rules/stock-transition.yaml +8 -1
- package/rules/stray-space.yaml +2 -2
- package/rules/summary-fact-mismatch.yaml +4 -1
- package/rules/table-header-variant.yaml +2 -1
- package/rules/table-row-overflow.yaml +90 -0
- package/rules/taigen-dome-in-prose.yaml +6 -0
- package/rules/tari-unpaired.yaml +58 -0
- package/rules/teki-overuse.yaml +2 -1
- package/rules/title-case-consistency.yaml +1 -1
- package/rules/title-length.yaml +7 -1
- package/rules/toc-heading-mismatch.yaml +136 -0
- package/rules/topic-predicate-distance.yaml +6 -1
- package/rules/total-mismatch.yaml +3 -1
- package/rules/translationese-density.yaml +4 -1
- package/rules/unbalanced-bracket.yaml +3 -3
- package/rules/unclosed-code-fence.yaml +107 -0
- package/rules/uncomparable-graded.yaml +65 -0
- package/rules/undefined-acronym.yaml +8 -1
- package/rules/unfilled-placeholder.yaml +1 -1
- package/rules/unit-mismatch.yaml +2 -1
- package/rules/unknown-word.yaml +4 -1
- package/rules/unqualified-superlative.yaml +6 -1
- package/rules/unrendered-emphasis.yaml +73 -0
- package/rules/unused-definition.yaml +4 -3
- package/rules/url-run-on.yaml +1 -1
- package/rules/use-before-definition.yaml +4 -3
- package/rules/vague-figure-reference.yaml +3 -1
- package/rules/vague-link-text.yaml +2 -1
- package/rules/vague-word-density.yaml +2 -1
- package/rules/very-adjective.yaml +5 -1
- package/rules/weasel-word.yaml +6 -1
- package/rules/which-that-consistency.yaml +64 -0
- package/rules/wordy-phrase.yaml +6 -1
- package/rules/yori-as-from.yaml +61 -0
- package/skills/chaff/SKILL.md +7 -7
- package/src/api.ts +13 -1
- package/src/cli-text.ts +22 -7
- package/src/cli.ts +8 -9
- package/src/command-help.ts +26 -0
- package/src/commands/eval.ts +2 -2
- package/src/commands/fix-plan.ts +13 -1
- package/src/commands/help.ts +8 -0
- package/src/commands/init-command.ts +13 -0
- package/src/commands/plugin-init.ts +150 -0
- package/src/commands/plugin-test.ts +111 -0
- package/src/config/load.ts +3 -0
- package/src/config/option-problems.ts +2 -2
- package/src/config/rule-problems.ts +2 -2
- package/src/custom/checked.ts +8 -0
- package/src/custom/dsl-fields.ts +102 -0
- package/src/custom/load.ts +2 -0
- package/src/custom/parse.ts +72 -50
- package/src/custom/problems.ts +16 -2
- package/src/detectors/amount-for-count.ts +90 -0
- package/src/detectors/blank-structure.ts +12 -4
- package/src/detectors/clause-shape.ts +199 -0
- package/src/detectors/code-fences.ts +113 -0
- package/src/detectors/cross-definitions.ts +49 -0
- package/src/detectors/cross-facts.ts +60 -0
- package/src/detectors/cross-registry/cross-doc-duplicate-definition.ts +4 -0
- package/src/detectors/cross-registry/cross-doc-fact-conflict.ts +4 -0
- package/src/detectors/currency-notation.ts +90 -0
- package/src/detectors/custom.ts +10 -2
- package/src/detectors/dangling-opener.ts +74 -0
- package/src/detectors/defined-term-form.ts +200 -0
- package/src/detectors/email-letter.ts +262 -0
- package/src/detectors/footnote-mismatch.ts +72 -0
- package/src/detectors/form-minority.ts +21 -0
- package/src/detectors/heading-end-mark.ts +82 -0
- package/src/detectors/impossible-date.ts +141 -0
- package/src/detectors/impossible-time.ts +151 -0
- package/src/detectors/link-shape.ts +70 -0
- package/src/detectors/list-marker.ts +60 -0
- package/src/detectors/mark-shape.ts +202 -0
- package/src/detectors/markdown-slip.ts +140 -0
- package/src/detectors/numeral-opener.ts +72 -0
- package/src/detectors/ordinal-suffix.ts +40 -0
- package/src/detectors/range-notation.ts +76 -0
- package/src/detectors/registry/adverb-polarity.ts +4 -0
- package/src/detectors/registry/amount-for-count.ts +4 -0
- package/src/detectors/registry/attachment-missing.ts +4 -0
- package/src/detectors/registry/bare-url-mix.ts +4 -0
- package/src/detectors/registry/caps-shouting.ts +4 -0
- package/src/detectors/registry/code-fence-language.ts +4 -0
- package/src/detectors/registry/currency-notation.ts +4 -0
- package/src/detectors/registry/dangling-opener.ts +4 -0
- package/src/detectors/registry/defined-name-repeated.ts +4 -0
- package/src/detectors/registry/defined-term-form.ts +4 -0
- package/src/detectors/registry/email-frame.ts +4 -0
- package/src/detectors/registry/email-subject.ts +4 -0
- package/src/detectors/registry/ending-run.ts +4 -0
- package/src/detectors/registry/footnote-mismatch.ts +4 -0
- package/src/detectors/registry/heading-command.ts +4 -0
- package/src/detectors/registry/heading-end-punctuation.ts +4 -0
- package/src/detectors/registry/hyphen-dash.ts +4 -0
- package/src/detectors/registry/image-file-name-alt.ts +4 -0
- package/src/detectors/registry/impossible-date.ts +4 -0
- package/src/detectors/registry/impossible-time.ts +4 -0
- package/src/detectors/registry/latin-abbreviation.ts +4 -0
- package/src/detectors/registry/link-text-url-mismatch.ts +4 -0
- package/src/detectors/registry/list-marker.ts +4 -0
- package/src/detectors/registry/mark-consistency.ts +4 -0
- package/src/detectors/registry/numeral-opener.ts +4 -0
- package/src/detectors/registry/ordinal-suffix.ts +4 -0
- package/src/detectors/registry/origin-particle.ts +4 -0
- package/src/detectors/registry/range-notation.ts +4 -0
- package/src/detectors/registry/relative-pronoun-mix.ts +4 -0
- package/src/detectors/registry/request-without-deadline.ts +4 -0
- package/src/detectors/registry/sentence-spacing.ts +4 -0
- package/src/detectors/registry/share-over-hundred.ts +4 -0
- package/src/detectors/registry/shell-prompt.ts +4 -0
- package/src/detectors/registry/step-reference.ts +4 -0
- package/src/detectors/registry/step-statement.ts +4 -0
- package/src/detectors/registry/table-row-overflow.ts +4 -0
- package/src/detectors/registry/toc-heading.ts +4 -0
- package/src/detectors/registry/unclosed-code-fence.ts +4 -0
- package/src/detectors/registry/unpaired-parallel.ts +4 -0
- package/src/detectors/registry/unrendered-emphasis.ts +4 -0
- package/src/detectors/relative-pronoun-mix.ts +79 -0
- package/src/detectors/request-owner.ts +141 -0
- package/src/detectors/share-over-hundred.ts +75 -0
- package/src/detectors/step-reference.ts +83 -0
- package/src/detectors/technical-docs.ts +230 -0
- package/src/detectors/toc-heading.ts +125 -0
- package/src/extension/pack-read.ts +128 -0
- package/src/extension/plugin-load.ts +49 -9
- package/src/extension/plugin-parse.ts +10 -3
- package/src/facts/address-values.ts +74 -0
- package/src/facts/cross-facts.ts +54 -0
- package/src/fix-plan/chosen-depth.ts +28 -0
- package/src/fix-plan/mode.ts +57 -11
- package/src/fix-plan/plan.ts +37 -5
- package/src/fix-plan/render.ts +24 -0
- package/src/fix-plan/text.ts +47 -10
- package/src/genre-load.ts +7 -4
- package/src/genre-parse.ts +19 -0
- package/src/init.ts +10 -10
- package/src/known-genres.ts +19 -0
- package/src/link-shape.ts +62 -0
- package/src/list-marks.ts +106 -0
- package/src/reasons.ts +16 -4
- package/src/render/explain.ts +14 -2
- package/src/render/friendly.ts +1 -1
- package/src/render/rules-json.ts +14 -3
- package/src/render/rules-table.ts +6 -6
- package/src/render/semantic.ts +2 -2
- package/src/render/suppressions.ts +2 -2
- package/src/rewrite-depth.ts +60 -0
- package/src/rule-fields.ts +123 -0
- package/src/rule-guide.ts +9 -0
- package/src/rule-load.ts +10 -4
- package/src/rule-offs.ts +50 -0
- package/src/structure/build.ts +21 -1
- package/src/structure/cross-definitions.ts +60 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// A work email or letter's own parts: a greeting and a closing, a subject line, and an attachment the text mentions.
|
|
2
|
+
// Pure. The words (お世話になっております, Dear, Best regards, 件名, attached) come from the language's email-greeting,
|
|
3
|
+
// email-closing, email-subject-field, attachment-mention, attachment-not-mention and attachment-line lexicons. The
|
|
4
|
+
// header and the signature are masked in doc.prose; a quoted reply is someone else's words and is left out.
|
|
5
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
6
|
+
import { quoteAt } from "./structure-tree.ts";
|
|
7
|
+
import type { Detector, Finding, ProseDocument, Span } from "../plugin.ts";
|
|
8
|
+
|
|
9
|
+
/** A greeting comes before the body: past the subject, the addressee (株式会社… / 総務部 山田様) and the writer's name. */
|
|
10
|
+
const GREETING_LINES = 5;
|
|
11
|
+
/** A closing comes at the end, before the writer's name, title, company, address and contact lines, or an acceptance block. */
|
|
12
|
+
const CLOSING_LINES = 10;
|
|
13
|
+
|
|
14
|
+
/** One line of the writer's own text. breaks: a heading, the header or the signature stood between it and the line before. */
|
|
15
|
+
export type BodyLine = { readonly start: number; readonly text: string; readonly breaks: boolean };
|
|
16
|
+
|
|
17
|
+
const patternsOf = (doc: ProseDocument, id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
|
|
18
|
+
|
|
19
|
+
const LATIN_START = /^[A-Za-z]/u;
|
|
20
|
+
const LETTER = /\p{L}/u;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether a line holds one of the words. A Latin word opens the line (Dear, Best regards) and is followed by no letter,
|
|
24
|
+
* so "Hi" does not match "History"; a Japanese word may stand anywhere in it (いつもお世話になっております).
|
|
25
|
+
*/
|
|
26
|
+
export const lineHas = (line: string, words: readonly string[]): boolean => {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
const folded = trimmed.toLowerCase();
|
|
29
|
+
return words.some((word) => {
|
|
30
|
+
if (!LATIN_START.test(word)) return trimmed.includes(word);
|
|
31
|
+
return folded.startsWith(word.toLowerCase()) && !LETTER.test(trimmed.charAt(word.length));
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether each offset, asked in rising order, is inside one of the spans (sorted by start). One pass over the spans for
|
|
37
|
+
* the whole document: an archive with a quoted reply in every message stays linear.
|
|
38
|
+
*/
|
|
39
|
+
const insideCursor = (spans: readonly Span[]): ((offset: number) => boolean) => {
|
|
40
|
+
const sorted = spans.toSorted((left, right) => left.start - right.start);
|
|
41
|
+
const cursor = { next: 0, farthestEnd: -1 };
|
|
42
|
+
return (offset) => {
|
|
43
|
+
while (cursor.next < sorted.length && (sorted[cursor.next]?.start ?? Infinity) <= offset) {
|
|
44
|
+
cursor.farthestEnd = Math.max(cursor.farthestEnd, sorted[cursor.next]?.end ?? 0);
|
|
45
|
+
cursor.next += 1;
|
|
46
|
+
}
|
|
47
|
+
return offset < cursor.farthestEnd;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A line prose masks words at the head of: a header field whose value stays (Subject: …), not the writer's text. Emphasis
|
|
53
|
+
* marks are masked too, but they hold no letter.
|
|
54
|
+
*/
|
|
55
|
+
const isFieldLine = (sourceLine: string, proseLine: string): boolean => {
|
|
56
|
+
const sourceChars = [...sourceLine];
|
|
57
|
+
const kept = [...proseLine].findIndex((char, index) => char.trim() !== "" || sourceChars[index]?.trim() === "");
|
|
58
|
+
return LETTER.test(sourceChars.slice(0, kept === -1 ? sourceChars.length : kept).join(""));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type SourceLine = { readonly start: number; readonly text: string };
|
|
62
|
+
|
|
63
|
+
const linesWithOffsets = (text: string): SourceLine[] => {
|
|
64
|
+
const lines: SourceLine[] = [];
|
|
65
|
+
text.split("\n").forEach((line) => lines.push({ start: (lines.at(-1)?.start ?? 0) + (lines.at(-1)?.text.length ?? -1) + 1, text: line }));
|
|
66
|
+
return lines;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The writer's own lines, in order: not blank once the header and signature are masked (prose), not a quoted reply, not a
|
|
71
|
+
* heading. A heading, or a line the source has and prose masks (a header, a signature), breaks the text into parts.
|
|
72
|
+
*/
|
|
73
|
+
export const bodyLines = (source: string, prose: string, replyQuotes: readonly Span[]): BodyLine[] => {
|
|
74
|
+
const quoted = insideCursor(replyQuotes);
|
|
75
|
+
const lines: BodyLine[] = [];
|
|
76
|
+
const state = { broken: false };
|
|
77
|
+
linesWithOffsets(prose).forEach(({ start, text }) => {
|
|
78
|
+
const trimmed = text.trimStart();
|
|
79
|
+
const sourceLine = source.slice(start, start + text.length);
|
|
80
|
+
const masked = trimmed === "" ? sourceLine.trim() !== "" : isFieldLine(sourceLine, text);
|
|
81
|
+
const inQuote = quoted(start);
|
|
82
|
+
if (masked || trimmed.startsWith("#")) state.broken = true;
|
|
83
|
+
else if (trimmed !== "" && !trimmed.startsWith(">") && !inQuote) {
|
|
84
|
+
lines.push({ start, text, breaks: state.broken });
|
|
85
|
+
state.broken = false;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return lines;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** The lines split where a heading, a header or a signature breaks them: a letter and its appendix, or one message and the next. */
|
|
92
|
+
const partsOf = (lines: readonly BodyLine[]): BodyLine[][] => {
|
|
93
|
+
const parts: BodyLine[][] = [];
|
|
94
|
+
lines.forEach((line) => {
|
|
95
|
+
const current = parts.at(-1);
|
|
96
|
+
if (line.breaks || current === undefined) parts.push([line]);
|
|
97
|
+
else current.push(line);
|
|
98
|
+
});
|
|
99
|
+
return parts;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type FrameSlip = { readonly variant: "greeting" | "closing"; readonly offset: number };
|
|
103
|
+
|
|
104
|
+
/** A missing greeting (none in the first lines) and a missing closing (none in the last lines of any part). */
|
|
105
|
+
export const frameSlips = (lines: readonly BodyLine[], greetings: readonly string[], closings: readonly string[]): FrameSlip[] => {
|
|
106
|
+
const first = lines[0];
|
|
107
|
+
const last = lines.at(-1);
|
|
108
|
+
if (first === undefined || last === undefined) return [];
|
|
109
|
+
const greeted = lines.slice(0, GREETING_LINES).some((line) => lineHas(line.text, greetings));
|
|
110
|
+
const closed = partsOf(lines).some((part) => part.slice(-CLOSING_LINES).some((line) => lineHas(line.text, closings)));
|
|
111
|
+
return [
|
|
112
|
+
...(greeted ? [] : [{ variant: "greeting" as const, offset: first.start }]),
|
|
113
|
+
...(closed ? [] : [{ variant: "closing" as const, offset: last.start }]),
|
|
114
|
+
];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const emailFrame: Detector = (doc): Finding[] =>
|
|
118
|
+
frameSlips(bodyLines(doc.source, doc.prose ?? doc.source, doc.replyQuotes ?? []), patternsOf(doc, "email-greeting"), patternsOf(doc, "email-closing")).map(
|
|
119
|
+
(slip) => ({
|
|
120
|
+
rule: "email-greeting-closing",
|
|
121
|
+
severity: "info",
|
|
122
|
+
line: 0,
|
|
123
|
+
column: 0,
|
|
124
|
+
quote: quoteAt(doc.source, slip.offset),
|
|
125
|
+
values: { offset: slip.offset },
|
|
126
|
+
variant: slip.variant,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
export type SubjectLine = { readonly start: number; readonly subject: string };
|
|
131
|
+
|
|
132
|
+
/** A field's name is one word of at most this many characters. */
|
|
133
|
+
const MAX_FIELD_NAME = 20;
|
|
134
|
+
const BLANK = /\s/u;
|
|
135
|
+
|
|
136
|
+
/** "Subject: Lunch" as its name and value; undefined for a line that is no field. */
|
|
137
|
+
export const fieldOf = (line: string): { readonly name: string; readonly value: string } | undefined => {
|
|
138
|
+
const colon = [...line].findIndex((char) => char === ":" || char === ":");
|
|
139
|
+
if (colon <= 0) return undefined;
|
|
140
|
+
const chars = [...line];
|
|
141
|
+
const name = chars.slice(0, colon).join("").trimEnd();
|
|
142
|
+
return name.length > MAX_FIELD_NAME || BLANK.test(name)
|
|
143
|
+
? undefined
|
|
144
|
+
: {
|
|
145
|
+
name,
|
|
146
|
+
value: chars
|
|
147
|
+
.slice(colon + 1)
|
|
148
|
+
.join("")
|
|
149
|
+
.trim(),
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** The subject lines (Subject: …, 件名:…), outside a quoted reply, with reply and forward marks (Re:, Fwd:) taken off the value. */
|
|
154
|
+
export const subjectLines = (source: string, fields: readonly string[], replyMarks: readonly string[], replyQuotes: readonly Span[]): SubjectLine[] => {
|
|
155
|
+
const names = new Set(fields.map((field) => field.toLowerCase()));
|
|
156
|
+
const quoted = insideCursor(replyQuotes);
|
|
157
|
+
return linesWithOffsets(source).flatMap(({ start, text }): SubjectLine[] => {
|
|
158
|
+
const field = fieldOf(text);
|
|
159
|
+
const inQuote = quoted(start);
|
|
160
|
+
return field === undefined || inQuote || !names.has(field.name.toLowerCase()) ? [] : [{ start, subject: withoutReplyMarks(field.value, replyMarks) }];
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** "Re: Fwd: Lunch" is "Lunch": the marks a mail program adds are not what the writer wrote. Read piece by piece between colons. */
|
|
165
|
+
export const withoutReplyMarks = (subject: string, marks: readonly string[]): string => {
|
|
166
|
+
const folded = new Set(marks.map((mark) => mark.toLowerCase()));
|
|
167
|
+
const pieces = subject.split(/[::]/u);
|
|
168
|
+
const kept = pieces.findIndex((piece, index) => index === pieces.length - 1 || !folded.has(piece.trim().toLowerCase()));
|
|
169
|
+
const dropped = pieces.slice(0, kept).reduce((length, piece) => length + piece.length + 1, 0);
|
|
170
|
+
return subject.slice(dropped).trim();
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const emailSubject: Detector = (doc, options): Finding[] =>
|
|
174
|
+
subjectLines(doc.source, patternsOf(doc, "email-subject-field"), patternsOf(doc, "email-reply-mark"), doc.replyQuotes ?? []).flatMap((line): Finding[] => {
|
|
175
|
+
const length = [...line.subject].length;
|
|
176
|
+
if (length > 0 && length <= options.limit) return [];
|
|
177
|
+
return [
|
|
178
|
+
{
|
|
179
|
+
rule: "email-subject-length",
|
|
180
|
+
severity: "warning",
|
|
181
|
+
line: 0,
|
|
182
|
+
column: 0,
|
|
183
|
+
quote: quoteAt(doc.source, line.start),
|
|
184
|
+
values: { count: length, limit: options.limit, offset: line.start },
|
|
185
|
+
...(length === 0 ? { variant: "empty" } : {}),
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/** Where the text first mentions an attachment (添付します, attached), outside the phrases that only look like one (attached to). */
|
|
191
|
+
export const firstMention = (lines: readonly BodyLine[], mentions: readonly string[], notMentions: readonly string[]): number | undefined => {
|
|
192
|
+
const folded = (text: string): string => text.toLowerCase();
|
|
193
|
+
const isMention = (text: string): boolean => {
|
|
194
|
+
const cleared = notMentions.reduce((rest, phrase) => rest.split(folded(phrase)).join(" "), folded(text));
|
|
195
|
+
return mentions.some((word) => cleared.includes(folded(word)));
|
|
196
|
+
};
|
|
197
|
+
const line = lines.find((candidate) => isMention(candidate.text));
|
|
198
|
+
return line === undefined ? undefined : line.start + line.text.length - line.text.trimStart().length;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** The marks around a label line: a heading's #, emphasis, 【】. */
|
|
202
|
+
const LINE_MARKS = new Set("#*_【】 \t\u3000");
|
|
203
|
+
|
|
204
|
+
const withoutMarks = (line: string): string => {
|
|
205
|
+
const chars = [...line];
|
|
206
|
+
const first = chars.findIndex((char) => !LINE_MARKS.has(char));
|
|
207
|
+
return first === -1 ? "" : chars.slice(first, chars.findLastIndex((char) => !LINE_MARKS.has(char)) + 1).join("");
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/** A line that labels what is attached: the label and a colon (Attachment: quote.pdf), or the label alone (### Enclosures, 【添付資料】). */
|
|
211
|
+
const labelsAttachment = (line: string, labels: readonly string[]): boolean => {
|
|
212
|
+
const bare = withoutMarks(line).toLowerCase();
|
|
213
|
+
return labels.some((label) => {
|
|
214
|
+
const folded = label.toLowerCase();
|
|
215
|
+
return bare === folded || (bare.startsWith(folded) && /^\s*[::]/u.test(bare.slice(folded.length)));
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/** An attachment shown: a line that labels it, a file name with a listed extension, or the archive's note. text is the writer's own (no quoted reply). */
|
|
220
|
+
export const showsAttachment = (text: string, labels: readonly string[], extensions: readonly string[], notes: readonly string[]): boolean => {
|
|
221
|
+
const labelled = text.split("\n").some((line) => labelsAttachment(line, labels));
|
|
222
|
+
const fileName = new RegExp(`[\\w\\-ぁ-んァ-ヶ一-龠]\\.(?:${extensions.map(escapeRegExp).join("|")})\\b`, "iu");
|
|
223
|
+
return labelled || (extensions.length > 0 && fileName.test(text)) || notes.some((note) => text.includes(note));
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/** The source without someone else's words: a quoted reply's spans blanked and its "> " lines dropped. An old message's attachment is not this one's. */
|
|
227
|
+
export const ownText = (source: string, replyQuotes: readonly Span[]): string => {
|
|
228
|
+
const pieces: string[] = [];
|
|
229
|
+
const cursor = { at: 0 };
|
|
230
|
+
replyQuotes
|
|
231
|
+
.toSorted((left, right) => left.start - right.start)
|
|
232
|
+
.forEach((span) => {
|
|
233
|
+
const from = Math.max(span.start, cursor.at);
|
|
234
|
+
pieces.push(source.slice(cursor.at, from), " ".repeat(Math.max(0, span.end - from)));
|
|
235
|
+
cursor.at = Math.max(cursor.at, span.end);
|
|
236
|
+
});
|
|
237
|
+
pieces.push(source.slice(cursor.at));
|
|
238
|
+
return pieces
|
|
239
|
+
.join("")
|
|
240
|
+
.split("\n")
|
|
241
|
+
.filter((line) => !line.trimStart().startsWith(">"))
|
|
242
|
+
.join("\n");
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export const attachmentMissing: Detector = (doc): Finding[] => {
|
|
246
|
+
const lines = bodyLines(doc.source, doc.prose ?? doc.source, doc.replyQuotes ?? []);
|
|
247
|
+
const mention = firstMention(lines, patternsOf(doc, "attachment-mention"), patternsOf(doc, "attachment-not-mention"));
|
|
248
|
+
if (mention === undefined) return [];
|
|
249
|
+
const extensions = patternsOf(doc, "attachment-file-extension").map((extension) => extension.replace(/^\./u, ""));
|
|
250
|
+
if (showsAttachment(ownText(doc.source, doc.replyQuotes ?? []), patternsOf(doc, "attachment-line"), extensions, patternsOf(doc, "email-attachment-note")))
|
|
251
|
+
return [];
|
|
252
|
+
return [
|
|
253
|
+
{
|
|
254
|
+
rule: "attachment-not-attached",
|
|
255
|
+
severity: "warning",
|
|
256
|
+
line: 0,
|
|
257
|
+
column: 0,
|
|
258
|
+
quote: quoteAt(doc.source, mention),
|
|
259
|
+
values: { offset: mention },
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Detector, Finding } from "../plugin.ts";
|
|
2
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
3
|
+
import { quoteAround } from "./quote-around.ts";
|
|
4
|
+
|
|
5
|
+
// 注の無い注の印と、本文が一度も付けない注。Markdown の脚注([^1] と行頭の [^1]:)と、語彙表 note-mark の印(※1 と行頭の ※1)を読む。
|
|
6
|
+
|
|
7
|
+
/** 合わない注 1 つ。missing は印に注が無い、unused は注に印が無い。label は書いたままの印([^1]、※1)。 */
|
|
8
|
+
export type NoteSlip = { readonly offset: number; readonly label: string; readonly reason: "missing" | "unused" };
|
|
9
|
+
|
|
10
|
+
/** 印 1 つ。kind は印の種類(markdown か note-mark の語)、key は番号、note は行の頭の注そのものか。 */
|
|
11
|
+
type NoteMark = { readonly offset: number; readonly label: string; readonly kind: string; readonly key: string; readonly note: boolean };
|
|
12
|
+
|
|
13
|
+
const MARKDOWN = "markdown";
|
|
14
|
+
const FULLWIDTH_OFFSET = 0xfee0;
|
|
15
|
+
/** 行の頭から印までに来てよいもの(字下げ、箇条書きの印、開き括弧)。 */
|
|
16
|
+
const NOTE_LINE_HEAD = /^[ \t]*(?:[-*+][ \t]+)?[((]?$/u;
|
|
17
|
+
|
|
18
|
+
const halfWidth = (text: string): string => text.replace(/[0-9]/gu, (char) => String.fromCharCode(char.charCodeAt(0) - FULLWIDTH_OFFSET));
|
|
19
|
+
|
|
20
|
+
const lineHeadOf = (text: string, offset: number): string => text.slice(text.lastIndexOf("\n", offset - 1) + 1, offset);
|
|
21
|
+
|
|
22
|
+
/** Markdown の脚注。名前は大文字と小文字を区別しない(CommonMark の参照と同じ)。 */
|
|
23
|
+
const markdownMarks = (text: string): NoteMark[] =>
|
|
24
|
+
[...text.matchAll(/\[\^(?<key>[^\]\s]+)\](?<colon>:)?/gu)].map((match) => {
|
|
25
|
+
const note = match.groups?.["colon"] !== undefined && /^[ \t]{0,3}$/u.test(lineHeadOf(text, match.index));
|
|
26
|
+
return { offset: match.index, label: `[^${match.groups?.["key"] ?? ""}]`, kind: MARKDOWN, key: (match.groups?.["key"] ?? "").toLowerCase(), note };
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** 印と一桁か二桁の番号。後ろに数字や小数が続けば年や数(※2018年度、※1.2倍)で、注の番号ではない。 */
|
|
30
|
+
const wordMarks = (text: string, mark: string): NoteMark[] =>
|
|
31
|
+
[...text.matchAll(new RegExp(`${escapeRegExp(mark)}(?<key>[0-90-9]{1,2})(?![0-90-9]|[..,,][0-90-9])`, "gu"))].map((match) => ({
|
|
32
|
+
offset: match.index,
|
|
33
|
+
label: match[0],
|
|
34
|
+
kind: mark,
|
|
35
|
+
key: halfWidth(match.groups?.["key"] ?? ""),
|
|
36
|
+
note: NOTE_LINE_HEAD.test(lineHeadOf(text, match.index)),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
/** 一つの種類の印のうち、合わないもの。印の語(※)は、その種類の注が一つも無ければ注を別に置いた文書と読んで言わない。 */
|
|
40
|
+
const slipsOfKind = (marks: readonly NoteMark[], kind: string): NoteSlip[] => {
|
|
41
|
+
const ofKind = marks.filter((mark) => mark.kind === kind);
|
|
42
|
+
const notes = new Set(ofKind.filter((mark) => mark.note).map((mark) => mark.key));
|
|
43
|
+
const referenced = new Set(ofKind.filter((mark) => !mark.note).map((mark) => mark.key));
|
|
44
|
+
if (notes.size === 0 && kind !== MARKDOWN) return [];
|
|
45
|
+
const missing = ofKind
|
|
46
|
+
.filter((mark) => !mark.note && !notes.has(mark.key))
|
|
47
|
+
.map((mark): NoteSlip => ({ offset: mark.offset, label: mark.label, reason: "missing" }));
|
|
48
|
+
const unused = ofKind
|
|
49
|
+
.filter((mark) => mark.note && !referenced.has(mark.key))
|
|
50
|
+
.map((mark): NoteSlip => ({ offset: mark.offset, label: mark.label, reason: "unused" }));
|
|
51
|
+
return [...missing, ...unused];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** 文字列の中の、注の無い印と印の無い注。 */
|
|
55
|
+
export const noteSlips = (text: string, words: readonly string[]): NoteSlip[] => {
|
|
56
|
+
const marks = [...markdownMarks(text), ...words.flatMap((word) => wordMarks(text, word))];
|
|
57
|
+
return [...new Set(marks.map((mark) => mark.kind))].flatMap((kind) => slipsOfKind(marks, kind)).toSorted((left, right) => left.offset - right.offset);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const footnoteMismatch: Detector = (doc): Finding[] => {
|
|
61
|
+
const text = doc.prose ?? doc.source;
|
|
62
|
+
const words = (doc.lexicons["note-mark"] ?? []).map((entry) => entry.pattern);
|
|
63
|
+
return noteSlips(text, words).map((slip) => ({
|
|
64
|
+
rule: "",
|
|
65
|
+
severity: "warning",
|
|
66
|
+
line: 0,
|
|
67
|
+
column: 0,
|
|
68
|
+
quote: quoteAround(text, slip.offset, slip.offset + slip.label.length),
|
|
69
|
+
values: { label: slip.label, offset: slip.offset },
|
|
70
|
+
variant: slip.reason,
|
|
71
|
+
}));
|
|
72
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// 一つの文書の中で、同じものを何通りかに書いたときの少ないほう。どの書き方が正しいかは決めない。
|
|
2
|
+
|
|
3
|
+
/** 少ないほうの書き方のものと、いちばん多い書き方の例。 */
|
|
4
|
+
export type FormMinority<T> = { readonly odd: readonly T[]; readonly usual: T };
|
|
5
|
+
|
|
6
|
+
const PERCENT = 100;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* いちばん多い書き方と違うもの。少ないほうが limit パーセントを超えれば使い分けている文書と読んで言わない。
|
|
10
|
+
* いちばん多い書き方が同じ数で並べば、どちらにも立たない。
|
|
11
|
+
*/
|
|
12
|
+
export const formMinority = <T extends { readonly form: string }>(items: readonly T[], limit: number): FormMinority<T> | undefined => {
|
|
13
|
+
const counts = new Map<string, number>();
|
|
14
|
+
items.forEach((item) => counts.set(item.form, (counts.get(item.form) ?? 0) + 1));
|
|
15
|
+
const [top, second] = [...counts].toSorted((left, right) => right[1] - left[1]);
|
|
16
|
+
if (top === undefined || second === undefined || top[1] === second[1]) return undefined;
|
|
17
|
+
const odd = items.filter((item) => item.form !== top[0]);
|
|
18
|
+
const usual = items.find((item) => item.form === top[0]);
|
|
19
|
+
if (usual === undefined || odd.length * PERCENT > items.length * limit) return undefined;
|
|
20
|
+
return { odd, usual };
|
|
21
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Detector, Finding, MarkupHeading, ProseDocument } from "../plugin.ts";
|
|
2
|
+
import { findingAt, writtenHeadings } from "./markup-finding.ts";
|
|
3
|
+
import { pageTitleOf } from "./heading-case.ts";
|
|
4
|
+
import { minorityWithin } from "../orthography.ts";
|
|
5
|
+
|
|
6
|
+
/** A mark a heading may end with or go without: a colon or a full stop, in either width. A question mark belongs to the words. */
|
|
7
|
+
const END_MARK = /[::.。]$/u;
|
|
8
|
+
|
|
9
|
+
/** An ellipsis: the full stops are the words trailing off. */
|
|
10
|
+
const ELLIPSIS = /(?:\.\.\.|…)$/u;
|
|
11
|
+
|
|
12
|
+
/** Emphasis closed at the very end (`## **Note:**`): the mark the reader sees is the one before it. */
|
|
13
|
+
const EMPHASIS_MARKS = new Set(["*", "_"]);
|
|
14
|
+
|
|
15
|
+
const withoutClosingEmphasis = (text: string): string => {
|
|
16
|
+
const kept = [...text];
|
|
17
|
+
while (EMPHASIS_MARKS.has(kept.at(-1) ?? "")) kept.pop();
|
|
18
|
+
return kept.join("");
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const LETTER = /\p{L}/u;
|
|
22
|
+
|
|
23
|
+
/** Past this share, both ways are in use on purpose, and neither is the odd one. */
|
|
24
|
+
const MINORITY_PERCENT = 25;
|
|
25
|
+
|
|
26
|
+
/** Fewer section headings than this, and the majority is not yet a habit. */
|
|
27
|
+
const MIN_HEADINGS = 4;
|
|
28
|
+
|
|
29
|
+
/** Whether the heading's last word is one that ends in a full stop of its own (etc., Inc., from the lexicon). */
|
|
30
|
+
const endsWithWord = (shown: string, words: readonly string[]): boolean =>
|
|
31
|
+
words.some((word) => shown.endsWith(word) && !LETTER.test(shown.charAt(shown.length - word.length - 1)));
|
|
32
|
+
|
|
33
|
+
/** The mark a heading ends with, "" for none, or undefined when its last full stop belongs to its words. */
|
|
34
|
+
export const endMarkOf = (heading: string, words: readonly string[] = []): string | undefined => {
|
|
35
|
+
const shown = withoutClosingEmphasis(heading.trim()).trimEnd();
|
|
36
|
+
if (ELLIPSIS.test(shown) || endsWithWord(shown, words)) return undefined;
|
|
37
|
+
return END_MARK.exec(shown)?.[0] ?? "";
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type Judged = { readonly heading: MarkupHeading; readonly mark: string };
|
|
41
|
+
|
|
42
|
+
/** The odd ones among headings of one depth, each with the usual side's first heading and count. */
|
|
43
|
+
const oddAtDepth = (doc: ProseDocument, judged: readonly Judged[]): Finding[] => {
|
|
44
|
+
if (judged.length < MIN_HEADINGS) return [];
|
|
45
|
+
const odd = minorityWithin(judged, (entry) => entry.mark !== "", MINORITY_PERCENT);
|
|
46
|
+
const usual = judged.filter((entry) => !odd.includes(entry));
|
|
47
|
+
const example = usual[0]?.heading.text.trim() ?? "";
|
|
48
|
+
return odd.map((entry) =>
|
|
49
|
+
findingAt(
|
|
50
|
+
doc,
|
|
51
|
+
entry.heading,
|
|
52
|
+
{ heading: entry.heading.text.trim(), mark: entry.mark, example, count: usual.length },
|
|
53
|
+
entry.mark === "" ? "bare" : "marked",
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Headings that end with a colon or a full stop where most headings of the same depth do not, or the other way round. Headings
|
|
60
|
+
* of one depth are one kind of heading; a depth of labels ("Optional:") is its own way. The page title (one top-level heading
|
|
61
|
+
* that opens the document) is a name and is not compared.
|
|
62
|
+
*/
|
|
63
|
+
export const headingEndMark: Detector = (doc, options): Finding[] => {
|
|
64
|
+
const words = (options.lexicon ?? []).map((entry) => entry.pattern);
|
|
65
|
+
const headings = writtenHeadings(doc);
|
|
66
|
+
const title = pageTitleOf(headings);
|
|
67
|
+
const judged = headings
|
|
68
|
+
.filter((heading) => heading !== title)
|
|
69
|
+
.flatMap((heading): Judged[] => {
|
|
70
|
+
const mark = endMarkOf(heading.text, words);
|
|
71
|
+
return mark === undefined ? [] : [{ heading, mark }];
|
|
72
|
+
});
|
|
73
|
+
const depths = [...new Set(judged.map((entry) => entry.heading.depth))];
|
|
74
|
+
return depths
|
|
75
|
+
.flatMap((depth) =>
|
|
76
|
+
oddAtDepth(
|
|
77
|
+
doc,
|
|
78
|
+
judged.filter((entry) => entry.heading.depth === depth),
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
.toSorted((left, right) => Number(left.values["offset"]) - Number(right.values["offset"]));
|
|
82
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Detector, Finding, ProseDocument } from "../plugin.ts";
|
|
2
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
3
|
+
import { quoteAround } from "./quote-around.ts";
|
|
4
|
+
|
|
5
|
+
// 暦に無い日付(2月30日、4月31日、2023-02-29)。月の長さとうるう年は暦で決まるので、書き損じは機械で言える。
|
|
6
|
+
// 月の名前は語彙表 month-name(初めの十二が一月から順に)、年月日の単位は date-time-unit(大きい順の初めの三つ)、元号は calendar-era が言う。
|
|
7
|
+
|
|
8
|
+
/** 日付を読むのに要る語。months は一月から順の月の名前と略した名前、units は年・月・日の単位、eras は元号。 */
|
|
9
|
+
export type CalendarWords = { readonly months: readonly string[]; readonly units: readonly string[]; readonly eras: readonly string[] };
|
|
10
|
+
|
|
11
|
+
/** 暦に無い日付 1 つ。reason は、月が無い(month)、その月にその日が無い(day)、うるう年でない年の 2 月 29 日(leap)。 */
|
|
12
|
+
export type ImpossibleDate = {
|
|
13
|
+
readonly offset: number;
|
|
14
|
+
readonly written: string;
|
|
15
|
+
readonly reason: "month" | "day" | "leap";
|
|
16
|
+
readonly month: number;
|
|
17
|
+
readonly days: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DIGIT = "[0-90-9]";
|
|
21
|
+
const NOT_AFTER_NUMBER = "(?<![\\w/.\\-0-9])";
|
|
22
|
+
/** 日付の後ろに来ない字。ただし日時(2023-02-29T10:00)の T は日付の終わり。 */
|
|
23
|
+
const NOT_BEFORE_NUMBER = "(?:(?=T\\d)|(?![\\w//\\-0-9]|\\.\\w))";
|
|
24
|
+
const MONTHS_IN_YEAR = 12;
|
|
25
|
+
const LONGEST_MONTH = 31;
|
|
26
|
+
const FEBRUARY = 2;
|
|
27
|
+
const LEAP_DAY = 29;
|
|
28
|
+
const FIRST_CALENDAR_YEAR = 1000;
|
|
29
|
+
const DAYS_IN_MONTH: readonly number[] = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
30
|
+
const FULLWIDTH_OFFSET = 0xfee0;
|
|
31
|
+
|
|
32
|
+
const halfWidth = (text: string): string => text.replace(/[0-9]/gu, (char) => String.fromCharCode(char.charCodeAt(0) - FULLWIDTH_OFFSET));
|
|
33
|
+
|
|
34
|
+
const isLeapYear = (year: number): boolean => (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
35
|
+
|
|
36
|
+
/** 書き方ごとの読み方。strict は数の組が日付だと単位や名前で分かる形で、月が 1〜12 にない・日が 31 を超えるのも言う。 */
|
|
37
|
+
type Shape = { readonly pattern: RegExp; readonly strict: boolean };
|
|
38
|
+
|
|
39
|
+
/** 年の数。元号の年や 4 桁でない年は、うるう年を決められないので undefined。 */
|
|
40
|
+
const yearOf = (groups: Readonly<Record<string, string | undefined>>): number | undefined => {
|
|
41
|
+
const written = groups["y"];
|
|
42
|
+
if (written === undefined || groups["era"] !== undefined) return undefined;
|
|
43
|
+
const year = Number(halfWidth(written));
|
|
44
|
+
return year >= FIRST_CALENDAR_YEAR ? year : undefined;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** 一月から順の月の名前(初めの十二)で、略した名前(Sept、Feb.)が何月か。 */
|
|
48
|
+
const monthNamed = (months: readonly string[], written: string): number => {
|
|
49
|
+
const name = written.replace(/\.$/u, "");
|
|
50
|
+
return months.slice(0, MONTHS_IN_YEAR).findIndex((full) => full.startsWith(name)) + 1;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type Groups = Readonly<Record<string, string | undefined>>;
|
|
54
|
+
|
|
55
|
+
const monthOf = (groups: Groups, months: readonly string[]): number =>
|
|
56
|
+
groups["name"] === undefined ? Number(halfWidth(groups["m"] ?? "")) : monthNamed(months, groups["name"]);
|
|
57
|
+
|
|
58
|
+
/** その月の日数。2 月は、年が分かってうるう年でなければ 28 日、分からなければ 29 日。 */
|
|
59
|
+
const daysIn = (month: number, year: number | undefined): number =>
|
|
60
|
+
month === FEBRUARY && year !== undefined && !isLeapYear(year) ? LEAP_DAY - 1 : (DAYS_IN_MONTH[month - 1] ?? LONGEST_MONTH);
|
|
61
|
+
|
|
62
|
+
const problemOf = (month: number, day: number, year: number | undefined, strict: boolean): ImpossibleDate["reason"] | undefined => {
|
|
63
|
+
if (month < 1 || month > MONTHS_IN_YEAR) return strict ? "month" : undefined;
|
|
64
|
+
if (day < 1 || day > LONGEST_MONTH) return strict ? "day" : undefined;
|
|
65
|
+
if (day <= daysIn(month, year)) return undefined;
|
|
66
|
+
return month === FEBRUARY && day === LEAP_DAY ? "leap" : "day";
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const shapesOf = (words: CalendarWords): Shape[] => {
|
|
70
|
+
const [yearUnit, monthUnit, dayUnit] = words.units.map(escapeRegExp);
|
|
71
|
+
const names = words.months.map(escapeRegExp).join("|");
|
|
72
|
+
const eras = words.eras.map(escapeRegExp).join("|");
|
|
73
|
+
const era = eras === "" ? "" : `(?:(?<era>${eras})\\s?)?`;
|
|
74
|
+
const unitShape =
|
|
75
|
+
yearUnit === undefined || monthUnit === undefined || dayUnit === undefined
|
|
76
|
+
? []
|
|
77
|
+
: [
|
|
78
|
+
{
|
|
79
|
+
pattern: new RegExp(
|
|
80
|
+
`(?<!${DIGIT})${era}(?:(?<y>${DIGIT}{1,4}|元)${yearUnit}\\s?)?(?<m>${DIGIT}{1,2})${monthUnit}\\s?(?<d>${DIGIT}{1,2})${dayUnit}`,
|
|
81
|
+
"gu",
|
|
82
|
+
),
|
|
83
|
+
strict: true,
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
const nameShapes =
|
|
87
|
+
names === ""
|
|
88
|
+
? []
|
|
89
|
+
: [
|
|
90
|
+
{ pattern: new RegExp(`\\b(?<name>${names})\\.?\\s(?<d>\\d{1,2})(?:st|nd|rd|th)?(?![\\d:])(?:,?\\s(?<y>\\d{4})\\b)?`, "gu"), strict: false },
|
|
91
|
+
{ pattern: new RegExp(`(?<![\\d.,])\\b(?<d>\\d{1,2})(?:st|nd|rd|th)?\\s(?<name>${names})\\b\\.?(?:,?\\s(?<y>\\d{4})\\b)?`, "gu"), strict: false },
|
|
92
|
+
];
|
|
93
|
+
return [
|
|
94
|
+
{ pattern: new RegExp(`${NOT_AFTER_NUMBER}(?<y>\\d{4})-(?<m>\\d{1,2})-(?<d>\\d{1,2})${NOT_BEFORE_NUMBER}`, "gu"), strict: false },
|
|
95
|
+
{ pattern: new RegExp(`${NOT_AFTER_NUMBER}(?<y>\\d{4})[//](?<m>\\d{1,2})[//](?<d>\\d{1,2})${NOT_BEFORE_NUMBER}`, "gu"), strict: false },
|
|
96
|
+
...unitShape,
|
|
97
|
+
...nameShapes,
|
|
98
|
+
];
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const impossibleIn = (text: string, shape: Shape, months: readonly string[]): ImpossibleDate[] =>
|
|
102
|
+
[...text.matchAll(shape.pattern)].flatMap((match) => {
|
|
103
|
+
const groups = match.groups ?? {};
|
|
104
|
+
const month = monthOf(groups, months);
|
|
105
|
+
const year = yearOf(groups);
|
|
106
|
+
const reason = problemOf(month, Number(halfWidth(groups["d"] ?? "")), year, shape.strict);
|
|
107
|
+
if (reason === undefined) return [];
|
|
108
|
+
return [{ offset: match.index, written: match[0].trim(), reason, month, days: daysIn(month, year) }];
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/** 文字列の中の、暦に無い日付。同じ所に二つの形が当たれば、先に見つけたほうだけ。 */
|
|
112
|
+
export const impossibleDates = (text: string, words: CalendarWords): ImpossibleDate[] => {
|
|
113
|
+
const byOffset = new Map<number, ImpossibleDate>();
|
|
114
|
+
shapesOf(words)
|
|
115
|
+
.flatMap((shape) => impossibleIn(text, shape, words.months))
|
|
116
|
+
.forEach((found) => {
|
|
117
|
+
if (!byOffset.has(found.offset)) byOffset.set(found.offset, found);
|
|
118
|
+
});
|
|
119
|
+
return [...byOffset.values()].toSorted((left, right) => left.offset - right.offset);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const patternsOf = (doc: ProseDocument, id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
|
|
123
|
+
|
|
124
|
+
export const calendarWordsOf = (doc: ProseDocument): CalendarWords => ({
|
|
125
|
+
months: patternsOf(doc, "month-name"),
|
|
126
|
+
units: patternsOf(doc, "date-time-unit").slice(0, 3),
|
|
127
|
+
eras: patternsOf(doc, "calendar-era"),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export const impossibleDate: Detector = (doc): Finding[] => {
|
|
131
|
+
const text = doc.prose ?? doc.source;
|
|
132
|
+
return impossibleDates(text, calendarWordsOf(doc)).map((found) => ({
|
|
133
|
+
rule: "",
|
|
134
|
+
severity: "warning",
|
|
135
|
+
line: 0,
|
|
136
|
+
column: 0,
|
|
137
|
+
quote: quoteAround(text, found.offset, found.offset + found.written.length),
|
|
138
|
+
values: { written: found.written, month: found.month, days: found.days, offset: found.offset },
|
|
139
|
+
variant: found.reason,
|
|
140
|
+
}));
|
|
141
|
+
};
|