chaffjs 0.15.0 → 0.17.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 +27 -2
- package/dist/address-chain.js +1 -1
- package/dist/address-chain.js.map +1 -1
- package/dist/backends/anthropic.d.ts +2 -1
- package/dist/backends/anthropic.d.ts.map +1 -1
- package/dist/backends/anthropic.js +4 -1
- package/dist/backends/anthropic.js.map +1 -1
- package/dist/backends/openai.d.ts +2 -1
- package/dist/backends/openai.d.ts.map +1 -1
- package/dist/backends/openai.js +1 -1
- package/dist/backends/openai.js.map +1 -1
- package/dist/backends/types.d.ts +2 -1
- package/dist/backends/types.d.ts.map +1 -1
- package/dist/backends/types.js +1 -1
- package/dist/backends/types.js.map +1 -1
- package/dist/baseline.js +1 -1
- package/dist/baseline.js.map +1 -1
- package/dist/calendar-number.d.ts +31 -0
- package/dist/calendar-number.d.ts.map +1 -0
- package/dist/calendar-number.js +58 -0
- package/dist/calendar-number.js.map +1 -0
- package/dist/cli-args.d.ts +0 -5
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +2 -2
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-text.d.ts +6 -2
- package/dist/cli-text.d.ts.map +1 -1
- package/dist/cli-text.js +19 -12
- package/dist/cli-text.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +43 -37
- package/dist/cli.js.map +1 -1
- package/dist/closing-numbers.d.ts +20 -0
- package/dist/closing-numbers.d.ts.map +1 -0
- package/dist/closing-numbers.js +56 -0
- package/dist/closing-numbers.js.map +1 -0
- package/dist/commands/cite.d.ts.map +1 -1
- package/dist/commands/cite.js +4 -4
- package/dist/commands/cite.js.map +1 -1
- package/dist/commands/eval.js +1 -1
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/init-ask.d.ts +19 -0
- package/dist/commands/init-ask.d.ts.map +1 -0
- package/dist/commands/init-ask.js +43 -0
- package/dist/commands/init-ask.js.map +1 -0
- package/dist/commands/test.d.ts +8 -0
- package/dist/commands/test.d.ts.map +1 -1
- package/dist/commands/test.js +78 -44
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/tree.d.ts +1 -8
- package/dist/commands/tree.d.ts.map +1 -1
- package/dist/commands/tree.js +7 -6
- package/dist/commands/tree.js.map +1 -1
- package/dist/config/by-path.d.ts.map +1 -1
- package/dist/config/by-path.js +10 -9
- package/dist/config/by-path.js.map +1 -1
- package/dist/config/load.d.ts +4 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +19 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/name-problems.d.ts +5 -0
- package/dist/config/name-problems.d.ts.map +1 -0
- package/dist/config/name-problems.js +11 -0
- package/dist/config/name-problems.js.map +1 -0
- package/dist/date-stamp.js +1 -1
- package/dist/date-stamp.js.map +1 -1
- package/dist/detectors/acronym-context.d.ts +2 -0
- package/dist/detectors/acronym-context.d.ts.map +1 -1
- package/dist/detectors/acronym-context.js +13 -0
- package/dist/detectors/acronym-context.js.map +1 -1
- package/dist/detectors/acronym-expansion.d.ts +2 -0
- package/dist/detectors/acronym-expansion.d.ts.map +1 -1
- package/dist/detectors/acronym-expansion.js +104 -1
- package/dist/detectors/acronym-expansion.js.map +1 -1
- package/dist/detectors/agreement-slip.d.ts +32 -0
- package/dist/detectors/agreement-slip.d.ts.map +1 -0
- package/dist/detectors/agreement-slip.js +137 -0
- package/dist/detectors/agreement-slip.js.map +1 -0
- package/dist/detectors/char-shape.d.ts.map +1 -1
- package/dist/detectors/char-shape.js +16 -6
- package/dist/detectors/char-shape.js.map +1 -1
- package/dist/detectors/content-morphemes.d.ts +4 -0
- package/dist/detectors/content-morphemes.d.ts.map +1 -0
- package/dist/detectors/content-morphemes.js +12 -0
- package/dist/detectors/content-morphemes.js.map +1 -0
- package/dist/detectors/en-shape.d.ts +0 -1
- package/dist/detectors/en-shape.d.ts.map +1 -1
- package/dist/detectors/en-shape.js +2 -143
- package/dist/detectors/en-shape.js.map +1 -1
- package/dist/detectors/enumerated-runs.d.ts.map +1 -1
- package/dist/detectors/enumerated-runs.js +4 -2
- package/dist/detectors/enumerated-runs.js.map +1 -1
- package/dist/detectors/gram-place.js +1 -1
- package/dist/detectors/gram-place.js.map +1 -1
- package/dist/detectors/heading-case.d.ts.map +1 -1
- package/dist/detectors/heading-case.js +3 -1
- package/dist/detectors/heading-case.js.map +1 -1
- package/dist/detectors/heading-echo.d.ts.map +1 -1
- package/dist/detectors/heading-echo.js +12 -18
- package/dist/detectors/heading-echo.js.map +1 -1
- package/dist/detectors/heading-overlap.d.ts +13 -0
- package/dist/detectors/heading-overlap.d.ts.map +1 -0
- package/dist/detectors/heading-overlap.js +52 -0
- package/dist/detectors/heading-overlap.js.map +1 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +10 -4
- package/dist/detectors/index.js.map +1 -1
- package/dist/detectors/letter-joined-name.d.ts +13 -0
- package/dist/detectors/letter-joined-name.d.ts.map +1 -0
- package/dist/detectors/letter-joined-name.js +11 -0
- package/dist/detectors/letter-joined-name.js.map +1 -0
- package/dist/detectors/lettered-index.d.ts +9 -0
- package/dist/detectors/lettered-index.d.ts.map +1 -0
- package/dist/detectors/lettered-index.js +54 -0
- package/dist/detectors/lettered-index.js.map +1 -0
- package/dist/detectors/lexicon.d.ts +5 -1
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +51 -5
- package/dist/detectors/lexicon.js.map +1 -1
- package/dist/detectors/orthography.d.ts.map +1 -1
- package/dist/detectors/orthography.js +24 -6
- package/dist/detectors/orthography.js.map +1 -1
- package/dist/detectors/oxford-comma.d.ts +3 -0
- package/dist/detectors/oxford-comma.d.ts.map +1 -0
- package/dist/detectors/oxford-comma.js +340 -0
- package/dist/detectors/oxford-comma.js.map +1 -0
- package/dist/detectors/phrase-match.d.ts +3 -1
- package/dist/detectors/phrase-match.d.ts.map +1 -1
- package/dist/detectors/phrase-match.js +20 -15
- package/dist/detectors/phrase-match.js.map +1 -1
- package/dist/detectors/repeated-head.d.ts.map +1 -1
- package/dist/detectors/repeated-head.js +5 -4
- package/dist/detectors/repeated-head.js.map +1 -1
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +45 -16
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/stacked-hedge.d.ts +16 -0
- package/dist/detectors/stacked-hedge.d.ts.map +1 -0
- package/dist/detectors/stacked-hedge.js +67 -0
- package/dist/detectors/stacked-hedge.js.map +1 -0
- package/dist/detectors/stray-space.d.ts +7 -0
- package/dist/detectors/stray-space.d.ts.map +1 -0
- package/dist/detectors/stray-space.js +34 -0
- package/dist/detectors/stray-space.js.map +1 -0
- package/dist/detectors/structure.d.ts +3 -1
- package/dist/detectors/structure.d.ts.map +1 -1
- package/dist/detectors/structure.js +4 -2
- package/dist/detectors/structure.js.map +1 -1
- package/dist/detectors/superlative-amount.d.ts +9 -0
- package/dist/detectors/superlative-amount.d.ts.map +1 -0
- package/dist/detectors/superlative-amount.js +20 -0
- package/dist/detectors/superlative-amount.js.map +1 -0
- package/dist/detectors/superlative-clause.d.ts +18 -0
- package/dist/detectors/superlative-clause.d.ts.map +1 -0
- package/dist/detectors/superlative-clause.js +61 -0
- package/dist/detectors/superlative-clause.js.map +1 -0
- package/dist/detectors/superlative-scope.d.ts +6 -0
- package/dist/detectors/superlative-scope.d.ts.map +1 -1
- package/dist/detectors/superlative-scope.js +3 -3
- package/dist/detectors/superlative-scope.js.map +1 -1
- package/dist/detectors/team.d.ts +0 -6
- package/dist/detectors/team.d.ts.map +1 -1
- package/dist/detectors/team.js +17 -1
- package/dist/detectors/team.js.map +1 -1
- package/dist/detectors/token-shape.d.ts.map +1 -1
- package/dist/detectors/token-shape.js +8 -6
- package/dist/detectors/token-shape.js.map +1 -1
- package/dist/document.d.ts +3 -0
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +33 -6
- package/dist/document.js.map +1 -1
- package/dist/eval.js +1 -1
- package/dist/eval.js.map +1 -1
- package/dist/feedback/draft.js +1 -1
- package/dist/feedback/draft.js.map +1 -1
- package/dist/feedback/excerpt.d.ts +0 -1
- package/dist/feedback/excerpt.d.ts.map +1 -1
- package/dist/feedback/excerpt.js +2 -2
- package/dist/feedback/excerpt.js.map +1 -1
- package/dist/file-header.d.ts +20 -0
- package/dist/file-header.d.ts.map +1 -0
- package/dist/file-header.js +21 -0
- package/dist/file-header.js.map +1 -0
- package/dist/files.js +3 -3
- package/dist/files.js.map +1 -1
- package/dist/genre-check.js +1 -1
- package/dist/genre-check.js.map +1 -1
- package/dist/genre-load.d.ts +6 -0
- package/dist/genre-load.d.ts.map +1 -0
- package/dist/genre-load.js +22 -0
- package/dist/genre-load.js.map +1 -0
- package/dist/genre-parse.d.ts +44 -0
- package/dist/genre-parse.d.ts.map +1 -0
- package/dist/genre-parse.js +117 -0
- package/dist/genre-parse.js.map +1 -0
- package/dist/genre-suggest.d.ts +15 -0
- package/dist/genre-suggest.d.ts.map +1 -0
- package/dist/genre-suggest.js +40 -0
- package/dist/genre-suggest.js.map +1 -0
- package/dist/genre.d.ts +1 -1
- package/dist/genre.d.ts.map +1 -1
- package/dist/genre.js +3 -13
- package/dist/genre.js.map +1 -1
- package/dist/init-choice.d.ts +12 -0
- package/dist/init-choice.d.ts.map +1 -0
- package/dist/init-choice.js +41 -0
- package/dist/init-choice.js.map +1 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +16 -5
- package/dist/init.js.map +1 -1
- package/dist/judge.d.ts +2 -1
- package/dist/judge.d.ts.map +1 -1
- package/dist/judge.js +1 -1
- package/dist/judge.js.map +1 -1
- package/dist/line-paragraphs.d.ts +4 -0
- package/dist/line-paragraphs.d.ts.map +1 -0
- package/dist/line-paragraphs.js +71 -0
- package/dist/line-paragraphs.js.map +1 -0
- package/dist/mask.d.ts.map +1 -1
- package/dist/mask.js +2 -10
- package/dist/mask.js.map +1 -1
- package/dist/number-name.js +1 -1
- package/dist/number-name.js.map +1 -1
- package/dist/opening-line.d.ts +3 -0
- package/dist/opening-line.d.ts.map +1 -0
- package/dist/opening-line.js +14 -0
- package/dist/opening-line.js.map +1 -0
- package/dist/orthography.d.ts +5 -1
- package/dist/orthography.d.ts.map +1 -1
- package/dist/orthography.js +7 -1
- package/dist/orthography.js.map +1 -1
- package/dist/paragraph-length.d.ts +7 -0
- package/dist/paragraph-length.d.ts.map +1 -0
- package/dist/paragraph-length.js +10 -0
- package/dist/paragraph-length.js.map +1 -0
- package/dist/plugin.d.ts +12 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/profile/check.d.ts +28 -0
- package/dist/profile/check.d.ts.map +1 -0
- package/dist/profile/check.js +12 -0
- package/dist/profile/check.js.map +1 -0
- package/dist/profile/for-file.d.ts +2 -2
- package/dist/profile/for-file.d.ts.map +1 -1
- package/dist/profile/for-file.js +9 -2
- package/dist/profile/for-file.js.map +1 -1
- package/dist/profile/load.js +1 -1
- package/dist/profile/load.js.map +1 -1
- package/dist/profile/select.d.ts +4 -2
- package/dist/profile/select.d.ts.map +1 -1
- package/dist/profile/select.js +8 -3
- package/dist/profile/select.js.map +1 -1
- package/dist/quoted-span.d.ts +13 -3
- package/dist/quoted-span.d.ts.map +1 -1
- package/dist/quoted-span.js +16 -11
- package/dist/quoted-span.js.map +1 -1
- package/dist/reasons.d.ts +1 -0
- package/dist/reasons.d.ts.map +1 -1
- package/dist/reasons.js +4 -1
- package/dist/reasons.js.map +1 -1
- package/dist/render/compact.d.ts.map +1 -1
- package/dist/render/compact.js +1 -2
- package/dist/render/compact.js.map +1 -1
- package/dist/render/eval.d.ts.map +1 -1
- package/dist/render/eval.js +4 -3
- package/dist/render/eval.js.map +1 -1
- package/dist/render/friendly.d.ts +2 -1
- package/dist/render/friendly.d.ts.map +1 -1
- package/dist/render/friendly.js +14 -6
- package/dist/render/friendly.js.map +1 -1
- package/dist/render/genres.d.ts +5 -0
- package/dist/render/genres.d.ts.map +1 -0
- package/dist/render/genres.js +28 -0
- package/dist/render/genres.js.map +1 -0
- package/dist/render/plural.d.ts +3 -0
- package/dist/render/plural.d.ts.map +1 -0
- package/dist/render/plural.js +5 -0
- package/dist/render/plural.js.map +1 -0
- package/dist/render/rules-json.d.ts.map +1 -1
- package/dist/render/rules-json.js +9 -3
- package/dist/render/rules-json.js.map +1 -1
- package/dist/render/semantic.d.ts +3 -2
- package/dist/render/semantic.d.ts.map +1 -1
- package/dist/render/semantic.js +94 -34
- package/dist/render/semantic.js.map +1 -1
- package/dist/render/summary.d.ts.map +1 -1
- package/dist/render/summary.js +4 -3
- package/dist/render/summary.js.map +1 -1
- package/dist/render/suppressions.d.ts.map +1 -1
- package/dist/render/suppressions.js +3 -2
- package/dist/render/suppressions.js.map +1 -1
- package/dist/render/text.d.ts +2 -0
- package/dist/render/text.d.ts.map +1 -1
- package/dist/render/text.js +11 -3
- package/dist/render/text.js.map +1 -1
- package/dist/rule-load.d.ts.map +1 -1
- package/dist/rule-load.js +12 -1
- package/dist/rule-load.js.map +1 -1
- package/dist/run-semantic.d.ts +3 -1
- package/dist/run-semantic.d.ts.map +1 -1
- package/dist/run-semantic.js +9 -4
- package/dist/run-semantic.js.map +1 -1
- package/dist/run.d.ts +4 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +29 -16
- package/dist/run.js.map +1 -1
- package/dist/semantic.d.ts +1 -1
- package/dist/semantic.d.ts.map +1 -1
- package/dist/semantic.js +10 -3
- package/dist/semantic.js.map +1 -1
- package/dist/sentence-shape.d.ts +0 -1
- package/dist/sentence-shape.d.ts.map +1 -1
- package/dist/sentence-shape.js +3 -5
- package/dist/sentence-shape.js.map +1 -1
- package/dist/setting-problems.d.ts +5 -0
- package/dist/setting-problems.d.ts.map +1 -0
- package/dist/setting-problems.js +15 -0
- package/dist/setting-problems.js.map +1 -0
- package/dist/span-merge.d.ts +4 -0
- package/dist/span-merge.d.ts.map +1 -0
- package/dist/span-merge.js +12 -0
- package/dist/span-merge.js.map +1 -0
- package/dist/speaker-labels.d.ts +4 -0
- package/dist/speaker-labels.d.ts.map +1 -0
- package/dist/speaker-labels.js +106 -0
- package/dist/speaker-labels.js.map +1 -0
- package/dist/stray-space.d.ts +17 -0
- package/dist/stray-space.d.ts.map +1 -0
- package/dist/stray-space.js +66 -0
- package/dist/stray-space.js.map +1 -0
- package/dist/structure/build.js +5 -5
- package/dist/structure/build.js.map +1 -1
- package/dist/structure/cite.js +1 -1
- package/dist/structure/cite.js.map +1 -1
- package/dist/structure/issues.d.ts.map +1 -1
- package/dist/structure/issues.js +5 -5
- package/dist/structure/issues.js.map +1 -1
- package/dist/structure/markdown-path.d.ts.map +1 -1
- package/dist/structure/markdown-path.js +2 -2
- package/dist/structure/markdown-path.js.map +1 -1
- package/dist/structure/numbering.d.ts.map +1 -1
- package/dist/structure/numbering.js +1 -1
- package/dist/structure/numbering.js.map +1 -1
- package/dist/structure/relative-find.js +1 -1
- package/dist/structure/relative-find.js.map +1 -1
- package/dist/structure/relative-resolve.d.ts.map +1 -1
- package/dist/structure/relative-resolve.js +26 -20
- package/dist/structure/relative-resolve.js.map +1 -1
- package/dist/structure/sexp.js +1 -1
- package/dist/structure/sexp.js.map +1 -1
- package/dist/structure/unnumbered.js +2 -2
- package/dist/structure/unnumbered.js.map +1 -1
- package/dist/team-names.d.ts +9 -0
- package/dist/team-names.d.ts.map +1 -0
- package/dist/team-names.js +31 -0
- package/dist/team-names.js.map +1 -0
- package/dist/ui.d.ts +2 -0
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +6 -0
- package/dist/ui.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/watch.d.ts +2 -2
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +5 -5
- package/dist/watch.js.map +1 -1
- package/genres.yaml +395 -0
- package/package.json +4 -3
- package/rules/adverb-overuse.yaml +2 -2
- package/rules/agentless-passive.yaml +1 -1
- package/rules/agreement-slip.yaml +36 -0
- package/rules/ai-generated-composite.yaml +2 -2
- package/rules/ai-tell.yaml +2 -2
- package/rules/bold-density.yaml +2 -2
- package/rules/closing-cliche.yaml +1 -1
- package/rules/concrete-evidence-density.yaml +2 -2
- package/rules/contraction-consistency.yaml +1 -1
- package/rules/cushion-phrase-density.yaml +1 -1
- package/rules/dangling-reference.yaml +1 -1
- package/rules/date-order.yaml +1 -1
- package/rules/date-weekday-mismatch.yaml +1 -1
- package/rules/double-keigo.yaml +1 -1
- package/rules/doubled-word.yaml +1 -1
- package/rules/duplicate-definition.yaml +1 -1
- package/rules/emoji-density.yaml +1 -1
- package/rules/empty-conclusion.yaml +3 -1
- package/rules/empty-intensifier.yaml +1 -1
- package/rules/excessive-hedging.yaml +16 -6
- package/rules/expletive-construction.yaml +2 -2
- package/rules/heading-echo.yaml +2 -1
- package/rules/hiragana-fukushi.yaml +1 -1
- package/rules/internal-jargon.yaml +2 -2
- package/rules/latin-spacing.yaml +5 -4
- package/rules/max-kanji-continuous.yaml +4 -3
- package/rules/max-paragraph-length.yaml +18 -4
- package/rules/max-sentence-length.yaml +18 -2
- package/rules/ngram-repetition.yaml +4 -4
- package/rules/no-doubled-joshi.yaml +2 -2
- package/rules/no-em-dash.yaml +2 -2
- package/rules/no-mixed-desumasu.yaml +1 -1
- package/rules/no-nakaguro-parallel.yaml +2 -2
- package/rules/numbering-gap.yaml +1 -1
- package/rules/oxford-comma-consistency.yaml +2 -1
- package/rules/padded-intro.yaml +2 -1
- package/rules/paragraph-length-variance.yaml +1 -1
- package/rules/preamble-length.yaml +2 -2
- package/rules/preferred-term.yaml +1 -1
- package/rules/proper-noun-density.yaml +3 -2
- package/rules/repeated-conjunction.yaml +2 -2
- package/rules/repeated-sentence-head.yaml +2 -2
- package/rules/required-sections.yaml +1 -1
- package/rules/rule-of-three.yaml +2 -2
- package/rules/sasete-itadaku.yaml +3 -3
- package/rules/section-length-uniformity.yaml +1 -1
- package/rules/sentence-initial-conjunction-run.yaml +2 -2
- package/rules/sentence-rhythm.yaml +1 -1
- package/rules/stray-space.yaml +38 -0
- package/rules/taigen-dome-in-prose.yaml +2 -2
- package/rules/title-case-consistency.yaml +1 -1
- package/rules/total-mismatch.yaml +1 -1
- package/rules/undefined-acronym.yaml +5 -4
- package/rules/unqualified-superlative.yaml +2 -2
- package/skills/chaff/SKILL.md +28 -9
- package/src/address-chain.ts +1 -1
- package/src/backends/anthropic.ts +5 -1
- package/src/backends/openai.ts +2 -1
- package/src/backends/types.ts +3 -2
- package/src/baseline.ts +1 -1
- package/src/calendar-number.ts +84 -0
- package/src/cli-args.ts +2 -2
- package/src/cli-text.ts +29 -15
- package/src/cli.ts +42 -41
- package/src/closing-numbers.ts +72 -0
- package/src/commands/cite.ts +4 -4
- package/src/commands/eval.ts +1 -1
- package/src/commands/init-ask.ts +53 -0
- package/src/commands/test.ts +118 -56
- package/src/commands/tree.ts +7 -6
- package/src/config/by-path.ts +8 -9
- package/src/config/load.ts +24 -0
- package/src/config/name-problems.ts +15 -0
- package/src/date-stamp.ts +1 -1
- package/src/detectors/acronym-context.ts +16 -0
- package/src/detectors/acronym-expansion.ts +132 -1
- package/src/detectors/agreement-slip.ts +191 -0
- package/src/detectors/char-shape.ts +21 -9
- package/src/detectors/content-morphemes.ts +19 -0
- package/src/detectors/en-shape.ts +2 -173
- package/src/detectors/enumerated-runs.ts +3 -2
- package/src/detectors/gram-place.ts +1 -1
- package/src/detectors/heading-case.ts +4 -1
- package/src/detectors/heading-echo.ts +12 -20
- package/src/detectors/heading-overlap.ts +65 -0
- package/src/detectors/index.ts +10 -4
- package/src/detectors/letter-joined-name.ts +15 -0
- package/src/detectors/lettered-index.ts +78 -0
- package/src/detectors/lexicon.ts +69 -8
- package/src/detectors/orthography.ts +31 -7
- package/src/detectors/oxford-comma.ts +402 -0
- package/src/detectors/phrase-match.ts +23 -12
- package/src/detectors/repeated-head.ts +3 -4
- package/src/detectors/signals.ts +47 -15
- package/src/detectors/stacked-hedge.ts +96 -0
- package/src/detectors/stray-space.ts +40 -0
- package/src/detectors/structure.ts +10 -2
- package/src/detectors/superlative-amount.ts +24 -0
- package/src/detectors/superlative-clause.ts +82 -0
- package/src/detectors/superlative-scope.ts +3 -3
- package/src/detectors/team.ts +18 -2
- package/src/detectors/token-shape.ts +8 -10
- package/src/document.ts +54 -7
- package/src/eval.ts +1 -1
- package/src/feedback/draft.ts +1 -1
- package/src/feedback/excerpt.ts +2 -2
- package/src/file-header.ts +31 -0
- package/src/files.ts +3 -3
- package/src/genre-check.ts +1 -1
- package/src/genre-load.ts +24 -0
- package/src/genre-parse.ts +151 -0
- package/src/genre-suggest.ts +50 -0
- package/src/genre.ts +4 -13
- package/src/init-choice.ts +47 -0
- package/src/init.ts +20 -6
- package/src/judge.ts +3 -1
- package/src/line-paragraphs.ts +86 -0
- package/src/mask.ts +2 -12
- package/src/number-name.ts +1 -1
- package/src/opening-line.ts +17 -0
- package/src/orthography.ts +10 -2
- package/src/paragraph-length.ts +9 -0
- package/src/plugin.ts +12 -0
- package/src/profile/check.ts +34 -0
- package/src/profile/for-file.ts +10 -3
- package/src/profile/load.ts +1 -1
- package/src/profile/select.ts +11 -4
- package/src/quoted-span.ts +26 -12
- package/src/reasons.ts +5 -1
- package/src/render/compact.ts +1 -3
- package/src/render/eval.ts +4 -3
- package/src/render/friendly.ts +21 -6
- package/src/render/genres.ts +31 -0
- package/src/render/plural.ts +6 -0
- package/src/render/rules-json.ts +10 -3
- package/src/render/semantic.ts +117 -34
- package/src/render/summary.ts +4 -3
- package/src/render/suppressions.ts +3 -2
- package/src/render/text.ts +18 -3
- package/src/rule-load.ts +14 -1
- package/src/run-semantic.ts +29 -21
- package/src/run.ts +33 -16
- package/src/semantic.ts +11 -3
- package/src/sentence-shape.ts +3 -7
- package/src/setting-problems.ts +24 -0
- package/src/span-merge.ts +13 -0
- package/src/speaker-labels.ts +129 -0
- package/src/stray-space.ts +81 -0
- package/src/structure/build.ts +5 -5
- package/src/structure/cite.ts +1 -1
- package/src/structure/issues.ts +6 -5
- package/src/structure/markdown-path.ts +2 -2
- package/src/structure/numbering.ts +1 -1
- package/src/structure/relative-find.ts +1 -1
- package/src/structure/relative-resolve.ts +34 -19
- package/src/structure/sexp.ts +1 -1
- package/src/structure/unnumbered.ts +2 -2
- package/src/team-names.ts +39 -0
- package/src/ui.ts +7 -0
- package/src/version.ts +1 -1
- package/src/watch.ts +5 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Detector, Finding, Sentence, Span } from "../plugin.ts";
|
|
1
|
+
import type { Detector, Finding, ProseDocument, Sentence, Span } from "../plugin.ts";
|
|
2
|
+
import { calendarRuns, calendarUnitsOf, type CalendarRun, type CalendarUnits } from "../calendar-number.ts";
|
|
2
3
|
import { latinBoundaries, minorityStyle, occurrencesOutside, type Boundary, type SpacingKind } from "../orthography.ts";
|
|
3
4
|
import { isWithinAny, quotedSpans } from "../quoted-span.ts";
|
|
4
5
|
import { digitRunAround, isNumberName, sequenceLabelStarts } from "../number-name.ts";
|
|
@@ -31,13 +32,32 @@ const digitBeside = (boundary: Boundary): number => {
|
|
|
31
32
|
return boundary.spaced ? boundary.offset + 1 : boundary.offset;
|
|
32
33
|
};
|
|
33
34
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
35
|
+
/** 文書全体で一度だけ読むもの。sequence は続き番号の行の頭、topUnits は都道府県の単位、calendar は日付・時刻の単位。 */
|
|
36
|
+
type NumberContext = { readonly sequence: ReadonlySet<number>; readonly topUnits: ReadonlySet<string>; readonly calendar: CalendarUnits };
|
|
37
|
+
|
|
38
|
+
/** 文の中の日付・時刻の数の、並びの頭の位置と、日付の中の位置。 */
|
|
39
|
+
const calendarPlaces = (sentence: Sentence, units: CalendarUnits): ReadonlyMap<number, CalendarRun["place"]> =>
|
|
40
|
+
new Map(calendarRuns(sentence.text, sentence.tokens, sentence.span.start, units).map(({ run, place }) => [run.start, place]));
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 日付・時刻の中の境目(「9月」の数と単位のあいだ、「2026年9月」の年と 9 のあいだ)は、詰めて書く決まりで好みではない。
|
|
44
|
+
* 日付の前の境目(「は 9月」の空白)は、数の前をどう空けるかという書き手の書き方なので数える。
|
|
45
|
+
*/
|
|
46
|
+
const isInsideCalendar = (boundary: Boundary, place: CalendarRun["place"] | undefined): boolean =>
|
|
47
|
+
place !== undefined && (boundary.kind === "after-digit" || place === "inner");
|
|
48
|
+
|
|
49
|
+
/** 番号・識別子として書かれた数(number-name.ts)と日付・時刻の中の境目は、空け方の好みではないので数えない。 */
|
|
50
|
+
const isCounted = (sentence: Sentence, boundary: Boundary, context: NumberContext, calendar: ReadonlyMap<number, CalendarRun["place"]>): boolean => {
|
|
36
51
|
if (boundary.kind === "letter") return true;
|
|
37
52
|
const run = digitRunAround(sentence.text, digitBeside(boundary));
|
|
38
|
-
|
|
53
|
+
if (run === undefined) return true;
|
|
54
|
+
if (isInsideCalendar(boundary, calendar.get(run.start))) return false;
|
|
55
|
+
return !isNumberName(sentence.text, run, sentence.tokens, sentence.span.start, context.sequence, context.topUnits);
|
|
39
56
|
};
|
|
40
57
|
|
|
58
|
+
const patternList = (doc: ProseDocument, id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
|
|
59
|
+
const patternsOf = (doc: ProseDocument, id: string): ReadonlySet<string> => new Set(patternList(doc, id));
|
|
60
|
+
|
|
41
61
|
/**
|
|
42
62
|
* 鉤括弧で引いた題名や発言(「…ガイドライン ver. 1.1」)の中の境目。空け方は引いた元のもので、書き手の書き方ではない。
|
|
43
63
|
* 括弧は日本語の字でも英数字でもないので、境目はまるごと括弧の中か外にある。
|
|
@@ -50,12 +70,16 @@ const isQuoted = (quoted: readonly Span[], boundary: Boundary): boolean => isWit
|
|
|
50
70
|
*/
|
|
51
71
|
export const latinSpacing: Detector = (doc, options): Finding[] => {
|
|
52
72
|
// 覆った文(prose)で探す。コードの中の「1 件」は並びに入れない。
|
|
53
|
-
const
|
|
54
|
-
|
|
73
|
+
const context: NumberContext = {
|
|
74
|
+
sequence: sequenceLabelStarts(doc.prose ?? doc.source),
|
|
75
|
+
topUnits: patternsOf(doc, "prefecture-unit"),
|
|
76
|
+
calendar: calendarUnitsOf(doc),
|
|
77
|
+
};
|
|
55
78
|
const located: Located[] = doc.sentences.flatMap((sentence) => {
|
|
56
79
|
const quoted = quotedSpans(sentence.text);
|
|
80
|
+
const calendar = calendarPlaces(sentence, context.calendar);
|
|
57
81
|
return latinBoundaries(sentence.text, doc.source.slice(sentence.span.start, sentence.span.end))
|
|
58
|
-
.filter((boundary) => !isQuoted(quoted, boundary) && isCounted(sentence, boundary,
|
|
82
|
+
.filter((boundary) => !isQuoted(quoted, boundary) && isCounted(sentence, boundary, context, calendar))
|
|
59
83
|
.map((boundary) => ({ sentence, ...boundary }));
|
|
60
84
|
});
|
|
61
85
|
return KINDS.flatMap((kind) => {
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import type { Detector, Finding, Lexicon, Token } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
const LIST_CONJUNCTION = new Set(["and", "or"]);
|
|
4
|
+
|
|
5
|
+
const isComma = (token: Token): boolean => token.surface === ",";
|
|
6
|
+
|
|
7
|
+
const commaBefore = (tokens: readonly Token[], at: number): boolean => tokens[at - 1]?.surface === ",";
|
|
8
|
+
|
|
9
|
+
/** 並列はこれをまたがない。セミコロンの前後は別の節。 */
|
|
10
|
+
const CLAUSE_BREAK = new Set([";", ":", "—"]);
|
|
11
|
+
|
|
12
|
+
/** 各 token の前で閉じていない括弧の数。括弧の中の読点(external users (e.g., guests), and ...)は外の並列を切らない。 */
|
|
13
|
+
const PAREN_STEP: Readonly<Record<string, number>> = { "(": 1, ")": -1 };
|
|
14
|
+
|
|
15
|
+
const depthsOf = (tokens: readonly Token[]): number[] =>
|
|
16
|
+
tokens.reduce<{ depths: number[]; open: number }>(
|
|
17
|
+
(acc, token) => ({ depths: [...acc.depths, acc.open], open: Math.max(0, acc.open + (PAREN_STEP[token.surface] ?? 0)) }),
|
|
18
|
+
{ depths: [], open: 0 },
|
|
19
|
+
).depths;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 並びを読むための語彙表。participle は解析器が分詞と読まないが読点のあとで分詞の句を始める語(meaning)、
|
|
23
|
+
* example は例を挙げる句(such as, e.g.)、pair は 2 つだけを結ぶ語とその接続詞(between and / either or)。
|
|
24
|
+
*/
|
|
25
|
+
type ListWords = { readonly participle: ReadonlySet<string>; readonly example: Lexicon; readonly pair: ReadonlySet<string> };
|
|
26
|
+
|
|
27
|
+
type Clause = { readonly tokens: readonly Token[]; readonly depths: readonly number[]; readonly words: ListWords };
|
|
28
|
+
|
|
29
|
+
/** 冠詞や引用符を飛ばした、項目の頭の品詞。the parser と an exporter と samples を同じ形と見る。 */
|
|
30
|
+
const NOMINAL = new Set(["NOUN", "PROPN", "PRON", "NUM", "ADJ"]);
|
|
31
|
+
|
|
32
|
+
const isContent = (token: Token): boolean => token.pos !== "DET" && token.pos !== "PUNCT" && token.pos !== "X";
|
|
33
|
+
|
|
34
|
+
const shapeOf = (item: readonly Token[]): string | undefined => {
|
|
35
|
+
const head = item.find(isContent);
|
|
36
|
+
if (head === undefined) return undefined;
|
|
37
|
+
return NOMINAL.has(head.pos) ? "NOMINAL" : head.pos;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const VERBAL = new Set(["VERB", "AUX"]);
|
|
41
|
+
|
|
42
|
+
/** 主語と述語のある項目。the team fixed the bug / these are crucial。gets us more は述語だけ。 */
|
|
43
|
+
const isClause = (item: readonly Token[]): boolean => {
|
|
44
|
+
const first = item.find((token) => token.pos !== "PUNCT" && token.pos !== "X");
|
|
45
|
+
if (first === undefined || !(first.pos === "DET" || NOMINAL.has(first.pos))) return false;
|
|
46
|
+
return item.some((token) => token !== first && VERBAL.has(token.pos));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** 節の頭から and / or の手前までを、同じ深さの読点で項目に切る。Oxford comma の読点のあとは空なので項目にならない。 */
|
|
50
|
+
const itemsBefore = (clause: Clause, at: number): Token[][] => {
|
|
51
|
+
const level = clause.depths[at] ?? 0;
|
|
52
|
+
const start = clause.tokens.slice(0, at).findLastIndex((token) => CLAUSE_BREAK.has(token.surface)) + 1;
|
|
53
|
+
return clause.tokens
|
|
54
|
+
.slice(start, at)
|
|
55
|
+
.reduce<Token[][]>(
|
|
56
|
+
(items, token, offset) => {
|
|
57
|
+
if (isComma(token) && clause.depths[start + offset] === level) return [...items, []];
|
|
58
|
+
return [...items.slice(0, -1), [...(items.at(-1) ?? []), token]];
|
|
59
|
+
},
|
|
60
|
+
[[]],
|
|
61
|
+
)
|
|
62
|
+
.filter((item) => item.length > 0);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* and / or で始まる項目で、前の並びは閉じている(offering, giving, or receiving, directly or indirectly)。
|
|
67
|
+
* 次の並びはその項目から始まる。項目が動詞で始まれば、その目的語から(Develop, maintain, and track courses, materials and events)。
|
|
68
|
+
*/
|
|
69
|
+
const afterClosedList = (items: readonly Token[][]): Token[][] => {
|
|
70
|
+
const closed = items.findLastIndex((item) => LIST_CONJUNCTION.has(openingOf(item)?.surface.toLowerCase() ?? ""));
|
|
71
|
+
const closing = items[closed];
|
|
72
|
+
if (closing === undefined) return [...items];
|
|
73
|
+
const body = closing.slice(closing.findIndex((token) => LIST_CONJUNCTION.has(token.surface.toLowerCase())) + 1);
|
|
74
|
+
const head = body.findIndex(isContent);
|
|
75
|
+
const rest = VERBAL.has(body[head]?.pos ?? "") ? body.slice(head + 1) : body;
|
|
76
|
+
// 項目が残らなければ、後ろは新しい句の頭。導入の句(directly or indirectly,)を外す。
|
|
77
|
+
return rest.some(isContent) ? [rest, ...items.slice(closed + 1)] : [...withoutLead(items.slice(closed + 1))];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** and / or の後ろの項目。次の読点か節の切れ目まで。 */
|
|
81
|
+
const itemAfter = (tokens: readonly Token[], at: number): Token[] => {
|
|
82
|
+
const end = tokens.findIndex((token, index) => index > at && (isComma(token) || CLAUSE_BREAK.has(token.surface)));
|
|
83
|
+
return tokens.slice(at + 1, end === -1 ? undefined : end);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 導入の句(After the review, / If it fails, / Finally, / To test it, / Based on the review,)は並列の項目ではない。
|
|
88
|
+
* 節の最初の項目が前置詞・接続詞・副詞・to・過去分詞で始まり、次の項目と頭の形が違えば外す。
|
|
89
|
+
* Quickly, quietly and carefully は残る。For managers, engineers and designers, も外れるが、前置詞のあとの名詞を
|
|
90
|
+
* 項目と読むと、Over this period, the subcommittees and the full committee ... を並びと誤る。こちらのほうが多い。
|
|
91
|
+
*/
|
|
92
|
+
const LEAD_POS = new Set(["ADP", "SCONJ", "ADV", "PART"]);
|
|
93
|
+
|
|
94
|
+
const isParticiple = (token: Token): boolean => token.features?.["VerbForm"] === "Part";
|
|
95
|
+
|
|
96
|
+
const openingOf = (item: readonly Token[]): Token | undefined => item.find((token) => token.pos !== "PUNCT" && token.pos !== "X");
|
|
97
|
+
|
|
98
|
+
const openingKind = (item: readonly Token[]): string | undefined => {
|
|
99
|
+
const opening = openingOf(item);
|
|
100
|
+
return opening !== undefined && isParticiple(opening) ? "PARTICIPLE" : shapeOf(item);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const withoutLead = (items: readonly Token[][]): readonly Token[][] => {
|
|
104
|
+
const [first, second] = items;
|
|
105
|
+
const opening = first === undefined ? undefined : openingOf(first);
|
|
106
|
+
if (first === undefined || second === undefined || opening === undefined) return items;
|
|
107
|
+
if (!LEAD_POS.has(opening.pos) && !isParticiple(opening)) return items;
|
|
108
|
+
return openingKind(first) === openingKind(second) ? items : items.slice(1);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 形容詞のあとの読点は、名詞の前で形容詞を重ねているだけのことが多い(the long, winding bridge)。
|
|
113
|
+
* 次の項目が名詞で終わるならつなげ直す。形容詞そのものの並び(quick, cheap and reliable)は切ったまま。
|
|
114
|
+
*/
|
|
115
|
+
const lastContent = (item: readonly Token[]): Token | undefined => item.findLast(isContent);
|
|
116
|
+
|
|
117
|
+
const joinAdjectives = (items: readonly Token[][]): Token[][] =>
|
|
118
|
+
items.reduce<Token[][]>((joined, item) => {
|
|
119
|
+
const previous = joined.at(-1);
|
|
120
|
+
const stacked = previous !== undefined && lastContent(previous)?.pos === "ADJ" && lastContent(item)?.pos !== "ADJ";
|
|
121
|
+
if (stacked) joined[joined.length - 1] = [...previous, ...item];
|
|
122
|
+
else joined.push(item);
|
|
123
|
+
return joined;
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 節を並べるなら、どの項目も節。Additionally, others can learn, and the mistake is rarer. の
|
|
128
|
+
* Additionally は項目ではなく、and の前の読点は節をつなぐ読点。名詞の並びは最初の項目に前置き
|
|
129
|
+
* (We shipped the parser)を抱えるので、この確かめは節の並びだけにする。
|
|
130
|
+
* and の後ろだけが節なのは主語の並び(The parser, the renderer and the exporter shipped.)なので外さない。
|
|
131
|
+
*/
|
|
132
|
+
const clausesAgree = (items: readonly Token[][], after: readonly Token[]): boolean => {
|
|
133
|
+
const last = items.at(-1);
|
|
134
|
+
if (last === undefined || !isClause(last)) return true;
|
|
135
|
+
return isClause(after) && items.every(isClause);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 動詞で始まる項目を並べるなら、どの項目にも動詞がある。The scope, in contrast, is larger, and covers ... の
|
|
140
|
+
* The scope は項目ではなく、後ろの述語の主語。
|
|
141
|
+
*/
|
|
142
|
+
const hasVerb = (item: readonly Token[]): boolean => item.some((token) => VERBAL.has(token.pos));
|
|
143
|
+
|
|
144
|
+
const predicatesAgree = (items: readonly Token[][], shape: string): boolean => !VERBAL.has(shape) || items.every(hasVerb);
|
|
145
|
+
|
|
146
|
+
/** -ing 形(VerbForm=Ger)と過去分詞(Part)。filling the gaps と sentenced to prison は並べても並列にならない。 */
|
|
147
|
+
const VERB_FORMS = new Set(["Ger", "Part"]);
|
|
148
|
+
|
|
149
|
+
const verbFormOf = (token: Token): string | undefined => {
|
|
150
|
+
const form = token.features?.["VerbForm"];
|
|
151
|
+
return form !== undefined && VERB_FORMS.has(form) ? form : undefined;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** 項目の頭の分詞とその形。副詞は飛ばす(originally written)。引用符の中の語(‘modelling’)は語の例なので分詞と読まない。 */
|
|
155
|
+
const participleOpening = (item: readonly Token[], words: ReadonlySet<string>): { readonly at: number; readonly form: string } | undefined => {
|
|
156
|
+
const at = item.findIndex((token) => token.pos !== "PUNCT" && token.pos !== "ADV");
|
|
157
|
+
const opening = item[at];
|
|
158
|
+
if (opening === undefined) return undefined;
|
|
159
|
+
const form = words.has(opening.surface.toLowerCase()) ? "Ger" : verbFormOf(opening);
|
|
160
|
+
return form === undefined ? undefined : { at, form };
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const hasParticiple = (item: readonly Token[], form: string): boolean => item.some((token) => verbFormOf(token) === form);
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 最初より後ろの項目が分詞で始まるとき。最初の項目にも同じ形の分詞があれば分詞の並び
|
|
167
|
+
* (are hiring engineers, buying laptops and renting desks / Tested, reviewed and approved)。後ろの項目が動詞で始まれば述語の並びで、
|
|
168
|
+
* 分詞は解析器の読み違い(the jury, cross-examined the witnesses, presented witnesses, and made)。そうでなければ分詞の句
|
|
169
|
+
* (from Latin, meaning ship or boat / the judge, sitting without a jury, and sentenced)で、並びがあるならその句の中から始まる。
|
|
170
|
+
* 分詞より前の項目と分詞そのものは項目ではない(programs, including the office, the desk and the team)。句の中の項目の形が揃わなければ
|
|
171
|
+
* 句は挿入で、並びは句の後ろだけ(relief, including an injunction, in any court ... and without / including before the date, to a
|
|
172
|
+
* Recipient ... “confidential”, “proprietary”, or the like)。
|
|
173
|
+
*/
|
|
174
|
+
/** 解析器は過去形と過去分詞(fixed)を見分けられない。動詞のある項目のあとの「過去分詞」の並びは、述語の並びと読む。 */
|
|
175
|
+
const pastPredicates = (first: readonly Token[], form: string, after: readonly Token[], words: ReadonlySet<string>): boolean =>
|
|
176
|
+
form === "Part" && participleOpening(after, words)?.form === "Part" && hasVerb(first);
|
|
177
|
+
|
|
178
|
+
const fromParticiplePhrase = (items: readonly Token[][], after: readonly Token[], words: ReadonlySet<string>): readonly Token[][] => {
|
|
179
|
+
const at = items.findIndex((item, index) => index > 0 && participleOpening(item, words) !== undefined);
|
|
180
|
+
const phrase = items[at];
|
|
181
|
+
const first = items[0];
|
|
182
|
+
const opening = phrase === undefined ? undefined : participleOpening(phrase, words);
|
|
183
|
+
if (first === undefined || phrase === undefined || opening === undefined) return items;
|
|
184
|
+
const rest = items.slice(at + 1);
|
|
185
|
+
const predicates = rest.some((item) => VERBAL.has(shapeOf(item) ?? "")) || pastPredicates(first, opening.form, after, words);
|
|
186
|
+
if (hasParticiple(first, opening.form) || predicates) return items;
|
|
187
|
+
const inPhrase = withoutLead([phrase.slice(opening.at + 1), ...rest].filter((item) => item.some(isContent)));
|
|
188
|
+
return new Set(inPhrase.map(shapeOf)).size > 1 ? rest : inPhrase;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/** 大文字小文字も比べる。頭文字の E.G. Evans / I.E. Evans は e.g. / i.e. ではない。文頭の句の前には項目が無いので、大文字で始まる句は要らない。 */
|
|
192
|
+
const sameSurfaces = (tokens: readonly Token[], words: readonly Token[]): boolean =>
|
|
193
|
+
tokens.length === words.length && words.every((word, index) => tokens[index]?.surface === word.surface);
|
|
194
|
+
|
|
195
|
+
/** 項目の中で、and / or と同じ深さにある最後の例の句(such as / e.g.)の直後の位置。無ければ -1。 */
|
|
196
|
+
const exampleEnd = (clause: Clause, level: number, item: readonly Token[]): number =>
|
|
197
|
+
item.reduce((end, token, at) => {
|
|
198
|
+
const words = clause.words.example.find(({ tokens = [] }) => tokens.length > 0 && sameSurfaces(item.slice(at, at + tokens.length), tokens))?.tokens ?? [];
|
|
199
|
+
return words.length > 0 && clause.depths[clause.tokens.indexOf(token)] === level ? at + words.length : end;
|
|
200
|
+
}, -1);
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 例の句(such as / including / e.g.)のあとが並び。句の前の項目は並びではない(the screen lock on your phone, such as a PIN or
|
|
204
|
+
* phone-based fingerprint は 2 つ)。句の中の項目の形が揃わなければ句で切らない(materials such as green steel, utilizing ..., and
|
|
205
|
+
* adopting ... / (e.g., by location, contract type, or contract preference))。句の中に and / or があれば句はもう閉じていて、
|
|
206
|
+
* 句ごと 1 つの項目(apples, oranges such as navels and mandarins, and pears)。
|
|
207
|
+
*/
|
|
208
|
+
const fromExample = (clause: Clause, at: number, items: readonly Token[][]): readonly Token[][] => {
|
|
209
|
+
const level = clause.depths[at] ?? 0;
|
|
210
|
+
const phraseAt = items.findLastIndex((item) => exampleEnd(clause, level, item) !== -1);
|
|
211
|
+
const phrase = items[phraseAt];
|
|
212
|
+
if (phrase === undefined) return items;
|
|
213
|
+
const examples = phrase.slice(exampleEnd(clause, level, phrase));
|
|
214
|
+
if (examples.some((token) => LIST_CONJUNCTION.has(token.surface.toLowerCase()))) return items;
|
|
215
|
+
const rest = items.slice(phraseAt + 1);
|
|
216
|
+
const inPhrase = [examples, ...rest].filter((item) => item.some(isContent));
|
|
217
|
+
return new Set(inPhrase.map(shapeOf)).size > 1 ? items : inPhrase;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const APPOSITIVE_ANCHOR = new Set(["NOUN", "PROPN"]);
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 名詞のすぐ後ろの、読点で挟まれた X and Y は同格(two HPV types, HPV16 and HPV18, that account / Governments, both state and
|
|
224
|
+
* federal, spend)。項目が 2 つに見え、and の前に読点が無く、and の後ろの項目が読点で閉じ、後ろの 2 つに動詞が無いときに同格と読む。
|
|
225
|
+
* 2 つを結ぶ同格に Oxford comma は打たないので、and の前に読点があれば 3 つの並び(the subcommittee, full committee, and
|
|
226
|
+
* chamber levels, as well as)。読点の無い本当の並び(apples, pears and plums, then ...)はこの形と見分けられず、判定から外れる。
|
|
227
|
+
*/
|
|
228
|
+
const isAppositive = (clause: Clause, at: number, items: readonly Token[][], after: readonly Token[]): boolean => {
|
|
229
|
+
const [anchor, head] = items;
|
|
230
|
+
if (items.length !== 2 || anchor === undefined || head === undefined || commaBefore(clause.tokens, at)) return false;
|
|
231
|
+
const end = at + 1 + after.length;
|
|
232
|
+
// 閉じた読点のあとに and / or が続くなら、並びのあとに節をつないでいる(apples, pears and plums, and went home)。
|
|
233
|
+
const joinsClause = LIST_CONJUNCTION.has(clause.tokens[end + 1]?.surface.toLowerCase() ?? "");
|
|
234
|
+
const closed = clause.tokens[end]?.surface === "," && clause.depths[end] === clause.depths[at] && !joinsClause;
|
|
235
|
+
return closed && APPOSITIVE_ANCHOR.has(lastContent(anchor)?.pos ?? "") && !hasVerb(head) && !hasVerb(after);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/** 副詞を飛ばした頭の形。explain and justify と ultimately ensure は同じ動詞の項目。 */
|
|
239
|
+
const leadShape = (item: readonly Token[]): string | undefined => shapeOf(item.filter((token) => token.pos !== "ADV"));
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* and の後ろの項目に読点と、同じ形の項目を連れた and / or が続くなら、この and は項目の中にある(searches and seizures, and
|
|
243
|
+
* the Eighth's ban)。頭の形か、節かどうかが違えば別の節をつなぐ and で、この and までが並び(apples, pears and plums, and went home / and figs fell)。
|
|
244
|
+
*/
|
|
245
|
+
const listContinues = (clause: Clause, at: number, after: readonly Token[]): boolean => {
|
|
246
|
+
const next = at + 1 + after.length;
|
|
247
|
+
if (clause.tokens[next]?.surface !== "," || !LIST_CONJUNCTION.has(clause.tokens[next + 1]?.surface.toLowerCase() ?? "")) return false;
|
|
248
|
+
const following = itemAfter(clause.tokens, next + 1);
|
|
249
|
+
const shape = leadShape(following);
|
|
250
|
+
return shape !== undefined && shape === leadShape(after) && isClause(following) === isClause(after);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const isPluralNounPhrase = (tokens: readonly Token[]): boolean =>
|
|
254
|
+
tokens.filter(isContent).every((token) => NOUN_PHRASE_TAIL.has(token.pos)) && lastContent(tokens)?.features?.["Number"] === "Plur";
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 語のあとが複数形の名詞句なら、語は名詞にかかる語で、2 つはもう揃っている(the renderer in both modes and the exporter)。
|
|
258
|
+
* and の後ろも冠詞の無い複数形の名詞句なら、2 つを結ぶ語(both project goals and business goals)。
|
|
259
|
+
*/
|
|
260
|
+
const pairComplete = (member: readonly Token[], after: readonly Token[]): boolean => {
|
|
261
|
+
const end = after.findIndex((token) => LIST_CONJUNCTION.has(token.surface.toLowerCase()));
|
|
262
|
+
const partner = end === -1 ? after : after.slice(0, end);
|
|
263
|
+
return isPluralNounPhrase(member) && !(isPluralNounPhrase(partner) && !partner.some((token) => token.pos === "DET"));
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 最後の項目に 2 つだけを結ぶ語(between / either)があり、この接続詞がその語の相手で(between … and / either … or)、そのあとに
|
|
268
|
+
* and / or がまだ無く、読点も無ければ、この and / or はその 2 つを結ぶ(the Key Terms between Provider and Customer, and any policies)。
|
|
269
|
+
* 相手でなければ語は名詞にかかるだけ(the impact of either option and implementation)。括弧の中の語は外の接続詞と組まない。
|
|
270
|
+
*/
|
|
271
|
+
const pairsInLastItem = (clause: Clause, at: number, items: readonly Token[][], after: readonly Token[]): boolean => {
|
|
272
|
+
const last = items.at(-1) ?? [];
|
|
273
|
+
const conjunction = clause.tokens[at]?.surface.toLowerCase() ?? "";
|
|
274
|
+
const level = clause.depths[at] ?? 0;
|
|
275
|
+
const opener = last.findLastIndex(
|
|
276
|
+
(token) => clause.words.pair.has(`${token.surface.toLowerCase()} ${conjunction}`) && clause.depths[clause.tokens.indexOf(token)] === level,
|
|
277
|
+
);
|
|
278
|
+
const member = last.slice(opener + 1);
|
|
279
|
+
if (opener === -1 || commaBefore(clause.tokens, at) || pairComplete(member, after)) return false;
|
|
280
|
+
return !member.some((token) => LIST_CONJUNCTION.has(token.surface.toLowerCase()));
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/** 名詞にかかる語。解析器は分詞を過去形とも読む(registered or certified mail の certified)ので、動詞も数える。 */
|
|
284
|
+
const MODIFIER = new Set(["ADJ", "VERB"]);
|
|
285
|
+
|
|
286
|
+
const isModifier = (token: Token | undefined): boolean => token?.pos === "ADJ" || (token !== undefined && isParticiple(token));
|
|
287
|
+
|
|
288
|
+
const NOUN_PHRASE_TAIL = new Set(["ADJ", "NOUN", "PROPN"]);
|
|
289
|
+
|
|
290
|
+
/** 修飾語と名詞だけの句(certified mail / artificial flavor)。冠詞や目的語を連れた動詞(adopt their resources)は違う。 */
|
|
291
|
+
const isModifiedNoun = (item: readonly Token[]): boolean => {
|
|
292
|
+
const [head, ...tail] = item.filter((token) => token.pos !== "PUNCT" && token.pos !== "X");
|
|
293
|
+
return (
|
|
294
|
+
MODIFIER.has(head?.pos ?? "") && tail.length > 0 && tail.every((token) => NOUN_PHRASE_TAIL.has(token.pos)) && APPOSITIVE_ANCHOR.has(tail.at(-1)?.pos ?? "")
|
|
295
|
+
);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 修飾語 1 つだけの項目と、修飾語で始まる名詞句を結ぶ and / or は、2 つの修飾語が名詞を共有している(natural and artificial flavor /
|
|
300
|
+
* registered or certified mail)。前の項目も修飾語で終われば、修飾語の並び(red, white and blue flags)。and / or の前に読点があれば、
|
|
301
|
+
* 項目の区切り(Federal government, military, and agricultural workers)。
|
|
302
|
+
*/
|
|
303
|
+
const sharesNoun = (clause: Clause, at: number, items: readonly Token[][], after: readonly Token[]): boolean => {
|
|
304
|
+
const [previous, last] = items.slice(-2);
|
|
305
|
+
const lone = (last ?? []).filter(isContent);
|
|
306
|
+
if (commaBefore(clause.tokens, at) || previous === undefined || lone.length !== 1) return false;
|
|
307
|
+
return isModifier(lone[0]) && !isModifier(lastContent(previous)) && isModifiedNoun(after);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/** 頭の語のあとに前置詞の句を連れた項目(updates on the Google Doc)。 */
|
|
311
|
+
const hasPrepositionalTail = (item: readonly Token[]): boolean => {
|
|
312
|
+
const head = item.findIndex(isContent);
|
|
313
|
+
return item.some((token, index) => index > head && token.pos === "ADP");
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* and / or の後ろの最初の名詞が、最後の項目の前置詞の目的語と同じ品詞で、どの項目の頭とも違う(chat in Slack or Google Hangouts /
|
|
318
|
+
* Teams (beta): 固有名詞どうし)。頭と同じ品詞なら項目とも読める(petitions for waivers and appeals)ので、並びのまま。
|
|
319
|
+
*/
|
|
320
|
+
const isNoun = (token: Token): boolean => APPOSITIVE_ANCHOR.has(token.pos);
|
|
321
|
+
|
|
322
|
+
const lastNoun = (item: readonly Token[]): Token | undefined => item.findLast(isNoun);
|
|
323
|
+
|
|
324
|
+
const likeLastObject = (items: readonly Token[][], after: readonly Token[]): boolean => {
|
|
325
|
+
const kind = after.find(isNoun)?.pos;
|
|
326
|
+
return kind !== undefined && lastNoun(items.at(-1) ?? [])?.pos === kind && items.every((item) => item.find(isContent)?.pos !== kind);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* どの項目も前置詞の句を連れ、and / or の後ろだけが連れておらず、その目的語に似ていれば、and / or は最後の項目の前置詞の目的語を結ぶ
|
|
331
|
+
* (updates on the Google Doc, chat in Slack or Google Hangouts は 2 つ)。and / or の前に読点があれば、項目の区切りなので並び。
|
|
332
|
+
*/
|
|
333
|
+
const joinsObjects = (clause: Clause, at: number, items: readonly Token[][], after: readonly Token[]): boolean =>
|
|
334
|
+
!commaBefore(clause.tokens, at) &&
|
|
335
|
+
items.every(hasPrepositionalTail) &&
|
|
336
|
+
!after.some((token) => token.pos === "ADP") &&
|
|
337
|
+
!hasVerb(after) &&
|
|
338
|
+
likeLastObject(items, after);
|
|
339
|
+
|
|
340
|
+
/** and / or が並びの最後の継ぎ目ではなく、最後の項目の中にある。 */
|
|
341
|
+
const insideLastItem = (clause: Clause, at: number, items: readonly Token[][], after: readonly Token[]): boolean =>
|
|
342
|
+
pairsInLastItem(clause, at, items, after) || sharesNoun(clause, at, items, after) || joinsObjects(clause, at, items, after);
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 読点で区切った項目が and / or の前に 2 つ以上あり、最後の項目と and の後ろが同じ形のときだけ並列。
|
|
346
|
+
* 導入の読点(After the review, the team fixed the bug and shipped it.)や、節をつなぐ読点
|
|
347
|
+
* (We tested it, and the team shipped it.)は、Oxford comma を打つかどうかの選択を見せない。
|
|
348
|
+
*/
|
|
349
|
+
const listAt = (clause: Clause, at: number): boolean | undefined => {
|
|
350
|
+
const after = itemAfter(clause.tokens, at);
|
|
351
|
+
const listed = fromExample(clause, at, afterClosedList(withoutLead(joinAdjectives(itemsBefore(clause, at)))));
|
|
352
|
+
const items = fromParticiplePhrase(listed, after, clause.words.participle);
|
|
353
|
+
if (listContinues(clause, at, after) || isAppositive(clause, at, items, after) || insideLastItem(clause, at, items, after)) return undefined;
|
|
354
|
+
const last = items.at(-1);
|
|
355
|
+
const shape = last === undefined ? undefined : shapeOf(last);
|
|
356
|
+
if (items.length < 2 || shape === undefined || shape !== shapeOf(after)) return undefined;
|
|
357
|
+
if (!clausesAgree(items, after) || !predicatesAgree(items, shape)) return undefined;
|
|
358
|
+
return commaBefore(clause.tokens, at);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 3 つ以上の並列の最後の and / or の前に読点を打つか。Oxford comma。
|
|
363
|
+
*
|
|
364
|
+
* どちらが正しいかは決めない。スタイルガイドで割れる論点に立場を取ると rule ごと無視される。
|
|
365
|
+
* 見るのは 1 つの文書で揃っているかだけ。spec §12.3。
|
|
366
|
+
*/
|
|
367
|
+
const oxfordIn = (tokens: readonly Token[], words: ListWords): boolean | undefined => {
|
|
368
|
+
const clause = { tokens, depths: depthsOf(tokens), words };
|
|
369
|
+
return tokens.reduce<boolean | undefined>(
|
|
370
|
+
(found, token, at) => found ?? (at > 0 && LIST_CONJUNCTION.has(token.surface.toLowerCase()) ? listAt(clause, at) : undefined),
|
|
371
|
+
undefined,
|
|
372
|
+
);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const patternsOf = (lexicon: Lexicon | undefined): ReadonlySet<string> => new Set((lexicon ?? []).map((entry) => entry.pattern.toLowerCase()));
|
|
376
|
+
|
|
377
|
+
export const oxfordComma: Detector = (doc, options): Finding[] => {
|
|
378
|
+
const words = {
|
|
379
|
+
participle: patternsOf(doc.lexicons["participle-word"]),
|
|
380
|
+
example: doc.lexicons["example-marker"] ?? [],
|
|
381
|
+
pair: patternsOf(doc.lexicons["pair-opener"]),
|
|
382
|
+
};
|
|
383
|
+
const judged = doc.sentences.flatMap((sentence) => {
|
|
384
|
+
const oxford = oxfordIn(sentence.tokens ?? [], words);
|
|
385
|
+
return oxford === undefined ? [] : [{ sentence, oxford }];
|
|
386
|
+
});
|
|
387
|
+
const withComma = judged.filter((entry) => entry.oxford).length;
|
|
388
|
+
const minorityUsesComma = withComma <= judged.length - withComma;
|
|
389
|
+
const few = minorityUsesComma ? withComma : judged.length - withComma;
|
|
390
|
+
// 同数なら少数派は無い。
|
|
391
|
+
if (few === 0 || few * 2 === judged.length || few > options.limit) return [];
|
|
392
|
+
return judged
|
|
393
|
+
.filter((entry) => entry.oxford === minorityUsesComma)
|
|
394
|
+
.map(({ sentence }) => ({
|
|
395
|
+
rule: "oxford-comma-consistency",
|
|
396
|
+
severity: "info",
|
|
397
|
+
line: 0,
|
|
398
|
+
column: 0,
|
|
399
|
+
quote: sentence.text.trim(),
|
|
400
|
+
values: { count: few, limit: options.limit, offset: sentence.span.start },
|
|
401
|
+
}));
|
|
402
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Detector, Finding, Lexicon, ProseDocument, Sentence } from "../plugin.ts";
|
|
1
|
+
import type { Detector, DetectorOptions, Finding, Lexicon, ProseDocument, Sentence } from "../plugin.ts";
|
|
2
2
|
import { entryIn } from "./lexicon-match.ts";
|
|
3
3
|
|
|
4
4
|
/** 冒頭は先頭 2 段落ぶん。「どの記事にも当てはまる書き出し」は冒頭にあるときだけ問題。 */
|
|
@@ -22,21 +22,32 @@ const hitsIn = (sentences: readonly Sentence[], lexicon: Lexicon): Hit[] =>
|
|
|
22
22
|
return matched === undefined ? [] : [{ sentence, matched: matched.pattern }];
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* これが L2 の全体像で、新しい言語は語彙表を書くだけで動く。
|
|
28
|
-
*/
|
|
29
|
-
export const phraseMatch: Detector = (doc, options): Finding[] => {
|
|
30
|
-
const lexicon = options.lexicon ?? [];
|
|
31
|
-
if (lexicon.length === 0) return [];
|
|
32
|
-
const hits = hitsIn(inScope(doc, options.where), lexicon);
|
|
33
|
-
if (hits.length < options.limit) return [];
|
|
34
|
-
return hits.map((hit) => ({
|
|
25
|
+
const findingsOf = (hits: readonly Hit[], limit: number): Finding[] =>
|
|
26
|
+
hits.map((hit) => ({
|
|
35
27
|
rule: "",
|
|
36
28
|
severity: "warning",
|
|
37
29
|
line: 0,
|
|
38
30
|
column: 0,
|
|
39
31
|
quote: hit.sentence.text.trim(),
|
|
40
|
-
values: { matched: hit.matched, count: hits.length, limit
|
|
32
|
+
values: { matched: hit.matched, count: hits.length, limit, offset: hit.sentence.span.start },
|
|
41
33
|
}));
|
|
34
|
+
|
|
35
|
+
const phraseHits = (doc: ProseDocument, options: DetectorOptions): Hit[] => {
|
|
36
|
+
const lexicon = options.lexicon ?? [];
|
|
37
|
+
return lexicon.length === 0 ? [] : hitsIn(inScope(doc, options.where), lexicon);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 語彙表との照合。detector は共通で、語彙だけが言語別に差し替わる。spec §11。
|
|
42
|
+
* これが L2 の全体像で、新しい言語は語彙表を書くだけで動く。limit は指摘に要る数で、1 なら 1 つ目から言う。
|
|
43
|
+
*/
|
|
44
|
+
export const phraseMatch: Detector = (doc, options): Finding[] => {
|
|
45
|
+
const hits = phraseHits(doc, options);
|
|
46
|
+
return hits.length < options.limit ? [] : findingsOf(hits, options.limit);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** 回数に上限がある語。limit は許す回数で、それを超えたときだけ言う。message の「N 回まで」と同じ向き。 */
|
|
50
|
+
export const phraseCount: Detector = (doc, options): Finding[] => {
|
|
51
|
+
const hits = phraseHits(doc, options);
|
|
52
|
+
return hits.length <= options.limit ? [] : findingsOf(hits, options.limit);
|
|
42
53
|
};
|
|
@@ -35,10 +35,9 @@ const runsOf = (sentences: readonly Sentence[], unit: LengthUnit, lists: readonl
|
|
|
35
35
|
// 短すぎる書き出しは「同じ」と言えない。単位ぶん揃って初めて連なりと見る。
|
|
36
36
|
const full = unit === "char" ? head.length === HEAD_CHARS : head.split(" ").length === HEAD_WORDS;
|
|
37
37
|
const joins = last !== undefined && last.head === head && last.container === container && !listBetween(last.members.at(-1), sentence, lists);
|
|
38
|
-
if (last !== undefined && joins && full) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return [...acc, { head, container, members: [sentence] }];
|
|
38
|
+
if (last !== undefined && joins && full) acc[acc.length - 1] = { head, container, members: [...last.members, sentence] };
|
|
39
|
+
else acc.push({ head, container, members: [sentence] });
|
|
40
|
+
return acc;
|
|
42
41
|
}, []);
|
|
43
42
|
|
|
44
43
|
export const repeatedHead: Detector = (doc, options): Finding[] =>
|