@jpeakai/prose-gates 0.1.2 → 0.2.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 +36 -4
- package/dist/bin.js +8400 -455
- package/dist/index.js +8428 -456
- package/dist/testing.js +18321 -0
- package/examples/plugin-skills/README.md +60 -0
- package/examples/plugin-skills/index.mjs +14 -0
- package/examples/plugin-skills/package.json +12 -0
- package/examples/plugin-skills/rules/frontmatter-description-multiline-string.mjs +63 -0
- package/examples/plugin-skills/rules/frontmatter-description-word-budget.mjs +35 -0
- package/examples/plugin-skills-typescript/README.md +42 -0
- package/examples/plugin-skills-typescript/index.ts +21 -0
- package/examples/plugin-skills-typescript/package.json +12 -0
- package/examples/plugin-skills-typescript/rules/frontmatter-description-multiline-string.ts +68 -0
- package/examples/plugin-skills-typescript/rules/frontmatter-description-word-budget.ts +40 -0
- package/examples/plugin-skills-typescript/tsconfig.json +14 -0
- package/package.json +10 -3
- package/src/bin.ts +2 -2
- package/src/check.ts +40 -12
- package/src/cli.ts +197 -28
- package/src/config.ts +119 -0
- package/src/errors.ts +191 -0
- package/src/fix/engine.ts +30 -8
- package/src/fix/verify.ts +29 -0
- package/src/index.ts +38 -2
- package/src/interpunct.ts +7 -7
- package/src/model.ts +130 -1
- package/src/plugins/contract.ts +41 -0
- package/src/plugins/discover.ts +140 -0
- package/src/plugins/index.ts +103 -0
- package/src/plugins/load.ts +234 -0
- package/src/plugins/runtime.ts +15 -0
- package/src/project.ts +81 -0
- package/src/rules/index.ts +32 -20
- package/src/rules/{pg008-labelled-run.ts → list-comma-labelled-run.ts} +3 -3
- package/src/rules/{pg007-inline-enum.ts → list-inline-enumeration-markers.ts} +2 -2
- package/src/rules/{pg006-interpunct-run.ts → list-interpunct-joined-run.ts} +2 -2
- package/src/rules/{pg003-semicolon-list.ts → list-semicolon-delimited-run.ts} +2 -2
- package/src/rules/{pg009-stacked-runs.ts → list-stacked-interpunct-runs.ts} +3 -3
- package/src/rules/{pg004-em-dash.ts → punctuation-em-dash-in-prose.ts} +3 -3
- package/src/rules/{pg005-interpunct.ts → punctuation-interpunct-in-prose.ts} +6 -6
- package/src/rules/registry.ts +133 -0
- package/src/rules/{pg001-wrap.ts → sentence-one-per-line.ts} +4 -4
- package/src/rules/{pg002-length.ts → sentence-word-budget-exceeded.ts} +3 -2
- package/src/rules/types.ts +62 -20
- package/src/testing.ts +60 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// The registry: the rules a run actually uses. It starts from the built-in
|
|
2
|
+
// catalogue, adds whatever plugins loaded, then applies the config, so a rule
|
|
3
|
+
// that is off loses its check and its fixer together. Nothing else reads the
|
|
4
|
+
// catalogue directly, which is what lets a plugin rule behave as a built-in.
|
|
5
|
+
|
|
6
|
+
import type { Config } from "../config.ts";
|
|
7
|
+
import { RuleOptionError, UnknownRuleError } from "../errors.ts";
|
|
8
|
+
import { interpunctRuns, isFlat } from "../interpunct.ts";
|
|
9
|
+
import type { DocModel } from "../model.ts";
|
|
10
|
+
import { sentences, words } from "../text.ts";
|
|
11
|
+
import { FIX_ORDER, RULES } from "./index.ts";
|
|
12
|
+
import { lengthMessage } from "./sentence-word-budget-exceeded.ts";
|
|
13
|
+
import {
|
|
14
|
+
isPluginRuleId,
|
|
15
|
+
type OptionType,
|
|
16
|
+
RULE,
|
|
17
|
+
type Rule,
|
|
18
|
+
type RuleContext,
|
|
19
|
+
type RuleId,
|
|
20
|
+
type RuleOptions,
|
|
21
|
+
} from "./types.ts";
|
|
22
|
+
|
|
23
|
+
export const BUILTIN_CATEGORIES: ReadonlyMap<string, string> = new Map([
|
|
24
|
+
["sentence", "How a sentence is laid out and how long it runs"],
|
|
25
|
+
["list", "A list hidden in running prose, promoted to a real markdown list"],
|
|
26
|
+
["punctuation", "A glyph that reads as generated text"],
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const TEXT_HELPERS = { words, sentences, lengthMessage } as const;
|
|
30
|
+
|
|
31
|
+
// Where a group of rules came from, for the line each run prints.
|
|
32
|
+
export interface LoadedSource {
|
|
33
|
+
kind: "local" | "package" | "config";
|
|
34
|
+
spec: string;
|
|
35
|
+
namespace: string;
|
|
36
|
+
rules: RuleId[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Registry {
|
|
40
|
+
rules: Rule[];
|
|
41
|
+
fixOrder: Rule[];
|
|
42
|
+
enabled: ReadonlySet<RuleId>;
|
|
43
|
+
options: ReadonlyMap<RuleId, RuleOptions>;
|
|
44
|
+
categories: ReadonlyMap<string, string>;
|
|
45
|
+
sources: LoadedSource[];
|
|
46
|
+
// Config settings dropped because they name a plugin rule that did not load.
|
|
47
|
+
ignoredSettings: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RegistryInput {
|
|
51
|
+
rules: Rule[];
|
|
52
|
+
fixOrder: Rule[];
|
|
53
|
+
categories: ReadonlyMap<string, string>;
|
|
54
|
+
sources?: LoadedSource[];
|
|
55
|
+
config?: Config;
|
|
56
|
+
// A lenient run drops a setting for an unknown plugin rule instead of failing.
|
|
57
|
+
ignoreUnknownPluginIds?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const typeOf = (value: unknown): OptionType | null => {
|
|
61
|
+
if (typeof value === "number" && Number.isFinite(value)) return "number";
|
|
62
|
+
if (typeof value === "string") return "string";
|
|
63
|
+
if (typeof value === "boolean") return "boolean";
|
|
64
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "string")) return "string[]";
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const checkOptions = (where: string, rule: Rule, options: RuleOptions): void => {
|
|
69
|
+
const accepted = Object.keys(rule.options ?? {});
|
|
70
|
+
for (const [name, value] of Object.entries(options)) {
|
|
71
|
+
const want = rule.options?.[name];
|
|
72
|
+
if (!want) {
|
|
73
|
+
const list = accepted.length > 0 ? `it accepts ${accepted.join(", ")}` : "it takes no options";
|
|
74
|
+
throw new RuleOptionError(where, rule.id, name, `rule "${rule.id}" has no option "${name}"; ${list}`);
|
|
75
|
+
}
|
|
76
|
+
if (typeOf(value) !== want) {
|
|
77
|
+
throw new RuleOptionError(where, rule.id, name, `option "${name}" of "${rule.id}" must be a ${want}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if ("maxWords" in options && !((options.maxWords as number) >= 1)) {
|
|
81
|
+
throw new RuleOptionError(where, rule.id, "maxWords", `option "maxWords" of "${rule.id}" must be at least 1`);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const buildRegistry = (input: RegistryInput): Registry => {
|
|
86
|
+
const { rules, fixOrder, categories, sources = [], config, ignoreUnknownPluginIds = false } = input;
|
|
87
|
+
const off = new Set<string>();
|
|
88
|
+
const options = new Map<RuleId, RuleOptions>();
|
|
89
|
+
const ignored: string[] = [];
|
|
90
|
+
const where = config?.file ?? "config";
|
|
91
|
+
for (const [id, setting] of config?.rules ?? []) {
|
|
92
|
+
const rule = rules.find((r) => r.id === id);
|
|
93
|
+
if (!rule) {
|
|
94
|
+
// A plugin that was skipped cannot have its rules configured, so a lenient
|
|
95
|
+
// run drops the setting and says so. A built-in id is never forgiven.
|
|
96
|
+
if (ignoreUnknownPluginIds && isPluginRuleId(id)) {
|
|
97
|
+
ignored.push(id);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
throw new UnknownRuleError(
|
|
101
|
+
where,
|
|
102
|
+
id,
|
|
103
|
+
rules.map((r) => r.id),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
checkOptions(where, rule, setting.options);
|
|
107
|
+
if (setting.severity === "off") off.add(id);
|
|
108
|
+
else options.set(rule.id, setting.options);
|
|
109
|
+
}
|
|
110
|
+
const active = rules.filter((r) => !off.has(r.id));
|
|
111
|
+
return {
|
|
112
|
+
rules: active,
|
|
113
|
+
fixOrder: fixOrder.filter((r) => !off.has(r.id)),
|
|
114
|
+
enabled: new Set(active.map((r) => r.id)),
|
|
115
|
+
options,
|
|
116
|
+
categories,
|
|
117
|
+
sources,
|
|
118
|
+
ignoredSettings: ignored,
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const BUILTIN: Registry = buildRegistry({ rules: RULES, fixOrder: FIX_ORDER, categories: BUILTIN_CATEGORIES });
|
|
123
|
+
|
|
124
|
+
// The data every rule shares, derived once per document. Interpunct runs are
|
|
125
|
+
// read by several rules, so computing them here keeps the fix engine from
|
|
126
|
+
// rebuilding them once per rule on every pass of the fixpoint loop. A run is
|
|
127
|
+
// owned by the flat or the stacked list rule, and it counts as reported only
|
|
128
|
+
// while its owner is on.
|
|
129
|
+
export const ruleContext = (doc: DocModel, registry: Registry = BUILTIN): RuleContext => {
|
|
130
|
+
const runs = interpunctRuns(doc);
|
|
131
|
+
const reported = runs.filter((run) => registry.enabled.has(isFlat(run) ? RULE.INTERPUNCT_RUN : RULE.STACKED_RUNS));
|
|
132
|
+
return { doc, runs, reported };
|
|
133
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// sentence-one-per-line: a newline inside a paragraph whose preceding non-space character
|
|
2
2
|
// does not end a sentence is a mid-sentence wrap. The fix reflows the
|
|
3
3
|
// paragraph to one sentence per line.
|
|
4
4
|
|
|
@@ -67,9 +67,9 @@ const check: Rule["check"] = ({ doc, file }) =>
|
|
|
67
67
|
|
|
68
68
|
const fix: Rule["fix"] = ({ doc, runs }) => {
|
|
69
69
|
const edits: Edit[] = [];
|
|
70
|
-
// Interpunct runs stacked on several lines are a nested list
|
|
70
|
+
// Interpunct runs stacked on several lines are a nested list list-stacked-interpunct-runs either
|
|
71
71
|
// promotes or leaves for a human. Joining the lines would destroy the
|
|
72
|
-
// structure and hand
|
|
72
|
+
// structure and hand list-interpunct-joined-run a flat run it would merge wrongly.
|
|
73
73
|
const stacked = new Set(paragraphsOf(doc, runs.filter(isStacked)));
|
|
74
74
|
for (const view of proseParagraphs(doc)) {
|
|
75
75
|
// A hard break is structure the author chose, so the paragraph is left
|
|
@@ -90,7 +90,7 @@ const fix: Rule["fix"] = ({ doc, runs }) => {
|
|
|
90
90
|
return edits;
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
-
export const
|
|
93
|
+
export const sentenceOnePerLine: Rule = {
|
|
94
94
|
id: RULE.WRAP,
|
|
95
95
|
category: "sentence",
|
|
96
96
|
summary: "mid-sentence line wrap (one sentence per line)",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// sentence-word-budget-exceeded: sentences over the word budget are hard to unpack, especially for
|
|
2
2
|
// ESL readers and translators. Never autofixed: shortening a sentence is a
|
|
3
3
|
// rewrite, and a rewrite needs discretion. The message estimates the clauses
|
|
4
4
|
// in the sentence so the rewrite splits it rather than compressing it.
|
|
@@ -37,9 +37,10 @@ const check: Rule["check"] = ({ doc, file, maxWords }) =>
|
|
|
37
37
|
}));
|
|
38
38
|
|
|
39
39
|
// No fixer: shortening a sentence changes its words, per PRS-0006.
|
|
40
|
-
export const
|
|
40
|
+
export const sentenceWordBudgetExceeded: Rule = {
|
|
41
41
|
id: RULE.LENGTH,
|
|
42
42
|
category: "sentence",
|
|
43
43
|
summary: "sentence longer than the word budget (default 25)",
|
|
44
|
+
options: { maxWords: "number" },
|
|
44
45
|
check,
|
|
45
46
|
};
|
package/src/rules/types.ts
CHANGED
|
@@ -6,18 +6,30 @@ import type { InterpunctRun } from "../interpunct.ts";
|
|
|
6
6
|
import type { DocModel, ParagraphView } from "../model.ts";
|
|
7
7
|
|
|
8
8
|
export const RULE = {
|
|
9
|
-
WRAP: "
|
|
10
|
-
LENGTH: "
|
|
11
|
-
SEMICOLON_LIST: "
|
|
12
|
-
EM_DASH: "
|
|
13
|
-
INTERPUNCT: "
|
|
14
|
-
INTERPUNCT_RUN: "
|
|
15
|
-
INLINE_ENUM: "
|
|
16
|
-
LABELLED_RUN: "
|
|
17
|
-
STACKED_RUNS: "
|
|
9
|
+
WRAP: "sentence-one-per-line",
|
|
10
|
+
LENGTH: "sentence-word-budget-exceeded",
|
|
11
|
+
SEMICOLON_LIST: "list-semicolon-delimited-run",
|
|
12
|
+
EM_DASH: "punctuation-em-dash-in-prose",
|
|
13
|
+
INTERPUNCT: "punctuation-interpunct-in-prose",
|
|
14
|
+
INTERPUNCT_RUN: "list-interpunct-joined-run",
|
|
15
|
+
INLINE_ENUM: "list-inline-enumeration-markers",
|
|
16
|
+
LABELLED_RUN: "list-comma-labelled-run",
|
|
17
|
+
STACKED_RUNS: "list-stacked-interpunct-runs",
|
|
18
18
|
} as const;
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// A core id has no slash. A plugin id is always namespace/name, so the slash
|
|
21
|
+
// is what tells the two apart.
|
|
22
|
+
export type CoreRuleId = (typeof RULE)[keyof typeof RULE];
|
|
23
|
+
|
|
24
|
+
// A rule function may be synchronous or return a promise, so a rule can call a tool
|
|
25
|
+
// or a service. The engine awaits every call.
|
|
26
|
+
export type Awaitable<T> = T | Promise<T>;
|
|
27
|
+
|
|
28
|
+
export type PluginRuleId = `${string}/${string}`;
|
|
29
|
+
|
|
30
|
+
export const isPluginRuleId = (id: string): id is PluginRuleId => id.includes("/");
|
|
31
|
+
|
|
32
|
+
export type RuleId = CoreRuleId | PluginRuleId;
|
|
21
33
|
|
|
22
34
|
export interface Finding {
|
|
23
35
|
file: string;
|
|
@@ -26,23 +38,47 @@ export interface Finding {
|
|
|
26
38
|
message: string;
|
|
27
39
|
}
|
|
28
40
|
|
|
29
|
-
//
|
|
30
|
-
// data is passed in rather than recomputed, so a check and a fix can
|
|
31
|
-
// disagree about what the document holds.
|
|
41
|
+
// The data every rule shares, computed once per document and handed to it.
|
|
42
|
+
// Shared data is passed in rather than recomputed, so a check and a fix can
|
|
43
|
+
// never disagree about what the document holds. `runs` is every interpunct
|
|
44
|
+
// run. `reported` is the runs some enabled rule owns, so a rule that defers
|
|
45
|
+
// to another stops deferring when that rule is switched off.
|
|
32
46
|
export interface RuleContext {
|
|
33
47
|
doc: DocModel;
|
|
34
48
|
runs: InterpunctRun[];
|
|
49
|
+
reported: InterpunctRun[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// What a rule may be configured with. A rule names each option it accepts
|
|
53
|
+
// and the type of its value, and the config is checked against that.
|
|
54
|
+
export type OptionType = "number" | "string" | "boolean" | "string[]";
|
|
55
|
+
|
|
56
|
+
export type OptionSpec = Readonly<Record<string, OptionType>>;
|
|
57
|
+
|
|
58
|
+
export type RuleOptions = Readonly<Record<string, unknown>>;
|
|
59
|
+
|
|
60
|
+
// Plain functions a plugin can use without importing this package, so a plugin
|
|
61
|
+
// never depends on resolving the host from its own location.
|
|
62
|
+
export interface TextHelpers {
|
|
63
|
+
words: (text: string) => number;
|
|
64
|
+
sentences: (text: string) => string[];
|
|
65
|
+
lengthMessage: (sentence: string, maxWords: number) => string;
|
|
35
66
|
}
|
|
36
67
|
|
|
37
68
|
// A check also names the file it reports against, and the sentence budget.
|
|
38
69
|
export interface CheckContext extends RuleContext {
|
|
39
70
|
file: string;
|
|
40
71
|
maxWords: number;
|
|
72
|
+
options: RuleOptions;
|
|
73
|
+
helpers: TextHelpers;
|
|
41
74
|
}
|
|
42
75
|
|
|
43
|
-
// A fix needs nothing beyond the shared context
|
|
44
|
-
// engine alone decides which of them survive
|
|
45
|
-
export
|
|
76
|
+
// A fix needs nothing beyond the shared context and its own options. It
|
|
77
|
+
// proposes edits, and the engine alone decides which of them survive.
|
|
78
|
+
export interface FixContext extends RuleContext {
|
|
79
|
+
options: RuleOptions;
|
|
80
|
+
helpers: TextHelpers;
|
|
81
|
+
}
|
|
46
82
|
|
|
47
83
|
// What the engine must be able to prove about the document after an edit.
|
|
48
84
|
export type Expectation =
|
|
@@ -50,6 +86,9 @@ export type Expectation =
|
|
|
50
86
|
| { kind: "same-tree" }
|
|
51
87
|
// Glyphs swapped inside text: the tree keeps its shape and every word.
|
|
52
88
|
| { kind: "same-shape" }
|
|
89
|
+
// The frontmatter is written differently and means the same: the YAML parses to
|
|
90
|
+
// identical data before and after, and nothing outside it moves.
|
|
91
|
+
| { kind: "same-frontmatter-data" }
|
|
53
92
|
// One paragraph becomes the blocks parsed from `fragment`, and nothing else
|
|
54
93
|
// in the document moves.
|
|
55
94
|
| { kind: "replace-paragraph"; view: ParagraphView; fragment: string; listItems: number };
|
|
@@ -66,15 +105,18 @@ export interface Edit {
|
|
|
66
105
|
// sentence: how a sentence is laid out and how long it runs.
|
|
67
106
|
// list: a list hidden in running prose, promoted to a real markdown list.
|
|
68
107
|
// punctuation: a glyph that reads as generated text.
|
|
108
|
+
// A plugin may add a category of its own by declaring it.
|
|
69
109
|
export const CATEGORIES = ["sentence", "list", "punctuation"] as const;
|
|
70
110
|
|
|
71
111
|
export type Category = (typeof CATEGORIES)[number];
|
|
72
112
|
|
|
73
113
|
export interface Rule {
|
|
74
114
|
id: RuleId;
|
|
75
|
-
category:
|
|
115
|
+
category: string;
|
|
76
116
|
summary: string;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
117
|
+
// The options the rule accepts. Absent when it takes none.
|
|
118
|
+
options?: OptionSpec;
|
|
119
|
+
check: (ctx: CheckContext) => Awaitable<Finding[]>;
|
|
120
|
+
// Absent for rules whose fix needs discretion (sentence-word-budget-exceeded).
|
|
121
|
+
fix?: (ctx: FixContext) => Awaitable<Edit[]>;
|
|
80
122
|
}
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// A test helper for plugin authors: run one plugin rule over real strings, with
|
|
2
|
+
// the same loader checks, the same context and the same verified fix engine as
|
|
3
|
+
// a real run. It is a separate entry point so a plugin's tests import it and
|
|
4
|
+
// production code never does.
|
|
5
|
+
|
|
6
|
+
import { checkMarkdown } from "./check.ts";
|
|
7
|
+
import { type FixReport, fixMarkdownReport } from "./fix/engine.ts";
|
|
8
|
+
import type { Plugin, PluginRule } from "./plugins/contract.ts";
|
|
9
|
+
import { parsePlugin, toRule } from "./plugins/load.ts";
|
|
10
|
+
import { BUILTIN_CATEGORIES, buildRegistry, type Registry } from "./rules/registry.ts";
|
|
11
|
+
import type { Finding, RuleOptions } from "./rules/types.ts";
|
|
12
|
+
|
|
13
|
+
export interface RunOptions {
|
|
14
|
+
// The rule's own options, as a config would give them.
|
|
15
|
+
options?: RuleOptions;
|
|
16
|
+
// The sentence budget, as --max-words would give it.
|
|
17
|
+
maxWords?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Wraps a local rule module, with its optional `categories` export, as the
|
|
21
|
+
// plugin the loader would build from the file under the `local` namespace.
|
|
22
|
+
export const localPlugin = (key: string, mod: { default: PluginRule; categories?: Record<string, string> }): Plugin =>
|
|
23
|
+
parsePlugin(
|
|
24
|
+
"local",
|
|
25
|
+
{
|
|
26
|
+
meta: { name: "local", namespace: "local", apiVersion: 1, categories: mod.categories },
|
|
27
|
+
rules: { [key]: mod.default },
|
|
28
|
+
},
|
|
29
|
+
new Set(),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const registryFor = (plugin: Plugin, key: string, options: RuleOptions): Registry => {
|
|
33
|
+
const checked = parsePlugin(plugin.meta.name, plugin, new Set());
|
|
34
|
+
const rule = checked.rules[key];
|
|
35
|
+
if (!rule)
|
|
36
|
+
throw new Error(
|
|
37
|
+
`plugin "${plugin.meta.name}" has no rule "${key}"; it has ${Object.keys(checked.rules).join(", ")}`,
|
|
38
|
+
);
|
|
39
|
+
const built = toRule(checked.meta.namespace, key, rule);
|
|
40
|
+
return buildRegistry({
|
|
41
|
+
rules: [built],
|
|
42
|
+
fixOrder: built.fix ? [built] : [],
|
|
43
|
+
categories: new Map([...BUILTIN_CATEGORIES, ...Object.entries(checked.meta.categories ?? {})]),
|
|
44
|
+
config: {
|
|
45
|
+
plugins: [],
|
|
46
|
+
pluginLoading: "strict",
|
|
47
|
+
file: null,
|
|
48
|
+
rules: new Map([[built.id, { severity: "error", options }]]),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// The findings one rule of the plugin reports for a document.
|
|
54
|
+
export const checkRule = async (plugin: Plugin, key: string, src: string, run: RunOptions = {}): Promise<Finding[]> =>
|
|
55
|
+
checkMarkdown(src, "test.md", run.maxWords, registryFor(plugin, key, run.options ?? {}));
|
|
56
|
+
|
|
57
|
+
// The document after the rule's fixer has run to a fixpoint, and what was
|
|
58
|
+
// applied or refused. A fixer that loses a word throws, exactly as in a real run.
|
|
59
|
+
export const fixRule = async (plugin: Plugin, key: string, src: string, run: RunOptions = {}): Promise<FixReport> =>
|
|
60
|
+
fixMarkdownReport(src, registryFor(plugin, key, run.options ?? {}));
|