chaffjs 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/api.d.ts +14 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js.map +1 -1
- package/dist/cli-args.d.ts +2 -0
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +2 -0
- package/dist/cli-args.js.map +1 -1
- 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 +13 -19
- package/dist/cli.js.map +1 -1
- package/dist/commands/feedback.d.ts +3 -0
- package/dist/commands/feedback.d.ts.map +1 -1
- package/dist/commands/feedback.js.map +1 -1
- package/dist/commands/fix-plan.d.ts.map +1 -1
- package/dist/commands/fix-plan.js +5 -0
- package/dist/commands/fix-plan.js.map +1 -1
- package/dist/commands/rules.d.ts +9 -0
- package/dist/commands/rules.d.ts.map +1 -0
- package/dist/commands/rules.js +26 -0
- package/dist/commands/rules.js.map +1 -0
- package/dist/config/load.d.ts +6 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +1 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/style.js +1 -1
- package/dist/config/style.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/detectors/bare-comparative.d.ts +26 -0
- package/dist/detectors/bare-comparative.d.ts.map +1 -0
- package/dist/detectors/bare-comparative.js +117 -0
- package/dist/detectors/bare-comparative.js.map +1 -0
- package/dist/detectors/chat-citation.d.ts +6 -0
- package/dist/detectors/chat-citation.d.ts.map +1 -1
- package/dist/detectors/chat-citation.js +26 -1
- package/dist/detectors/chat-citation.js.map +1 -1
- package/dist/detectors/clause-shape.d.ts.map +1 -1
- package/dist/detectors/clause-shape.js +19 -5
- package/dist/detectors/clause-shape.js.map +1 -1
- package/dist/detectors/definition-use.d.ts.map +1 -1
- package/dist/detectors/definition-use.js +4 -2
- package/dist/detectors/definition-use.js.map +1 -1
- package/dist/detectors/ellipsis.d.ts +10 -0
- package/dist/detectors/ellipsis.d.ts.map +1 -0
- package/dist/detectors/ellipsis.js +56 -0
- package/dist/detectors/ellipsis.js.map +1 -0
- package/dist/detectors/generic-heading.d.ts +8 -0
- package/dist/detectors/generic-heading.d.ts.map +1 -0
- package/dist/detectors/generic-heading.js +26 -0
- package/dist/detectors/generic-heading.js.map +1 -0
- package/dist/detectors/ime-residue.d.ts +13 -0
- package/dist/detectors/ime-residue.d.ts.map +1 -0
- package/dist/detectors/ime-residue.js +27 -0
- package/dist/detectors/ime-residue.js.map +1 -0
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +9 -2
- package/dist/detectors/lexicon.js.map +1 -1
- package/dist/detectors/lookalike-character.d.ts +18 -0
- package/dist/detectors/lookalike-character.d.ts.map +1 -0
- package/dist/detectors/lookalike-character.js +50 -0
- package/dist/detectors/lookalike-character.js.map +1 -0
- package/dist/detectors/lowercase-name.d.ts +9 -0
- package/dist/detectors/lowercase-name.d.ts.map +1 -0
- package/dist/detectors/lowercase-name.js +40 -0
- package/dist/detectors/lowercase-name.js.map +1 -0
- package/dist/detectors/registry/bare-comparative.d.ts +3 -0
- package/dist/detectors/registry/bare-comparative.d.ts.map +1 -0
- package/dist/detectors/registry/bare-comparative.js +3 -0
- package/dist/detectors/registry/bare-comparative.js.map +1 -0
- package/dist/detectors/registry/calendar-name-case.d.ts +3 -0
- package/dist/detectors/registry/calendar-name-case.d.ts.map +1 -0
- package/dist/detectors/registry/calendar-name-case.js +3 -0
- package/dist/detectors/registry/calendar-name-case.js.map +1 -0
- package/dist/detectors/registry/ellipsis-consistency.d.ts +3 -0
- package/dist/detectors/registry/ellipsis-consistency.d.ts.map +1 -0
- package/dist/detectors/registry/ellipsis-consistency.js +3 -0
- package/dist/detectors/registry/ellipsis-consistency.js.map +1 -0
- package/dist/detectors/registry/generic-heading.d.ts +3 -0
- package/dist/detectors/registry/generic-heading.d.ts.map +1 -0
- package/dist/detectors/registry/generic-heading.js +3 -0
- package/dist/detectors/registry/generic-heading.js.map +1 -0
- package/dist/detectors/registry/ime-residue.d.ts +3 -0
- package/dist/detectors/registry/ime-residue.d.ts.map +1 -0
- package/dist/detectors/registry/ime-residue.js +3 -0
- package/dist/detectors/registry/ime-residue.js.map +1 -0
- package/dist/detectors/registry/lookalike-character.d.ts +3 -0
- package/dist/detectors/registry/lookalike-character.d.ts.map +1 -0
- package/dist/detectors/registry/lookalike-character.js +3 -0
- package/dist/detectors/registry/lookalike-character.js.map +1 -0
- package/dist/detectors/registry/unit-spacing-consistency.d.ts +3 -0
- package/dist/detectors/registry/unit-spacing-consistency.d.ts.map +1 -0
- package/dist/detectors/registry/unit-spacing-consistency.js +3 -0
- package/dist/detectors/registry/unit-spacing-consistency.js.map +1 -0
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +8 -4
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +32 -7
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/technical-docs.d.ts.map +1 -1
- package/dist/detectors/technical-docs.js +14 -0
- package/dist/detectors/technical-docs.js.map +1 -1
- package/dist/detectors/unit-spacing.d.ts +25 -0
- package/dist/detectors/unit-spacing.d.ts.map +1 -0
- package/dist/detectors/unit-spacing.js +50 -0
- package/dist/detectors/unit-spacing.js.map +1 -0
- package/dist/extension/load.d.ts +3 -0
- package/dist/extension/load.d.ts.map +1 -1
- package/dist/extension/load.js +1 -0
- package/dist/extension/load.js.map +1 -1
- package/dist/extension/pack-read.d.ts.map +1 -1
- package/dist/extension/pack-read.js +1 -0
- package/dist/extension/pack-read.js.map +1 -1
- package/dist/extension/plugin-load.js.map +1 -1
- package/dist/extension/plugin-parse.d.ts +7 -0
- package/dist/extension/plugin-parse.d.ts.map +1 -1
- package/dist/extension/plugin-parse.js +18 -2
- package/dist/extension/plugin-parse.js.map +1 -1
- package/dist/extension/problem-text.d.ts.map +1 -1
- package/dist/extension/problem-text.js +5 -0
- package/dist/extension/problem-text.js.map +1 -1
- package/dist/fix-plan/plan.d.ts +4 -0
- package/dist/fix-plan/plan.d.ts.map +1 -1
- package/dist/fix-plan/plan.js +1 -0
- package/dist/fix-plan/plan.js.map +1 -1
- package/dist/fix-plan/render.d.ts.map +1 -1
- package/dist/fix-plan/render.js +9 -0
- package/dist/fix-plan/render.js.map +1 -1
- package/dist/fix-plan/text.d.ts +4 -0
- package/dist/fix-plan/text.d.ts.map +1 -1
- package/dist/fix-plan/text.js +8 -2
- package/dist/fix-plan/text.js.map +1 -1
- package/dist/genre-guide/key-rules.d.ts +10 -0
- package/dist/genre-guide/key-rules.d.ts.map +1 -0
- package/dist/genre-guide/key-rules.js +23 -0
- package/dist/genre-guide/key-rules.js.map +1 -0
- package/dist/genre-guide/layer.d.ts +44 -0
- package/dist/genre-guide/layer.d.ts.map +1 -0
- package/dist/genre-guide/layer.js +43 -0
- package/dist/genre-guide/layer.js.map +1 -0
- package/dist/genre-guide/lines.d.ts +15 -0
- package/dist/genre-guide/lines.d.ts.map +1 -0
- package/dist/genre-guide/lines.js +39 -0
- package/dist/genre-guide/lines.js.map +1 -0
- package/dist/genre-guide/of-config.d.ts +13 -0
- package/dist/genre-guide/of-config.d.ts.map +1 -0
- package/dist/genre-guide/of-config.js +18 -0
- package/dist/genre-guide/of-config.js.map +1 -0
- package/dist/genre-guide/problem-text.d.ts +5 -0
- package/dist/genre-guide/problem-text.d.ts.map +1 -0
- package/dist/genre-guide/problem-text.js +29 -0
- package/dist/genre-guide/problem-text.js.map +1 -0
- package/dist/genre-guide/render.d.ts +5 -0
- package/dist/genre-guide/render.d.ts.map +1 -0
- package/dist/genre-guide/render.js +38 -0
- package/dist/genre-guide/render.js.map +1 -0
- package/dist/genre-guide/report.d.ts +32 -0
- package/dist/genre-guide/report.d.ts.map +1 -0
- package/dist/genre-guide/report.js +42 -0
- package/dist/genre-guide/report.js.map +1 -0
- package/dist/genre-guide/resolve.d.ts +16 -0
- package/dist/genre-guide/resolve.d.ts.map +1 -0
- package/dist/genre-guide/resolve.js +31 -0
- package/dist/genre-guide/resolve.js.map +1 -0
- package/dist/genre-parse.d.ts +5 -1
- package/dist/genre-parse.d.ts.map +1 -1
- package/dist/genre-parse.js +8 -1
- package/dist/genre-parse.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +14 -0
- package/dist/init.js.map +1 -1
- package/dist/quoted-span.d.ts +7 -0
- package/dist/quoted-span.d.ts.map +1 -1
- package/dist/quoted-span.js +20 -0
- package/dist/quoted-span.js.map +1 -1
- package/dist/render/rules-json.d.ts +2 -1
- package/dist/render/rules-json.d.ts.map +1 -1
- package/dist/render/rules-json.js +3 -1
- package/dist/render/rules-json.js.map +1 -1
- package/dist/render/sarif.d.ts +3 -1
- package/dist/render/sarif.d.ts.map +1 -1
- package/dist/render/sarif.js +3 -1
- package/dist/render/sarif.js.map +1 -1
- package/dist/structure/definition-use.d.ts +14 -4
- package/dist/structure/definition-use.d.ts.map +1 -1
- package/dist/structure/definition-use.js +29 -9
- package/dist/structure/definition-use.js.map +1 -1
- package/dist/style-parse.d.ts +3 -0
- package/dist/style-parse.d.ts.map +1 -1
- package/dist/style-parse.js +18 -2
- package/dist/style-parse.js.map +1 -1
- package/genres.yaml +329 -0
- package/package.json +3 -3
- package/rules/calendar-name-case.yaml +55 -0
- package/rules/comparative-without-baseline.yaml +71 -0
- package/rules/cross-doc-broken-link.yaml +1 -1
- package/rules/cross-doc-duplicate-definition.yaml +1 -1
- package/rules/cross-doc-fact-conflict.yaml +1 -1
- package/rules/cross-doc-term-variant.yaml +5 -3
- package/rules/ellipsis-consistency.yaml +73 -0
- package/rules/figurative-density.yaml +84 -0
- package/rules/ime-residue.yaml +55 -0
- package/rules/lookalike-character.yaml +82 -0
- package/rules/numbered-generic-heading.yaml +111 -0
- package/rules/undefined-acronym.yaml +1 -0
- package/rules/unit-spacing-consistency.yaml +74 -0
- package/skills/chaff/SKILL.md +6 -0
- package/src/api.ts +13 -0
- package/src/cli-args.ts +3 -0
- package/src/cli-text.ts +2 -0
- package/src/cli.ts +16 -20
- package/src/commands/feedback.ts +3 -0
- package/src/commands/fix-plan.ts +8 -0
- package/src/commands/rules.ts +34 -0
- package/src/config/load.ts +7 -1
- package/src/config/style.ts +1 -1
- package/src/config/warnings.ts +3 -1
- package/src/detectors/bare-comparative.ts +152 -0
- package/src/detectors/chat-citation.ts +28 -1
- package/src/detectors/clause-shape.ts +20 -6
- package/src/detectors/definition-use.ts +13 -3
- package/src/detectors/ellipsis.ts +74 -0
- package/src/detectors/generic-heading.ts +30 -0
- package/src/detectors/ime-residue.ts +37 -0
- package/src/detectors/lexicon.ts +11 -2
- package/src/detectors/lookalike-character.ts +75 -0
- package/src/detectors/lowercase-name.ts +58 -0
- package/src/detectors/registry/bare-comparative.ts +4 -0
- package/src/detectors/registry/calendar-name-case.ts +4 -0
- package/src/detectors/registry/ellipsis-consistency.ts +4 -0
- package/src/detectors/registry/generic-heading.ts +4 -0
- package/src/detectors/registry/ime-residue.ts +4 -0
- package/src/detectors/registry/lookalike-character.ts +4 -0
- package/src/detectors/registry/unit-spacing-consistency.ts +4 -0
- package/src/detectors/signals.ts +9 -5
- package/src/detectors/structure-tree.ts +55 -30
- package/src/detectors/technical-docs.ts +13 -0
- package/src/detectors/unit-spacing.ts +71 -0
- package/src/extension/load.ts +4 -0
- package/src/extension/pack-read.ts +1 -0
- package/src/extension/plugin-load.ts +1 -1
- package/src/extension/plugin-parse.ts +27 -4
- package/src/extension/problem-text.ts +7 -1
- package/src/fix-plan/plan.ts +5 -0
- package/src/fix-plan/render.ts +9 -0
- package/src/fix-plan/text.ts +12 -2
- package/src/genre-guide/key-rules.ts +37 -0
- package/src/genre-guide/layer.ts +77 -0
- package/src/genre-guide/lines.ts +41 -0
- package/src/genre-guide/of-config.ts +26 -0
- package/src/genre-guide/problem-text.ts +34 -0
- package/src/genre-guide/render.ts +50 -0
- package/src/genre-guide/report.ts +63 -0
- package/src/genre-guide/resolve.ts +36 -0
- package/src/genre-parse.ts +13 -3
- package/src/init.ts +14 -0
- package/src/quoted-span.ts +25 -0
- package/src/render/rules-json.ts +4 -0
- package/src/render/sarif.ts +4 -1
- package/src/structure/definition-use.ts +42 -10
- package/src/style-parse.ts +19 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Detector, Finding } from "../plugin.ts";
|
|
2
|
+
import { quoteAround } from "./quote-around.ts";
|
|
3
|
+
|
|
4
|
+
// A single Latin letter left inside a Japanese word by the input method (おもしrおい, 対応sた): a key pressed and not converted.
|
|
5
|
+
|
|
6
|
+
/** One stray letter, and where it is in the text. */
|
|
7
|
+
export type StrayLetter = { readonly offset: number; readonly letter: string };
|
|
8
|
+
|
|
9
|
+
// Hiragana or kanji before (after katakana a letter is a label: サイズm, プランb), kana after.
|
|
10
|
+
const STRAY = /(?<=[\p{Script=Hiragana}\p{Script=Han}々])[a-z](?=[\p{Script=Hiragana}\p{Script=Katakana}ー])/gu;
|
|
11
|
+
/** Marks that make the run around the letter a path, an address or an assignment (/docs/あaい, a=b). */
|
|
12
|
+
const PATH_MARK = /[/\\=@#]/u;
|
|
13
|
+
|
|
14
|
+
const runBefore = (text: string, offset: number): string => text.slice(0, offset).split(/\s/u).at(-1) ?? "";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One lower-case letter between Japanese: hiragana or kanji before, kana after. A letter followed by a particle names
|
|
18
|
+
* something (変数xが, まずxを, をxで), so it is left alone; the particles come from the lexicon.
|
|
19
|
+
*/
|
|
20
|
+
export const strayLettersIn = (text: string, particles: readonly string[]): StrayLetter[] =>
|
|
21
|
+
[...text.matchAll(STRAY)].flatMap((match) => {
|
|
22
|
+
const namesSomething = particles.some((particle) => text.startsWith(particle, match.index + 1));
|
|
23
|
+
return namesSomething || PATH_MARK.test(runBefore(text, match.index)) ? [] : [{ offset: match.index, letter: match[0] }];
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const imeResidue: Detector = (doc): Finding[] => {
|
|
27
|
+
const text = doc.prose ?? doc.source;
|
|
28
|
+
const particles = (doc.lexicons["case-particle"] ?? []).map((entry) => entry.pattern);
|
|
29
|
+
return strayLettersIn(text, particles).map((stray) => ({
|
|
30
|
+
rule: "",
|
|
31
|
+
severity: "warning",
|
|
32
|
+
line: 0,
|
|
33
|
+
column: 0,
|
|
34
|
+
quote: quoteAround(text, stray.offset, stray.offset + 1),
|
|
35
|
+
values: { letter: stray.letter, offset: stray.offset },
|
|
36
|
+
}));
|
|
37
|
+
};
|
package/src/detectors/lexicon.ts
CHANGED
|
@@ -212,15 +212,24 @@ export const repeatedConjunction: Detector = (doc, options): Finding[] => {
|
|
|
212
212
|
});
|
|
213
213
|
};
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* 書き手が書いた語。引用符の中だけの語(words such as "delves" and "pivotal")は、例に挙げたか人の言葉を引いたもの。
|
|
217
|
+
* 品詞があれば当たった語の並びごとに見る(活用した形も原形で当たる)。無ければ字で照らす。
|
|
218
|
+
*/
|
|
219
|
+
const writtenIn = (sentence: Sentence, entry: LexiconEntry): boolean =>
|
|
220
|
+
sentence.tokens === undefined || entry.tokens === undefined || entry.tokens.length === 0
|
|
221
|
+
? entryIn(sentence, entry) && !onlyQuoted(sentence, entry)
|
|
222
|
+
: entryRanges(sentence, entry).some((range) => !quotedRange(sentence, range));
|
|
223
|
+
|
|
215
224
|
/**
|
|
216
225
|
* AI 生成の signal。1 つでは何も言えないので、重みを足し合わせて文書の点にする。
|
|
217
226
|
* 単独で断じない。§20.2 の複合シグナルの入口で、これだけで「AI が書いた」とは言わない。
|
|
218
227
|
*/
|
|
219
228
|
export const aiTell: Detector = (doc, options): Finding[] => {
|
|
220
229
|
const entries = [...new Map((options.lexicon ?? []).map((entry) => [entry.pattern.toLowerCase(), entry])).values()];
|
|
221
|
-
const found = entries.filter((entry) => doc.sentences.some((sentence) =>
|
|
230
|
+
const found = entries.filter((entry) => doc.sentences.some((sentence) => writtenIn(sentence, entry)));
|
|
222
231
|
const score = Math.round(found.reduce((sum, entry) => sum + (entry.weight ?? 1), 0) * 10);
|
|
223
|
-
const first = doc.sentences.find((sentence) => found.some((entry) =>
|
|
232
|
+
const first = doc.sentences.find((sentence) => found.some((entry) => writtenIn(sentence, entry)));
|
|
224
233
|
if (first === undefined || score <= options.limit) return [];
|
|
225
234
|
return [
|
|
226
235
|
{
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Detector, Finding, Span } from "../plugin.ts";
|
|
2
|
+
import { codePointName } from "./invisible-character.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A character that looks like a common one but is another code point, so search, sorting and a dictionary miss it.
|
|
6
|
+
* radical: a Kangxi or supplementary radical (⼈ U+2F08 for 人), from PDF text or an input method's radical list.
|
|
7
|
+
* compatibility: a CJK compatibility ideograph (or one of its supplement) that Unicode maps to a unified one (樂 U+F914 for 樂).
|
|
8
|
+
* decomposed: a letter and a separate combining mark (か + U+3099 for が, e + U+0301 for é), from file names or text
|
|
9
|
+
* copied on macOS. spacing-mark: kana followed by the spacing ゛ or ゜, typed in place of the combined letter.
|
|
10
|
+
*/
|
|
11
|
+
export type LookalikeKind = "radical" | "compatibility" | "decomposed" | "spacing-mark";
|
|
12
|
+
|
|
13
|
+
export type Lookalike = { readonly span: Span; readonly kind: LookalikeKind; readonly written: string; readonly usual: string };
|
|
14
|
+
|
|
15
|
+
const RADICAL = /[\u2E80-\u2EFF\u2F00-\u2FDF]/u;
|
|
16
|
+
const COMPATIBILITY = /[\uF900-\uFAFF\u{2F800}-\u{2FA1F}]/u;
|
|
17
|
+
/** A letter or mark base and the combining marks after it. */
|
|
18
|
+
const WITH_MARKS = /\P{M}\p{M}+/gu;
|
|
19
|
+
/** Kana followed by the spacing voiced or semi-voiced mark (か゛, は゜). */
|
|
20
|
+
const SPACING_MARK = /[\p{Script=Hiragana}\p{Script=Katakana}][゛゜]/gu;
|
|
21
|
+
const COMBINING_OF: Readonly<Record<string, string>> = { "゛": "゙", "゜": "゚" };
|
|
22
|
+
const SINGLE_CHAR = /[\u2E80-\u2EFF\u2F00-\u2FDF\uF900-\uFAFF\u{2F800}-\u{2FA1F}]/gu;
|
|
23
|
+
|
|
24
|
+
/** A radical or compatibility ideograph, with the unified ideograph it stands for. One that maps to nothing is left alone. */
|
|
25
|
+
const singleLookalikes = (source: string): Lookalike[] =>
|
|
26
|
+
[...source.matchAll(SINGLE_CHAR)].flatMap((match) => {
|
|
27
|
+
const written = match[0];
|
|
28
|
+
const radical = RADICAL.test(written);
|
|
29
|
+
const usual = written.normalize(radical ? "NFKC" : "NFC");
|
|
30
|
+
if (usual === written || (!radical && !COMPATIBILITY.test(written))) return [];
|
|
31
|
+
return [{ span: { start: match.index, end: match.index + written.length }, kind: radical ? "radical" : "compatibility", written, usual }];
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** A base and combining marks that compose into one precomposed letter. Marks with no precomposed form are writing, not a slip. */
|
|
35
|
+
const decomposedLookalikes = (source: string): Lookalike[] =>
|
|
36
|
+
[...source.matchAll(WITH_MARKS)].flatMap((match) => {
|
|
37
|
+
const usual = match[0].normalize("NFC");
|
|
38
|
+
if (Array.from(usual).length >= Array.from(match[0]).length) return [];
|
|
39
|
+
return [{ span: { start: match.index, end: match.index + match[0].length }, kind: "decomposed", written: match[0], usual }];
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** Kana and a spacing ゛ or ゜ whose combining form makes one letter (か゛ → が). */
|
|
43
|
+
const spacingMarkLookalikes = (source: string): Lookalike[] =>
|
|
44
|
+
[...source.matchAll(SPACING_MARK)].flatMap((match) => {
|
|
45
|
+
const [kana = "", mark = ""] = Array.from(match[0]);
|
|
46
|
+
const usual = `${kana}${COMBINING_OF[mark] ?? ""}`.normalize("NFC");
|
|
47
|
+
if (Array.from(usual).length !== 1) return [];
|
|
48
|
+
return [{ span: { start: match.index, end: match.index + match[0].length }, kind: "spacing-mark", written: match[0], usual }];
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const lookalikesIn = (source: string): Lookalike[] =>
|
|
52
|
+
[...singleLookalikes(source), ...decomposedLookalikes(source), ...spacingMarkLookalikes(source)].toSorted(
|
|
53
|
+
(left, right) => left.span.start - right.span.start,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const QUOTE_REACH = 20;
|
|
57
|
+
|
|
58
|
+
const quoteOf = (source: string, span: Span): string =>
|
|
59
|
+
source
|
|
60
|
+
.slice(Math.max(0, span.start - QUOTE_REACH), span.end + QUOTE_REACH)
|
|
61
|
+
.replace(/\s+/gu, " ")
|
|
62
|
+
.trim();
|
|
63
|
+
|
|
64
|
+
const codesOf = (text: string): string => Array.from(text).map(codePointName).join(" ");
|
|
65
|
+
|
|
66
|
+
export const lookalikeCharacter: Detector = (doc): Finding[] =>
|
|
67
|
+
lookalikesIn(doc.source).map((lookalike) => ({
|
|
68
|
+
rule: "",
|
|
69
|
+
severity: "warning",
|
|
70
|
+
line: 0,
|
|
71
|
+
column: 0,
|
|
72
|
+
quote: quoteOf(doc.source, lookalike.span),
|
|
73
|
+
values: { written: lookalike.written, usual: lookalike.usual, code: codesOf(lookalike.written), offset: lookalike.span.start },
|
|
74
|
+
variant: lookalike.kind,
|
|
75
|
+
}));
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Detector, Finding } from "../plugin.ts";
|
|
2
|
+
import { quoteAround } from "./quote-around.ts";
|
|
3
|
+
|
|
4
|
+
// A word English always capitalises (a weekday, a month) written in lower case ("on monday"). The words come from the rule's
|
|
5
|
+
// lexicon "calendar-name"; the pattern is the capitalised form.
|
|
6
|
+
|
|
7
|
+
export type LowercaseName = { readonly offset: number; readonly written: string; readonly usual: string };
|
|
8
|
+
|
|
9
|
+
/** Characters that make the word part of a name in code, a path, an address or a tag (monday.md, /july/, @friday, #june). */
|
|
10
|
+
const NOT_PROSE_BESIDE = /[\p{L}\p{N}_/.@#:=-]/u;
|
|
11
|
+
|
|
12
|
+
/** A period or colon ends the sentence or clause unless a name continues after it (monday.md, june:2). */
|
|
13
|
+
const SENTENCE_STOP = /[.:]/u;
|
|
14
|
+
const WORD_CHAR = /[\p{L}\p{N}_]/u;
|
|
15
|
+
|
|
16
|
+
const joinsAfter = (text: string, end: number): boolean =>
|
|
17
|
+
SENTENCE_STOP.test(text.charAt(end)) ? WORD_CHAR.test(text.charAt(end + 1)) : NOT_PROSE_BESIDE.test(text.charAt(end));
|
|
18
|
+
|
|
19
|
+
/** A value after a key or a field marker (day: monday, day = monday, "day": "monday", * * monday) is data, not prose. */
|
|
20
|
+
const VALUE_MARK = /[:=*]/u;
|
|
21
|
+
const STRAIGHT_QUOTE = /["']/u;
|
|
22
|
+
const SPACE_OR_QUOTE = /[\s"']/u;
|
|
23
|
+
/** How far back to look past spaces and quotes for the key's mark. */
|
|
24
|
+
const VALUE_REACH = 8;
|
|
25
|
+
|
|
26
|
+
const markBefore = (text: string, start: number): string =>
|
|
27
|
+
Array.from(text.slice(Math.max(0, start - VALUE_REACH), start))
|
|
28
|
+
.reverse()
|
|
29
|
+
.find((char) => !SPACE_OR_QUOTE.test(char)) ?? "";
|
|
30
|
+
|
|
31
|
+
const isValue = (text: string, start: number): boolean => STRAIGHT_QUOTE.test(text.charAt(start - 1)) || VALUE_MARK.test(markBefore(text, start));
|
|
32
|
+
|
|
33
|
+
const capitalOf = (word: string): string => `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
|
|
34
|
+
|
|
35
|
+
export const lowercaseNamesIn = (text: string, names: readonly string[]): LowercaseName[] => {
|
|
36
|
+
const lower = new Map(names.map((name) => [name.toLowerCase(), capitalOf(name)]));
|
|
37
|
+
return [...text.matchAll(/\p{Ll}+/gu)].flatMap((match) => {
|
|
38
|
+
const usual = lower.get(match[0]);
|
|
39
|
+
const end = match.index + match[0].length;
|
|
40
|
+
if (usual === undefined || NOT_PROSE_BESIDE.test(text.charAt(match.index - 1)) || isValue(text, match.index) || joinsAfter(text, end)) return [];
|
|
41
|
+
return [{ offset: match.index, written: match[0], usual }];
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const lowercaseName: Detector = (doc): Finding[] => {
|
|
46
|
+
const text = doc.prose ?? doc.source;
|
|
47
|
+
return lowercaseNamesIn(
|
|
48
|
+
text,
|
|
49
|
+
(doc.lexicons["calendar-name"] ?? []).map((entry) => entry.pattern),
|
|
50
|
+
).map((name) => ({
|
|
51
|
+
rule: "",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
line: 0,
|
|
54
|
+
column: 0,
|
|
55
|
+
quote: quoteAround(text, name.offset, name.offset + name.written.length),
|
|
56
|
+
values: { written: name.written, usual: name.usual, offset: name.offset },
|
|
57
|
+
}));
|
|
58
|
+
};
|
package/src/detectors/signals.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { evidenceSpans, hasNumeral, startsWithin } from "./concrete-evidence.ts"
|
|
|
11
11
|
import { letteredIndexEntries } from "./lettered-index.ts";
|
|
12
12
|
import { hasPredicateIn } from "./gram-predicate.ts";
|
|
13
13
|
import { nameSpans, touchesAny } from "../team-names.ts";
|
|
14
|
+
import { referenceListSpans } from "../reference-lists.ts";
|
|
14
15
|
import type { Detector, Finding, ProseDocument, Section, Sentence } from "../plugin.ts";
|
|
15
16
|
|
|
16
17
|
const PER = 1000;
|
|
@@ -250,9 +251,12 @@ const firstHits = (hits: readonly AcronymHit[]): Map<string, Hit> => {
|
|
|
250
251
|
return seen;
|
|
251
252
|
};
|
|
252
253
|
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
/**
|
|
255
|
+
* 範囲の外の略語。日付の中の月(SEP 01, 2022)は略語ではない。文献一覧の誌名や会議名(Appeared in: LREC 2008.)は
|
|
256
|
+
* 引いた文献の書き方で、本文が説明する略語ではない。
|
|
257
|
+
*/
|
|
258
|
+
const outsideSpans = (hits: readonly AcronymHit[], spans: readonly Span[]): AcronymHit[] =>
|
|
259
|
+
hits.filter(({ hit }) => !spans.some((span) => span.start <= hit.offset && hit.offset < span.end));
|
|
256
260
|
|
|
257
261
|
/**
|
|
258
262
|
* どこか 1 か所で展開してあればよい。初出が節の見出し代わりの語(「5.3. DPA.」)で、
|
|
@@ -300,11 +304,11 @@ export const undefinedAcronym: Detector = (doc, options): Finding[] => {
|
|
|
300
304
|
const headings = doc.sections.map((section) => section.heading);
|
|
301
305
|
const explainedAlone = (word: string): boolean =>
|
|
302
306
|
common.has(word) || entries.has(word) || isExpanded(body, word, expandedAt) || headings.some((heading) => expandsInHeading(heading, word, expandedAt));
|
|
303
|
-
const hits = acronymsOf(doc, notationOf(doc));
|
|
307
|
+
const hits = outsideSpans(acronymsOf(doc, notationOf(doc)), referenceListSpans(doc.source, patternsOf(doc, "reference-list-heading")));
|
|
304
308
|
const unexplained = new Set([...firstHits(hits).keys()].filter((acronym) => !isExplained(acronym, explainedAlone)));
|
|
305
309
|
// 日付を読むには文書の木を作る。上限に届かない文書では作らない。
|
|
306
310
|
if (unexplained.size < options.limit) return [];
|
|
307
|
-
const bare = [...firstHits(
|
|
311
|
+
const bare = [...firstHits(outsideSpans(hits, dateSpans(doc)))].filter(([acronym]) => unexplained.has(acronym));
|
|
308
312
|
if (bare.length < options.limit) return [];
|
|
309
313
|
return bare.map(([acronym, hit]) => ({
|
|
310
314
|
rule: "undefined-acronym",
|
|
@@ -6,6 +6,7 @@ import { dateOrderBreaks } from "../structure/date-order.ts";
|
|
|
6
6
|
import { totalMismatches, type Amount } from "../structure/total.ts";
|
|
7
7
|
import { rangeFrameOf, reversedRanges, type DatedSpan, type RangeWords } from "../structure/date-range.ts";
|
|
8
8
|
import { percentSumMismatches, type ShareWords } from "../structure/percent-sum.ts";
|
|
9
|
+
import { isQuotedAlone } from "../quoted-span.ts";
|
|
9
10
|
|
|
10
11
|
const QUOTE_LENGTH = 80;
|
|
11
12
|
|
|
@@ -42,22 +43,43 @@ const dayName = (doc: ProseDocument, day: unknown): string => {
|
|
|
42
43
|
return doc.lexicons["weekday"]?.[index]?.pattern ?? String(index);
|
|
43
44
|
};
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
const datedSpans = (tree: NonNullable<ProseDocument["structure"]>): DatedSpan[] =>
|
|
47
|
+
inDocumentOrder(tree).flatMap((node) => (node.kind === "date" ? [{ offset: node.span.start, end: node.span.end, value: String(node.attrs["value"]) }] : []));
|
|
48
|
+
|
|
49
|
+
/** 引用符の中で日付の横にあってよい語は、曜日の一語だけ("Monday, December 5, 2026"、「2026年12月5日(月)」)。 */
|
|
50
|
+
const WEEKDAY_WORDS = 1;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 引用符で日付だけを挙げたもの("Monday, December 5, 2026" falls on a Saturday)は、誤りの例で、文書の日付ではない。
|
|
54
|
+
* from から until の日付の終わりまでを見る。
|
|
55
|
+
*/
|
|
56
|
+
const quotedDates = (doc: ProseDocument, from: DatedSpan | undefined, until: DatedSpan | undefined, wordsBesides = 0): boolean =>
|
|
57
|
+
from !== undefined && until !== undefined && isQuotedAlone(doc.source, { start: from.offset, end: until.end }, wordsBesides);
|
|
58
|
+
|
|
59
|
+
const dateAt = (dates: readonly DatedSpan[], offset: number): number => dates.findIndex((date) => date.offset === offset);
|
|
60
|
+
|
|
61
|
+
export const dateWeekdayMismatch: Detector = (doc): Finding[] => {
|
|
62
|
+
if (doc.structure === undefined) return [];
|
|
63
|
+
const dates = datedSpans(doc.structure);
|
|
64
|
+
return weekdayMismatches(doc.structure)
|
|
65
|
+
.filter((issue) => {
|
|
66
|
+
const date = dates[dateAt(dates, issue.offset)];
|
|
67
|
+
return !quotedDates(doc, date, date, WEEKDAY_WORDS);
|
|
68
|
+
})
|
|
69
|
+
.map((issue) => ({
|
|
70
|
+
rule: "date-weekday-mismatch",
|
|
71
|
+
severity: "error",
|
|
72
|
+
line: 0,
|
|
73
|
+
column: 0,
|
|
74
|
+
quote: quoteAt(doc.source, issue.offset),
|
|
75
|
+
values: {
|
|
76
|
+
date: String(issue.values["date"]),
|
|
77
|
+
written: dayName(doc, issue.values["written"]),
|
|
78
|
+
actual: dayName(doc, issue.values["actual"]),
|
|
79
|
+
offset: issue.offset,
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
82
|
+
};
|
|
61
83
|
|
|
62
84
|
/** 木の日付を、原文の位置と一緒に並べる。 */
|
|
63
85
|
const datedPoints = (tree: NonNullable<ProseDocument["structure"]>): { offset: number; value: string }[] =>
|
|
@@ -99,9 +121,6 @@ export const totalMismatch: Detector = (doc): Finding[] =>
|
|
|
99
121
|
values: { ...issue.values, offset: issue.offset },
|
|
100
122
|
}));
|
|
101
123
|
|
|
102
|
-
const datedSpans = (tree: NonNullable<ProseDocument["structure"]>): DatedSpan[] =>
|
|
103
|
-
inDocumentOrder(tree).flatMap((node) => (node.kind === "date" ? [{ offset: node.span.start, end: node.span.end, value: String(node.attrs["value"]) }] : []));
|
|
104
|
-
|
|
105
124
|
const rangeWordsOf = (doc: ProseDocument): RangeWords => ({
|
|
106
125
|
connectors: (doc.lexicons["range-connector"] ?? []).map((entry) => entry.pattern),
|
|
107
126
|
openers: (doc.lexicons["range-opener"] ?? []).map((entry) => entry.pattern),
|
|
@@ -111,17 +130,23 @@ const rangeWordsOf = (doc: ProseDocument): RangeWords => ({
|
|
|
111
130
|
});
|
|
112
131
|
|
|
113
132
|
/** 期間の終わりが始まりより前。範囲の記号と語は言語パッケージの語彙表(range-connector、range-opener、range-closer)から取る。 */
|
|
114
|
-
export const dateRangeReversed: Detector = (doc): Finding[] =>
|
|
115
|
-
doc.structure === undefined
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
export const dateRangeReversed: Detector = (doc): Finding[] => {
|
|
134
|
+
if (doc.structure === undefined) return [];
|
|
135
|
+
const dates = datedSpans(doc.structure);
|
|
136
|
+
return reversedRanges(doc.source, dates, rangeWordsOf(doc))
|
|
137
|
+
.filter((issue) => {
|
|
138
|
+
const start = dateAt(dates, issue.offset);
|
|
139
|
+
return !quotedDates(doc, dates[start], dates[start + 1]);
|
|
140
|
+
})
|
|
141
|
+
.map((issue) => ({
|
|
142
|
+
rule: "date-range-reversed",
|
|
143
|
+
severity: "warning",
|
|
144
|
+
line: 0,
|
|
145
|
+
column: 0,
|
|
146
|
+
quote: quoteAt(doc.source, issue.offset),
|
|
147
|
+
values: { ...issue.values, offset: issue.offset },
|
|
148
|
+
}));
|
|
149
|
+
};
|
|
125
150
|
|
|
126
151
|
const shareWordsOf = (doc: ProseDocument): ShareWords => ({
|
|
127
152
|
labels: (doc.lexicons["share-label"] ?? []).map((entry) => entry.pattern),
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// shell-prompt lexicons.
|
|
5
5
|
import { codeFences, codeSpans, type CodeFence } from "./code-fences.ts";
|
|
6
6
|
import { quoteAt } from "./structure-tree.ts";
|
|
7
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
7
8
|
import type { Detector, Finding, ProseDocument, Span, Token } from "../plugin.ts";
|
|
8
9
|
|
|
9
10
|
const patternsOf = (doc: ProseDocument, id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
|
|
@@ -112,10 +113,22 @@ export const commandsIn = (heading: string): string[] => {
|
|
|
112
113
|
const collapse = (text: string): string => text.replace(/\s+/gu, " ");
|
|
113
114
|
const OPTION_CHAR = /[A-Za-z0-9_-]/u;
|
|
114
115
|
|
|
116
|
+
/**
|
|
117
|
+
* A command with arguments whose program the code runs under its package's name: chaff grade as npx chaffjs grade,
|
|
118
|
+
* python -m venv as python3 -m venv. The arguments must follow as written.
|
|
119
|
+
*/
|
|
120
|
+
const showsUnderPackageName = (text: string, wanted: string): boolean => {
|
|
121
|
+
const [program = "", ...rest] = wanted.split(" ");
|
|
122
|
+
if (rest.length === 0) return false;
|
|
123
|
+
const pattern = `(?<![A-Za-z0-9_.-])${escapeRegExp(program)}[A-Za-z0-9_.-]* ${rest.map(escapeRegExp).join(" ")}(?![A-Za-z0-9_-])`;
|
|
124
|
+
return new RegExp(pattern, "u").test(text);
|
|
125
|
+
};
|
|
126
|
+
|
|
115
127
|
/** Whether the code holds the command as a whole word: --force is not --force-with-lease, init is not initial. */
|
|
116
128
|
export const showsCommand = (code: string, command: string): boolean => {
|
|
117
129
|
const text = collapse(code);
|
|
118
130
|
const wanted = collapse(command.trim());
|
|
131
|
+
if (showsUnderPackageName(text, wanted)) return true;
|
|
119
132
|
const state = { at: text.indexOf(wanted) };
|
|
120
133
|
while (state.at !== -1) {
|
|
121
134
|
const before = text.charAt(state.at - 1);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Detector, Finding } from "../plugin.ts";
|
|
2
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
3
|
+
import { formMinority } from "./form-minority.ts";
|
|
4
|
+
import { quoteAround } from "./quote-around.ts";
|
|
5
|
+
|
|
6
|
+
// One document writing a number and a unit symbol both with a space between them ("5 GB") and without ("5GB"). Neither is
|
|
7
|
+
// called right; per unit, the less common way is reported. The symbols come from each language's lexicon "unit-symbol".
|
|
8
|
+
|
|
9
|
+
/** A number and its unit as written. form is "spaced" or "touching". */
|
|
10
|
+
export type WrittenQuantity = { readonly offset: number; readonly written: string; readonly unit: string; readonly form: string };
|
|
11
|
+
|
|
12
|
+
/** The less common way for one unit, with an example of the usual way and how many quantities of that unit there are. */
|
|
13
|
+
export type UnitMinority = { readonly odd: WrittenQuantity; readonly usual: string; readonly count: number; readonly of: number };
|
|
14
|
+
|
|
15
|
+
// Grouping commas, then at most one decimal point: 1.2.3 is a version, not a number.
|
|
16
|
+
const NUMBER = "[0-9]+(?:,[0-9]{3})*(?:\\.[0-9]+)?";
|
|
17
|
+
// A plain space, a no-break space or a narrow no-break space (the SI brochure's thin space between number and unit).
|
|
18
|
+
const GAP = "[ \\u00A0\\u202F]?";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Numbers followed by a unit symbol, at most one space between. The number must not be part of a word, a version or an
|
|
22
|
+
* address (v1.2GB, x86, 1.2.3GB), nor follow a hyphen: "10-8 cm" is as often a lost exponent as a range (5–10 GB, with
|
|
23
|
+
* an en dash, is read). The symbol must not run on into a word, a rate or a file name (5 GBit, 5 GB/s, 5GB.zip).
|
|
24
|
+
*/
|
|
25
|
+
export const quantitiesIn = (text: string, symbols: readonly string[]): WrittenQuantity[] => {
|
|
26
|
+
if (symbols.length === 0) return [];
|
|
27
|
+
const units = symbols
|
|
28
|
+
.toSorted((left, right) => right.length - left.length)
|
|
29
|
+
.map(escapeRegExp)
|
|
30
|
+
.join("|");
|
|
31
|
+
const pattern = new RegExp(`(?<![A-Za-z0-9_.,/:#-])${NUMBER}(?<gap>${GAP})(?<unit>${units})(?![A-Za-z0-9_/]|\\.[A-Za-z0-9_])`, "gu");
|
|
32
|
+
return [...text.matchAll(pattern)].map((match) => ({
|
|
33
|
+
offset: match.index,
|
|
34
|
+
written: match[0],
|
|
35
|
+
unit: match.groups?.["unit"] ?? "",
|
|
36
|
+
form: match.groups?.["gap"] === "" ? "touching" : "spaced",
|
|
37
|
+
}));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const minorityOf = (ofUnit: readonly WrittenQuantity[], limit: number): UnitMinority[] => {
|
|
41
|
+
const minority = formMinority(ofUnit, limit);
|
|
42
|
+
if (minority === undefined) return [];
|
|
43
|
+
return minority.odd.map((quantity) => ({ odd: quantity, usual: minority.usual.written, count: minority.odd.length, of: ofUnit.length }));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Per unit, the quantities written the less common way. "5 GB" and "500ms" in one document are two units, each consistent. */
|
|
47
|
+
export const unitMinorities = (quantities: readonly WrittenQuantity[], limit: number): UnitMinority[] =>
|
|
48
|
+
[...new Set(quantities.map((quantity) => quantity.unit))]
|
|
49
|
+
.flatMap((unit) =>
|
|
50
|
+
minorityOf(
|
|
51
|
+
quantities.filter((quantity) => quantity.unit === unit),
|
|
52
|
+
limit,
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
.toSorted((left, right) => left.odd.offset - right.odd.offset);
|
|
56
|
+
|
|
57
|
+
export const unitSpacing: Detector = (doc, options): Finding[] => {
|
|
58
|
+
const text = doc.prose ?? doc.source;
|
|
59
|
+
const quantities = quantitiesIn(
|
|
60
|
+
text,
|
|
61
|
+
(doc.lexicons["unit-symbol"] ?? []).map((entry) => entry.pattern),
|
|
62
|
+
);
|
|
63
|
+
return unitMinorities(quantities, options.limit).map(({ odd, usual, count, of }) => ({
|
|
64
|
+
rule: "",
|
|
65
|
+
severity: "info",
|
|
66
|
+
line: 0,
|
|
67
|
+
column: 0,
|
|
68
|
+
quote: quoteAround(text, odd.offset, odd.offset + odd.written.length),
|
|
69
|
+
values: { written: odd.written, usual, count, of, offset: odd.offset },
|
|
70
|
+
}));
|
|
71
|
+
};
|
package/src/extension/load.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import type { Config } from "../config/load.ts";
|
|
3
3
|
import type { Detector, Lexicon, RuleDefinition } from "../plugin.ts";
|
|
4
4
|
import type { StyleDefinition } from "../style-parse.ts";
|
|
5
|
+
import type { GuideLayer } from "../genre-guide/layer.ts";
|
|
5
6
|
import { customRulesOf } from "../custom/load.ts";
|
|
6
7
|
import { withStyle } from "../config/style.ts";
|
|
7
8
|
import { moduleDetector } from "./module-detector.ts";
|
|
@@ -33,6 +34,8 @@ export type Extensions = {
|
|
|
33
34
|
readonly lexicons: Readonly<Record<string, Readonly<Record<string, Lexicon>>>>;
|
|
34
35
|
/** The plugins' house styles. */
|
|
35
36
|
readonly styles: readonly StyleDefinition[];
|
|
37
|
+
/** The plugins' changes to the genre guides, in the order chaff.yaml lists the plugins. */
|
|
38
|
+
readonly guides?: readonly GuideLayer[];
|
|
36
39
|
readonly problems: readonly LoadProblem[];
|
|
37
40
|
readonly pluginProblems: readonly PluginProblem[];
|
|
38
41
|
};
|
|
@@ -85,6 +88,7 @@ const loadExtensions = async (config: Config): Promise<Extensions> => {
|
|
|
85
88
|
rules: plugins.flatMap((plugin) => plugin.rules),
|
|
86
89
|
lexicons: lexiconsByLanguage(plugins),
|
|
87
90
|
styles: plugins.flatMap((plugin) => plugin.styles),
|
|
91
|
+
guides: plugins.flatMap((plugin) => (plugin.guide === undefined ? [] : [plugin.guide])),
|
|
88
92
|
problems: modules.flatMap((entry) => ("problem" in entry.loaded ? [entry.loaded.problem] : [])),
|
|
89
93
|
pluginProblems,
|
|
90
94
|
};
|
|
@@ -123,6 +123,7 @@ export const readPack = (dir: string, manifest: Readonly<Record<string, unknown>
|
|
|
123
123
|
rules: rules.value,
|
|
124
124
|
lexicons: lexicons.value,
|
|
125
125
|
styles: styles.value,
|
|
126
|
+
...(manifest["guide"] === undefined ? {} : { guide: manifest["guide"] }),
|
|
126
127
|
},
|
|
127
128
|
};
|
|
128
129
|
};
|
|
@@ -19,7 +19,7 @@ export type LoadedPlugins = { readonly plugins: readonly ParsedPlugin[]; readonl
|
|
|
19
19
|
/** pack: the folder of a rule pack (YAML only), which is read rather than imported. */
|
|
20
20
|
type Located = { readonly origin: PluginOrigin; readonly pack?: string } | { readonly problem: PluginProblem };
|
|
21
21
|
|
|
22
|
-
const problemOf = (kind: Exclude<PluginProblem["kind"], "rule">, plugin: string, detail = ""): { problem: PluginProblem } => ({
|
|
22
|
+
const problemOf = (kind: Exclude<PluginProblem["kind"], "rule" | "guide">, plugin: string, detail = ""): { problem: PluginProblem } => ({
|
|
23
23
|
problem: { kind, plugin, detail },
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -7,6 +7,7 @@ import type { UntrustedDetector } from "./module-detector.ts";
|
|
|
7
7
|
import { describeValue } from "./returned-findings.ts";
|
|
8
8
|
import { isPluginName } from "./plugin-name.ts";
|
|
9
9
|
import { knownGenres } from "../known-genres.ts";
|
|
10
|
+
import { guideLayerOf, type GuideLayer, type GuideProblem } from "../genre-guide/layer.ts";
|
|
10
11
|
|
|
11
12
|
// What a plugin package exports (api.ts PluginSpec), read into what chaff runs: its rules with ids under the plugin's
|
|
12
13
|
// name, their detectors, its word lists and its house styles. A rule is read exactly like a rule in custom_rules, with
|
|
@@ -32,7 +33,8 @@ type PluginProblemKind =
|
|
|
32
33
|
/** plugin: as chaff.yaml writes it. detail: what was there instead, the name expected, the rule, list or style at fault. */
|
|
33
34
|
export type PluginProblem =
|
|
34
35
|
| { readonly kind: PluginProblemKind; readonly plugin: string; readonly detail: string }
|
|
35
|
-
| { readonly kind: "rule"; readonly plugin: string; readonly problem: CustomProblem }
|
|
36
|
+
| { readonly kind: "rule"; readonly plugin: string; readonly problem: CustomProblem }
|
|
37
|
+
| { readonly kind: "guide"; readonly plugin: string; readonly problem: GuideProblem };
|
|
36
38
|
|
|
37
39
|
/** Where a plugin came from: as chaff.yaml writes it, the file imported, and the name its package's name gives (none for a path). */
|
|
38
40
|
export type PluginOrigin = { readonly written: string; readonly file: string; readonly expectedName: string | undefined };
|
|
@@ -47,11 +49,13 @@ export type ParsedPlugin = {
|
|
|
47
49
|
/** Word lists by language, then by name with the plugin's prefix. */
|
|
48
50
|
readonly lexicons: Readonly<Record<string, Readonly<Record<string, Lexicon>>>>;
|
|
49
51
|
readonly styles: readonly StyleDefinition[];
|
|
52
|
+
/** Its changes to the genre guides, applied whenever the plugin is loaded: under a style's and chaff.yaml's. */
|
|
53
|
+
readonly guide?: GuideLayer | undefined;
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
export type PluginRead = { readonly plugin: ParsedPlugin | undefined; readonly problems: readonly PluginProblem[] };
|
|
53
57
|
|
|
54
|
-
type Problem = (kind: Exclude<PluginProblem["kind"], "rule">, detail: string) => PluginProblem;
|
|
58
|
+
type Problem = (kind: Exclude<PluginProblem["kind"], "rule" | "guide">, detail: string) => PluginProblem;
|
|
55
59
|
|
|
56
60
|
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
57
61
|
|
|
@@ -215,6 +219,16 @@ const stylesOf = (raw: unknown, name: string, problem: Problem): Styles => {
|
|
|
215
219
|
};
|
|
216
220
|
};
|
|
217
221
|
|
|
222
|
+
// ───────── guide ─────────
|
|
223
|
+
|
|
224
|
+
type Guide = { readonly guide: GuideLayer | undefined; readonly problems: readonly PluginProblem[] };
|
|
225
|
+
|
|
226
|
+
const guideOf = (raw: unknown, name: string, written: string): Guide => {
|
|
227
|
+
if (raw === undefined) return { guide: undefined, problems: [] };
|
|
228
|
+
const read = guideLayerOf(raw, `plugins: ${name}`, knownGenres());
|
|
229
|
+
return { guide: read.layer, problems: read.problems.map((problem) => ({ kind: "guide", plugin: written, problem })) };
|
|
230
|
+
};
|
|
231
|
+
|
|
218
232
|
// ───────── the plugin ─────────
|
|
219
233
|
|
|
220
234
|
/** A plugin's default export, read. Every problem is reported; a plugin with any is left out whole, and the run is stopped. */
|
|
@@ -229,8 +243,17 @@ export const parsePlugin = (exported: unknown, origin: PluginOrigin, useFor: rea
|
|
|
229
243
|
const lexicons = lexiconsOf(exported["lexicons"], name, problem);
|
|
230
244
|
const rules = rulesOf(exported["rules"], { name, origin, useFor, lists: lexicons.lists });
|
|
231
245
|
const styles = stylesOf(exported["styles"], name, problem);
|
|
232
|
-
const
|
|
246
|
+
const guide = guideOf(exported["guide"], name, origin.written);
|
|
247
|
+
const problems = [...rules.problems, ...lexicons.problems, ...styles.problems, ...guide.problems];
|
|
233
248
|
if (problems.length > 0) return { plugin: undefined, problems };
|
|
234
|
-
const plugin = {
|
|
249
|
+
const plugin = {
|
|
250
|
+
name,
|
|
251
|
+
written: origin.written,
|
|
252
|
+
rules: rules.rules,
|
|
253
|
+
detectors: rules.detectors,
|
|
254
|
+
lexicons: lexicons.lexicons,
|
|
255
|
+
styles: styles.styles,
|
|
256
|
+
...(guide.guide === undefined ? {} : { guide: guide.guide }),
|
|
257
|
+
};
|
|
235
258
|
return { plugin, problems: [] };
|
|
236
259
|
};
|