chaffjs 0.20.0 → 0.21.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 +5 -4
- 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 +14 -5
- package/dist/cli-text.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -0
- 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/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 +11 -1
- package/dist/custom/problems.js.map +1 -1
- 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/custom.d.ts.map +1 -1
- package/dist/detectors/custom.js +4 -1
- package/dist/detectors/custom.js.map +1 -1
- 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/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/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/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/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/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/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/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/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/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/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/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-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/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/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/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/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/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 +23 -0
- package/dist/rule-fields.d.ts.map +1 -0
- package/dist/rule-fields.js +64 -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/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/cushion-phrase-density.yaml +8 -1
- package/rules/dangling-figure-reference.yaml +3 -1
- 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/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-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/link-text-url-mismatch.yaml +72 -0
- package/rules/list-item-form-mix.yaml +7 -1
- 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/number-style-consistency.yaml +6 -1
- package/rules/number-without-unit.yaml +2 -1
- package/rules/numbering-gap.yaml +1 -1
- package/rules/one-sentence-paragraph-run.yaml +5 -1
- 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/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/shell-prompt-mix.yaml +128 -0
- package/rules/space-before-punctuation.yaml +1 -1
- package/rules/spelling-consistency.yaml +3 -1
- 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/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/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/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/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/wordy-phrase.yaml +6 -1
- package/rules/yori-as-from.yaml +61 -0
- package/src/api.ts +13 -1
- package/src/cli-text.ts +18 -7
- package/src/cli.ts +3 -0
- 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/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 +18 -2
- 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/custom.ts +10 -2
- package/src/detectors/defined-term-form.ts +200 -0
- package/src/detectors/email-letter.ts +262 -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/mark-shape.ts +202 -0
- package/src/detectors/registry/adverb-polarity.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/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/heading-command.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/mark-consistency.ts +4 -0
- package/src/detectors/registry/origin-particle.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/shell-prompt.ts +4 -0
- package/src/detectors/registry/step-statement.ts +4 -0
- package/src/detectors/registry/unpaired-parallel.ts +4 -0
- package/src/detectors/request-owner.ts +141 -0
- package/src/detectors/technical-docs.ts +230 -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/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 +95 -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,8 @@
|
|
|
1
|
+
import type { CustomProblem } from "./parse.ts";
|
|
2
|
+
|
|
3
|
+
/** One field of one rule, read: its value when it can be used, and what is wrong with it otherwise. */
|
|
4
|
+
export type Checked<T> = { readonly value: T | undefined; readonly problems: readonly CustomProblem[] };
|
|
5
|
+
|
|
6
|
+
export const ok = <T>(value: T): Checked<T> => ({ value, problems: [] });
|
|
7
|
+
|
|
8
|
+
export const failed = <T>(...problems: CustomProblem[]): Checked<T> => ({ value: undefined, problems });
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { LevelTable, Localized, Severity } from "../plugin.ts";
|
|
2
|
+
import { RULE_GROUPS, ruleGuideOf, type RuleGroup, type RuleGuide } from "../rule-guide.ts";
|
|
3
|
+
import { fieldProblems } from "../rule-fields.ts";
|
|
4
|
+
import { failed, ok, type Checked } from "./checked.ts";
|
|
5
|
+
|
|
6
|
+
// The fields a team's rule (custom_rules, or a plugin's rules) writes the way chaff's own rules/*.yaml do: levels,
|
|
7
|
+
// use_for, group, summary, the example by language and rewrite. Each is optional; without it the rule reads as a team
|
|
8
|
+
// rule always has. The shared fields are checked by rule-fields.ts, the one place chaff's own rules are checked too. Pure.
|
|
9
|
+
|
|
10
|
+
const SEVERITIES: readonly Severity[] = ["info", "warning", "error"];
|
|
11
|
+
const LEVEL_NAMES = ["strict", "normal", "relaxed"] as const;
|
|
12
|
+
|
|
13
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
|
|
15
|
+
const printed = (value: unknown): string => (typeof value === "string" ? value : (JSON.stringify(value) ?? typeof value));
|
|
16
|
+
|
|
17
|
+
const rankOf = (severity: Severity): number => SEVERITIES.indexOf(severity) + 1;
|
|
18
|
+
|
|
19
|
+
/** level: one severity; normal is the rule's own, relaxed one lower, strict one higher, where there is one. */
|
|
20
|
+
const tableFor = (severity: Severity): LevelTable => {
|
|
21
|
+
const rank = rankOf(severity);
|
|
22
|
+
return { ...(rank < SEVERITIES.length ? { strict: rank + 1 } : {}), normal: rank, ...(rank > 1 ? { relaxed: rank - 1 } : {}) };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const severityAt = (value: unknown): Severity | undefined => SEVERITIES.find((entry) => entry === value);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* levels: a severity for each of strict, normal and relaxed, as chaff's rules with nothing to count write them. A
|
|
29
|
+
* team's detector reports places, not counts, so a number has nothing to be compared with and is refused.
|
|
30
|
+
*/
|
|
31
|
+
const tableOf = (written: unknown): { table: LevelTable; severity: Severity } | undefined => {
|
|
32
|
+
if (!isRecord(written) || Object.keys(written).some((key) => !LEVEL_NAMES.some((name) => name === key))) return undefined;
|
|
33
|
+
const ranks = LEVEL_NAMES.flatMap((name) => {
|
|
34
|
+
const severity = severityAt(written[name]);
|
|
35
|
+
return severity === undefined ? [] : [[name, rankOf(severity)] as const];
|
|
36
|
+
});
|
|
37
|
+
const normal = severityAt(written["normal"]);
|
|
38
|
+
if (normal === undefined || ranks.length !== Object.keys(written).length) return undefined;
|
|
39
|
+
return { table: Object.fromEntries(ranks), severity: normal };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type RuleLevels = { readonly table: LevelTable; readonly severity: Severity };
|
|
43
|
+
|
|
44
|
+
/** level (one severity, as before) or levels (a severity per level). Not both: they would disagree on normal. */
|
|
45
|
+
export const levelsOf = (raw: Readonly<Record<string, unknown>>, at: string): Checked<RuleLevels> => {
|
|
46
|
+
const [level, levels] = [raw["level"], raw["levels"]];
|
|
47
|
+
if (level !== undefined && levels !== undefined) return failed({ kind: "level-and-levels", at });
|
|
48
|
+
if (levels !== undefined) {
|
|
49
|
+
const read = tableOf(levels);
|
|
50
|
+
return read === undefined ? failed({ kind: "bad-levels", at, written: printed(levels) }) : ok(read);
|
|
51
|
+
}
|
|
52
|
+
if (level === undefined) return ok({ table: tableFor("warning"), severity: "warning" });
|
|
53
|
+
const severity = severityAt(level);
|
|
54
|
+
return severity === undefined ? failed({ kind: "bad-level", at, written: printed(level) }) : ok({ table: tableFor(severity), severity });
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** An example written by language ({ ja: { before, after } }) rather than as { before, after }. */
|
|
58
|
+
const isExampleByLanguage = (example: unknown): example is Record<string, unknown> =>
|
|
59
|
+
isRecord(example) && Object.keys(example).length > 0 && !("before" in example) && !("after" in example);
|
|
60
|
+
|
|
61
|
+
/** example's before and after, each by language, whichever form it is written in. */
|
|
62
|
+
export const exampleSides = (example: unknown): { readonly before: unknown; readonly after: unknown } => {
|
|
63
|
+
if (!isRecord(example)) return { before: undefined, after: undefined };
|
|
64
|
+
if (!isExampleByLanguage(example)) return { before: example["before"], after: example["after"] };
|
|
65
|
+
const pairs = Object.entries(example).flatMap(([language, pair]) => (isRecord(pair) ? [[language, pair] as const] : []));
|
|
66
|
+
return {
|
|
67
|
+
before: Object.fromEntries(pairs.map(([language, pair]) => [language, pair["before"]])),
|
|
68
|
+
after: Object.fromEntries(pairs.map(([language, pair]) => [language, pair["after"]])),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type Described = Pick<RuleGuide, "rewrite" | "rewriteDepth"> & {
|
|
73
|
+
readonly group: RuleGroup;
|
|
74
|
+
/** undefined: the rule's name stands for its summary. */
|
|
75
|
+
readonly summary: Localized | undefined;
|
|
76
|
+
/** undefined: every genre, as a team's rule always has. */
|
|
77
|
+
readonly useFor: readonly string[] | undefined;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const textOf = (value: unknown): Localized | undefined => {
|
|
81
|
+
if (typeof value === "string") return value.trim() === "" ? undefined : { ja: value.trim(), en: value.trim() };
|
|
82
|
+
return isRecord(value) ? ruleGuideOf({ summary: value }).summary : undefined;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The fields a team's rule shares with chaff's own, checked in rule-fields.ts. An example written as { before, after }
|
|
87
|
+
* is left to the caller, which has always read and reported it.
|
|
88
|
+
*/
|
|
89
|
+
export const describedOf = (raw: Readonly<Record<string, unknown>>, at: string, genres: readonly string[]): Checked<Described> => {
|
|
90
|
+
const shared = { ...raw, example: isExampleByLanguage(raw["example"]) ? raw["example"] : undefined };
|
|
91
|
+
const problems = fieldProblems(shared, genres).map((problem) => ({ kind: problem.kind, at, written: problem.written }));
|
|
92
|
+
if (problems.length > 0) return failed(...problems);
|
|
93
|
+
const { rewrite, rewriteDepth } = ruleGuideOf({ rewrite: raw["rewrite"] });
|
|
94
|
+
const useFor = raw["use_for"];
|
|
95
|
+
return ok({
|
|
96
|
+
group: RULE_GROUPS.find((group) => group === raw["group"]) ?? "team",
|
|
97
|
+
summary: textOf(raw["summary"]),
|
|
98
|
+
useFor: Array.isArray(useFor) ? useFor.map((entry) => String(entry)) : undefined,
|
|
99
|
+
rewrite,
|
|
100
|
+
rewriteDepth,
|
|
101
|
+
});
|
|
102
|
+
};
|
package/src/custom/load.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Config } from "../config/load.ts";
|
|
|
2
2
|
import type { RuleDefinition } from "../plugin.ts";
|
|
3
3
|
import { loadRules } from "../rule-load.ts";
|
|
4
4
|
import { loadGenres } from "../genre-load.ts";
|
|
5
|
+
import { knownGenres } from "../known-genres.ts";
|
|
5
6
|
import { parseCustomRules, type CustomRules } from "./parse.ts";
|
|
6
7
|
|
|
7
8
|
/** The team's custom_rules as rules, with what could not be read. A team's rule applies to every genre, like the team's words. */
|
|
@@ -9,6 +10,7 @@ export const customRulesOf = (config: Pick<Config, "customRules" | "baseDir">):
|
|
|
9
10
|
parseCustomRules(config.customRules, {
|
|
10
11
|
builtIn: new Set(loadRules("en").map((rule) => rule.id)),
|
|
11
12
|
useFor: loadGenres().groups.map((group) => group.id),
|
|
13
|
+
genres: knownGenres(),
|
|
12
14
|
baseDir: config.baseDir,
|
|
13
15
|
});
|
|
14
16
|
|
package/src/custom/parse.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { CustomSpec,
|
|
1
|
+
import type { CustomSpec, Localized, RuleDefinition, TokenCondition } from "../plugin.ts";
|
|
2
2
|
import { regexRefusal, type RegexRefusal } from "./regex-safety.ts";
|
|
3
3
|
import { posTags } from "./token-pattern.ts";
|
|
4
4
|
import { modulePathOf, type ModulePathRefusal } from "./module-path.ts";
|
|
5
|
+
import type { FieldProblem } from "../rule-fields.ts";
|
|
6
|
+
import { failed, ok, type Checked } from "./checked.ts";
|
|
7
|
+
import { describedOf, exampleSides, levelsOf, type Described, type RuleLevels } from "./dsl-fields.ts";
|
|
5
8
|
|
|
6
9
|
// custom_rules in chaff.yaml: a team's own deterministic rules, written without code. Each becomes a RuleDefinition like the
|
|
7
10
|
// built-in ones, so findings, explain, rules --json, stet, the baseline and SARIF treat it the same. Pure: the YAML comes in parsed.
|
|
@@ -26,18 +29,25 @@ export type CustomProblem =
|
|
|
26
29
|
| { readonly kind: "bad-pattern"; readonly at: string; readonly refusal: RegexRefusal }
|
|
27
30
|
| { readonly kind: "no-tokens"; readonly at: string }
|
|
28
31
|
| { readonly kind: "bad-token"; readonly at: string; readonly index: number }
|
|
29
|
-
| { readonly kind: "unknown-pos"; readonly at: string; readonly written: string }
|
|
32
|
+
| { readonly kind: "unknown-pos"; readonly at: string; readonly written: string }
|
|
33
|
+
| { readonly kind: "level-and-levels"; readonly at: string }
|
|
34
|
+
| { readonly kind: "bad-levels"; readonly at: string; readonly written: string }
|
|
35
|
+
| (FieldProblem & { readonly at: string });
|
|
30
36
|
|
|
31
37
|
export type CustomRules = { readonly rules: readonly RuleDefinition[]; readonly problems: readonly CustomProblem[] };
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
|
-
* What parsing needs to know: the built-in rule ids (a team's rule may not take one), the genres a rule applies to
|
|
35
|
-
* the folder a module's path is relative to (where chaff.yaml is).
|
|
40
|
+
* What parsing needs to know: the built-in rule ids (a team's rule may not take one), the genres a rule applies to when
|
|
41
|
+
* it does not say (useFor), every genre a use_for may name, and the folder a module's path is relative to (where chaff.yaml is).
|
|
36
42
|
*/
|
|
37
|
-
export type CustomContext = {
|
|
43
|
+
export type CustomContext = {
|
|
44
|
+
readonly builtIn: ReadonlySet<string>;
|
|
45
|
+
readonly useFor: readonly string[];
|
|
46
|
+
readonly genres: readonly string[];
|
|
47
|
+
readonly baseDir: string;
|
|
48
|
+
};
|
|
38
49
|
|
|
39
50
|
const RULE_ID = /^[a-z][a-z0-9-]*$/u;
|
|
40
|
-
const SEVERITIES: readonly Severity[] = ["info", "warning", "error"];
|
|
41
51
|
|
|
42
52
|
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
43
53
|
|
|
@@ -58,12 +68,6 @@ const EVERY_LANGUAGE = "*";
|
|
|
58
68
|
|
|
59
69
|
const exampleSideOf = (value: unknown): Localized | undefined => (nonEmpty(value) ? { [EVERY_LANGUAGE]: value.trim() } : byLanguageOf(value));
|
|
60
70
|
|
|
61
|
-
/** One rule's own problems, with the rule's id (or its place in the list) as where. */
|
|
62
|
-
type Checked<T> = { readonly value: T | undefined; readonly problems: readonly CustomProblem[] };
|
|
63
|
-
|
|
64
|
-
const ok = <T>(value: T): Checked<T> => ({ value, problems: [] });
|
|
65
|
-
const failed = <T>(...problems: CustomProblem[]): Checked<T> => ({ value: undefined, problems });
|
|
66
|
-
|
|
67
71
|
type Pair = { readonly avoid: string; readonly use: string };
|
|
68
72
|
|
|
69
73
|
/** words as a map (avoid: use) or a list (words to point at, with nothing to use instead). */
|
|
@@ -125,7 +129,8 @@ const moduleOf = (raw: Record<string, unknown>, at: string, baseDir: string): Ch
|
|
|
125
129
|
};
|
|
126
130
|
|
|
127
131
|
const specOf = (raw: Record<string, unknown>, type: string, at: string, baseDir: string): Checked<CustomSpec> => {
|
|
128
|
-
|
|
132
|
+
// A words rule may take its words from a word list instead, one per language (a rule pack's lexicons/<language>/).
|
|
133
|
+
if (type === "words") return takesWordList(raw, type) ? ok({ type: "words", words: [] }) : wordsOf(raw["words"], at);
|
|
129
134
|
if (type === "pattern") return patternOf(raw, at);
|
|
130
135
|
if (type === "module") return moduleOf(raw, at, baseDir);
|
|
131
136
|
return tokensOf(raw["tokens"], at);
|
|
@@ -137,28 +142,16 @@ const typeOf = (raw: Record<string, unknown>, at: string): Checked<string> => {
|
|
|
137
142
|
return CUSTOM_TYPES.some((known) => known === type) ? ok(type) : failed({ kind: "unknown-type", at, written: type });
|
|
138
143
|
};
|
|
139
144
|
|
|
140
|
-
const severityOf = (raw: unknown, at: string): Checked<Severity> => {
|
|
141
|
-
if (raw === undefined) return ok("warning");
|
|
142
|
-
const severity = SEVERITIES.find((entry) => entry === raw);
|
|
143
|
-
return severity === undefined ? failed({ kind: "bad-level", at, written: typeof raw === "string" ? raw : printed(raw) }) : ok(severity);
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/** The four words as severities: normal is the rule's own level, relaxed one lower, strict one higher, where there is one. */
|
|
147
|
-
const levelsFor = (severity: Severity): LevelTable => {
|
|
148
|
-
const rank = SEVERITIES.indexOf(severity) + 1;
|
|
149
|
-
return { ...(rank < SEVERITIES.length ? { strict: rank + 1 } : {}), normal: rank, ...(rank > 1 ? { relaxed: rank - 1 } : {}) };
|
|
150
|
-
};
|
|
151
|
-
|
|
152
145
|
type Texts = { readonly name: Localized; readonly why: Localized; readonly how_to_fix: Localized; readonly before: Localized; readonly after: Localized };
|
|
153
146
|
|
|
154
147
|
const textsOf = (raw: Record<string, unknown>, at: string): Checked<Texts> => {
|
|
155
|
-
const example =
|
|
148
|
+
const example = exampleSides(raw["example"]);
|
|
156
149
|
const found = {
|
|
157
150
|
name: localizedOf(raw["name"]),
|
|
158
151
|
why: localizedOf(raw["why"]),
|
|
159
152
|
how_to_fix: localizedOf(raw["how_to_fix"]),
|
|
160
|
-
before: exampleSideOf(example
|
|
161
|
-
after: exampleSideOf(example
|
|
153
|
+
before: exampleSideOf(example.before),
|
|
154
|
+
after: exampleSideOf(example.after),
|
|
162
155
|
};
|
|
163
156
|
const missing = Object.entries(found).flatMap(([field, text]) =>
|
|
164
157
|
text === undefined ? [{ kind: "missing" as const, at, field: field === "before" || field === "after" ? `example.${field}` : field }] : [],
|
|
@@ -179,9 +172,13 @@ const requiresOf = (raw: unknown, type: string, at: string): Checked<readonly st
|
|
|
179
172
|
return unknown === undefined ? ok([...new Set(written)]) : failed({ kind: "bad-requires", at, written: unknown });
|
|
180
173
|
};
|
|
181
174
|
|
|
182
|
-
/** A module
|
|
183
|
-
const
|
|
184
|
-
|
|
175
|
+
/** A module reads a word list; so does a words rule that writes word_list in place of words. The others carry their words. */
|
|
176
|
+
const takesWordList = (raw: Record<string, unknown>, type: string): boolean =>
|
|
177
|
+
raw["word_list"] !== undefined && (type === "module" || (type === "words" && raw["words"] === undefined));
|
|
178
|
+
|
|
179
|
+
/** The word list by name; the detector gets it, in the document's language, as options.lexicon. */
|
|
180
|
+
const wordListOf = (raw: unknown, takesList: boolean, at: string): Checked<string | undefined> => {
|
|
181
|
+
if (raw === undefined || !takesList) return ok(undefined);
|
|
185
182
|
return nonEmpty(raw) ? ok(raw.trim()) : failed({ kind: "bad-word-list", at, written: printed(raw) });
|
|
186
183
|
};
|
|
187
184
|
|
|
@@ -190,7 +187,7 @@ type Needs = { readonly requires: readonly string[]; readonly wordList: string |
|
|
|
190
187
|
|
|
191
188
|
const needsOf = (raw: Record<string, unknown>, type: string, at: string): Checked<Needs> => {
|
|
192
189
|
const requires = requiresOf(raw["requires"], type, at);
|
|
193
|
-
const wordList = wordListOf(raw["word_list"], type, at);
|
|
190
|
+
const wordList = wordListOf(raw["word_list"], takesWordList(raw, type), at);
|
|
194
191
|
if (requires.value === undefined || wordList.problems.length > 0) return failed(...requires.problems, ...wordList.problems);
|
|
195
192
|
return ok({ requires: requires.value, wordList: wordList.value });
|
|
196
193
|
};
|
|
@@ -226,10 +223,11 @@ type Example = { readonly before: string; readonly after: string };
|
|
|
226
223
|
type Parts = {
|
|
227
224
|
readonly spec: CustomSpec;
|
|
228
225
|
readonly needs: Needs;
|
|
229
|
-
readonly
|
|
226
|
+
readonly levels: RuleLevels;
|
|
230
227
|
readonly texts: Texts;
|
|
231
228
|
readonly languages: readonly string[] | undefined;
|
|
232
229
|
readonly examples: Readonly<Record<string, Example>>;
|
|
230
|
+
readonly described: Described;
|
|
233
231
|
};
|
|
234
232
|
|
|
235
233
|
const READER_LANGUAGES: readonly string[] = ["ja", "en"];
|
|
@@ -253,7 +251,7 @@ const definitionOf = (id: string, raw: Record<string, unknown>, parts: Parts, us
|
|
|
253
251
|
message: localizedOf(raw["message"]) ?? defaultMessage(parts.spec, parts.texts.name),
|
|
254
252
|
messages: {},
|
|
255
253
|
placeholders: PLACEHOLDERS,
|
|
256
|
-
levels:
|
|
254
|
+
levels: parts.levels.table,
|
|
257
255
|
level_sets: "severity",
|
|
258
256
|
by_genre: {},
|
|
259
257
|
how_to_find: HOW_TO_FIND[parts.spec.type],
|
|
@@ -266,18 +264,19 @@ const definitionOf = (id: string, raw: Record<string, unknown>, parts: Parts, us
|
|
|
266
264
|
uses: [],
|
|
267
265
|
from: [],
|
|
268
266
|
languages: parts.languages,
|
|
269
|
-
use_for: useFor,
|
|
270
|
-
severity: parts.severity,
|
|
267
|
+
use_for: parts.described.useFor ?? useFor,
|
|
268
|
+
severity: parts.levels.severity,
|
|
271
269
|
custom: parts.spec,
|
|
272
270
|
// The reference and explain read a rule's plain-language fields from guide; a team's rule is listed with the team's words.
|
|
273
271
|
guide: {
|
|
274
|
-
group:
|
|
275
|
-
summary: parts.texts.name,
|
|
272
|
+
group: parts.described.group,
|
|
273
|
+
summary: parts.described.summary ?? parts.texts.name,
|
|
276
274
|
examples: parts.examples,
|
|
277
275
|
notFlagged: {},
|
|
278
276
|
levelMeaning: {},
|
|
279
277
|
sources: [],
|
|
280
|
-
rewrite:
|
|
278
|
+
rewrite: parts.described.rewrite,
|
|
279
|
+
rewriteDepth: parts.described.rewriteDepth,
|
|
281
280
|
},
|
|
282
281
|
});
|
|
283
282
|
|
|
@@ -289,24 +288,47 @@ const idOf = (raw: Record<string, unknown>, index: number, seen: ReadonlySet<str
|
|
|
289
288
|
return seen.has(id) ? failed({ kind: "duplicate-id", at }) : ok(id);
|
|
290
289
|
};
|
|
291
290
|
|
|
291
|
+
/** Each field of one rule, checked. */
|
|
292
|
+
type Checks = {
|
|
293
|
+
readonly id: Checked<string>;
|
|
294
|
+
readonly spec: Checked<CustomSpec>;
|
|
295
|
+
readonly needs: Checked<Needs>;
|
|
296
|
+
readonly levels: Checked<RuleLevels>;
|
|
297
|
+
readonly texts: Checked<Texts>;
|
|
298
|
+
readonly languages: Checked<readonly string[] | undefined>;
|
|
299
|
+
readonly described: Checked<Described>;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/** The rule's parts once every check passed; undefined when one has no value. */
|
|
303
|
+
const partsOf = (checks: Checks, examples: Readonly<Record<string, Example>>): { readonly id: string; readonly parts: Parts } | undefined => {
|
|
304
|
+
const { id, spec, needs, levels, texts, described } = checks;
|
|
305
|
+
if (id.value === undefined || spec.value === undefined || needs.value === undefined) return undefined;
|
|
306
|
+
if (levels.value === undefined || texts.value === undefined || described.value === undefined) return undefined;
|
|
307
|
+
const parts = { spec: spec.value, needs: needs.value, levels: levels.value, texts: texts.value, languages: checks.languages.value, examples };
|
|
308
|
+
return { id: id.value, parts: { ...parts, described: described.value } };
|
|
309
|
+
};
|
|
310
|
+
|
|
292
311
|
const ruleOf = (raw: unknown, index: number, seen: ReadonlySet<string>, context: CustomContext): Checked<RuleDefinition> => {
|
|
293
312
|
if (!isRecord(raw)) return failed({ kind: "not-a-map", at: `#${String(index + 1)}` });
|
|
294
313
|
const id = idOf(raw, index, seen, context.builtIn);
|
|
295
314
|
const at = id.value ?? (nonEmpty(raw["id"]) ? raw["id"] : `#${String(index + 1)}`);
|
|
296
315
|
const type = typeOf(raw, at);
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
316
|
+
const checks: Checks = {
|
|
317
|
+
id,
|
|
318
|
+
spec: type.value === undefined ? failed<CustomSpec>() : specOf(raw, type.value, at, context.baseDir),
|
|
319
|
+
needs: needsOf(raw, type.value ?? "", at),
|
|
320
|
+
levels: levelsOf(raw, at),
|
|
321
|
+
texts: textsOf(raw, at),
|
|
322
|
+
languages: languagesOf(raw["languages"], at),
|
|
323
|
+
described: describedOf(raw, at, context.genres),
|
|
324
|
+
};
|
|
325
|
+
const { texts, languages } = checks;
|
|
302
326
|
const examples = texts.value === undefined ? {} : examplesOf(texts.value, languages.value);
|
|
303
327
|
const unpaired: CustomProblem[] = texts.value !== undefined && Object.keys(examples).length === 0 ? [{ kind: "unpaired-example", at }] : [];
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const parts = { spec: spec.value, needs: needs.value, severity: severity.value, texts: texts.value, languages: languages.value, examples };
|
|
309
|
-
return ok(definitionOf(id.value, raw, parts, context.useFor));
|
|
328
|
+
const ordered: Checked<unknown>[] = [id, type, checks.spec, checks.needs, checks.levels, texts, languages, checks.described];
|
|
329
|
+
const problems = [...ordered.flatMap((checked) => checked.problems), ...unpaired];
|
|
330
|
+
const ready = problems.length > 0 ? undefined : partsOf(checks, examples);
|
|
331
|
+
return ready === undefined ? failed(...problems) : ok(definitionOf(ready.id, raw, ready.parts, context.useFor));
|
|
310
332
|
};
|
|
311
333
|
|
|
312
334
|
/** custom_rules as written. Every problem is reported; a rule with a problem is left out, and the run is stopped by the caller. */
|
package/src/custom/problems.ts
CHANGED
|
@@ -5,10 +5,13 @@ import { customRulesOf } from "./load.ts";
|
|
|
5
5
|
import { MAX_PATTERN_LENGTH, MAX_REPEATS, type RegexRefusal } from "./regex-safety.ts";
|
|
6
6
|
import { POS_WRITTEN_NAMES } from "./token-pattern.ts";
|
|
7
7
|
import type { ModulePathRefusal } from "./module-path.ts";
|
|
8
|
+
import { fieldProblemSentence, type FieldKind, type FieldProblem } from "../rule-fields.ts";
|
|
8
9
|
|
|
9
10
|
/** Each problem's sentence, with {at}, {written}, {field}, {index}, {refusal} and {names} filled in from the problem. */
|
|
10
11
|
type Text = {
|
|
11
|
-
readonly problems: Readonly<Record<CustomProblem["kind"], string>>;
|
|
12
|
+
readonly problems: Readonly<Record<Exclude<CustomProblem["kind"], FieldKind>, string>>;
|
|
13
|
+
/** What comes before a shared field's own sentence (rule-fields.ts). */
|
|
14
|
+
readonly fieldAt: string;
|
|
12
15
|
readonly refusal: Readonly<Record<RegexRefusal, string>>;
|
|
13
16
|
readonly modulePath: Readonly<Record<ModulePathRefusal, string>>;
|
|
14
17
|
};
|
|
@@ -45,7 +48,11 @@ const TEXT: Texts<Text> = {
|
|
|
45
48
|
"bad-token": "{scope} の {at}: tokens の {index} 番目に pos・base・surface のどれもありません",
|
|
46
49
|
"unknown-pos": "{scope} の {at}: 品詞 {written} は知りません({names})",
|
|
47
50
|
"bad-pattern": "{scope} の {at}: pattern を使えません。{refusal}",
|
|
51
|
+
"level-and-levels": "{scope} の {at}: level と levels の両方があります。どちらか一つにしてください",
|
|
52
|
+
"bad-levels":
|
|
53
|
+
"{scope} の {at}: levels: {written} は読めません。strict・normal・relaxed に重さ(error / warning / info)を書き、normal は必ず書きます。チームのルールは箇所を出すので、数は書けません",
|
|
48
54
|
},
|
|
55
|
+
fieldAt: "{scope} の {at}: ",
|
|
49
56
|
},
|
|
50
57
|
en: {
|
|
51
58
|
modulePath: {
|
|
@@ -78,7 +85,11 @@ const TEXT: Texts<Text> = {
|
|
|
78
85
|
"bad-token": "{scope} {at}: token {index} has none of pos, base and surface",
|
|
79
86
|
"unknown-pos": "{scope} {at}: unknown part of speech {written} ({names})",
|
|
80
87
|
"bad-pattern": "{scope} {at}: the pattern cannot be used: {refusal}",
|
|
88
|
+
"level-and-levels": "{scope} {at}: has both level and levels; keep one",
|
|
89
|
+
"bad-levels":
|
|
90
|
+
"{scope} {at}: cannot read levels: {written}. Give strict, normal and relaxed a severity (error / warning / info), normal always. A team's rule reports places, so a number has nothing to count",
|
|
81
91
|
},
|
|
92
|
+
fieldAt: "{scope} {at}: ",
|
|
82
93
|
},
|
|
83
94
|
};
|
|
84
95
|
|
|
@@ -100,10 +111,15 @@ const valuesOf = (problem: CustomProblem, text: Text, scope: string): Readonly<R
|
|
|
100
111
|
names: POS_WRITTEN_NAMES.join(" / "),
|
|
101
112
|
});
|
|
102
113
|
|
|
114
|
+
const FIELD_KINDS: ReadonlySet<string> = new Set<FieldKind>(["bad-depth", "bad-group", "bad-use-for", "bad-summary", "bad-example"]);
|
|
115
|
+
|
|
116
|
+
const isFieldProblem = (problem: CustomProblem): problem is FieldProblem & { readonly at: string } => FIELD_KINDS.has(problem.kind);
|
|
117
|
+
|
|
103
118
|
/** One problem as a sentence. scope names the list the rule is in: custom_rules, or a plugin's rules. */
|
|
104
119
|
export const customProblemSentence = (problem: CustomProblem, ui: UiLanguage, scope = "custom_rules"): string => {
|
|
105
120
|
const values = valuesOf(problem, TEXT[ui], scope);
|
|
106
|
-
|
|
121
|
+
const fill = (template: string): string => template.replace(PLACEHOLDER, (_whole, key: string) => values[key] ?? "");
|
|
122
|
+
return isFieldProblem(problem) ? `${fill(TEXT[ui].fieldAt)}${fieldProblemSentence(problem, ui)}` : fill(TEXT[ui].problems[problem.kind]);
|
|
107
123
|
};
|
|
108
124
|
|
|
109
125
|
/** What in custom_rules cannot run. Each stops the run: a team's rule that silently does not run looks like a clean document. */
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { Detector, Finding, ProseDocument, Span } from "../plugin.ts";
|
|
4
4
|
import { readMarkdown } from "../markdown-read.ts";
|
|
5
5
|
import { spanOf, type MarkdownNode } from "../markdown-node.ts";
|
|
6
|
-
import { eachPreOrder } from "../tree-walk.ts";
|
|
6
|
+
import { eachPreOrder, foldPostOrder } from "../tree-walk.ts";
|
|
7
7
|
import { quoteAt } from "./structure-tree.ts";
|
|
8
8
|
|
|
9
9
|
/** Fewer body rows than this, and one blank is as likely a choice as a gap. */
|
|
@@ -21,14 +21,22 @@ const EMPHASIS = /[*_]/gu;
|
|
|
21
21
|
const COMMENT = /^<!--[\s\S]*-->$/u;
|
|
22
22
|
const VISIBLE_LEAVES = new Set(["inlineCode", "image", "imageReference"]);
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
const
|
|
24
|
+
/** What a leaf shows on its own; undefined for a node whose children decide. */
|
|
25
|
+
const leafShows = (node: MarkdownNode): boolean | undefined => {
|
|
26
26
|
if (node.type === "text") return (node.value ?? "").replace(EMPHASIS, "").trim() !== "";
|
|
27
27
|
if (node.type === "html") return !COMMENT.test((node.value ?? "").trim());
|
|
28
28
|
if (VISIBLE_LEAVES.has(node.type)) return true;
|
|
29
|
-
return
|
|
29
|
+
return undefined;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
/** Whether a node shows the reader anything: text that is not blank, code, an image, or HTML that is not a comment. */
|
|
33
|
+
const shows = (node: MarkdownNode): boolean =>
|
|
34
|
+
foldPostOrder<MarkdownNode, boolean>(
|
|
35
|
+
node,
|
|
36
|
+
(each) => each.children ?? [],
|
|
37
|
+
(each, children) => leafShows(each) ?? children.some((child) => child),
|
|
38
|
+
);
|
|
39
|
+
|
|
32
40
|
const sourceOf = (source: string, node: MarkdownNode | undefined): string => {
|
|
33
41
|
const span = node === undefined ? undefined : spanOf(node);
|
|
34
42
|
return span === undefined ? "" : source.slice(span.start, span.end).replace(/\|/gu, "").trim();
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Detector, DetectorOptions, Finding, Lexicon, ProseDocument, Sentence, Token } from "../plugin.ts";
|
|
2
|
+
import { isWithinAny, quotedIn } from "../quoted-span.ts";
|
|
3
|
+
|
|
4
|
+
// The shape of a clause read from its words: how it ends, a paired word left alone, an adverb that needs a
|
|
5
|
+
// negation, a particle used where another one belongs. Every word comes from a lexicon; this file knows parts of speech.
|
|
6
|
+
|
|
7
|
+
const findingAt = (sentence: Sentence, offset: number, values: Readonly<Record<string, string | number>>): Finding => ({
|
|
8
|
+
rule: "",
|
|
9
|
+
severity: "info",
|
|
10
|
+
line: 0,
|
|
11
|
+
column: 0,
|
|
12
|
+
quote: sentence.text.trim(),
|
|
13
|
+
values: { ...values, offset },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const patternsOf = (lexicon: Lexicon | undefined, group?: string): ReadonlySet<string> =>
|
|
17
|
+
new Set((lexicon ?? []).filter((entry) => group === undefined || entry.group === group).map((entry) => entry.pattern));
|
|
18
|
+
|
|
19
|
+
const isSpace = (token: Token | undefined): boolean => token !== undefined && token.surface.trim() === "";
|
|
20
|
+
|
|
21
|
+
/** The tokens of a sentence outside anything quoted in 「」『』, with their index in the sentence. */
|
|
22
|
+
const ownTokens = (sentence: Sentence): { readonly token: Token; readonly at: number }[] => {
|
|
23
|
+
const quoted = quotedIn(sentence);
|
|
24
|
+
return (sentence.tokens ?? []).flatMap((token, at) => (isWithinAny(quoted, token.span) ? [] : [{ token, at }]));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const wordOf = (token: Token): string => token.lemma ?? token.surface;
|
|
28
|
+
|
|
29
|
+
const isListed = (words: ReadonlySet<string>, token: Token | undefined): boolean =>
|
|
30
|
+
token !== undefined && (words.has(token.surface) || words.has(wordOf(token)));
|
|
31
|
+
|
|
32
|
+
// --- ending-run: sentences in a row that end with the same words ---------------------------------------------
|
|
33
|
+
|
|
34
|
+
const TRAILING_POS: ReadonlySet<string> = new Set(["PUNCT", "SYM"]);
|
|
35
|
+
const TAIL_POS: ReadonlySet<string> = new Set(["AUX", "PART"]);
|
|
36
|
+
const VERBAL_POS: ReadonlySet<string> = new Set(["VERB"]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How a sentence ends: its closing auxiliaries, and the verb before them as written (しました, あります, 思います).
|
|
40
|
+
* A noun before them is left out, so 必要です and 重要です end the same way. No auxiliary, no ending to compare.
|
|
41
|
+
*/
|
|
42
|
+
export const endingOf = (sentence: Sentence): string | undefined => {
|
|
43
|
+
const tokens = (sentence.tokens ?? []).filter((token) => !isSpace(token));
|
|
44
|
+
const last = tokens.findLastIndex((token) => !TRAILING_POS.has(token.pos));
|
|
45
|
+
const head = tokens.slice(0, last + 1).findLastIndex((token) => !TAIL_POS.has(token.pos));
|
|
46
|
+
if (head === last) return undefined;
|
|
47
|
+
const headToken = tokens[head];
|
|
48
|
+
const tail = tokens.slice(head + 1, last + 1).map((token) => token.surface);
|
|
49
|
+
return [headToken !== undefined && VERBAL_POS.has(headToken.pos) ? headToken.surface : "", ...tail].join("");
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type Ended = { readonly sentence: Sentence; readonly ending: string | undefined };
|
|
53
|
+
|
|
54
|
+
/** Runs of consecutive sentences with the same ending. A sentence with no ending to compare closes a run. */
|
|
55
|
+
export const endingRuns = (sentences: readonly Sentence[]): Ended[][] =>
|
|
56
|
+
sentences
|
|
57
|
+
.map((sentence) => ({ sentence, ending: endingOf(sentence) }))
|
|
58
|
+
.reduce<Ended[][]>((runs, ended) => {
|
|
59
|
+
const last = runs.at(-1) ?? [];
|
|
60
|
+
const continues = ended.ending !== undefined && last[0]?.ending === ended.ending;
|
|
61
|
+
return continues ? [...runs.slice(0, -1), [...last, ended]] : [...runs, [ended]];
|
|
62
|
+
}, [])
|
|
63
|
+
.filter((run) => run[0]?.ending !== undefined);
|
|
64
|
+
|
|
65
|
+
const runFinding = (run: readonly Ended[], limit: number): Finding => {
|
|
66
|
+
const first = run[0];
|
|
67
|
+
return {
|
|
68
|
+
rule: "",
|
|
69
|
+
severity: "info",
|
|
70
|
+
line: 0,
|
|
71
|
+
column: 0,
|
|
72
|
+
quote: run.map(({ sentence }) => sentence.text.trim()).join(""),
|
|
73
|
+
values: { word: first?.ending ?? "", count: run.length, limit, offset: first?.sentence.span.start ?? 0 },
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** More than limit sentences in a row within one paragraph that end with the same words. */
|
|
78
|
+
export const endingRun: Detector = (doc, options): Finding[] =>
|
|
79
|
+
doc.paragraphs.flatMap((paragraph) =>
|
|
80
|
+
endingRuns(paragraph.sentences)
|
|
81
|
+
.filter((run) => run.length > options.limit)
|
|
82
|
+
.map((run) => runFinding(run, options.limit)),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// --- unpaired-parallel: a word that lists by pairs (〜たり) used once, followed by another clause -----------------
|
|
86
|
+
|
|
87
|
+
const CONTENT_POS: ReadonlySet<string> = new Set(["VERB", "ADJ", "NOUN", "PROPN"]);
|
|
88
|
+
|
|
89
|
+
const nextWord = (tokens: readonly Token[], at: number): Token | undefined => tokens.slice(at + 1).find((token) => !isSpace(token));
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The marker is left alone when the sentence holds it once and what follows starts a new clause: a comma,
|
|
93
|
+
* or a content word other than the lexicon's closer (〜たり、映画を見る / 〜たり見る). 〜たりする and 〜たりもする are whole.
|
|
94
|
+
*/
|
|
95
|
+
const leftAlone = (tokens: readonly Token[], at: number, closers: ReadonlySet<string>, commas: ReadonlySet<string>): boolean => {
|
|
96
|
+
const next = nextWord(tokens, at);
|
|
97
|
+
if (next === undefined) return false;
|
|
98
|
+
if (commas.has(next.surface)) return true;
|
|
99
|
+
return CONTENT_POS.has(next.pos) && !isListed(closers, next);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const CONJUNCTION = "CCONJ";
|
|
103
|
+
|
|
104
|
+
const markersIn = (sentence: Sentence, markers: ReadonlySet<string>): { readonly token: Token; readonly at: number }[] =>
|
|
105
|
+
ownTokens(sentence).filter(({ token }) => token.pos === CONJUNCTION && markers.has(token.surface));
|
|
106
|
+
|
|
107
|
+
/** A sentence that uses the lexicon's paired marker once and goes on without its partner. */
|
|
108
|
+
export const unpairedParallel: Detector = (doc, options): Finding[] => {
|
|
109
|
+
const markers = patternsOf(options.lexicon, "marker");
|
|
110
|
+
const closers = patternsOf(options.lexicon, "closer");
|
|
111
|
+
const commas = patternsOf(options.lexicon, "comma");
|
|
112
|
+
return doc.sentences.flatMap((sentence) => {
|
|
113
|
+
const found = markersIn(sentence, markers);
|
|
114
|
+
const only = found.length === 1 ? found[0] : undefined;
|
|
115
|
+
if (only === undefined || !leftAlone(sentence.tokens ?? [], only.at, closers, commas)) return [];
|
|
116
|
+
return [findingAt(sentence, only.token.span.start, { word: only.token.surface })];
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// --- adverb-polarity: an adverb that wants a negation, in a sentence that has none ------------------------------
|
|
121
|
+
|
|
122
|
+
type Negations = { readonly words: ReadonlySet<string>; readonly prefixes: readonly string[] };
|
|
123
|
+
|
|
124
|
+
const negationsOf = (lexicon: Lexicon | undefined): Negations => ({
|
|
125
|
+
words: new Set((lexicon ?? []).filter((entry) => entry.group === undefined).map((entry) => entry.pattern)),
|
|
126
|
+
prefixes: [...patternsOf(lexicon, "prefix")],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/** A negating word (ない, 違う) or a word opening with a negating prefix (不十分, 無理). */
|
|
130
|
+
const isNegation = (token: Token, negations: Negations): boolean =>
|
|
131
|
+
isListed(negations.words, token) || negations.prefixes.some((prefix) => token.surface.length > prefix.length && token.surface.startsWith(prefix));
|
|
132
|
+
|
|
133
|
+
type Placed = { readonly token: Token; readonly at: number };
|
|
134
|
+
|
|
135
|
+
/** A negation of the writer's own after the adverb; one inside a quotation does not answer it. */
|
|
136
|
+
const hasNegation = (own: readonly Placed[], from: number, negations: Negations): boolean =>
|
|
137
|
+
own.some(({ token, at }) => at > from && isNegation(token, negations));
|
|
138
|
+
|
|
139
|
+
/** Each of the lexicon's adverbs (全然, 決して) with no word of the negation list after it in its sentence. */
|
|
140
|
+
export const adverbPolarity: Detector = (doc, options): Finding[] => {
|
|
141
|
+
const adverbs = patternsOf(options.lexicon);
|
|
142
|
+
const negations = negationsOf(doc.lexicons["negation-word"]);
|
|
143
|
+
return doc.sentences.flatMap((sentence) => {
|
|
144
|
+
const own = ownTokens(sentence);
|
|
145
|
+
return own
|
|
146
|
+
.filter(({ token, at }) => adverbs.has(token.surface) && !hasNegation(own, at, negations))
|
|
147
|
+
.map(({ token }) => findingAt(sentence, token.span.start, { word: token.surface }));
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// --- origin-particle: a comparison particle (より) used for "from" -------------------------------------------------
|
|
152
|
+
|
|
153
|
+
/** Adverbs (順次) and punctuation (本日より、発送) stand between the particle and its head. */
|
|
154
|
+
const SKIPPED_POS: ReadonlySet<string> = new Set(["ADV", "PUNCT"]);
|
|
155
|
+
const PARTICLE = "ADP";
|
|
156
|
+
const NOMINAL_POS: ReadonlySet<string> = new Set(["NOUN", "PROPN"]);
|
|
157
|
+
|
|
158
|
+
/** The first word after the particle, past adverbs (順次) and a bound honorific prefix (ご連絡). */
|
|
159
|
+
const headAfter = (tokens: readonly Token[], at: number): number =>
|
|
160
|
+
tokens.findIndex((token, index) => index > at && !isSpace(token) && !SKIPPED_POS.has(token.pos) && token.features?.["Bound"] !== "Yes");
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The head names a start or a sending (開始, 参る, 連絡). A noun head must be the action itself: followed by an object
|
|
164
|
+
* marker or a light verb (受付を開始, 開始します), not a noun being compared (昨年より販売が増えた).
|
|
165
|
+
*/
|
|
166
|
+
const isOriginHead = (tokens: readonly Token[], head: number, heads: ReadonlySet<string>, objectMarkers: ReadonlySet<string>): boolean => {
|
|
167
|
+
const token = tokens[head];
|
|
168
|
+
if (token === undefined || !isListed(heads, token)) return false;
|
|
169
|
+
if (!NOMINAL_POS.has(token.pos)) return true;
|
|
170
|
+
const next = nextWord(tokens, head);
|
|
171
|
+
return next !== undefined && (objectMarkers.has(next.surface) || next.features?.["VerbType"] === "Light");
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
type ParticleWords = { readonly particles: ReadonlySet<string>; readonly heads: ReadonlySet<string>; readonly objectMarkers: ReadonlySet<string> };
|
|
175
|
+
|
|
176
|
+
const originsIn = (sentence: Sentence, words: ParticleWords): Token[] => {
|
|
177
|
+
const tokens = sentence.tokens ?? [];
|
|
178
|
+
return ownTokens(sentence)
|
|
179
|
+
.filter(
|
|
180
|
+
({ token, at }) =>
|
|
181
|
+
token.pos === PARTICLE && words.particles.has(token.surface) && isOriginHead(tokens, headAfter(tokens, at), words.heads, words.objectMarkers),
|
|
182
|
+
)
|
|
183
|
+
.map(({ token }) => token);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const particleWords = (doc: ProseDocument, options: DetectorOptions): ParticleWords => ({
|
|
187
|
+
particles: patternsOf(options.lexicon),
|
|
188
|
+
heads: patternsOf(doc.lexicons["origin-head"], "head"),
|
|
189
|
+
objectMarkers: patternsOf(doc.lexicons["origin-head"], "object"),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
/** The lexicon's particle where it marks a starting point (4月1日より開始), which the lexicon's rewrite says instead. */
|
|
193
|
+
export const originParticle: Detector = (doc, options): Finding[] => {
|
|
194
|
+
const words = particleWords(doc, options);
|
|
195
|
+
const rewrites = new Map((options.lexicon ?? []).map((entry) => [entry.pattern, entry.rewrite ?? ""]));
|
|
196
|
+
return doc.sentences.flatMap((sentence) =>
|
|
197
|
+
originsIn(sentence, words).map((token) => findingAt(sentence, token.span.start, { word: token.surface, suggestion: rewrites.get(token.surface) ?? "" })),
|
|
198
|
+
);
|
|
199
|
+
};
|