chaffjs 0.25.0 → 0.26.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 +179 -42
- package/dist/adapter-import.d.ts +2 -0
- package/dist/adapter-import.d.ts.map +1 -0
- package/dist/adapter-import.js +11 -0
- package/dist/adapter-import.js.map +1 -0
- package/dist/adapter-load.d.ts.map +1 -1
- package/dist/adapter-load.js +2 -9
- package/dist/adapter-load.js.map +1 -1
- package/dist/ai-score/human-shares.d.ts +4 -0
- package/dist/ai-score/human-shares.d.ts.map +1 -0
- package/dist/ai-score/human-shares.js +205 -0
- package/dist/ai-score/human-shares.js.map +1 -0
- package/dist/ai-score/lint-line.d.ts +4 -0
- package/dist/ai-score/lint-line.d.ts.map +1 -0
- package/dist/ai-score/lint-line.js +9 -0
- package/dist/ai-score/lint-line.js.map +1 -0
- package/dist/ai-score/of-document.d.ts +7 -0
- package/dist/ai-score/of-document.d.ts.map +1 -0
- package/dist/ai-score/of-document.js +40 -0
- package/dist/ai-score/of-document.js.map +1 -0
- package/dist/ai-score/render.d.ts +24 -0
- package/dist/ai-score/render.d.ts.map +1 -0
- package/dist/ai-score/render.js +100 -0
- package/dist/ai-score/render.js.map +1 -0
- package/dist/ai-score/score.d.ts +77 -0
- package/dist/ai-score/score.d.ts.map +1 -0
- package/dist/ai-score/score.js +52 -0
- package/dist/ai-score/score.js.map +1 -0
- package/dist/ai-score/signals.d.ts +19 -0
- package/dist/ai-score/signals.d.ts.map +1 -0
- package/dist/ai-score/signals.js +30 -0
- package/dist/ai-score/signals.js.map +1 -0
- package/dist/ai-score/text.d.ts +37 -0
- package/dist/ai-score/text.d.ts.map +1 -0
- package/dist/ai-score/text.js +59 -0
- package/dist/ai-score/text.js.map +1 -0
- package/dist/ai-score/view.d.ts +5 -0
- package/dist/ai-score/view.d.ts.map +1 -0
- package/dist/ai-score/view.js +19 -0
- package/dist/ai-score/view.js.map +1 -0
- package/dist/backends/anthropic.d.ts +0 -1
- package/dist/backends/anthropic.d.ts.map +1 -1
- package/dist/backends/anthropic.js +0 -1
- package/dist/backends/anthropic.js.map +1 -1
- package/dist/backends/default-model.d.ts +3 -0
- package/dist/backends/default-model.d.ts.map +1 -0
- package/dist/backends/default-model.js +5 -0
- package/dist/backends/default-model.js.map +1 -0
- package/dist/backends/openai.d.ts +0 -1
- package/dist/backends/openai.d.ts.map +1 -1
- package/dist/backends/openai.js +0 -1
- package/dist/backends/openai.js.map +1 -1
- package/dist/browser/adapter-import.d.ts +2 -0
- package/dist/browser/adapter-import.d.ts.map +1 -0
- package/dist/browser/adapter-import.js +17 -0
- package/dist/browser/adapter-import.js.map +1 -0
- package/dist/browser/bounded-match.d.ts +3 -0
- package/dist/browser/bounded-match.d.ts.map +1 -0
- package/dist/browser/bounded-match.js +4 -0
- package/dist/browser/bounded-match.js.map +1 -0
- package/dist/browser/check-text.d.ts +43 -0
- package/dist/browser/check-text.d.ts.map +1 -0
- package/dist/browser/check-text.js +50 -0
- package/dist/browser/check-text.js.map +1 -0
- package/dist/browser/detectors.d.ts +4 -0
- package/dist/browser/detectors.d.ts.map +1 -0
- package/dist/browser/detectors.js +22 -0
- package/dist/browser/detectors.js.map +1 -0
- package/dist/browser/package-files.d.ts +5 -0
- package/dist/browser/package-files.d.ts.map +1 -0
- package/dist/browser/package-files.js +8 -0
- package/dist/browser/package-files.js.map +1 -0
- package/dist/browser/path.d.ts +9 -0
- package/dist/browser/path.d.ts.map +1 -0
- package/dist/browser/path.js +70 -0
- package/dist/browser/path.js.map +1 -0
- package/dist/browser/working-dir.d.ts +2 -0
- package/dist/browser/working-dir.d.ts.map +1 -0
- package/dist/browser/working-dir.js +3 -0
- package/dist/browser/working-dir.js.map +1 -0
- package/dist/browser-files.d.ts +21 -0
- package/dist/browser-files.d.ts.map +1 -0
- package/dist/browser-files.js +65 -0
- package/dist/browser-files.js.map +1 -0
- package/dist/browser.d.ts +9 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +13 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli-text.d.ts.map +1 -1
- package/dist/cli-text.js +2 -0
- package/dist/cli-text.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/ai-score.d.ts +8 -0
- package/dist/commands/ai-score.d.ts.map +1 -0
- package/dist/commands/ai-score.js +59 -0
- package/dist/commands/ai-score.js.map +1 -0
- package/dist/commands/document-commands.d.ts.map +1 -1
- package/dist/commands/document-commands.js +2 -0
- package/dist/commands/document-commands.js.map +1 -1
- package/dist/config/by-path-problems.d.ts +5 -0
- package/dist/config/by-path-problems.d.ts.map +1 -0
- package/dist/config/by-path-problems.js +11 -0
- package/dist/config/by-path-problems.js.map +1 -0
- package/dist/config/load.d.ts +9 -2
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +21 -8
- package/dist/config/load.js.map +1 -1
- package/dist/config/read.d.ts +2 -0
- package/dist/config/read.d.ts.map +1 -1
- package/dist/config/read.js +4 -1
- package/dist/config/read.js.map +1 -1
- package/dist/config/warnings.d.ts +1 -1
- package/dist/config/warnings.d.ts.map +1 -1
- package/dist/config/warnings.js +3 -1
- package/dist/config/warnings.js.map +1 -1
- package/dist/config/working-dir.d.ts +2 -0
- package/dist/config/working-dir.d.ts.map +1 -0
- package/dist/config/working-dir.js +3 -0
- package/dist/config/working-dir.js.map +1 -0
- package/dist/custom/bounded-match.d.ts +1 -12
- package/dist/custom/bounded-match.d.ts.map +1 -1
- package/dist/custom/bounded-match.js +1 -11
- package/dist/custom/bounded-match.js.map +1 -1
- package/dist/custom/pattern-timeout.d.ts +13 -0
- package/dist/custom/pattern-timeout.d.ts.map +1 -0
- package/dist/custom/pattern-timeout.js +13 -0
- package/dist/custom/pattern-timeout.js.map +1 -0
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +4 -5
- package/dist/detect.js.map +1 -1
- package/dist/detectors/date-year.d.ts +10 -2
- package/dist/detectors/date-year.d.ts.map +1 -1
- package/dist/detectors/date-year.js +44 -8
- package/dist/detectors/date-year.js.map +1 -1
- package/dist/detectors/defined-term-form.d.ts +5 -0
- package/dist/detectors/defined-term-form.d.ts.map +1 -1
- package/dist/detectors/defined-term-form.js +22 -5
- package/dist/detectors/defined-term-form.js.map +1 -1
- package/dist/detectors/orthographic-variant.d.ts.map +1 -1
- package/dist/detectors/orthographic-variant.js +44 -17
- package/dist/detectors/orthographic-variant.js.map +1 -1
- package/dist/detectors/party-role-name.d.ts +3 -0
- package/dist/detectors/party-role-name.d.ts.map +1 -0
- package/dist/detectors/party-role-name.js +35 -0
- package/dist/detectors/party-role-name.js.map +1 -0
- package/dist/detectors/quote-without-source.d.ts +17 -0
- package/dist/detectors/quote-without-source.d.ts.map +1 -0
- package/dist/detectors/quote-without-source.js +64 -0
- package/dist/detectors/quote-without-source.js.map +1 -0
- package/dist/detectors/reference-text.js +1 -1
- package/dist/detectors/reference-text.js.map +1 -1
- package/dist/detectors/registry/party-role-name.d.ts +3 -0
- package/dist/detectors/registry/party-role-name.d.ts.map +1 -0
- package/dist/detectors/registry/party-role-name.js +3 -0
- package/dist/detectors/registry/party-role-name.js.map +1 -0
- package/dist/detectors/registry/quote-without-source.d.ts +3 -0
- package/dist/detectors/registry/quote-without-source.d.ts.map +1 -0
- package/dist/detectors/registry/quote-without-source.js +3 -0
- package/dist/detectors/registry/quote-without-source.js.map +1 -0
- package/dist/detectors/registry/vague-deadline.d.ts +3 -0
- package/dist/detectors/registry/vague-deadline.d.ts.map +1 -0
- package/dist/detectors/registry/vague-deadline.js +3 -0
- package/dist/detectors/registry/vague-deadline.js.map +1 -0
- package/dist/detectors/run-in-label.d.ts +7 -0
- package/dist/detectors/run-in-label.d.ts.map +1 -0
- package/dist/detectors/run-in-label.js +53 -0
- package/dist/detectors/run-in-label.js.map +1 -0
- package/dist/detectors/sentence-counts.d.ts +3 -1
- package/dist/detectors/sentence-counts.d.ts.map +1 -1
- package/dist/detectors/sentence-counts.js +11 -4
- package/dist/detectors/sentence-counts.js.map +1 -1
- package/dist/detectors/sentence-ending.d.ts +1 -1
- package/dist/detectors/sentence-ending.d.ts.map +1 -1
- package/dist/detectors/sentence-ending.js +2 -1
- package/dist/detectors/sentence-ending.js.map +1 -1
- package/dist/detectors/structure-tree.d.ts +4 -1
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +21 -2
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/unbalanced-bracket.d.ts +1 -1
- package/dist/detectors/unbalanced-bracket.d.ts.map +1 -1
- package/dist/detectors/unbalanced-bracket.js +23 -11
- package/dist/detectors/unbalanced-bracket.js.map +1 -1
- package/dist/detectors/vague-deadline.d.ts +25 -0
- package/dist/detectors/vague-deadline.d.ts.map +1 -0
- package/dist/detectors/vague-deadline.js +97 -0
- package/dist/detectors/vague-deadline.js.map +1 -0
- package/dist/furigana.d.ts +7 -0
- package/dist/furigana.d.ts.map +1 -0
- package/dist/furigana.js +16 -0
- package/dist/furigana.js.map +1 -0
- package/dist/genre-load.d.ts.map +1 -1
- package/dist/genre-load.js +5 -7
- package/dist/genre-load.js.map +1 -1
- package/dist/grade/ai-score.d.ts +16 -0
- package/dist/grade/ai-score.d.ts.map +1 -0
- package/dist/grade/ai-score.js +17 -0
- package/dist/grade/ai-score.js.map +1 -0
- package/dist/grade/finding.d.ts +6 -0
- package/dist/grade/finding.d.ts.map +1 -0
- package/dist/grade/finding.js +13 -0
- package/dist/grade/finding.js.map +1 -0
- package/dist/grade/grade-item.d.ts.map +1 -1
- package/dist/grade/grade-item.js +4 -11
- package/dist/grade/grade-item.js.map +1 -1
- package/dist/grade/render-variants.d.ts.map +1 -1
- package/dist/grade/render-variants.js +3 -0
- package/dist/grade/render-variants.js.map +1 -1
- package/dist/grade/render.d.ts.map +1 -1
- package/dist/grade/render.js +1 -0
- package/dist/grade/render.js.map +1 -1
- package/dist/grade/result.d.ts +3 -0
- package/dist/grade/result.d.ts.map +1 -1
- package/dist/grade/results-read.d.ts.map +1 -1
- package/dist/grade/results-read.js +13 -2
- package/dist/grade/results-read.js.map +1 -1
- package/dist/grade/summary.d.ts +3 -0
- package/dist/grade/summary.d.ts.map +1 -1
- package/dist/grade/summary.js +6 -0
- package/dist/grade/summary.js.map +1 -1
- package/dist/grade/text.d.ts +2 -0
- package/dist/grade/text.d.ts.map +1 -1
- package/dist/grade/text.js +2 -0
- package/dist/grade/text.js.map +1 -1
- package/dist/grade/variants-text.d.ts +3 -0
- package/dist/grade/variants-text.d.ts.map +1 -1
- package/dist/grade/variants-text.js +4 -0
- package/dist/grade/variants-text.js.map +1 -1
- package/dist/grade/variants.d.ts +3 -0
- package/dist/grade/variants.d.ts.map +1 -1
- package/dist/grade/variants.js +1 -0
- package/dist/grade/variants.js.map +1 -1
- package/dist/grade-api.d.ts +1 -0
- package/dist/grade-api.d.ts.map +1 -1
- package/dist/grade-api.js +2 -1
- package/dist/grade-api.js.map +1 -1
- package/dist/judge.d.ts +0 -1
- package/dist/judge.d.ts.map +1 -1
- package/dist/judge.js +0 -1
- package/dist/judge.js.map +1 -1
- package/dist/known-genres.js +2 -2
- package/dist/known-genres.js.map +1 -1
- package/dist/package-files.d.ts +6 -0
- package/dist/package-files.d.ts.map +1 -0
- package/dist/package-files.js +11 -0
- package/dist/package-files.js.map +1 -0
- package/dist/profile/load.d.ts.map +1 -1
- package/dist/profile/load.js +5 -6
- package/dist/profile/load.js.map +1 -1
- package/dist/rule-load.d.ts.map +1 -1
- package/dist/rule-load.js +5 -6
- package/dist/rule-load.js.map +1 -1
- package/dist/rule-offs.js +3 -3
- package/dist/rule-offs.js.map +1 -1
- package/dist/run.js +1 -1
- package/dist/run.js.map +1 -1
- package/dist/structure/date-range.d.ts +4 -0
- package/dist/structure/date-range.d.ts.map +1 -1
- package/dist/structure/date-range.js +23 -3
- package/dist/structure/date-range.js.map +1 -1
- package/dist/structure/issues.d.ts +17 -1
- package/dist/structure/issues.d.ts.map +1 -1
- package/dist/structure/issues.js +54 -5
- package/dist/structure/issues.js.map +1 -1
- package/dist/structure/party-role.d.ts +30 -0
- package/dist/structure/party-role.d.ts.map +1 -0
- package/dist/structure/party-role.js +58 -0
- package/dist/structure/party-role.js.map +1 -0
- package/dist/structure/prose-total.d.ts +28 -0
- package/dist/structure/prose-total.d.ts.map +1 -0
- package/dist/structure/prose-total.js +99 -0
- package/dist/structure/prose-total.js.map +1 -0
- package/dist/structure/reference-text.d.ts +5 -0
- package/dist/structure/reference-text.d.ts.map +1 -1
- package/dist/structure/reference-text.js +17 -4
- package/dist/structure/reference-text.js.map +1 -1
- package/dist/structure/term-prefix.d.ts +18 -0
- package/dist/structure/term-prefix.d.ts.map +1 -0
- package/dist/structure/term-prefix.js +40 -0
- package/dist/structure/term-prefix.js.map +1 -0
- package/dist/structure/total.d.ts +2 -0
- package/dist/structure/total.d.ts.map +1 -1
- package/dist/structure/total.js +2 -0
- package/dist/structure/total.js.map +1 -1
- package/dist/structure-shape/baseline-load.d.ts.map +1 -1
- package/dist/structure-shape/baseline-load.js +4 -5
- package/dist/structure-shape/baseline-load.js.map +1 -1
- package/dist/style-load.d.ts.map +1 -1
- package/dist/style-load.js +5 -6
- package/dist/style-load.js.map +1 -1
- package/package.json +24 -3
- package/rules/cliche.yaml +28 -8
- package/rules/date-without-year.yaml +28 -3
- package/rules/defined-term-form.yaml +9 -6
- package/rules/demonstrative-opener-run.yaml +24 -7
- package/rules/no-mixed-desumasu.yaml +2 -2
- package/rules/orthographic-variant.yaml +2 -2
- package/rules/party-role-name.yaml +94 -0
- package/rules/quote-without-source.yaml +61 -0
- package/rules/total-mismatch.yaml +8 -8
- package/rules/vague-deadline.yaml +95 -0
- package/rules/weasel-word.yaml +25 -7
- package/skills/chaff/SKILL.md +12 -2
- package/src/adapter-import.ts +3 -0
- package/src/adapter-load.ts +2 -1
- package/src/ai-score/human-shares.ts +207 -0
- package/src/ai-score/lint-line.ts +10 -0
- package/src/ai-score/of-document.ts +46 -0
- package/src/ai-score/render.ts +124 -0
- package/src/ai-score/score.ts +121 -0
- package/src/ai-score/signals.ts +39 -0
- package/src/ai-score/text.ts +103 -0
- package/src/ai-score/view.ts +21 -0
- package/src/backends/anthropic.ts +0 -2
- package/src/backends/default-model.ts +9 -0
- package/src/backends/openai.ts +0 -2
- package/src/browser/adapter-import.ts +19 -0
- package/src/browser/bounded-match.ts +9 -0
- package/src/browser/check-text.ts +106 -0
- package/src/browser/detectors.ts +40 -0
- package/src/browser/import-meta-glob.d.ts +5 -0
- package/src/browser/package-files.ts +13 -0
- package/src/browser/path.ts +73 -0
- package/src/browser/working-dir.ts +3 -0
- package/src/browser-files.ts +87 -0
- package/src/browser.ts +19 -0
- package/src/cli-text.ts +2 -0
- package/src/cli.ts +4 -1
- package/src/commands/ai-score.ts +77 -0
- package/src/commands/document-commands.ts +2 -0
- package/src/config/by-path-problems.ts +17 -0
- package/src/config/load.ts +21 -8
- package/src/config/read.ts +5 -1
- package/src/config/warnings.ts +3 -1
- package/src/config/working-dir.ts +3 -0
- package/src/custom/bounded-match.ts +1 -16
- package/src/custom/pattern-timeout.ts +17 -0
- package/src/detect.ts +4 -5
- package/src/detectors/date-year.ts +51 -12
- package/src/detectors/defined-term-form.ts +28 -6
- package/src/detectors/orthographic-variant.ts +59 -18
- package/src/detectors/party-role-name.ts +44 -0
- package/src/detectors/quote-without-source.ts +90 -0
- package/src/detectors/reference-text.ts +1 -1
- package/src/detectors/registry/party-role-name.ts +4 -0
- package/src/detectors/registry/quote-without-source.ts +4 -0
- package/src/detectors/registry/vague-deadline.ts +4 -0
- package/src/detectors/run-in-label.ts +60 -0
- package/src/detectors/sentence-counts.ts +13 -4
- package/src/detectors/sentence-ending.ts +3 -2
- package/src/detectors/structure-tree.ts +28 -6
- package/src/detectors/unbalanced-bracket.ts +25 -10
- package/src/detectors/vague-deadline.ts +132 -0
- package/src/furigana.ts +20 -0
- package/src/genre-load.ts +5 -7
- package/src/grade/ai-score.ts +32 -0
- package/src/grade/finding.ts +16 -0
- package/src/grade/grade-item.ts +4 -13
- package/src/grade/render-variants.ts +5 -0
- package/src/grade/render.ts +1 -0
- package/src/grade/result.ts +3 -0
- package/src/grade/results-read.ts +17 -2
- package/src/grade/summary.ts +9 -0
- package/src/grade/text.ts +6 -0
- package/src/grade/variants-text.ts +7 -0
- package/src/grade/variants.ts +4 -0
- package/src/grade-api.ts +3 -1
- package/src/judge.ts +0 -2
- package/src/known-genres.ts +2 -2
- package/src/package-files.ts +15 -0
- package/src/profile/load.ts +5 -6
- package/src/rule-load.ts +5 -6
- package/src/rule-offs.ts +3 -3
- package/src/run.ts +1 -1
- package/src/structure/date-range.ts +28 -3
- package/src/structure/issues.ts +61 -6
- package/src/structure/party-role.ts +94 -0
- package/src/structure/prose-total.ts +135 -0
- package/src/structure/reference-text.ts +17 -4
- package/src/structure/term-prefix.ts +54 -0
- package/src/structure/total.ts +4 -0
- package/src/structure-shape/baseline-load.ts +4 -5
- package/src/style-load.ts +5 -6
package/src/config/load.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { workingDir } from "./working-dir.ts";
|
|
3
3
|
import { isLevel } from "../levels.ts";
|
|
4
|
-
import { defaultModel } from "../
|
|
4
|
+
import { defaultModel } from "../backends/default-model.ts";
|
|
5
5
|
import { isBackend, type BackendName } from "../backends/types.ts";
|
|
6
6
|
import type { Level } from "../plugin.ts";
|
|
7
7
|
import type { PathRule } from "./by-path.ts";
|
|
@@ -40,6 +40,8 @@ export type Config = {
|
|
|
40
40
|
readonly include?: readonly string[];
|
|
41
41
|
/** パスごとの上書き。設定ファイルのある場所からの相対で照合する。 */
|
|
42
42
|
readonly byPath: readonly PathRule[];
|
|
43
|
+
/** by_path entries that could not be read (a map instead of a list, an entry without files), as written. */
|
|
44
|
+
readonly unreadableByPath?: readonly string[];
|
|
43
45
|
readonly baseDir: string;
|
|
44
46
|
/** Options set on rules that take them: { rule id: { option: value } }, as written. rule-options.ts checks them against the rules. */
|
|
45
47
|
readonly options?: Readonly<Record<string, unknown>>;
|
|
@@ -95,7 +97,7 @@ export const EMPTY: Config = {
|
|
|
95
97
|
names: [],
|
|
96
98
|
unreadableNames: [],
|
|
97
99
|
byPath: [],
|
|
98
|
-
baseDir:
|
|
100
|
+
baseDir: workingDir(),
|
|
99
101
|
};
|
|
100
102
|
|
|
101
103
|
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -169,15 +171,25 @@ const namesOf = (raw: unknown): { readonly names: string[]; readonly unreadable:
|
|
|
169
171
|
};
|
|
170
172
|
};
|
|
171
173
|
|
|
172
|
-
|
|
174
|
+
/** by_path は files を持つ項目の並び。並びでない値と、読めない項目は読めないものとして返す。黙って捨てると、書いた人は効いたつもりでいる。 */
|
|
175
|
+
export const byPathOf = (raw: unknown): { readonly rules: PathRule[]; readonly unreadable: string[] } => {
|
|
176
|
+
if (raw === undefined || raw === null) return { rules: [], unreadable: [] };
|
|
177
|
+
if (!Array.isArray(raw)) return { rules: [], unreadable: [printed(raw)] };
|
|
178
|
+
const entries: unknown[] = raw;
|
|
179
|
+
const read = entries.map((entry) => ({ entry, rule: toPathRule(entry) }));
|
|
180
|
+
return {
|
|
181
|
+
rules: read.flatMap(({ rule }) => (rule === undefined ? [] : [rule])),
|
|
182
|
+
unreadable: read.filter(({ rule }) => rule === undefined).map(({ entry }) => printed(entry)),
|
|
183
|
+
};
|
|
184
|
+
};
|
|
173
185
|
|
|
174
|
-
/**
|
|
175
|
-
export const
|
|
176
|
-
const raw: unknown = readYamlFile(path);
|
|
186
|
+
/** 読んだ chaff.yaml の中身(raw)を設定にする。ファイルは読まない。path は raw を読んだ場所。 */
|
|
187
|
+
export const configOf = (raw: unknown, path: string): Config => {
|
|
177
188
|
if (!isRecord(raw)) return { ...EMPTY, path };
|
|
178
189
|
const declared: unknown = raw["ai_backend"];
|
|
179
190
|
const backend: BackendName = isBackend(declared) ? declared : DEFAULT_BACKEND;
|
|
180
191
|
const names = namesOf(raw["names"]);
|
|
192
|
+
const byPath = byPathOf(raw["by_path"]);
|
|
181
193
|
const options: unknown = raw["options"];
|
|
182
194
|
return {
|
|
183
195
|
genre: str(raw["genre"]),
|
|
@@ -197,7 +209,8 @@ export const loadConfig = (path: string): Config => {
|
|
|
197
209
|
names: names.names,
|
|
198
210
|
unreadableNames: names.unreadable,
|
|
199
211
|
include: globsOf(raw["include"]),
|
|
200
|
-
byPath:
|
|
212
|
+
byPath: byPath.rules,
|
|
213
|
+
unreadableByPath: byPath.unreadable,
|
|
201
214
|
baseDir: dirname(path),
|
|
202
215
|
options: isRecord(options) ? options : {},
|
|
203
216
|
unreadableOptions: options === undefined || options === null || isRecord(options) ? undefined : printed(options),
|
package/src/config/read.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { CONFIG_FILE, EMPTY,
|
|
3
|
+
import { CONFIG_FILE, EMPTY, configOf, type Config } from "./load.ts";
|
|
4
4
|
import { withStyle } from "./style.ts";
|
|
5
|
+
import { readYamlFile } from "./yaml-file.ts";
|
|
5
6
|
import { loadStyles } from "../style-load.ts";
|
|
6
7
|
|
|
8
|
+
/** 設定ファイルが無くても動く。あっても、既定から変えたものだけが書かれている。spec §18。 */
|
|
9
|
+
export const loadConfig = (path: string): Config => configOf(readYamlFile(path), path);
|
|
10
|
+
|
|
7
11
|
/** The chaff.yaml in dir with its style applied, or the defaults when there is none. */
|
|
8
12
|
export const readConfigIn = (dir: string): Config => (existsSync(join(dir, CONFIG_FILE)) ? withStyle(loadConfig(join(dir, CONFIG_FILE)), loadStyles()) : EMPTY);
|
package/src/config/warnings.ts
CHANGED
|
@@ -3,13 +3,15 @@ import type { RuleDefinition } from "../plugin.ts";
|
|
|
3
3
|
import type { UiLanguage } from "../ui.ts";
|
|
4
4
|
import { ruleProblems } from "./rule-problems.ts";
|
|
5
5
|
import { nameProblems } from "./name-problems.ts";
|
|
6
|
+
import { byPathProblems } from "./by-path-problems.ts";
|
|
6
7
|
import { configOptionProblems } from "./option-problems.ts";
|
|
7
8
|
import { configGuideProblems } from "../genre-guide/of-config.ts";
|
|
8
9
|
|
|
9
|
-
/** Everything in chaff.yaml that has no effect: unknown rules or levels, unreadable names, options that do not apply, guides that cannot be read. Said before the results. */
|
|
10
|
+
/** Everything in chaff.yaml that has no effect: unknown rules or levels, unreadable names or by_path entries, options that do not apply, guides that cannot be read. Said before the results. */
|
|
10
11
|
export const settingWarnings = (config: Config, rules: readonly RuleDefinition[], ui: UiLanguage): string[] => [
|
|
11
12
|
...ruleProblems(config, rules, ui),
|
|
12
13
|
...nameProblems(config, ui),
|
|
14
|
+
...byPathProblems(config, ui),
|
|
13
15
|
...configOptionProblems(config, rules, ui),
|
|
14
16
|
...configGuideProblems(config, ui),
|
|
15
17
|
];
|
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
import { createContext, Script } from "node:vm";
|
|
2
|
+
import { PATTERN_BUDGET_MS, PatternTimeout, type TextMatches } from "./pattern-timeout.ts";
|
|
2
3
|
|
|
3
4
|
// A team's regular expression, run with a time limit. regex-safety.ts refuses the shapes known to run away before a pattern
|
|
4
5
|
// is used; no reading of the pattern can prove every shape safe, so this is the backstop. V8 can interrupt a regular
|
|
5
6
|
// expression only through vm's timeout, so the matching runs in a vm context.
|
|
6
7
|
|
|
7
|
-
/** How long one rule's pattern may take over one document. Far above what a sane pattern needs on any real document. */
|
|
8
|
-
export const PATTERN_BUDGET_MS = 1000;
|
|
9
|
-
|
|
10
|
-
/** A pattern that did not finish within its budget. run.ts lists the rule as not run, with this as the reason. */
|
|
11
|
-
export class PatternTimeout extends Error {
|
|
12
|
-
readonly budget_ms: number;
|
|
13
|
-
constructor(budgetMs: number) {
|
|
14
|
-
super(`the pattern did not finish within ${String(budgetMs)} ms`);
|
|
15
|
-
this.name = "PatternTimeout";
|
|
16
|
-
this.budget_ms = budgetMs;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Each text's matches: where it starts and what it matched. Empty matches are left out. */
|
|
21
|
-
export type TextMatches = readonly (readonly { readonly index: number; readonly text: string }[])[];
|
|
22
|
-
|
|
23
8
|
const SCRIPT = new Script(
|
|
24
9
|
`texts.map((text) => [...text.matchAll(new RegExp(source, flags))].filter((m) => m[0] !== "").map((m) => ({ index: m.index, text: m[0] })))`,
|
|
25
10
|
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// What a team's pattern may take, and what it is when it takes longer: the same wherever the pattern runs.
|
|
2
|
+
|
|
3
|
+
/** How long one rule's pattern may take over one document. Far above what a sane pattern needs on any real document. */
|
|
4
|
+
export const PATTERN_BUDGET_MS = 1000;
|
|
5
|
+
|
|
6
|
+
/** A pattern that did not finish within its budget. run.ts lists the rule as not run, with this as the reason. */
|
|
7
|
+
export class PatternTimeout extends Error {
|
|
8
|
+
readonly budget_ms: number;
|
|
9
|
+
constructor(budgetMs: number) {
|
|
10
|
+
super(`the pattern did not finish within ${String(budgetMs)} ms`);
|
|
11
|
+
this.name = "PatternTimeout";
|
|
12
|
+
this.budget_ms = budgetMs;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Each text's matches: where it starts and what it matched. Empty matches are left out. */
|
|
17
|
+
export type TextMatches = readonly (readonly { readonly index: number; readonly text: string }[])[];
|
package/src/detect.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
1
|
+
import { join } from "node:path";
|
|
4
2
|
import { parse } from "yaml";
|
|
5
3
|
import { japaneseRatio, latinRatio } from "./detect-language.ts";
|
|
6
4
|
import { referenceListSpans } from "./reference-lists.ts";
|
|
7
5
|
import { withoutSpans } from "./soft-break.ts";
|
|
8
6
|
import { languageSample } from "./language-sample.ts";
|
|
7
|
+
import { PACKAGE_DIR, readText } from "./package-files.ts";
|
|
9
8
|
|
|
10
9
|
export type LanguageGuess = { readonly language: string; readonly confidence: number; readonly from: string };
|
|
11
10
|
|
|
@@ -15,12 +14,12 @@ export type LanguageGuess = { readonly language: string; readonly confidence: nu
|
|
|
15
14
|
*/
|
|
16
15
|
const JAPANESE_FLOOR = 0.15;
|
|
17
16
|
|
|
18
|
-
const HEADINGS_FILE = join(
|
|
17
|
+
const HEADINGS_FILE = join(PACKAGE_DIR, "reference-headings.yaml");
|
|
19
18
|
|
|
20
19
|
const isStringList = (value: unknown): value is string[] => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
21
20
|
|
|
22
21
|
const readHeadings = (): string[] => {
|
|
23
|
-
const raw: unknown = parse(
|
|
22
|
+
const raw: unknown = parse(readText(HEADINGS_FILE));
|
|
24
23
|
const headings: unknown = typeof raw === "object" && raw !== null && "headings" in raw ? raw.headings : undefined;
|
|
25
24
|
if (!isStringList(headings)) throw new Error(`${HEADINGS_FILE}: headings は文字列の並びであること`);
|
|
26
25
|
return headings;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// A date written without its year where the document's other dates carry one, or where the year cannot be read off
|
|
2
2
|
// because the document's dates cross the turn of a year. Pure; reads the date nodes of the structure tree.
|
|
3
|
-
import type { Detector, Finding, ProseDocument, StructureNode } from "../plugin.ts";
|
|
3
|
+
import type { Detector, Finding, LexiconEntry, ProseDocument, StructureNode } from "../plugin.ts";
|
|
4
4
|
import { inDocumentOrder } from "../structure/issues.ts";
|
|
5
5
|
import { quoteAt } from "./structure-tree.ts";
|
|
6
6
|
|
|
@@ -9,11 +9,19 @@ export type DatePoint = { readonly offset: number; readonly end: number; readonl
|
|
|
9
9
|
|
|
10
10
|
export type YearGap = { readonly point: DatePoint; readonly kind: "minority" | "boundary"; readonly dated: number };
|
|
11
11
|
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The words beside a date that give its year in words (その年の, 同年, 毎年; "of each year"): the date has a year, or means
|
|
14
|
+
* every year. An entry stands before the date unless it says `position: after`.
|
|
15
|
+
*/
|
|
13
16
|
export const YEAR_REFERENCE_LEXICON = "year-reference";
|
|
14
17
|
|
|
18
|
+
/** A year word as the lexicon gives it. `before`: the word ends within the reach before the date ("annually, by"). */
|
|
19
|
+
export type YearWord = { readonly word: string; readonly position?: "before" | "after" | undefined };
|
|
20
|
+
|
|
15
21
|
const FULL = /^\d{4}-(\d{2})-\d{2}$/u;
|
|
16
22
|
const MONTH_DAY = /^(\d{2})-\d{2}$/u;
|
|
23
|
+
/** The month of any date with one: 2026-12-01, 2026-12 or 12-01. */
|
|
24
|
+
const ANY_MONTH = /^(?:\d{4}-(\d{2})(?:-\d{2})?|(\d{2})-\d{2})$/u;
|
|
17
25
|
|
|
18
26
|
/** How far before a date a year word may stand (「同年12月」「その年の12月」). */
|
|
19
27
|
const YEAR_WORD_REACH = 6;
|
|
@@ -33,24 +41,53 @@ const sideOf = (month: number | undefined): "end" | "start" | undefined => {
|
|
|
33
41
|
return YEAR_START.has(month) ? "start" : undefined;
|
|
34
42
|
};
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
const
|
|
44
|
+
const anyMonthOf = (value: string): number | undefined => {
|
|
45
|
+
const found = ANY_MONTH.exec(value);
|
|
46
|
+
const month = found?.[1] ?? found?.[2];
|
|
47
|
+
return month === undefined ? undefined : Number(month);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const opposite = (side: "end" | "start"): "end" | "start" => (side === "end" ? "start" : "end");
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A yearless date where the dates turn over the year: it sits at one turn, the date just before it at the other
|
|
54
|
+
* (12月10日 then 1月15日), and a dated one sits at that other turn too (2026年12月1日). A 2月15日 far from any 12月 is a
|
|
55
|
+
* day of the year, not a crossing.
|
|
56
|
+
*/
|
|
57
|
+
const crossesYear = (point: DatePoint, previous: DatePoint | undefined, datedSides: ReadonlySet<string>): boolean => {
|
|
38
58
|
const side = sideOf(monthOf(MONTH_DAY, point.value));
|
|
39
|
-
|
|
59
|
+
if (side === undefined || !datedSides.has(opposite(side))) return false;
|
|
60
|
+
return previous !== undefined && sideOf(anyMonthOf(previous.value)) === opposite(side);
|
|
40
61
|
};
|
|
41
62
|
|
|
42
63
|
const lineStart = (source: string, offset: number): number => source.lastIndexOf("\n", offset - 1) + 1;
|
|
43
64
|
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
return
|
|
65
|
+
const lineEnd = (source: string, offset: number): number => {
|
|
66
|
+
const end = source.indexOf("\n", offset);
|
|
67
|
+
return end === -1 ? source.length : end;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A word without a position is found anywhere in the reach before the date (「同年12月」). One that says `before` may end
|
|
72
|
+
* there and start further back ("annually, by June 30"); one that says `after` starts within the reach after the date
|
|
73
|
+
* ("June 30 of each year").
|
|
74
|
+
*/
|
|
75
|
+
const hasYearWord = (point: DatePoint, source: string, yearWords: readonly YearWord[]): boolean => {
|
|
76
|
+
const start = lineStart(source, point.offset);
|
|
77
|
+
const near = source.slice(Math.max(start, point.offset - YEAR_WORD_REACH), point.offset);
|
|
78
|
+
return yearWords.some(({ word, position }) => {
|
|
79
|
+
if (word === "") return false;
|
|
80
|
+
if (position === "after") return source.slice(point.end, Math.min(lineEnd(source, point.end), point.end + YEAR_WORD_REACH + word.length)).includes(word);
|
|
81
|
+
if (position === "before") return source.slice(Math.max(start, point.offset - YEAR_WORD_REACH - word.length), point.offset).includes(word);
|
|
82
|
+
return near.includes(word);
|
|
83
|
+
});
|
|
47
84
|
};
|
|
48
85
|
|
|
49
86
|
/**
|
|
50
87
|
* A yearless date whose year the text gives anyway: a year word just before it (「同年12月28日」「その年の12月31日」), or,
|
|
51
88
|
* earlier on the same line, a date with a year or with a year word (「2026年9月25日から10月7日まで」「その年の1月1日から12月31日まで」).
|
|
52
89
|
*/
|
|
53
|
-
const yearGiven = (point: DatePoint, anchors: readonly DatePoint[], source: string, yearWords: readonly
|
|
90
|
+
const yearGiven = (point: DatePoint, anchors: readonly DatePoint[], source: string, yearWords: readonly YearWord[]): boolean => {
|
|
54
91
|
const start = lineStart(source, point.offset);
|
|
55
92
|
return hasYearWord(point, source, yearWords) || anchors.some((other) => other.offset >= start && other.offset < point.offset);
|
|
56
93
|
};
|
|
@@ -60,22 +97,24 @@ const yearGiven = (point: DatePoint, anchors: readonly DatePoint[], source: stri
|
|
|
60
97
|
* one is the odd one out. Otherwise writing the year once is the document's style, and only a date across the turn of a
|
|
61
98
|
* year is reported. A date whose year the text gives in words or earlier on its line is never reported, nor counted.
|
|
62
99
|
*/
|
|
63
|
-
export const yearGaps = (points: readonly DatePoint[], source = "", yearWords: readonly
|
|
100
|
+
export const yearGaps = (points: readonly DatePoint[], source = "", yearWords: readonly YearWord[] = []): YearGap[] => {
|
|
64
101
|
const dated = points.filter((point) => FULL.test(point.value));
|
|
65
102
|
const anchors = [...dated, ...points.filter((point) => hasYearWord(point, source, yearWords))];
|
|
66
103
|
const yearless = points.filter((point) => MONTH_DAY.test(point.value) && !yearGiven(point, anchors, source, yearWords));
|
|
67
104
|
const minority = dated.length >= 2 && yearless.length < dated.length;
|
|
68
105
|
const datedSides = new Set(dated.flatMap((point) => sideOf(monthOf(FULL, point.value)) ?? []));
|
|
106
|
+
const previousOf = new Map(points.map((point, index) => [point, points[index - 1]]));
|
|
69
107
|
return yearless.flatMap((point): YearGap[] => {
|
|
70
108
|
if (minority) return [{ point, kind: "minority", dated: dated.length }];
|
|
71
|
-
return crossesYear(point, datedSides) ? [{ point, kind: "boundary", dated: dated.length }] : [];
|
|
109
|
+
return crossesYear(point, previousOf.get(point), datedSides) ? [{ point, kind: "boundary", dated: dated.length }] : [];
|
|
72
110
|
});
|
|
73
111
|
};
|
|
74
112
|
|
|
75
113
|
export const datePoints = (tree: StructureNode): DatePoint[] =>
|
|
76
114
|
inDocumentOrder(tree).flatMap((node) => (node.kind === "date" ? [{ offset: node.span.start, end: node.span.end, value: String(node.attrs["value"]) }] : []));
|
|
77
115
|
|
|
78
|
-
const yearWordsOf = (doc: ProseDocument):
|
|
116
|
+
const yearWordsOf = (doc: ProseDocument): YearWord[] =>
|
|
117
|
+
(doc.lexicons[YEAR_REFERENCE_LEXICON] ?? []).map((entry: LexiconEntry) => ({ word: entry.pattern, position: entry.position }));
|
|
79
118
|
|
|
80
119
|
export const dateWithoutYear: Detector = (doc: ProseDocument): Finding[] =>
|
|
81
120
|
doc.structure === undefined
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// name is read from the tokens' parts of speech.
|
|
4
4
|
import { definedTerms, usesOf, type BodyText, type DefinedTerm } from "../structure/definition-use.ts";
|
|
5
5
|
import { escapeRegExp } from "../orthography.ts";
|
|
6
|
+
import { prefixGroupsOf, prefixVariants, variantUses } from "../structure/term-prefix.ts";
|
|
6
7
|
import { quoteAt } from "./structure-tree.ts";
|
|
7
8
|
import type { Detector, Finding, ProseDocument, Sentence, Span, Token } from "../plugin.ts";
|
|
8
9
|
|
|
@@ -91,10 +92,22 @@ const isVerbAt = (doc: ProseDocument, offset: number): boolean => {
|
|
|
91
92
|
return VERBAL.has(token.pos) || afterSubject;
|
|
92
93
|
};
|
|
93
94
|
|
|
95
|
+
/** The term written with another prefix of its group (本件業務 where 本業務 is defined), after the sentence that defines it. */
|
|
96
|
+
const prefixFindings = (doc: ProseDocument, texts: readonly BodyText[], term: DefinedTerm, definingEnd: number, defined: readonly string[]): Finding[] =>
|
|
97
|
+
prefixVariants(term.term, prefixGroupsOf(doc.lexicons["defined-term-prefix"] ?? [])).flatMap((variant) =>
|
|
98
|
+
defined.includes(variant)
|
|
99
|
+
? []
|
|
100
|
+
: variantUses(doc.source, texts, variant, definingEnd, defined).map((offset) =>
|
|
101
|
+
finding(doc, "defined-term-form", offset, "prefix", { term: term.term, written: variant, line: term.line }),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
|
|
94
105
|
export const definedTermForm: Detector = (doc, options): Finding[] => {
|
|
95
106
|
if (doc.structure === undefined) return [];
|
|
96
107
|
const texts = bodyOf(doc);
|
|
97
|
-
|
|
108
|
+
const terms = definedTerms(doc.structure);
|
|
109
|
+
const defined = terms.map((term) => term.term);
|
|
110
|
+
return firstDefinitions(terms).flatMap(({ term, spans }) => {
|
|
98
111
|
const uses = usesOf(term.term, texts, spans, "loose");
|
|
99
112
|
const definingEnd = definingSentenceEnd(doc, term.span);
|
|
100
113
|
const quoted = quotedUses(doc.source, term.term, uses, definingEnd).map((offset) =>
|
|
@@ -104,7 +117,7 @@ export const definedTermForm: Detector = (doc, options): Finding[] => {
|
|
|
104
117
|
const lower = lowerCaseUses(doc.source, term.term, nounUses, definingEnd, options.limit).map((offset) =>
|
|
105
118
|
finding(doc, "defined-term-form", offset, "case", { term: term.term, line: term.line }),
|
|
106
119
|
);
|
|
107
|
-
return [...quoted, ...lower];
|
|
120
|
+
return [...quoted, ...lower, ...prefixFindings(doc, texts, term, definingEnd, defined)];
|
|
108
121
|
});
|
|
109
122
|
};
|
|
110
123
|
|
|
@@ -162,13 +175,21 @@ const isNameLine = (source: string, offset: number, name: string, term: string):
|
|
|
162
175
|
const end = source.indexOf("\n", offset);
|
|
163
176
|
const line = source.slice(start, end === -1 ? source.length : end).trim();
|
|
164
177
|
const SLACK = 12;
|
|
165
|
-
|
|
178
|
+
// The short name inside the long one ("Pinecone" in "Pinecone Software Ltd") is not the short name written beside it.
|
|
179
|
+
return line.replaceAll(name, "").includes(term) || [...line].length <= [...name].length + SLACK;
|
|
166
180
|
};
|
|
167
181
|
|
|
168
182
|
/** A name inside a longer word (東京大学 in 東京大学大学院, Acme in Acmeware) is part of another name. */
|
|
169
183
|
const WORD_CHAR = /[\p{Script=Han}\p{Script=Katakana}\p{Script=Latin}\p{N}ー]/u;
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
/**
|
|
185
|
+
* A name inside a longer word (東京大学 in 東京大学大学院, Acme in Acmeware) is part of another name. A joining word right after
|
|
186
|
+
* it (北浜精機株式会社及び乙) starts the next item of a list, and the name stands alone.
|
|
187
|
+
*/
|
|
188
|
+
export const standsAlone = (source: string, offset: number, name: string, joiners: readonly string[] = []): boolean => {
|
|
189
|
+
const after = offset + name.length;
|
|
190
|
+
const joined = joiners.some((joiner) => joiner !== "" && source.startsWith(joiner, after));
|
|
191
|
+
return !WORD_CHAR.test(source.charAt(offset - 1)) && (joined || !WORD_CHAR.test(source.charAt(after)));
|
|
192
|
+
};
|
|
172
193
|
|
|
173
194
|
/**
|
|
174
195
|
* Each use of a long name after the definition gave it a short name, outside a signature line. A name that is itself a
|
|
@@ -176,6 +197,7 @@ const standsAlone = (source: string, offset: number, name: string): boolean =>
|
|
|
176
197
|
*/
|
|
177
198
|
export const repeatedNames = (doc: ProseDocument, terms: readonly DefinedTerm[]): RepeatedName[] => {
|
|
178
199
|
const defined = new Set(terms.map((term) => term.term));
|
|
200
|
+
const joiners = (doc.lexicons["enumeration-joiner"] ?? []).map((entry) => entry.pattern);
|
|
179
201
|
return terms
|
|
180
202
|
.filter((term) => term.inline)
|
|
181
203
|
.flatMap((term) => {
|
|
@@ -187,7 +209,7 @@ export const repeatedNames = (doc: ProseDocument, terms: readonly DefinedTerm[])
|
|
|
187
209
|
return doc.sentences
|
|
188
210
|
.filter((later) => later.span.start > term.span.end)
|
|
189
211
|
.flatMap((later) => [...later.text.matchAll(pattern)].map((match) => later.span.start + match.index))
|
|
190
|
-
.filter((offset) => standsAlone(doc.source, offset, name) && !isNameLine(doc.source, offset, name, term.term))
|
|
212
|
+
.filter((offset) => standsAlone(doc.source, offset, name, joiners) && !isNameLine(doc.source, offset, name, term.term))
|
|
191
213
|
.map((offset) => ({ offset, name, term: term.term, line: term.line }));
|
|
192
214
|
});
|
|
193
215
|
};
|
|
@@ -4,6 +4,7 @@ import { kanjiSkeleton, katakanaKey, lemmaReading } from "../kana-spelling.ts";
|
|
|
4
4
|
import { isKatakanaWord, stemOf } from "../long-vowel.ts";
|
|
5
5
|
import { QUOTATION_MARKS, isWithinAny, quotedSpans } from "../quoted-span.ts";
|
|
6
6
|
import { nameSpans, touchesAny } from "../team-names.ts";
|
|
7
|
+
import { furiganaSpans } from "../furigana.ts";
|
|
7
8
|
|
|
8
9
|
/** A word found in the document: where it is, which sentence it is in, its key and spelling, and how it is written when that differs. */
|
|
9
10
|
type Placed = KeyedWord & { readonly sentence: Sentence; readonly offset: number; readonly shown?: string };
|
|
@@ -22,42 +23,82 @@ const MIN_LATIN_KEY = 3;
|
|
|
22
23
|
/** Lexicons: readings and spellings whose kanji and kana forms mean different things (成る and なる, 者 and もの), and adverbs hiragana-fukushi reads. */
|
|
23
24
|
const DISTINCT = "orthographic-variant-distinct";
|
|
24
25
|
const FUKUSHI = "hiragana-fukushi";
|
|
26
|
+
/** Lexicon: idioms written as noun, particle and verb (気をつける). The verb there is compared apart from the verb on its own. */
|
|
27
|
+
const IDIOM = "orthographic-variant-idiom";
|
|
25
28
|
|
|
26
29
|
const patternsOf = (doc: ProseDocument, id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
|
|
27
30
|
|
|
28
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Whether a stretch of the sentence is outside a quotation, the team's names and a reading in brackets (HTTP(えいちてぃーてぃーぴー)):
|
|
33
|
+
* the first two keep their own spelling, and a reading is not a spelling at all.
|
|
34
|
+
*/
|
|
29
35
|
const isOpenIn = (sentence: Sentence, names: readonly string[]): IsOpen => {
|
|
30
|
-
const
|
|
36
|
+
const closed = [...quotedSpans(sentence.text, QUOTATION_MARKS), ...furiganaSpans(sentence.text)];
|
|
31
37
|
const named = nameSpans(sentence.text, names);
|
|
32
|
-
return (start, end) => !isWithinAny(
|
|
38
|
+
return (start, end) => !isWithinAny(closed, { start, end }) && !touchesAny(named, start, end);
|
|
33
39
|
};
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
/** A word and the two words before it, in its sentence. */
|
|
42
|
+
type InContext = { readonly token: Token; readonly previous: Token | undefined; readonly before: Token | undefined };
|
|
43
|
+
|
|
44
|
+
/** A verb's て-form the tokenizer read as a conjunction (追って at the head of a sentence): a kanji stem and て or で. Connectives (そして, それで) are kana. */
|
|
45
|
+
const TE_FORM_CONJUNCTION = /^\p{Script=Han}.*[てで]$/u;
|
|
46
|
+
|
|
47
|
+
/** After a て-form (見てみる). The tokenizer reads the て as a joining word, or the whole て-form as a conjunction (追って / みる). */
|
|
48
|
+
const followsTeForm = (previous: Token | undefined): boolean =>
|
|
49
|
+
(previous?.pos === "SCONJ" && (previous.surface === "て" || previous.surface === "で")) ||
|
|
50
|
+
(previous?.pos === "CCONJ" && TE_FORM_CONJUNCTION.test(previous.surface));
|
|
51
|
+
|
|
52
|
+
const isHelper = ({ token, previous }: InContext): boolean => token.features?.["Bound"] === "Yes" || followsTeForm(previous);
|
|
53
|
+
|
|
54
|
+
const isTouching = (left: Token | undefined, right: Token): boolean => left !== undefined && left.span.end === right.span.start;
|
|
55
|
+
|
|
56
|
+
const HIRAGANA_ONLY = /^[ぁ-ゖー]+$/u;
|
|
57
|
+
|
|
58
|
+
/** A kana noun written onto a noun (全員ぶん): a suffix, which may not be the free noun of the same reading (文). */
|
|
59
|
+
const isKanaSuffix = ({ token, previous }: InContext): boolean =>
|
|
60
|
+
token.pos === "NOUN" && previous?.pos === "NOUN" && isTouching(previous, token) && HIRAGANA_ONLY.test(token.surface);
|
|
61
|
+
|
|
62
|
+
/** The verb of an idiom (気をつける): the noun, the particle and the verb's dictionary form are in the idiom lexicon. */
|
|
63
|
+
const isIdiomVerb = ({ token, previous, before }: InContext, idioms: ReadonlySet<string>): boolean =>
|
|
64
|
+
token.pos === "VERB" && previous?.pos === "ADP" && before !== undefined && idioms.has(`${before.surface}${previous.surface}${token.lemma ?? token.surface}`);
|
|
37
65
|
|
|
38
66
|
/**
|
|
39
67
|
* A helper verb or a dependent noun (見て下さい, その事) is written kana by many guides while the full word keeps its kanji (資料を下さい).
|
|
40
|
-
* The two uses are compared apart, so a document that follows that convention is consistent. A suffix (
|
|
41
|
-
* it is not the dependent noun of the same reading (このように).
|
|
68
|
+
* The two uses are compared apart, so a document that follows that convention is consistent. A suffix (業務用, 全員ぶん) is a third use:
|
|
69
|
+
* it is not the dependent noun of the same reading (このように). The verb of an idiom (気をつける) is a fourth.
|
|
42
70
|
*/
|
|
43
|
-
const useOf = (
|
|
44
|
-
if (
|
|
45
|
-
|
|
71
|
+
const useOf = (word: InContext, idioms: ReadonlySet<string>): string => {
|
|
72
|
+
if (isIdiomVerb(word, idioms)) return "idiom";
|
|
73
|
+
if (isKanaSuffix(word)) return "suffix";
|
|
74
|
+
if (!isHelper(word)) return "free";
|
|
75
|
+
return word.token.pos === "NOUN" && word.token.features?.["NounType"] !== "Dependent" ? "suffix" : "bound";
|
|
46
76
|
};
|
|
47
77
|
|
|
48
|
-
|
|
78
|
+
/** One katakana and then kana that is not katakana (スる): no verb stem is one katakana long, as サボる and ググる are two. */
|
|
79
|
+
const ONE_KATAKANA_HEAD = /^\p{Script=Katakana}(?!\p{Script=Katakana})/u;
|
|
80
|
+
const KATAKANA_END = /\p{Script=Katakana}$/u;
|
|
81
|
+
|
|
82
|
+
/** A piece the tokenizer cut out of a katakana run (ミスっ read as ミ and スる): not a word of its own. */
|
|
83
|
+
const isCutFromKatakana = ({ token, previous }: InContext): boolean =>
|
|
84
|
+
ONE_KATAKANA_HEAD.test(token.surface) && token.surface.length > 1 && isTouching(previous, token) && KATAKANA_END.test(previous?.surface ?? "");
|
|
85
|
+
|
|
86
|
+
type Skips = { readonly skip: ReadonlySet<string>; readonly idioms: ReadonlySet<string> };
|
|
87
|
+
|
|
88
|
+
const readingKeyOf = (word: InContext, { skip, idioms }: Skips): string | undefined => {
|
|
89
|
+
const { token } = word;
|
|
49
90
|
const lemma = token.lemma;
|
|
50
91
|
if (!CONTENT.has(token.pos) || token.reading === undefined || lemma === undefined || !JAPANESE_WORD.test(lemma) || skip.has(lemma)) return undefined;
|
|
51
92
|
const reading = lemmaReading(token.surface, lemma, token.reading);
|
|
52
|
-
if (reading === undefined || [...reading].length < MIN_READING || skip.has(reading)) return undefined;
|
|
53
|
-
return `${token.pos}|${useOf(
|
|
93
|
+
if (reading === undefined || [...reading].length < MIN_READING || skip.has(reading) || isCutFromKatakana(word)) return undefined;
|
|
94
|
+
return `${token.pos}|${useOf(word, idioms)}|${reading}`;
|
|
54
95
|
};
|
|
55
96
|
|
|
56
97
|
/** Japanese words keyed by part of speech, use and the reading of the dictionary form, spelled as the dictionary form. */
|
|
57
|
-
const readingWords = (sentence: Sentence, isOpen: IsOpen,
|
|
98
|
+
const readingWords = (sentence: Sentence, isOpen: IsOpen, skips: Skips): Placed[] => {
|
|
58
99
|
const tokens = sentence.tokens ?? [];
|
|
59
100
|
return tokens.flatMap((token, at): Placed[] => {
|
|
60
|
-
const key = readingKeyOf(token, tokens[at - 1],
|
|
101
|
+
const key = readingKeyOf({ token, previous: tokens[at - 1], before: tokens[at - 2] }, skips);
|
|
61
102
|
const local = token.span.start - sentence.span.start;
|
|
62
103
|
if (key === undefined || token.lemma === undefined || !isOpen(local, local + token.surface.length)) return [];
|
|
63
104
|
return [{ key, spelling: token.lemma, sentence, offset: token.span.start }];
|
|
@@ -140,11 +181,11 @@ const findingOf = (words: readonly Placed[], odd: OddSpelling<Placed>): Finding
|
|
|
140
181
|
* document writes less is pointed at. No way is called right; limit is the largest share, in percent, the minority may have.
|
|
141
182
|
*/
|
|
142
183
|
export const orthographicVariant: Detector = (doc, options): Finding[] => {
|
|
143
|
-
const
|
|
184
|
+
const skips: Skips = { skip: new Set([...patternsOf(doc, DISTINCT), ...patternsOf(doc, FUKUSHI)]), idioms: new Set(patternsOf(doc, IDIOM)) };
|
|
144
185
|
const placed = doc.sentences.map((sentence) => ({ sentence, isOpen: isOpenIn(sentence, doc.names ?? []) }));
|
|
145
|
-
const reading = byKanji(placed.flatMap(({ sentence, isOpen }) => readingWords(sentence, isOpen,
|
|
186
|
+
const reading = byKanji(placed.flatMap(({ sentence, isOpen }) => readingWords(sentence, isOpen, skips)));
|
|
146
187
|
const katakana = placed.flatMap(({ sentence, isOpen }) => katakanaWords(sentence, isOpen));
|
|
147
|
-
const latin = placed.flatMap(({ sentence, isOpen }) => latinWords(sentence, isOpen, skip));
|
|
188
|
+
const latin = placed.flatMap(({ sentence, isOpen }) => latinWords(sentence, isOpen, skips.skip));
|
|
148
189
|
const odd = [reading, katakana, latin].flatMap((words) => oddSpellings(words, options.limit));
|
|
149
190
|
const all = [...reading, ...katakana, ...latin];
|
|
150
191
|
// A word can be odd by its reading and by its katakana key at once; one place is said once.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// A party called by a role word (受託者, the Vendor) after the contract gave it a short name (乙, the "Supplier"). The
|
|
2
|
+
// definitions come from the document's structure tree; the role words, short names and company forms from the language
|
|
3
|
+
// package's word lists (party-role, party-short-name, company-form).
|
|
4
|
+
import { definedTerms } from "../structure/definition-use.ts";
|
|
5
|
+
import { partiesOf, roleUses } from "../structure/party-role.ts";
|
|
6
|
+
import { quoteAt } from "./structure-tree.ts";
|
|
7
|
+
import type { Detector, Finding, ProseDocument } from "../plugin.ts";
|
|
8
|
+
|
|
9
|
+
const patternsOf = (doc: ProseDocument, lexicon: string): string[] => (doc.lexicons[lexicon] ?? []).map((entry) => entry.pattern);
|
|
10
|
+
|
|
11
|
+
/** The sentence holding a definition: the role words in the sentences that name the parties are part of naming them. */
|
|
12
|
+
const sentenceAt = (doc: ProseDocument, offset: number): { readonly start: number; readonly end: number } =>
|
|
13
|
+
doc.sentences.find((sentence) => sentence.span.start <= offset && offset < sentence.span.end)?.span ?? { start: offset, end: offset };
|
|
14
|
+
|
|
15
|
+
const NAME_SEPARATOR: Readonly<Record<string, string>> = { ja: "、" };
|
|
16
|
+
|
|
17
|
+
export const partyRoleName: Detector = (doc): Finding[] => {
|
|
18
|
+
if (doc.structure === undefined) return [];
|
|
19
|
+
const terms = definedTerms(doc.structure);
|
|
20
|
+
const words = { roles: patternsOf(doc, "party-role"), shortNames: patternsOf(doc, "party-short-name"), companyForms: patternsOf(doc, "company-form") };
|
|
21
|
+
const parties = partiesOf(doc.source, terms, words);
|
|
22
|
+
if (parties.length === 0) return [];
|
|
23
|
+
// From the sentence that names the first party: a party named later (a subcontractor ("Recipient")) does not hide the role words before it.
|
|
24
|
+
const naming = parties.map((party) => sentenceAt(doc, party.end - 1));
|
|
25
|
+
const after = Math.min(...naming.map((sentence) => sentence.end));
|
|
26
|
+
const names = [...new Set(parties.map((party) => party.term))].join(NAME_SEPARATOR[doc.language] ?? ", ");
|
|
27
|
+
const texts = doc.sentences.map((sentence) => ({ start: sentence.span.start, text: sentence.text }));
|
|
28
|
+
return roleUses(
|
|
29
|
+
doc.source,
|
|
30
|
+
texts,
|
|
31
|
+
words.roles,
|
|
32
|
+
terms.map((term) => term.term),
|
|
33
|
+
after,
|
|
34
|
+
)
|
|
35
|
+
.filter((use) => !naming.some((sentence) => sentence.start <= use.offset && use.offset < sentence.end))
|
|
36
|
+
.map((use) => ({
|
|
37
|
+
rule: "party-role-name",
|
|
38
|
+
severity: "info",
|
|
39
|
+
line: 0,
|
|
40
|
+
column: 0,
|
|
41
|
+
quote: quoteAt(doc.source, use.offset),
|
|
42
|
+
values: { role: use.role, names, offset: use.offset },
|
|
43
|
+
}));
|
|
44
|
+
};
|