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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// What a sentence adds to a heading, counted in content morphemes: words that stand by themselves and name something.
|
|
2
|
+
// Pure; heading-echo measures a language written without spaces with it, where a character count says nothing
|
|
3
|
+
// (「会社に勤めています」 carries two new words, 「について説明します」 one, in the same nine characters).
|
|
4
|
+
import type { Token } from "../plugin.ts";
|
|
5
|
+
|
|
6
|
+
/** 代名詞・連体詞・接続詞は、すでに書いたものを指すかつなぐだけで、中身を足さない。 */
|
|
7
|
+
const CONTENT_POS = new Set(["NOUN", "PROPN", "NUM", "VERB", "ADJ", "ADV"]);
|
|
8
|
+
|
|
9
|
+
/** 接頭詞・接尾・非自立の語(Bound)と、動詞にするだけの「する」(VerbType=Light)は、付いた語が中身を持つ。 */
|
|
10
|
+
const isContent = (token: Token): boolean => CONTENT_POS.has(token.pos) && token.features?.["Bound"] !== "Yes" && token.features?.["VerbType"] !== "Light";
|
|
11
|
+
|
|
12
|
+
/** 原形で照らす(変えました → 変える)。原形の無い語は書いた形で。 */
|
|
13
|
+
const baseForm = (token: Token): string => (token.lemma ?? token.surface).toLowerCase();
|
|
14
|
+
|
|
15
|
+
/** The sentence's content morphemes whose base form is not one of the heading's content morphemes, every occurrence counted. */
|
|
16
|
+
export const newContentMorphemes = (heading: readonly Token[], sentence: readonly Token[]): number => {
|
|
17
|
+
const said = new Set(heading.filter(isContent).map(baseForm));
|
|
18
|
+
return sentence.filter((token) => isContent(token) && !said.has(baseForm(token))).length;
|
|
19
|
+
};
|
|
@@ -79,11 +79,11 @@ const firstWord = (sentence: Sentence): string =>
|
|
|
79
79
|
* 何が主張なのかが分からなくなる。
|
|
80
80
|
*/
|
|
81
81
|
export const conjunctionRun: Detector = (doc, options): Finding[] => {
|
|
82
|
-
const heads = (options.lexicon ?? []).map((entry) => entry.pattern.toLowerCase());
|
|
82
|
+
const heads = new Set((options.lexicon ?? []).map((entry) => entry.pattern.toLowerCase()));
|
|
83
83
|
const runs = doc.sentences.filter(isClosed).reduce<Sentence[][]>(
|
|
84
84
|
(acc, sentence) => {
|
|
85
85
|
const last = acc.at(-1) ?? [];
|
|
86
|
-
if (!heads.
|
|
86
|
+
if (!heads.has(firstWord(sentence).toLowerCase())) return [...acc.slice(0, -1), last, []];
|
|
87
87
|
return [...acc.slice(0, -1), [...last, sentence]];
|
|
88
88
|
},
|
|
89
89
|
[[]],
|
|
@@ -134,174 +134,3 @@ export const titleCaseMix: Detector = (doc, options): Finding[] => {
|
|
|
134
134
|
values: { count: few.length, limit: options.limit, offset: section.span.start },
|
|
135
135
|
}));
|
|
136
136
|
};
|
|
137
|
-
|
|
138
|
-
const LIST_CONJUNCTION = new Set(["and", "or"]);
|
|
139
|
-
|
|
140
|
-
const isComma = (token: Token): boolean => token.surface === ",";
|
|
141
|
-
|
|
142
|
-
const commaBefore = (tokens: readonly Token[], at: number): boolean => tokens[at - 1]?.surface === ",";
|
|
143
|
-
|
|
144
|
-
/** 並列はこれをまたがない。セミコロンの前後は別の節。 */
|
|
145
|
-
const CLAUSE_BREAK = new Set([";", ":", "—"]);
|
|
146
|
-
|
|
147
|
-
/** 各 token の前で閉じていない括弧の数。括弧の中の読点(external users (e.g., guests), and ...)は外の並列を切らない。 */
|
|
148
|
-
const PAREN_STEP: Readonly<Record<string, number>> = { "(": 1, ")": -1 };
|
|
149
|
-
|
|
150
|
-
const depthsOf = (tokens: readonly Token[]): number[] =>
|
|
151
|
-
tokens.reduce<{ depths: number[]; open: number }>(
|
|
152
|
-
(acc, token) => ({ depths: [...acc.depths, acc.open], open: Math.max(0, acc.open + (PAREN_STEP[token.surface] ?? 0)) }),
|
|
153
|
-
{ depths: [], open: 0 },
|
|
154
|
-
).depths;
|
|
155
|
-
|
|
156
|
-
type Clause = { readonly tokens: readonly Token[]; readonly depths: readonly number[] };
|
|
157
|
-
|
|
158
|
-
/** 冠詞や引用符を飛ばした、項目の頭の品詞。the parser と an exporter と samples を同じ形と見る。 */
|
|
159
|
-
const NOMINAL = new Set(["NOUN", "PROPN", "PRON", "NUM", "ADJ"]);
|
|
160
|
-
|
|
161
|
-
const isContent = (token: Token): boolean => token.pos !== "DET" && token.pos !== "PUNCT" && token.pos !== "X";
|
|
162
|
-
|
|
163
|
-
const shapeOf = (item: readonly Token[]): string | undefined => {
|
|
164
|
-
const head = item.find(isContent);
|
|
165
|
-
if (head === undefined) return undefined;
|
|
166
|
-
return NOMINAL.has(head.pos) ? "NOMINAL" : head.pos;
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const VERBAL = new Set(["VERB", "AUX"]);
|
|
170
|
-
|
|
171
|
-
/** 主語と述語のある項目。the team fixed the bug / these are crucial。gets us more は述語だけ。 */
|
|
172
|
-
const isClause = (item: readonly Token[]): boolean => {
|
|
173
|
-
const first = item.find((token) => token.pos !== "PUNCT" && token.pos !== "X");
|
|
174
|
-
if (first === undefined || !(first.pos === "DET" || NOMINAL.has(first.pos))) return false;
|
|
175
|
-
return item.some((token) => token !== first && VERBAL.has(token.pos));
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
/** 節の頭から and / or の手前までを、同じ深さの読点で項目に切る。Oxford comma の読点のあとは空なので項目にならない。 */
|
|
179
|
-
const itemsBefore = (clause: Clause, at: number): Token[][] => {
|
|
180
|
-
const level = clause.depths[at] ?? 0;
|
|
181
|
-
const start = clause.tokens.slice(0, at).findLastIndex((token) => CLAUSE_BREAK.has(token.surface)) + 1;
|
|
182
|
-
return clause.tokens
|
|
183
|
-
.slice(start, at)
|
|
184
|
-
.reduce<Token[][]>(
|
|
185
|
-
(items, token, offset) => {
|
|
186
|
-
if (isComma(token) && clause.depths[start + offset] === level) return [...items, []];
|
|
187
|
-
return [...items.slice(0, -1), [...(items.at(-1) ?? []), token]];
|
|
188
|
-
},
|
|
189
|
-
[[]],
|
|
190
|
-
)
|
|
191
|
-
.filter((item) => item.length > 0);
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
/** and / or の後ろの項目。次の読点か節の切れ目まで。 */
|
|
195
|
-
const itemAfter = (tokens: readonly Token[], at: number): Token[] => {
|
|
196
|
-
const end = tokens.findIndex((token, index) => index > at && (isComma(token) || CLAUSE_BREAK.has(token.surface)));
|
|
197
|
-
return tokens.slice(at + 1, end === -1 ? undefined : end);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* 導入の句(After the review, / If it fails, / Finally, / To test it, / Based on the review,)は並列の項目ではない。
|
|
202
|
-
* 節の最初の項目が前置詞・接続詞・副詞・to・過去分詞で始まり、次の項目と頭の形が違えば外す。
|
|
203
|
-
* Quickly, quietly and carefully は残る。For managers, engineers and designers, も外れるが、前置詞のあとの名詞を
|
|
204
|
-
* 項目と読むと、Over this period, the subcommittees and the full committee ... を並びと誤る。こちらのほうが多い。
|
|
205
|
-
*/
|
|
206
|
-
const LEAD_POS = new Set(["ADP", "SCONJ", "ADV", "PART"]);
|
|
207
|
-
|
|
208
|
-
const isParticiple = (token: Token): boolean => token.features?.["VerbForm"] === "Part";
|
|
209
|
-
|
|
210
|
-
const openingOf = (item: readonly Token[]): Token | undefined => item.find((token) => token.pos !== "PUNCT" && token.pos !== "X");
|
|
211
|
-
|
|
212
|
-
const openingKind = (item: readonly Token[]): string | undefined => {
|
|
213
|
-
const opening = openingOf(item);
|
|
214
|
-
return opening !== undefined && isParticiple(opening) ? "PARTICIPLE" : shapeOf(item);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const withoutLead = (items: readonly Token[][]): readonly Token[][] => {
|
|
218
|
-
const [first, second] = items;
|
|
219
|
-
const opening = first === undefined ? undefined : openingOf(first);
|
|
220
|
-
if (first === undefined || second === undefined || opening === undefined) return items;
|
|
221
|
-
if (!LEAD_POS.has(opening.pos) && !isParticiple(opening)) return items;
|
|
222
|
-
return openingKind(first) === openingKind(second) ? items : items.slice(1);
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* 形容詞のあとの読点は、名詞の前で形容詞を重ねているだけのことが多い(the long, winding bridge)。
|
|
227
|
-
* 次の項目が名詞で終わるならつなげ直す。形容詞そのものの並び(quick, cheap and reliable)は切ったまま。
|
|
228
|
-
*/
|
|
229
|
-
const lastContent = (item: readonly Token[]): Token | undefined => item.findLast(isContent);
|
|
230
|
-
|
|
231
|
-
const joinAdjectives = (items: readonly Token[][]): Token[][] =>
|
|
232
|
-
items.reduce<Token[][]>((joined, item) => {
|
|
233
|
-
const previous = joined.at(-1);
|
|
234
|
-
const stacked = previous !== undefined && lastContent(previous)?.pos === "ADJ" && lastContent(item)?.pos !== "ADJ";
|
|
235
|
-
return stacked ? [...joined.slice(0, -1), [...previous, ...item]] : [...joined, item];
|
|
236
|
-
}, []);
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* 節を並べるなら、どの項目も節。Additionally, others can learn, and the mistake is rarer. の
|
|
240
|
-
* Additionally は項目ではなく、and の前の読点は節をつなぐ読点。名詞の並びは最初の項目に前置き
|
|
241
|
-
* (We shipped the parser)を抱えるので、この確かめは節の並びだけにする。
|
|
242
|
-
* and の後ろだけが節なのは主語の並び(The parser, the renderer and the exporter shipped.)なので外さない。
|
|
243
|
-
*/
|
|
244
|
-
const clausesAgree = (items: readonly Token[][], after: readonly Token[]): boolean => {
|
|
245
|
-
const last = items.at(-1);
|
|
246
|
-
if (last === undefined || !isClause(last)) return true;
|
|
247
|
-
return isClause(after) && items.every(isClause);
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* 動詞で始まる項目を並べるなら、どの項目にも動詞がある。The scope, in contrast, is larger, and covers ... の
|
|
252
|
-
* The scope は項目ではなく、後ろの述語の主語。
|
|
253
|
-
*/
|
|
254
|
-
const hasVerb = (item: readonly Token[]): boolean => item.some((token) => VERBAL.has(token.pos));
|
|
255
|
-
|
|
256
|
-
const predicatesAgree = (items: readonly Token[][], shape: string): boolean => !VERBAL.has(shape) || items.every(hasVerb);
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* 読点で区切った項目が and / or の前に 2 つ以上あり、最後の項目と and の後ろが同じ形のときだけ並列。
|
|
260
|
-
* 導入の読点(After the review, the team fixed the bug and shipped it.)や、節をつなぐ読点
|
|
261
|
-
* (We tested it, and the team shipped it.)は、Oxford comma を打つかどうかの選択を見せない。
|
|
262
|
-
*/
|
|
263
|
-
const listAt = (clause: Clause, at: number): boolean | undefined => {
|
|
264
|
-
const items = withoutLead(joinAdjectives(itemsBefore(clause, at)));
|
|
265
|
-
const after = itemAfter(clause.tokens, at);
|
|
266
|
-
const last = items.at(-1);
|
|
267
|
-
const shape = last === undefined ? undefined : shapeOf(last);
|
|
268
|
-
if (items.length < 2 || shape === undefined || shape !== shapeOf(after)) return undefined;
|
|
269
|
-
if (!clausesAgree(items, after) || !predicatesAgree(items, shape)) return undefined;
|
|
270
|
-
return commaBefore(clause.tokens, at);
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* 3 つ以上の並列の最後の and / or の前に読点を打つか。Oxford comma。
|
|
275
|
-
*
|
|
276
|
-
* どちらが正しいかは決めない。スタイルガイドで割れる論点に立場を取ると rule ごと無視される。
|
|
277
|
-
* 見るのは 1 つの文書で揃っているかだけ。spec §12.3。
|
|
278
|
-
*/
|
|
279
|
-
const oxfordIn = (tokens: readonly Token[]): boolean | undefined => {
|
|
280
|
-
const clause = { tokens, depths: depthsOf(tokens) };
|
|
281
|
-
return tokens.reduce<boolean | undefined>(
|
|
282
|
-
(found, token, at) => found ?? (at > 0 && LIST_CONJUNCTION.has(token.surface.toLowerCase()) ? listAt(clause, at) : undefined),
|
|
283
|
-
undefined,
|
|
284
|
-
);
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
export const oxfordComma: Detector = (doc, options): Finding[] => {
|
|
288
|
-
const judged = doc.sentences.flatMap((sentence) => {
|
|
289
|
-
const oxford = oxfordIn(sentence.tokens ?? []);
|
|
290
|
-
return oxford === undefined ? [] : [{ sentence, oxford }];
|
|
291
|
-
});
|
|
292
|
-
const withComma = judged.filter((entry) => entry.oxford).length;
|
|
293
|
-
const minorityUsesComma = withComma <= judged.length - withComma;
|
|
294
|
-
const few = minorityUsesComma ? withComma : judged.length - withComma;
|
|
295
|
-
// 同数なら少数派は無い。
|
|
296
|
-
if (few === 0 || few * 2 === judged.length || few > options.limit) return [];
|
|
297
|
-
return judged
|
|
298
|
-
.filter((entry) => entry.oxford === minorityUsesComma)
|
|
299
|
-
.map(({ sentence }) => ({
|
|
300
|
-
rule: "oxford-comma-consistency",
|
|
301
|
-
severity: "info",
|
|
302
|
-
line: 0,
|
|
303
|
-
column: 0,
|
|
304
|
-
quote: sentence.text.trim(),
|
|
305
|
-
values: { count: few, limit: options.limit, offset: sentence.span.start },
|
|
306
|
-
}));
|
|
307
|
-
};
|
|
@@ -61,8 +61,9 @@ export const enumeratedRuns = (paragraphs: readonly Span[], starts: readonly num
|
|
|
61
61
|
const enumerated = paragraphs.filter((paragraph) => opensWithItem(paragraph, starts, source));
|
|
62
62
|
const runs = enumerated.reduce<Span[]>((joined, paragraph) => {
|
|
63
63
|
const last = joined.at(-1);
|
|
64
|
-
if (last === undefined || !isBlank(source, last.end, paragraph.start))
|
|
65
|
-
|
|
64
|
+
if (last === undefined || !isBlank(source, last.end, paragraph.start)) joined.push(paragraph);
|
|
65
|
+
else joined[joined.length - 1] = { start: last.start, end: paragraph.end };
|
|
66
|
+
return joined;
|
|
66
67
|
}, []);
|
|
67
68
|
return runs.filter((run) => itemsIn(run, starts, source) >= MIN_ITEMS);
|
|
68
69
|
};
|
|
@@ -32,7 +32,7 @@ export const compacted = (text: string, unit: LengthUnit): Compacted => {
|
|
|
32
32
|
}
|
|
33
33
|
chars.push(char);
|
|
34
34
|
// 位置は UTF-16 の単位で持つ。placeOf は indexOf の位置で引くので、絵文字(2 単位)には 2 つ置く。
|
|
35
|
-
Array.from({ length: char.length }, (_,
|
|
35
|
+
Array.from({ length: char.length }, (_, codeUnit) => offsets.push(at + codeUnit));
|
|
36
36
|
});
|
|
37
37
|
return { text: chars.join(""), offsets };
|
|
38
38
|
};
|
|
@@ -3,11 +3,14 @@ const WORD = /[A-Za-z][A-Za-z'-]*/gu;
|
|
|
3
3
|
/** 小さい語は Title Case でも小文字のままなので、大文字化の判定から外す。 */
|
|
4
4
|
const MINOR = new Set(["a", "an", "the", "and", "or", "but", "of", "in", "on", "at", "to", "for", "with", "as", "by", "from", "is"]);
|
|
5
5
|
|
|
6
|
+
/** 大文字だけの語(PR・FCPs)は略語で、どちらの流儀でも大文字のまま。Title Case の証拠にならない。 */
|
|
7
|
+
const ACRONYM = /^[A-Z]{2,}s?$/u;
|
|
8
|
+
|
|
6
9
|
const TITLE_DEPTH = 1;
|
|
7
10
|
|
|
8
11
|
/** 見出しが Title Case か。判定できなければ undefined。 */
|
|
9
12
|
export const isTitleCase = (heading: string): boolean | undefined => {
|
|
10
|
-
const words = [...heading.matchAll(WORD)].map((match) => match[0]).filter((word) => !MINOR.has(word.toLowerCase()));
|
|
13
|
+
const words = [...heading.matchAll(WORD)].map((match) => match[0]).filter((word) => !MINOR.has(word.toLowerCase()) && !ACRONYM.test(word));
|
|
11
14
|
// 1 語の見出しは、どちらの流儀でも先頭が大文字になる。判定できない。
|
|
12
15
|
if (words.length < 2) return undefined;
|
|
13
16
|
const capitalized = words.filter((word) => word[0] === word[0]?.toUpperCase()).length;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { lengthOf } from "../measure.ts";
|
|
1
|
+
import { lengthOf, proseText } from "../measure.ts";
|
|
2
|
+
import { newContentMorphemes } from "./content-morphemes.ts";
|
|
3
|
+
import { echoedHeadingUnits, trigrams } from "./heading-overlap.ts";
|
|
2
4
|
import { handsOver } from "./lead-in.ts";
|
|
3
5
|
import { withoutQuotedVariants } from "./quoted-variant.ts";
|
|
4
6
|
import type { Detector, Finding, LengthUnit, ProseDocument, Section } from "../plugin.ts";
|
|
@@ -6,17 +8,6 @@ import type { Detector, Finding, LengthUnit, ProseDocument, Section } from "../p
|
|
|
6
8
|
/** 見出しが短すぎると、偶然の一致で 100% になる。これ未満の見出しは見ない。 */
|
|
7
9
|
const MIN_GRAMS = 4;
|
|
8
10
|
|
|
9
|
-
/**
|
|
10
|
-
* 文字 3-gram で測る。
|
|
11
|
-
* 語 n-gram にすると wordSplit capability が要り、L1(言語を問わず動く)から外れる。
|
|
12
|
-
*/
|
|
13
|
-
const trigrams = (text: string): Set<string> => {
|
|
14
|
-
// 大文字小文字を畳む。英語では見出しが Title Case、本文が小文字になり、
|
|
15
|
-
// 同じ語でも一致しなくなる(Generating Output → generated output)。
|
|
16
|
-
const clean = [...text.toLowerCase().replace(/\s+/gu, "")];
|
|
17
|
-
return new Set(clean.slice(0, Math.max(0, clean.length - 2)).map((__char, index) => clean.slice(index, index + 3).join("")));
|
|
18
|
-
};
|
|
19
|
-
|
|
20
11
|
/**
|
|
21
12
|
* Jaccard ではなく包含率を使う。
|
|
22
13
|
*
|
|
@@ -39,18 +30,19 @@ const containment = (heading: Set<string>, sentence: Set<string>): number => {
|
|
|
39
30
|
*/
|
|
40
31
|
const NEW_MATERIAL = { word: 6, char: 20 };
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
: heading.replace(/\s+/gu, "").length;
|
|
33
|
+
/**
|
|
34
|
+
* 空白で語を分けない言語は、品詞が読めれば文字ではなく見出しに無い内容語で測る。文字数は中身の量を言わない:
|
|
35
|
+
* 「会社に勤めています」は 9 文字で 会社・勤める を足し、「について説明します」は同じ 9 文字で 説明 しか足さない。
|
|
36
|
+
* 内容語 2 つで足りる(こころさんのお父さんは、会社に勤めています。)。日本語の corpus で測った(yarn corpus)。
|
|
37
|
+
*/
|
|
38
|
+
const NEW_CONTENT_MORPHEMES = 1;
|
|
49
39
|
|
|
50
40
|
const addsLittle = (section: Section, unit: LengthUnit): boolean => {
|
|
51
41
|
const first = section.firstSentence;
|
|
52
42
|
if (first === undefined) return false;
|
|
53
|
-
|
|
43
|
+
if (unit === "char" && first.tokens !== undefined && section.headingTokens !== undefined)
|
|
44
|
+
return newContentMorphemes(section.headingTokens, first.tokens) <= NEW_CONTENT_MORPHEMES;
|
|
45
|
+
return lengthOf(first, unit) - echoedHeadingUnits(section.heading, proseText(first), unit) <= NEW_MATERIAL[unit];
|
|
54
46
|
};
|
|
55
47
|
|
|
56
48
|
/** 最初の文が、同じ節の後ろ(箇条書き・表・コード)へ読者を渡している。 */
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// How much of a section's heading its first sentence takes up: the heading's character trigrams, and the part of the
|
|
2
|
+
// heading the sentence repeats, in the document's length unit. Pure; heading-echo decides with these.
|
|
3
|
+
import type { LengthUnit } from "../plugin.ts";
|
|
4
|
+
|
|
5
|
+
const foldedChars = (text: string): string[] => [...text.toLowerCase().replace(/\s+/gu, "")];
|
|
6
|
+
|
|
7
|
+
const trigramAt = (chars: readonly string[], index: number): string => chars.slice(index, index + 3).join("");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 文字 3-gram で測る。
|
|
11
|
+
* 語 n-gram にすると wordSplit capability が要り、L1(言語を問わず動く)から外れる。
|
|
12
|
+
*/
|
|
13
|
+
export const trigrams = (text: string): Set<string> => {
|
|
14
|
+
// 大文字小文字を畳む。英語では見出しが Title Case、本文が小文字になり、
|
|
15
|
+
// 同じ語でも一致しなくなる(Generating Output → generated output)。
|
|
16
|
+
const clean = foldedChars(text);
|
|
17
|
+
return new Set(clean.slice(0, Math.max(0, clean.length - 2)).map((__char, index) => trigramAt(clean, index)));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** A word as compared across heading and sentence: case folded, the punctuation around it dropped ("Spacewalks?" is spacewalks). */
|
|
21
|
+
const foldedWord = (word: string): string => {
|
|
22
|
+
const chars = [...word.toLowerCase()];
|
|
23
|
+
const isWordChar = (char: string): boolean => /[\p{L}\p{N}]/u.test(char);
|
|
24
|
+
return chars.slice(chars.findIndex(isWordChar), chars.findLastIndex(isWordChar) + 1).join("");
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const headingWords = (heading: string): string[] =>
|
|
28
|
+
heading
|
|
29
|
+
.trim()
|
|
30
|
+
.split(/\s+/u)
|
|
31
|
+
.filter((word) => word.length > 0);
|
|
32
|
+
|
|
33
|
+
/** Below this length a word repeats another only as itself: "on" is not repeated by "one", nor "go" by "going". */
|
|
34
|
+
const MIN_STEM = 4;
|
|
35
|
+
|
|
36
|
+
/** The same word, or one of them the other with an ending added (level → levels, reference → references). */
|
|
37
|
+
const isSameWord = (headingWord: string, sentenceWord: string): boolean => {
|
|
38
|
+
if (headingWord === sentenceWord) return true;
|
|
39
|
+
const [shorter, longer] = headingWord.length < sentenceWord.length ? [headingWord, sentenceWord] : [sentenceWord, headingWord];
|
|
40
|
+
return shorter.length >= MIN_STEM && longer.startsWith(shorter);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** The heading's words that the sentence repeats; a word of punctuation alone is never repeated. */
|
|
44
|
+
const echoedWords = (heading: string, sentence: string): number => {
|
|
45
|
+
const said = sentence.split(/\s+/u).map(foldedWord);
|
|
46
|
+
return headingWords(heading)
|
|
47
|
+
.map(foldedWord)
|
|
48
|
+
.filter((word) => word !== "" && said.some((other) => isSameWord(word, other))).length;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** The heading's characters (spaces aside) that a trigram the sentence also has covers. */
|
|
52
|
+
const echoedChars = (heading: string, sentence: string): number => {
|
|
53
|
+
const said = trigrams(sentence);
|
|
54
|
+
const clean = foldedChars(heading);
|
|
55
|
+
const covered = new Set(clean.flatMap((__char, index) => (said.has(trigramAt(clean, index)) ? [index, index + 1, index + 2] : [])));
|
|
56
|
+
return covered.size;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* How much of the heading the sentence repeats, in the document's length unit. A sentence that repeats only part of it
|
|
61
|
+
* ("How Do Astronauts Go on Spacewalks?" → "When astronauts go on spacewalks, they wear spacesuits…") keeps the rest of
|
|
62
|
+
* its length as its own material.
|
|
63
|
+
*/
|
|
64
|
+
export const echoedHeadingUnits = (heading: string, sentence: string, unit: LengthUnit): number =>
|
|
65
|
+
unit === "word" ? echoedWords(heading, sentence) : echoedChars(heading, sentence);
|
package/src/detectors/index.ts
CHANGED
|
@@ -4,18 +4,21 @@ import { sentenceLength } from "./sentence-length.ts";
|
|
|
4
4
|
import { headingEcho } from "./heading-echo.ts";
|
|
5
5
|
import { repeatedHead } from "./repeated-head.ts";
|
|
6
6
|
import { sentenceRhythm } from "./sentence-rhythm.ts";
|
|
7
|
-
import { phraseMatch } from "./phrase-match.ts";
|
|
7
|
+
import { phraseCount, phraseMatch } from "./phrase-match.ts";
|
|
8
8
|
import { agentlessPassive } from "./agentless-passive.ts";
|
|
9
9
|
import { doubledWord } from "./doubled-word.ts";
|
|
10
|
+
import { agreementSlip } from "./agreement-slip.ts";
|
|
10
11
|
import { sentenceEnding } from "./sentence-ending.ts";
|
|
11
12
|
import { doubledParticle, nounEnding } from "./token-shape.ts";
|
|
12
13
|
import { kanjiRun, middleDot } from "./char-shape.ts";
|
|
13
|
-
import { adverbDensity, conjunctionRun, expletive,
|
|
14
|
+
import { adverbDensity, conjunctionRun, expletive, titleCaseMix } from "./en-shape.ts";
|
|
15
|
+
import { oxfordComma } from "./oxford-comma.ts";
|
|
14
16
|
import { paragraphLength, paragraphVariance, preambleLength, ruleOfThree, sectionUniformity } from "./structure.ts";
|
|
15
17
|
import { concreteEvidence, dashDensity, emojiDensity, ngramRepetition, undefinedAcronym } from "./signals.ts";
|
|
16
|
-
import { aiTell, contractionMix, cushionDensity,
|
|
18
|
+
import { aiTell, contractionMix, cushionDensity, hedging, repeatedConjunction, unqualifiedSuperlative } from "./lexicon.ts";
|
|
17
19
|
import { internalJargon, properNounDensity, requiredSections } from "./team.ts";
|
|
18
20
|
import { latinSpacing, preferredTerm } from "./orthography.ts";
|
|
21
|
+
import { straySpace } from "./stray-space.ts";
|
|
19
22
|
import { danglingReference, dateOrder, dateWeekdayMismatch, duplicateDefinition, numberingGap, totalMismatch } from "./structure-tree.ts";
|
|
20
23
|
|
|
21
24
|
/** rule 定義の how_to_find がここを引く。rule 側は実装を知らない。 */
|
|
@@ -26,11 +29,13 @@ export const DETECTORS: Readonly<Record<string, Detector>> = {
|
|
|
26
29
|
"repeated-head": repeatedHead,
|
|
27
30
|
"sentence-rhythm": sentenceRhythm,
|
|
28
31
|
"phrase-match": phraseMatch,
|
|
32
|
+
"phrase-count": phraseCount,
|
|
29
33
|
"agentless-passive": agentlessPassive,
|
|
30
34
|
"sentence-ending": sentenceEnding,
|
|
31
35
|
"noun-ending": nounEnding,
|
|
32
36
|
"doubled-particle": doubledParticle,
|
|
33
37
|
"doubled-word": doubledWord,
|
|
38
|
+
"agreement-slip": agreementSlip,
|
|
34
39
|
"kanji-run": kanjiRun,
|
|
35
40
|
"middle-dot": middleDot,
|
|
36
41
|
"adverb-density": adverbDensity,
|
|
@@ -48,7 +53,7 @@ export const DETECTORS: Readonly<Record<string, Detector>> = {
|
|
|
48
53
|
"ngram-repetition": ngramRepetition,
|
|
49
54
|
"undefined-acronym": undefinedAcronym,
|
|
50
55
|
"concrete-evidence": concreteEvidence,
|
|
51
|
-
|
|
56
|
+
hedging: hedging,
|
|
52
57
|
"cushion-density": cushionDensity,
|
|
53
58
|
"unqualified-superlative": unqualifiedSuperlative,
|
|
54
59
|
"repeated-conjunction": repeatedConjunction,
|
|
@@ -59,6 +64,7 @@ export const DETECTORS: Readonly<Record<string, Detector>> = {
|
|
|
59
64
|
"proper-noun-density": properNounDensity,
|
|
60
65
|
"preferred-term": preferredTerm,
|
|
61
66
|
"latin-spacing": latinSpacing,
|
|
67
|
+
"stray-space": straySpace,
|
|
62
68
|
"dangling-reference": danglingReference,
|
|
63
69
|
"date-weekday-mismatch": dateWeekdayMismatch,
|
|
64
70
|
"date-order": dateOrder,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一文字を - で語の前に繋いだ名前(J-STAGE、X-RAY、T-Mobile、e-Gov)。前の一文字が新しい名前を作るので、中の大文字の語は
|
|
3
|
+
* 略語ではない(J-STAGE は STAGE の説明を待たない)。後ろに一文字を繋いだ語(ITU-T、GOODS-S)は略語の一種で、略語は
|
|
4
|
+
* 説明を待つまま。部品が二つとも二文字以上(RT-PCR)なら繋いだ略語のまま。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
type Span = { readonly start: number; readonly end: number };
|
|
8
|
+
|
|
9
|
+
// 語を空白で区切る文字(é、α、分けて書いた e と ́)や数字が接していれば長い語の一部。仮名や漢字は接していてよい(論文はJ-STAGEで)。
|
|
10
|
+
const SPACED_WORD_CHAR = String.raw`\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}\p{M}\p{N}_&`;
|
|
11
|
+
const LETTER_JOINED_NAME = new RegExp(String.raw`(?<![${SPACED_WORD_CHAR}.-])[A-Za-z]-[A-Za-z]{2,}(?![${SPACED_WORD_CHAR}-]|\.[A-Za-z0-9])`, "gu");
|
|
12
|
+
|
|
13
|
+
/** 文の中の、一文字を - で語の前に繋いだ名前の範囲。 */
|
|
14
|
+
export const letterJoinedNameSpans = (text: string): Span[] =>
|
|
15
|
+
[...text.matchAll(LETTER_JOINED_NAME)].map((match) => ({ start: match.index, end: match.index + match[0].length }));
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Section } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
/** 索引の区切りは 1 文字の見出し(A, あ)で、それ自体は本文を持たない。 */
|
|
4
|
+
const SINGLE_LETTER = /^\p{L}$/u;
|
|
5
|
+
const FIRST_LETTER = /\p{L}/u;
|
|
6
|
+
|
|
7
|
+
/** 区切りが 1 つだけなら、たまたま 1 文字の見出しかもしれない。並んで初めて索引と読む。 */
|
|
8
|
+
const MIN_DIVIDERS = 2;
|
|
9
|
+
|
|
10
|
+
/** カタカナとひらがなは Unicode で同じ並びにあり、この差だけ離れている。 */
|
|
11
|
+
const KATAKANA = { first: 0x30a1, last: 0x30f6, toHiragana: 0x60 };
|
|
12
|
+
|
|
13
|
+
type Group = { readonly run: number; readonly letter: string; readonly entries: Section[] };
|
|
14
|
+
|
|
15
|
+
/** 索引で引くときの文字。最初の文字から濁点やアクセントを外し、カタカナはひらがなに、小文字は大文字にする。 */
|
|
16
|
+
export const indexLetterOf = (text: string): string => {
|
|
17
|
+
const letter = FIRST_LETTER.exec(text.normalize("NFD"))?.[0] ?? "";
|
|
18
|
+
const code = letter.codePointAt(0) ?? 0;
|
|
19
|
+
const base = code >= KATAKANA.first && code <= KATAKANA.last ? String.fromCodePoint(code - KATAKANA.toHiragana) : letter;
|
|
20
|
+
return base.toUpperCase();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const isDivider = (section: Section): boolean => SINGLE_LETTER.test(section.heading.normalize("NFC")) && section.sentences.length === 0;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ある深さの区切りごとに、その下の節を集める。区切りと同じ深さか浅い見出しで閉じる。
|
|
27
|
+
* 区切りでない見出しを挟んだ区切りは、別の並び(run)になる。別の親の下の A と B を 1 つの索引に綴じないため。
|
|
28
|
+
*/
|
|
29
|
+
const groupsAt = (sections: readonly Section[], depth: number): Group[] => {
|
|
30
|
+
const groups: Group[] = [];
|
|
31
|
+
sections.reduce<{ run: number; open: Group | undefined }>(
|
|
32
|
+
({ run, open }, section) => {
|
|
33
|
+
if (section.depth === depth && isDivider(section)) {
|
|
34
|
+
const group = { run, letter: indexLetterOf(section.heading), entries: [] };
|
|
35
|
+
groups.push(group);
|
|
36
|
+
return { run, open: group };
|
|
37
|
+
}
|
|
38
|
+
if (open !== undefined && section.depth > depth) {
|
|
39
|
+
open.entries.push(section);
|
|
40
|
+
return { run, open };
|
|
41
|
+
}
|
|
42
|
+
return { run: run + 1, open: undefined };
|
|
43
|
+
},
|
|
44
|
+
{ run: 0, open: undefined },
|
|
45
|
+
);
|
|
46
|
+
return groups;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const runsOf = (groups: readonly Group[]): Group[][] =>
|
|
50
|
+
[...new Set(groups.map((group) => group.run))].map((run) => groups.filter((group) => group.run === run));
|
|
51
|
+
|
|
52
|
+
const ascending = (groups: readonly Group[]): boolean => groups.every((group, index) => index === 0 || (groups[index - 1]?.letter ?? "") < group.letter);
|
|
53
|
+
|
|
54
|
+
/** 区切りのすぐ下の見出し(見出し語)の過半が、区切りの文字で始まる。語りの節を 1 文字の見出しで束ねただけなら、こうはならない。 */
|
|
55
|
+
const headwordsFollowLetters = (groups: readonly Group[], depth: number): boolean => {
|
|
56
|
+
const matches = groups.flatMap((group) =>
|
|
57
|
+
group.entries.filter((entry) => entry.depth === depth + 1).map((entry) => indexLetterOf(entry.heading) === group.letter),
|
|
58
|
+
);
|
|
59
|
+
return matches.filter((match) => match).length * 2 > matches.length;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const isIndex = (groups: readonly Group[], depth: number): boolean =>
|
|
63
|
+
groups.length >= MIN_DIVIDERS && ascending(groups) && headwordsFollowLetters(groups, depth);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 文字で区切った索引(用語集・A to Z)の項目。区切りの見出しより深い節が、次の同じ深さ以上の見出しまで続く。
|
|
67
|
+
* 項目は語の定義や表記の決まりで、根拠の要る主張ではない。
|
|
68
|
+
*/
|
|
69
|
+
export const letteredIndexEntries = (sections: readonly Section[]): ReadonlySet<Section> => {
|
|
70
|
+
const depths = new Set(sections.filter(isDivider).map((section) => section.depth));
|
|
71
|
+
return new Set(
|
|
72
|
+
[...depths].flatMap((depth) =>
|
|
73
|
+
runsOf(groupsAt(sections, depth))
|
|
74
|
+
.filter((run) => isIndex(run, depth))
|
|
75
|
+
.flatMap((run) => run.flatMap((group) => group.entries)),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
};
|
package/src/detectors/lexicon.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { joinWords } from "./word-list.ts";
|
|
2
|
+
import { straightApostrophes } from "../orthography.ts";
|
|
2
3
|
import { wordsOf } from "./structure.ts";
|
|
3
|
-
import type { Detector, Finding, Lexicon, LexiconEntry, ProseDocument, Sentence } from "../plugin.ts";
|
|
4
|
-
import { entryIn, entryOpens, entryRanges } from "./lexicon-match.ts";
|
|
4
|
+
import type { Detector, Finding, Lexicon, LexiconEntry, ProseDocument, Sentence, Span, Token } from "../plugin.ts";
|
|
5
|
+
import { entryIn, entryOpens, entryRanges, type TokenRange } from "./lexicon-match.ts";
|
|
5
6
|
import { scoped, scopeMarkersOf, type ScopeMarkers } from "./superlative-scope.ts";
|
|
6
7
|
import { namesQuantity } from "./superlative-name.ts";
|
|
8
|
+
import { namesAmount } from "./superlative-amount.ts";
|
|
9
|
+
import { restricted, type Restrictors } from "./superlative-clause.ts";
|
|
10
|
+
import { stackedHedges, type StackedHedge } from "./stacked-hedge.ts";
|
|
7
11
|
|
|
8
12
|
const PER = 1000;
|
|
9
13
|
|
|
@@ -37,25 +41,79 @@ const densityRule =
|
|
|
37
41
|
}));
|
|
38
42
|
};
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
const hedgingDensity = densityRule("excessive-hedging");
|
|
41
45
|
export const cushionDensity = densityRule("cushion-phrase-density");
|
|
42
46
|
|
|
47
|
+
/** 書いたとおりの字。行の折り返しをまたいでいたら空白 1 つに。 */
|
|
48
|
+
const writtenAt = (source: string, span: Span): string => source.slice(span.start, span.end).replace(/\s+/gu, " ").trim();
|
|
49
|
+
|
|
50
|
+
const stackedFinding = (doc: ProseDocument, stacked: StackedHedge): Finding => ({
|
|
51
|
+
rule: "excessive-hedging",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
line: 0,
|
|
54
|
+
column: 0,
|
|
55
|
+
quote: stacked.sentence.text.trim(),
|
|
56
|
+
variant: "stacked",
|
|
57
|
+
values: {
|
|
58
|
+
matched: joinWords(
|
|
59
|
+
stacked.spans.map((span) => writtenAt(doc.source, span)),
|
|
60
|
+
doc.language,
|
|
61
|
+
),
|
|
62
|
+
count: stacked.spans.length,
|
|
63
|
+
offset: stacked.spans[0]?.start ?? stacked.sentence.span.start,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 逃げの表現は 2 つの見方で探す。1 つの文に重ねたもの(短い文書でも 1 文で分かる)と、文書全体の密度。
|
|
69
|
+
* 重ねた文は密度の側では出さない。同じ文を同じ rule で 2 度言わない。
|
|
70
|
+
*/
|
|
71
|
+
export const hedging: Detector = (doc, options): Finding[] => {
|
|
72
|
+
const stacked = stackedHedges(doc.sentences, {
|
|
73
|
+
hedges: options.lexicon ?? [],
|
|
74
|
+
frames: doc.lexicons["hedge-frame"] ?? [],
|
|
75
|
+
scope: doc.lexicons["hedge-scope"] ?? [],
|
|
76
|
+
});
|
|
77
|
+
const reported = new Set(stacked.map((entry) => entry.sentence.span.start));
|
|
78
|
+
const density = hedgingDensity(doc, options).filter((finding) => !reported.has(Number(finding.values["offset"])));
|
|
79
|
+
return [...stacked.map((entry) => stackedFinding(doc, entry)), ...density];
|
|
80
|
+
};
|
|
81
|
+
|
|
43
82
|
/** 数字は語ではないので言語を問わない。数があれば測った結果を言っている。品詞の数(NUM)は "the best one" の one まで含むので使わない。 */
|
|
44
83
|
const DIGIT = /\d/u;
|
|
45
84
|
|
|
46
|
-
type Qualifiers = {
|
|
85
|
+
type Qualifiers = {
|
|
86
|
+
readonly comparison: Lexicon;
|
|
87
|
+
readonly scope: ScopeMarkers;
|
|
88
|
+
readonly quantityNouns: Lexicon;
|
|
89
|
+
readonly amounts: Lexicon;
|
|
90
|
+
readonly restrictors: Restrictors;
|
|
91
|
+
};
|
|
47
92
|
|
|
48
93
|
const qualifiersOf = (doc: ProseDocument): Qualifiers => ({
|
|
49
94
|
comparison: doc.lexicons["comparison-marker"] ?? [],
|
|
50
95
|
scope: scopeMarkersOf(doc.lexicons["superlative-scope"] ?? []),
|
|
51
96
|
quantityNouns: doc.lexicons["quantity-noun"] ?? [],
|
|
97
|
+
amounts: doc.lexicons["superlative-amount"] ?? [],
|
|
98
|
+
restrictors: {
|
|
99
|
+
relatives: doc.lexicons["relative-word"] ?? [],
|
|
100
|
+
bounds: doc.lexicons["superlative-bound"] ?? [],
|
|
101
|
+
subjects: doc.lexicons["subject-pronoun"] ?? [],
|
|
102
|
+
},
|
|
52
103
|
});
|
|
53
104
|
|
|
54
|
-
/**
|
|
105
|
+
/** 範囲・節・分詞・形容詞のどれかが最上級を限っているか、最上級が量か名前を言っているか。 */
|
|
106
|
+
const qualifiedAt = (tokens: readonly Token[], range: TokenRange, qualifiers: Qualifiers): boolean =>
|
|
107
|
+
scoped(tokens, range, qualifiers.scope) ||
|
|
108
|
+
restricted(tokens, range, qualifiers.restrictors) ||
|
|
109
|
+
namesQuantity(tokens, range, qualifiers.quantityNouns) ||
|
|
110
|
+
namesAmount(tokens, range, qualifiers.amounts);
|
|
111
|
+
|
|
112
|
+
/** どの出現も限られているか量を言うときだけ。1 つでもそうでない出現があれば、その文には限定の無い最上級がある。 */
|
|
55
113
|
const everyQualified = (sentence: Sentence, entry: LexiconEntry, qualifiers: Qualifiers): boolean => {
|
|
56
114
|
const tokens = sentence.tokens ?? [];
|
|
57
115
|
const ranges = entryRanges(sentence, entry);
|
|
58
|
-
return ranges.length > 0 && ranges.every((range) =>
|
|
116
|
+
return ranges.length > 0 && ranges.every((range) => qualifiedAt(tokens, range, qualifiers));
|
|
59
117
|
};
|
|
60
118
|
|
|
61
119
|
const qualified = (sentence: Sentence, entry: LexiconEntry, qualifiers: Qualifiers): boolean =>
|
|
@@ -170,9 +228,12 @@ export const aiTell: Detector = (doc, options): Finding[] => {
|
|
|
170
228
|
*/
|
|
171
229
|
const containsWord = (text: string, word: string): boolean => new RegExp(`\\b${word.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}\\b`, "u").test(text);
|
|
172
230
|
|
|
231
|
+
/** アポストロフィの形(don’t と don't)は短縮形かどうかを変えない。違うのは印刷の形で、文体ではない。 */
|
|
232
|
+
const comparableText = (sentence: Sentence): string => straightApostrophes(sentence.text).toLowerCase();
|
|
233
|
+
|
|
173
234
|
export const contractionMix: Detector = (doc, options): Finding[] => {
|
|
174
235
|
const pairs = (options.lexicon ?? []).flatMap((entry) => (entry.instead_of === undefined ? [] : [{ short: entry.pattern, long: entry.instead_of }]));
|
|
175
|
-
const body = doc.sentences.map(
|
|
236
|
+
const body = doc.sentences.map(comparableText).join(" ");
|
|
176
237
|
const used = pairs.filter((pair) => containsWord(body, pair.short.toLowerCase()));
|
|
177
238
|
const spelled = pairs.filter((pair) => containsWord(body, pair.long.toLowerCase()));
|
|
178
239
|
// 片方しか無ければ一貫している。両方あるときだけ、少数派を指摘する。
|
|
@@ -182,7 +243,7 @@ export const contractionMix: Detector = (doc, options): Finding[] => {
|
|
|
182
243
|
if (few.length > options.limit) return [];
|
|
183
244
|
const wanted = few.map((pair) => (minorityIsShort ? pair.short : pair.long).toLowerCase());
|
|
184
245
|
const hits = doc.sentences.flatMap((sentence) => {
|
|
185
|
-
const text = sentence
|
|
246
|
+
const text = comparableText(sentence);
|
|
186
247
|
const matched = wanted.find((word) => containsWord(text, word));
|
|
187
248
|
return matched === undefined ? [] : [{ sentence, matched }];
|
|
188
249
|
});
|