chaffjs 0.5.1 → 0.7.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/dist/adapter-load.d.ts.map +1 -1
- package/dist/adapter-load.js +18 -2
- package/dist/adapter-load.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +15 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/cite.d.ts +16 -0
- package/dist/commands/cite.d.ts.map +1 -0
- package/dist/commands/cite.js +69 -0
- package/dist/commands/cite.js.map +1 -0
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +1 -1
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/tree.d.ts +33 -0
- package/dist/commands/tree.d.ts.map +1 -0
- package/dist/commands/tree.js +76 -0
- package/dist/commands/tree.js.map +1 -0
- package/dist/config/load.d.ts +9 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +27 -1
- package/dist/config/load.js.map +1 -1
- package/dist/config/rule-problems.d.ts +10 -0
- package/dist/config/rule-problems.d.ts.map +1 -0
- package/dist/config/rule-problems.js +20 -0
- package/dist/config/rule-problems.js.map +1 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +7 -0
- package/dist/detectors/index.js.map +1 -1
- package/dist/detectors/orthography.d.ts +9 -0
- package/dist/detectors/orthography.d.ts.map +1 -0
- package/dist/detectors/orthography.js +51 -0
- package/dist/detectors/orthography.js.map +1 -0
- package/dist/detectors/structure-tree.d.ts +5 -0
- package/dist/detectors/structure-tree.d.ts.map +1 -0
- package/dist/detectors/structure-tree.js +25 -0
- package/dist/detectors/structure-tree.js.map +1 -0
- package/dist/document.d.ts +12 -0
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +34 -3
- package/dist/document.js.map +1 -1
- package/dist/eval.d.ts +5 -1
- package/dist/eval.d.ts.map +1 -1
- package/dist/eval.js +23 -7
- package/dist/eval.js.map +1 -1
- package/dist/heading-text.d.ts +14 -0
- package/dist/heading-text.d.ts.map +1 -0
- package/dist/heading-text.js +28 -0
- package/dist/heading-text.js.map +1 -0
- package/dist/orthography.d.ts +21 -0
- package/dist/orthography.d.ts.map +1 -0
- package/dist/orthography.js +110 -0
- package/dist/orthography.js.map +1 -0
- package/dist/plugin.d.ts +71 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/position.d.ts.map +1 -1
- package/dist/position.js +21 -1
- package/dist/position.js.map +1 -1
- package/dist/render/rules-json.d.ts.map +1 -1
- package/dist/render/rules-json.js +11 -1
- package/dist/render/rules-json.js.map +1 -1
- package/dist/run.d.ts +3 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +11 -5
- package/dist/run.js.map +1 -1
- package/dist/structure/build.d.ts +20 -0
- package/dist/structure/build.d.ts.map +1 -0
- package/dist/structure/build.js +165 -0
- package/dist/structure/build.js.map +1 -0
- package/dist/structure/cite.d.ts +21 -0
- package/dist/structure/cite.d.ts.map +1 -0
- package/dist/structure/cite.js +91 -0
- package/dist/structure/cite.js.map +1 -0
- package/dist/structure/issues.d.ts +15 -0
- package/dist/structure/issues.d.ts.map +1 -0
- package/dist/structure/issues.js +53 -0
- package/dist/structure/issues.js.map +1 -0
- package/dist/structure/lines.d.ts +15 -0
- package/dist/structure/lines.d.ts.map +1 -0
- package/dist/structure/lines.js +33 -0
- package/dist/structure/lines.js.map +1 -0
- package/dist/structure/markdown-path.d.ts +3 -0
- package/dist/structure/markdown-path.d.ts.map +1 -0
- package/dist/structure/markdown-path.js +5 -0
- package/dist/structure/markdown-path.js.map +1 -0
- package/dist/structure/numbering.d.ts +9 -0
- package/dist/structure/numbering.d.ts.map +1 -0
- package/dist/structure/numbering.js +38 -0
- package/dist/structure/numbering.js.map +1 -0
- package/dist/structure/of.d.ts +13 -0
- package/dist/structure/of.d.ts.map +1 -0
- package/dist/structure/of.js +8 -0
- package/dist/structure/of.js.map +1 -0
- package/dist/structure/sexp.d.ts +7 -0
- package/dist/structure/sexp.d.ts.map +1 -0
- package/dist/structure/sexp.js +25 -0
- package/dist/structure/sexp.js.map +1 -0
- package/dist/structure/universal.d.ts +7 -0
- package/dist/structure/universal.d.ts.map +1 -0
- package/dist/structure/universal.js +21 -0
- package/dist/structure/universal.js.map +1 -0
- package/package.json +3 -3
- package/rules/dangling-reference.yaml +27 -0
- package/rules/duplicate-definition.yaml +26 -0
- package/rules/latin-spacing.yaml +27 -0
- package/rules/numbering-gap.yaml +26 -0
- package/rules/preferred-term.yaml +27 -0
- package/src/adapter-load.ts +19 -2
- package/src/cli.ts +16 -1
- package/src/commands/cite.ts +74 -0
- package/src/commands/eval.ts +1 -0
- package/src/commands/tree.ts +94 -0
- package/src/config/load.ts +43 -1
- package/src/config/rule-problems.ts +24 -0
- package/src/detectors/index.ts +7 -0
- package/src/detectors/orthography.ts +55 -0
- package/src/detectors/structure-tree.ts +32 -0
- package/src/document.ts +49 -6
- package/src/eval.ts +27 -7
- package/src/heading-text.ts +26 -0
- package/src/orthography.ts +115 -0
- package/src/plugin.ts +75 -0
- package/src/position.ts +21 -1
- package/src/render/rules-json.ts +10 -1
- package/src/run.ts +22 -5
- package/src/structure/build.ts +205 -0
- package/src/structure/cite.ts +115 -0
- package/src/structure/issues.ts +63 -0
- package/src/structure/lines.ts +32 -0
- package/src/structure/markdown-path.ts +6 -0
- package/src/structure/numbering.ts +44 -0
- package/src/structure/of.ts +15 -0
- package/src/structure/sexp.ts +28 -0
- package/src/structure/universal.ts +21 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Detector, Finding, Sentence } from "../plugin.ts";
|
|
2
|
+
import { latinBoundaries, minorityStyle, occurrencesOutside, type SpacingKind } from "../orthography.ts";
|
|
3
|
+
|
|
4
|
+
/** チームが chaff.yaml の prefer に書いた「使わない書き方」。書いていなければ何も言わない。 */
|
|
5
|
+
export const preferredTerm: Detector = (doc, options): Finding[] => {
|
|
6
|
+
const pairs = (options.lexicon ?? []).flatMap((entry) => (entry.instead_of === undefined ? [] : [{ avoid: entry.pattern, use: entry.instead_of }]));
|
|
7
|
+
const hits = doc.sentences.flatMap((sentence) =>
|
|
8
|
+
pairs.flatMap((pair) => occurrencesOutside(sentence.text, pair.avoid, pair.use).map((at) => ({ sentence, at, pair }))),
|
|
9
|
+
);
|
|
10
|
+
if (hits.length === 0 || hits.length < options.limit) return [];
|
|
11
|
+
return hits.map((hit) => ({
|
|
12
|
+
rule: "",
|
|
13
|
+
severity: "warning",
|
|
14
|
+
line: 0,
|
|
15
|
+
column: 0,
|
|
16
|
+
quote: hit.sentence.text.trim(),
|
|
17
|
+
values: { matched: hit.pair.avoid, preferred: hit.pair.use, count: hits.length, limit: options.limit, offset: hit.sentence.span.start + hit.at },
|
|
18
|
+
}));
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const KIND_NAME: Readonly<Record<SpacingKind, string>> = { letter: "英字", "before-digit": "後ろの数字", "after-digit": "前の数字" };
|
|
22
|
+
const KINDS: readonly SpacingKind[] = ["letter", "before-digit", "after-digit"];
|
|
23
|
+
|
|
24
|
+
type Located = { readonly sentence: Sentence; readonly offset: number; readonly kind: SpacingKind; readonly spaced: boolean };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 日本語と英字・数字のあいだを、空けるか詰めるか。文書の中で混ざっていたら、少ないほうを指摘する。
|
|
28
|
+
* どちらが正しいかは決めない。決めるのはチームで、chaff はそろっているかだけを見る。
|
|
29
|
+
*/
|
|
30
|
+
export const latinSpacing: Detector = (doc, options): Finding[] => {
|
|
31
|
+
const located: Located[] = doc.sentences.flatMap((sentence) => latinBoundaries(sentence.text).map((boundary) => ({ sentence, ...boundary })));
|
|
32
|
+
return KINDS.flatMap((kind) => {
|
|
33
|
+
const ofKind = located.filter((entry) => entry.kind === kind);
|
|
34
|
+
const minority = minorityStyle(ofKind);
|
|
35
|
+
if (minority === undefined) return [];
|
|
36
|
+
const odd = ofKind.filter((entry) => entry.spaced === minority);
|
|
37
|
+
if (odd.length < options.limit) return [];
|
|
38
|
+
return odd.map((entry) => ({
|
|
39
|
+
rule: "",
|
|
40
|
+
severity: "warning",
|
|
41
|
+
line: 0,
|
|
42
|
+
column: 0,
|
|
43
|
+
quote: entry.sentence.text.trim(),
|
|
44
|
+
values: {
|
|
45
|
+
kind: KIND_NAME[kind],
|
|
46
|
+
style: minority ? "空けています" : "詰めています",
|
|
47
|
+
usual: minority ? "詰める" : "空ける",
|
|
48
|
+
count: odd.length,
|
|
49
|
+
of: ofKind.length,
|
|
50
|
+
limit: options.limit,
|
|
51
|
+
offset: entry.sentence.span.start + entry.offset,
|
|
52
|
+
},
|
|
53
|
+
}));
|
|
54
|
+
});
|
|
55
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Detector, Finding, ProseDocument } from "../plugin.ts";
|
|
2
|
+
import { danglingReferences, duplicateDefinitions, type StructureIssue } from "../structure/issues.ts";
|
|
3
|
+
import { numberingBreaks } from "../structure/numbering.ts";
|
|
4
|
+
|
|
5
|
+
const QUOTE_LENGTH = 80;
|
|
6
|
+
|
|
7
|
+
/** 指摘の位置から行末までを引用する。条の見出しや参照を含む一行が、読み手の探す手がかりになる。 */
|
|
8
|
+
const quoteAt = (source: string, offset: number): string => {
|
|
9
|
+
const end = source.indexOf("\n", offset);
|
|
10
|
+
return source
|
|
11
|
+
.slice(offset, end === -1 ? source.length : end)
|
|
12
|
+
.slice(0, QUOTE_LENGTH)
|
|
13
|
+
.trim();
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const findingsOf =
|
|
17
|
+
(rule: string, issuesOf: (tree: NonNullable<ProseDocument["structure"]>) => StructureIssue[]): Detector =>
|
|
18
|
+
(doc): Finding[] =>
|
|
19
|
+
doc.structure === undefined
|
|
20
|
+
? []
|
|
21
|
+
: issuesOf(doc.structure).map((issue) => ({
|
|
22
|
+
rule,
|
|
23
|
+
severity: "error",
|
|
24
|
+
line: 0,
|
|
25
|
+
column: 0,
|
|
26
|
+
quote: quoteAt(doc.source, issue.offset),
|
|
27
|
+
values: { ...issue.values, offset: issue.offset },
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
export const danglingReference: Detector = findingsOf("dangling-reference", danglingReferences);
|
|
31
|
+
export const numberingGap: Detector = findingsOf("numbering-gap", numberingBreaks);
|
|
32
|
+
export const duplicateDefinition: Detector = findingsOf("duplicate-definition", duplicateDefinitions);
|
package/src/document.ts
CHANGED
|
@@ -3,8 +3,11 @@ import { gfmTable } from "micromark-extension-gfm-table";
|
|
|
3
3
|
import { gfmTableFromMarkdown } from "mdast-util-gfm-table";
|
|
4
4
|
import { frontmatter } from "micromark-extension-frontmatter";
|
|
5
5
|
import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
|
|
6
|
+
import { atxHeadingText, headingText } from "./heading-text.ts";
|
|
6
7
|
import { maskSpans } from "./mask.ts";
|
|
7
|
-
import
|
|
8
|
+
import { buildTree, NO_OUTLINE, type Outline } from "./structure/build.ts";
|
|
9
|
+
import { isMarkdownPath } from "./structure/markdown-path.ts";
|
|
10
|
+
import type { BulletList, LanguageAdapter, Paragraph, ProseDocument, Section, Sentence, Span, StructureNode } from "./plugin.ts";
|
|
8
11
|
|
|
9
12
|
type Place = { readonly offset?: number | undefined };
|
|
10
13
|
type Node = {
|
|
@@ -120,7 +123,7 @@ const textOf = (node: Node, source: string): string => {
|
|
|
120
123
|
return parts.join("");
|
|
121
124
|
};
|
|
122
125
|
|
|
123
|
-
type Heading = { readonly depth: number; readonly text: string; readonly start: number; readonly end: number };
|
|
126
|
+
export type Heading = { readonly depth: number; readonly text: string; readonly start: number; readonly end: number };
|
|
124
127
|
|
|
125
128
|
const headingsOf = (root: Node, source: string): Heading[] => {
|
|
126
129
|
const found: Heading[] = [];
|
|
@@ -128,7 +131,10 @@ const headingsOf = (root: Node, source: string): Heading[] => {
|
|
|
128
131
|
if (node.type !== "heading") return;
|
|
129
132
|
const span = spanOf(node);
|
|
130
133
|
const depth = typeof node === "object" && "depth" in node && typeof node.depth === "number" ? node.depth : 1;
|
|
131
|
-
if (span
|
|
134
|
+
if (span === undefined) return;
|
|
135
|
+
// ATX(行頭が #)なら閉じの # も外す。setext の見出しの末尾の # は言葉なので残す。
|
|
136
|
+
const read = source.startsWith("#", span.start) ? atxHeadingText : headingText;
|
|
137
|
+
found.push({ depth, text: read(textOf(node, source)), start: span.start, end: span.end });
|
|
132
138
|
});
|
|
133
139
|
return found;
|
|
134
140
|
};
|
|
@@ -227,13 +233,23 @@ const listsOf = (root: Node, source: string): BulletList[] => {
|
|
|
227
233
|
};
|
|
228
234
|
|
|
229
235
|
/** チームが chaff.yaml に書いたもの。語彙表と同じ器に入れて、detector には出所を見せない。 */
|
|
230
|
-
export type TeamRules = {
|
|
236
|
+
export type TeamRules = {
|
|
237
|
+
readonly jargon: readonly string[];
|
|
238
|
+
readonly requiredSections: readonly string[];
|
|
239
|
+
/** { 使わない書き方: 使う書き方 }。preferred-term が語彙表として読む。 */
|
|
240
|
+
readonly prefer?: Readonly<Record<string, string>>;
|
|
241
|
+
};
|
|
231
242
|
|
|
232
243
|
const EMPTY_TEAM: TeamRules = { jargon: [], requiredSections: [] };
|
|
233
244
|
|
|
234
245
|
/** Config から取り出す。document は Config の形を知らない。 */
|
|
235
|
-
export const teamRules = (config: {
|
|
246
|
+
export const teamRules = (config: {
|
|
247
|
+
readonly jargon: readonly string[];
|
|
248
|
+
readonly requiredSections: readonly string[];
|
|
249
|
+
readonly prefer?: Readonly<Record<string, string>>;
|
|
250
|
+
}): TeamRules => ({
|
|
236
251
|
jargon: config.jargon,
|
|
252
|
+
prefer: config.prefer ?? {},
|
|
237
253
|
requiredSections: config.requiredSections,
|
|
238
254
|
});
|
|
239
255
|
|
|
@@ -247,6 +263,9 @@ export const buildDocument = (path: string, source: string, adapter: LanguageAda
|
|
|
247
263
|
const paragraphSpans = spansOfType(root, "paragraph");
|
|
248
264
|
const listItems = spansOfType(root, "listItem");
|
|
249
265
|
const sentences = sentencesOf(prose, paragraphSpans, adapter);
|
|
266
|
+
const patterns = adapter.structure;
|
|
267
|
+
// null は「作ったが構造を読めない言語だった」、undefined は「まだ作っていない」。
|
|
268
|
+
const tree: { value: StructureNode | null | undefined } = { value: undefined };
|
|
250
269
|
return {
|
|
251
270
|
path,
|
|
252
271
|
source,
|
|
@@ -258,7 +277,31 @@ export const buildDocument = (path: string, source: string, adapter: LanguageAda
|
|
|
258
277
|
listSpans: listItems,
|
|
259
278
|
paragraphs: paragraphsOf(paragraphSpans, sentences, listItems),
|
|
260
279
|
lists: listsOf(root, source),
|
|
261
|
-
lexicons: {
|
|
280
|
+
lexicons: {
|
|
281
|
+
...adapter.lexicons,
|
|
282
|
+
"internal-jargon": team.jargon.map((pattern) => ({ pattern })),
|
|
283
|
+
// 使わない書き方を pattern に、使う書き方を instead_of に置く。語彙表の「同じことの別の書き方」と同じ向き。
|
|
284
|
+
"preferred-term": Object.entries(team.prefer ?? {}).map(([pattern, use]) => ({ pattern, instead_of: use })),
|
|
285
|
+
},
|
|
262
286
|
requiredSections: team.requiredSections,
|
|
287
|
+
// 構造の rule(参照先が無い・番号の抜け)が読む木。どの rule も読まなければ作らない。何万行の契約書で、他の rule の lint に代金を払わせない。
|
|
288
|
+
get structure(): StructureNode | undefined {
|
|
289
|
+
tree.value ??=
|
|
290
|
+
patterns === undefined
|
|
291
|
+
? null
|
|
292
|
+
: buildTree({ path, source, language: adapter.id, outline: isMarkdownPath(path) ? outlineOf(root, source) : NO_OUTLINE }, patterns);
|
|
293
|
+
return tree.value ?? undefined;
|
|
294
|
+
},
|
|
263
295
|
};
|
|
264
296
|
};
|
|
297
|
+
|
|
298
|
+
/** 番号を探してはいけない範囲。コードの中の「第3条」は条ではなく、参照でもない。 */
|
|
299
|
+
const OPAQUE = ["code", "inlineCode", "html", "yaml", "toml"];
|
|
300
|
+
|
|
301
|
+
const outlineOf = (root: Node, source: string): Outline => ({
|
|
302
|
+
headings: headingsOf(root, source),
|
|
303
|
+
opaque: OPAQUE.flatMap((type) => spansOfType(root, type)),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
/** 構造を読むための Markdown の手がかり。見出しと、中を読まない範囲。 */
|
|
307
|
+
export const markdownOutline = (source: string): Outline => outlineOf(parse(source), source);
|
package/src/eval.ts
CHANGED
|
@@ -35,11 +35,12 @@ const stricterIsHigher = (rule: RuleDefinition): boolean => (rule.levels.strict
|
|
|
35
35
|
const MULTIPLIERS = [0.5, 0.7, 0.85, 1, 1.25, 1.5, 2, 3];
|
|
36
36
|
|
|
37
37
|
/** rule 自身が持つ段を基点に掃引する。rule ごとに単位が違うので、倍率で散らす。 */
|
|
38
|
-
const candidates = (rule: RuleDefinition): number[] => {
|
|
38
|
+
const candidates = (rule: RuleDefinition, current: number): number[] => {
|
|
39
39
|
const base = rule.levels.normal ?? 1;
|
|
40
40
|
const fromLevels = [rule.levels.strict, rule.levels.normal, rule.levels.relaxed].filter((value) => value !== undefined);
|
|
41
41
|
const scaled = MULTIPLIERS.map((factor) => Math.max(1, Math.round(base * factor)));
|
|
42
|
-
|
|
42
|
+
// 数値で決めた上限は、段の表に無くても掃引に入れる。入れないと「現在」の行が出ない。
|
|
43
|
+
return [...new Set([...fromLevels, ...scaled, current])].sort((left, right) => left - right);
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
const countAt = (docs: readonly ProseDocument[], rule: RuleDefinition, limit: number): Point => {
|
|
@@ -63,6 +64,16 @@ const recommend = (rule: RuleDefinition, sweep: readonly Point[], total: number)
|
|
|
63
64
|
return strictest;
|
|
64
65
|
};
|
|
65
66
|
|
|
67
|
+
/** lint と同じ要求の読み方。知らない要求は満たさないものとする。pos に読み替えると、動かない rule を測ってしまう。 */
|
|
68
|
+
const meets = (need: string, docs: readonly ProseDocument[]): boolean => {
|
|
69
|
+
const capabilities = docs[0]?.capabilities;
|
|
70
|
+
if (need === "pos") return capabilities?.pos === true;
|
|
71
|
+
if (need === "lemma") return capabilities?.lemma === true;
|
|
72
|
+
// 木は adapter が structure を持つときだけ作れる。無いまま測ると、どの閾値でも 0 件になる。
|
|
73
|
+
if (need === "structure") return docs.length > 0 && docs.every((doc) => doc.structure !== undefined);
|
|
74
|
+
return false;
|
|
75
|
+
};
|
|
76
|
+
|
|
66
77
|
/**
|
|
67
78
|
* lint で動かない rule は掃引しない。
|
|
68
79
|
* 動かないものの閾値を測ると、「どの閾値でも 0 件」が「よく校正されている」に見える。
|
|
@@ -71,16 +82,25 @@ const measurable = (rule: RuleDefinition, docs: readonly ProseDocument[], genre:
|
|
|
71
82
|
if (rule.layer === "L4" || !rule.use_for.some((target) => genre.startsWith(target))) return false;
|
|
72
83
|
if (rule.languages !== undefined && !rule.languages.includes(language)) return false;
|
|
73
84
|
if (rule.from.length > 0) return false;
|
|
74
|
-
|
|
75
|
-
return !rule.requires.some((need) => (need === "lemma" ? capabilities?.lemma : capabilities?.pos) !== true);
|
|
85
|
+
return rule.requires.every((need) => meets(need, docs));
|
|
76
86
|
};
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
/**
|
|
89
|
+
* limits は chaff.yaml に数値で書いた上限。書いてあれば、それが lint で効いている「現在」の値。
|
|
90
|
+
* 段の言葉(relaxed など)で書いた設定は、これまでどおり normal の値を「現在」とする。
|
|
91
|
+
*/
|
|
92
|
+
export const evaluate = (
|
|
93
|
+
docs: readonly ProseDocument[],
|
|
94
|
+
rules: readonly RuleDefinition[],
|
|
95
|
+
genre: string,
|
|
96
|
+
language: string,
|
|
97
|
+
limits: Readonly<Record<string, number>> = {},
|
|
98
|
+
): RuleReport[] =>
|
|
79
99
|
rules
|
|
80
100
|
.filter((rule) => measurable(rule, docs, genre, language))
|
|
81
101
|
.map((rule) => {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
102
|
+
const current = limits[rule.id] ?? rule.levels.normal ?? 1;
|
|
103
|
+
const sweep = candidates(rule, current).map((limit) => countAt(docs, rule, limit));
|
|
84
104
|
return {
|
|
85
105
|
rule: rule.id,
|
|
86
106
|
name: rule.name[language] ?? rule.id,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 見出しの末尾に付けた属性を外した、見出しの言葉。`## ステップ 1 {#step1}` は「ステップ 1」。
|
|
3
|
+
* kramdown(`{#id}`、`{: .class}`)と pandoc(`{#id .class}`)の書き方。属性は読者に見えないので、
|
|
4
|
+
* 残すと見出しの言い直しや番号の読み取りが、見えない文字に引きずられる。
|
|
5
|
+
* 波括弧で終わっていても、中身が `#` `.` `:` で始まらないもの(`## {name} の設定`)は言葉のうち。
|
|
6
|
+
*/
|
|
7
|
+
export const headingText = (raw: string): string => {
|
|
8
|
+
const trimmed = raw.trim();
|
|
9
|
+
if (!trimmed.endsWith("}")) return trimmed;
|
|
10
|
+
const open = trimmed.lastIndexOf("{");
|
|
11
|
+
if (open === -1) return trimmed;
|
|
12
|
+
const inner = trimmed.slice(open + 1, -1).trimStart();
|
|
13
|
+
const isAttributes = inner.startsWith("#") || inner.startsWith(".") || inner.startsWith(":");
|
|
14
|
+
return isAttributes ? trimmed.slice(0, open).trim() : trimmed;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ATX 見出し(`## 見出し ##`)の言葉。属性を外してから、閉じの `#` を外す。
|
|
19
|
+
* `## Install ## {#install}` は閉じの `#` の後ろに属性があるので、この順でないと `##` が言葉に残る。
|
|
20
|
+
* 下線で書く見出し(setext)には使わない。そちらの末尾の `##` は本当に言葉の一部。
|
|
21
|
+
*/
|
|
22
|
+
export const atxHeadingText = (content: string): string => {
|
|
23
|
+
const withoutAttributes = headingText(content);
|
|
24
|
+
const closing = withoutAttributes.search(/(?:^|[ \t])#+$/u);
|
|
25
|
+
return (closing === -1 ? withoutAttributes : withoutAttributes.slice(0, closing)).trim();
|
|
26
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// 表記のそろい。書き方の決まりのうち、文字だけで決まるもの。
|
|
2
|
+
|
|
3
|
+
const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
4
|
+
|
|
5
|
+
/** text の中で word が現れる [始まり, 終わり) を、左から順に。大文字小文字を区別しないときは i で探す(位置は元の文字列のまま)。 */
|
|
6
|
+
const spansOf = (text: string, word: string, ignoreCase: boolean): [number, number][] => {
|
|
7
|
+
const pattern = new RegExp(escapeRegExp(word), ignoreCase ? "giu" : "gu");
|
|
8
|
+
const found: [number, number][] = [];
|
|
9
|
+
for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
|
|
10
|
+
found.push([match.index, match.index + match[0].length]);
|
|
11
|
+
// 重なって現れるものも拾う。次は 1 つ先から探す。
|
|
12
|
+
pattern.lastIndex = match.index + 1;
|
|
13
|
+
}
|
|
14
|
+
return found;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* avoid が現れる位置のうち、use の一部として現れたものを除いたもの。
|
|
19
|
+
* 「ユーザ」を避けて「ユーザー」と書く決まりのとき、「ユーザー」の中の「ユーザ」は指摘しない。
|
|
20
|
+
* 大文字小文字は区別しない(文頭の「E-mail」も「e-mail」)。ただし二つが大文字小文字だけ違う組
|
|
21
|
+
* (「Javascript」→「JavaScript」)は、それ自体が大文字小文字の決まりなので区別して探す。
|
|
22
|
+
*/
|
|
23
|
+
export const occurrencesOutside = (text: string, avoid: string, use: string): number[] => {
|
|
24
|
+
if (avoid === "") return [];
|
|
25
|
+
const ignoreCase = avoid.toLowerCase() !== use.toLowerCase();
|
|
26
|
+
const covered = use === "" ? [] : spansOf(text, use, ignoreCase);
|
|
27
|
+
const found: number[] = [];
|
|
28
|
+
// covered は始まりの順に並ぶ。始まりが at 以前のものの終わりの最大が end 以上なら、どれかに収まっている。
|
|
29
|
+
let next = 0;
|
|
30
|
+
let reach = -1;
|
|
31
|
+
let at = 0;
|
|
32
|
+
for (const [start, end] of spansOf(text, avoid, ignoreCase)) {
|
|
33
|
+
if (start < at) continue;
|
|
34
|
+
while (next < covered.length && (covered[next]?.[0] ?? Infinity) <= start) {
|
|
35
|
+
reach = Math.max(reach, covered[next]?.[1] ?? -1);
|
|
36
|
+
next += 1;
|
|
37
|
+
}
|
|
38
|
+
if (reach < end) found.push(start);
|
|
39
|
+
at = end;
|
|
40
|
+
}
|
|
41
|
+
return found;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type SpacingKind = "letter" | "before-digit" | "after-digit";
|
|
45
|
+
export type Boundary = { readonly offset: number; readonly kind: SpacingKind; readonly spaced: boolean };
|
|
46
|
+
|
|
47
|
+
const JAPANESE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}ー]/u;
|
|
48
|
+
const LETTER = /[A-Za-z]/u;
|
|
49
|
+
const DIGIT = /\d/u;
|
|
50
|
+
|
|
51
|
+
/** Latin 側の字の種類。digitSide は、その数字が日本語の右(前に日本語)か左(後ろに日本語)か。 */
|
|
52
|
+
const kindOf = (char: string | undefined, digitSide: "before-digit" | "after-digit"): SpacingKind | undefined => {
|
|
53
|
+
if (char === undefined) return undefined;
|
|
54
|
+
if (LETTER.test(char)) return "letter";
|
|
55
|
+
return DIGIT.test(char) ? digitSide : undefined;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isJapanese = (char: string | undefined): boolean => char !== undefined && JAPANESE.test(char);
|
|
59
|
+
|
|
60
|
+
/** japanese が日本語の字のとき、other の英字・数字の種類。 */
|
|
61
|
+
const latinBeside = (japanese: string | undefined, other: string | undefined, digitSide: "before-digit" | "after-digit"): SpacingKind | undefined =>
|
|
62
|
+
isJapanese(japanese) ? kindOf(other, digitSide) : undefined;
|
|
63
|
+
|
|
64
|
+
/** 日本語の字の隣にある英字・数字との境目。間が半角空白 1 つなら「空けている」、何も無ければ「詰めている」。 */
|
|
65
|
+
const ALPHANUMERIC = /[A-Za-z0-9.]/u;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 日本語の左にある英数字の並びの、先頭の字。「3GBの」の「GB」は数量の単位で、並びは数字で始まる。
|
|
69
|
+
* そうした並びと日本語の境目は、英字の空け方ではなく数字の後ろの空け方として数える(「3回」と同じ)。
|
|
70
|
+
*/
|
|
71
|
+
const runStart = (chars: readonly string[], last: number): string | undefined => {
|
|
72
|
+
let first = last;
|
|
73
|
+
while (first > 0 && ALPHANUMERIC.test(chars[first - 1] ?? "")) first -= 1;
|
|
74
|
+
return chars[first];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** 左の並びの種類。数字で始まる並び(3GB、10ms)は数字として扱う。 */
|
|
78
|
+
const leftRunBeside = (chars: readonly string[], last: number, japanese: string | undefined): SpacingKind | undefined => {
|
|
79
|
+
const kind = latinBeside(japanese, chars[last], "after-digit");
|
|
80
|
+
return kind === "letter" && DIGIT.test(runStart(chars, last) ?? "") ? "after-digit" : kind;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const boundaryAt = (chars: readonly string[], index: number): Omit<Boundary, "offset"> | undefined => {
|
|
84
|
+
const [left, right, after] = [chars[index], chars[index + 1], chars[index + 2]];
|
|
85
|
+
const touching = latinBeside(left, right, "before-digit") ?? leftRunBeside(chars, index, right);
|
|
86
|
+
if (touching !== undefined) return { kind: touching, spaced: false };
|
|
87
|
+
if (right !== " ") return undefined;
|
|
88
|
+
const spaced = latinBeside(left, after, "before-digit") ?? leftRunBeside(chars, index, after);
|
|
89
|
+
return spaced === undefined ? undefined : { kind: spaced, spaced: true };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** 文の中の境目を全部。offset は境目の位置(左の字の直後、UTF-16)。配列を作り直さずに一度なめる。 */
|
|
93
|
+
export const latinBoundaries = (text: string): Boundary[] => {
|
|
94
|
+
const chars = [...text];
|
|
95
|
+
const found: Boundary[] = [];
|
|
96
|
+
let offset = 0;
|
|
97
|
+
chars.forEach((char, index) => {
|
|
98
|
+
offset += char.length;
|
|
99
|
+
const boundary = boundaryAt(chars, index);
|
|
100
|
+
if (boundary !== undefined) found.push({ ...boundary, offset });
|
|
101
|
+
});
|
|
102
|
+
return found;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 混ざっているとき、そろえるべき少数派。種類(英字・数字の前・数字の後ろ)ごとに数える。
|
|
107
|
+
* 同数なら、文書が先に使った書き方をその文書の書き方とみなし、後から出た書き方を少数派にする。
|
|
108
|
+
*/
|
|
109
|
+
export const minorityStyle = (boundaries: readonly Boundary[]): boolean | undefined => {
|
|
110
|
+
const spaced = boundaries.filter((boundary) => boundary.spaced).length;
|
|
111
|
+
const touching = boundaries.length - spaced;
|
|
112
|
+
if (spaced === 0 || touching === 0) return undefined;
|
|
113
|
+
if (spaced !== touching) return spaced < touching;
|
|
114
|
+
return !(boundaries[0]?.spaced ?? false);
|
|
115
|
+
};
|
package/src/plugin.ts
CHANGED
|
@@ -71,6 +71,79 @@ export type LanguageAdapter = {
|
|
|
71
71
|
readonly segment: (text: string) => Segmentation;
|
|
72
72
|
/** L2 rule が word_list で引く。アダプタが自分の言語のぶんだけを持つ。 */
|
|
73
73
|
readonly lexicons: Readonly<Record<string, Lexicon>>;
|
|
74
|
+
/** 文書の構造を読む型。無い言語では `chaff tree` がそう言って止まる。 */
|
|
75
|
+
readonly structure?: StructurePatterns;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ───────── 文書の構造 ─────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 行頭の番号 1 つ。「第3条」「2」「(a)」を読むのは言語パッケージで、
|
|
82
|
+
* 入れ子と番地を決めるのは core。core は番号の書き方を知らない。
|
|
83
|
+
*/
|
|
84
|
+
export type NumberedLine = {
|
|
85
|
+
readonly kind: "chapter" | "article" | "item";
|
|
86
|
+
/** 入れ子の深さ。条が 1、項が 2、号が 3 のように、言語パッケージが決める。条より外の編・章・節は 0 以下。 */
|
|
87
|
+
readonly depth: number;
|
|
88
|
+
/** 番地の部品。"3"、"4.2"、"a"。 */
|
|
89
|
+
readonly number: string;
|
|
90
|
+
/** true なら number だけで番地が決まる(条番号、4.2 のような通し番号)。false なら親の番地に続ける。 */
|
|
91
|
+
readonly absolute: boolean;
|
|
92
|
+
/** 書かれたままの番号。「第3条」「Section 4.2」。 */
|
|
93
|
+
readonly label: string;
|
|
94
|
+
/** 番号に続く見出し。「第3条(支払)」の「支払」。本文しか無ければ空。 */
|
|
95
|
+
readonly heading: string;
|
|
96
|
+
/** 番号の後ろの文字列全部。定義や参照はここから探す。 */
|
|
97
|
+
readonly rest: string;
|
|
98
|
+
/**
|
|
99
|
+
* 並びの中の位置。第三条は 3、(ii) は 2、(b) は 2、4.2 は 2。番号の抜けを見る rule が比べる。
|
|
100
|
+
* 枝番号(第3条の2)は並びの外なので付けない。読み方を知っているのは言語パッケージだけなので、core は計算しない。
|
|
101
|
+
*/
|
|
102
|
+
readonly ordinal?: number | undefined;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** 番号を読むときに見える周り。「(i)」がローマ数字か英字かは、開いている番号で決まる。 */
|
|
106
|
+
export type NumberingContext = {
|
|
107
|
+
readonly open: readonly NumberedLine[];
|
|
108
|
+
/** Markdown の見出しの行か。本文の「1. 」は箇条書きで、見出しの「1. 」は章番号。 */
|
|
109
|
+
readonly isHeading: boolean;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** 行の中で見つけたもの。start / end は渡した文字列の中の位置。 */
|
|
113
|
+
export type Mention = { readonly start: number; readonly end: number; readonly attrs: Readonly<Record<string, string | number>> };
|
|
114
|
+
|
|
115
|
+
export type StructurePatterns = {
|
|
116
|
+
readonly numbered: (line: string, context: NumberingContext) => NumberedLine | undefined;
|
|
117
|
+
/** 定義。attrs.term に定義された語。 */
|
|
118
|
+
readonly definitions: (text: string) => readonly Mention[];
|
|
119
|
+
/** 参照。attrs.target に正規化した番地("12.1")、attrs.label に書かれたまま。 */
|
|
120
|
+
readonly references: (text: string) => readonly Mention[];
|
|
121
|
+
/** 義務・禁止・許可。attrs.marker に語、attrs.type に must / must-not / may。 */
|
|
122
|
+
readonly obligations: (text: string) => readonly Mention[];
|
|
123
|
+
/** 数量。attrs.value に数、attrs.unit に単位。 */
|
|
124
|
+
readonly quantities: (text: string) => readonly Mention[];
|
|
125
|
+
/** 日付。attrs.value に "2024-04-01"・"2024-04"・"04-01"・"2024" のどれか。無い言語は日付を読まない。 */
|
|
126
|
+
readonly dates?: (text: string) => readonly Mention[];
|
|
127
|
+
/**
|
|
128
|
+
* 「1.5 倍」「2.5 days」の 1.5 は通し番号ではない。番号と後ろの文字列を渡し、後ろが単位なら true。
|
|
129
|
+
* 数字と点だけの通し番号は core が言語を問わず読むので、それを数量と見分けられるのは言語パッケージだけ。
|
|
130
|
+
*/
|
|
131
|
+
readonly countedAfter?: (number: string, rest: string) => boolean;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type StructureKind = "doc" | "section" | "chapter" | "article" | "item" | "definition" | "reference" | "obligation" | "quantity" | "date";
|
|
135
|
+
|
|
136
|
+
/** 番地の付いた木の節点。すべて元の文書の位置(UTF-16)と行を持つ。 */
|
|
137
|
+
export type StructureNode = {
|
|
138
|
+
readonly kind: StructureKind;
|
|
139
|
+
/** 節・条・項の番地。"3.2"、見出しは "h2.1"。定義や参照のような葉は空。 */
|
|
140
|
+
readonly address: string;
|
|
141
|
+
readonly span: Span;
|
|
142
|
+
readonly line: number;
|
|
143
|
+
readonly attrs: Readonly<Record<string, string | number>>;
|
|
144
|
+
readonly children: readonly StructureNode[];
|
|
145
|
+
/** 番号付きのまとまりの、並びの中の位置(NumberedLine.ordinal)。S 式には出さない。 */
|
|
146
|
+
readonly ordinal?: number;
|
|
74
147
|
};
|
|
75
148
|
|
|
76
149
|
// ───────── 文書モデルと rule ─────────
|
|
@@ -115,6 +188,8 @@ export type ProseDocument = {
|
|
|
115
188
|
readonly lexicons: Readonly<Record<string, Lexicon>>;
|
|
116
189
|
/** この種類の文書に無いと困る見出し。チームが chaff.yaml で決める。 */
|
|
117
190
|
readonly requiredSections: readonly string[];
|
|
191
|
+
/** 番地の付いた木(§27)。adapter が structure を持たない言語では無い。 */
|
|
192
|
+
readonly structure: StructureNode | undefined;
|
|
118
193
|
};
|
|
119
194
|
|
|
120
195
|
export type Severity = "error" | "warning" | "info";
|
package/src/position.ts
CHANGED
|
@@ -12,7 +12,27 @@ export const lineStarts = (source: string): number[] => {
|
|
|
12
12
|
return starts;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* offset を含む行。starts は昇順なので二分探索で引く。指摘が何千もある長い文書で、指摘ごとに全行をなめない。
|
|
17
|
+
* offset より前に行頭が無ければ(負の値、NaN)先頭の行とする。
|
|
18
|
+
*/
|
|
19
|
+
const lineIndexOf = (starts: readonly number[], offset: number): number => {
|
|
20
|
+
let low = 0;
|
|
21
|
+
let high = starts.length - 1;
|
|
22
|
+
let found = 0;
|
|
23
|
+
while (low <= high) {
|
|
24
|
+
const middle = (low + high) >> 1;
|
|
25
|
+
if ((starts[middle] ?? Number.POSITIVE_INFINITY) <= offset) {
|
|
26
|
+
found = middle;
|
|
27
|
+
low = middle + 1;
|
|
28
|
+
} else {
|
|
29
|
+
high = middle - 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return found;
|
|
33
|
+
};
|
|
34
|
+
|
|
15
35
|
export const placeOf = (starts: readonly number[], offset: number): Place => {
|
|
16
|
-
const index = starts
|
|
36
|
+
const index = lineIndexOf(starts, offset);
|
|
17
37
|
return { line: index + 1, column: offset - (starts[index] ?? 0) + 1 };
|
|
18
38
|
};
|
package/src/render/rules-json.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { RuleDefinition } from "../plugin.ts";
|
|
|
5
5
|
const now = (rule: RuleDefinition, config: Config, genre: string): Record<string, unknown> => {
|
|
6
6
|
const explicit = config.rules[rule.id];
|
|
7
7
|
if (explicit === "off") return { level: "off", why_off: "設定で止めている" };
|
|
8
|
+
const limit = rule.layer === "L4" ? undefined : config.limits[rule.id];
|
|
9
|
+
if (limit !== undefined) return { level: "normal", limit, set_as: "number" };
|
|
8
10
|
if (explicit !== undefined) return { level: explicit, limit: resolve(rule, explicit, genre).limit };
|
|
9
11
|
if (rule.status === "experimental" && !config.experimental) {
|
|
10
12
|
return { level: "off", why_off: "experimental な rule は既定で動かさない", turn_on_with: `npx chaff lint --experimental` };
|
|
@@ -12,6 +14,13 @@ const now = (rule: RuleDefinition, config: Config, genre: string): Record<string
|
|
|
12
14
|
return { level: "normal", limit: resolve(rule, "normal", genre).limit };
|
|
13
15
|
};
|
|
14
16
|
|
|
17
|
+
const yourSetting = (rule: RuleDefinition, config: Config): Record<string, unknown> | null => {
|
|
18
|
+
const level = config.rules[rule.id];
|
|
19
|
+
if (level === undefined) return null;
|
|
20
|
+
const limit = rule.layer === "L4" ? undefined : config.limits[rule.id];
|
|
21
|
+
return limit === undefined ? { level, from: config.path } : { level, limit, from: config.path };
|
|
22
|
+
};
|
|
23
|
+
|
|
15
24
|
/** ジャンルで数字が変わる rule があるので、いま効いている表と既定の表の両方を出す。 */
|
|
16
25
|
const levelsOf = (rule: RuleDefinition, genre: string): Record<string, unknown> => {
|
|
17
26
|
const effective = Object.fromEntries(
|
|
@@ -42,7 +51,7 @@ export const rulesJson = (rules: readonly RuleDefinition[], config: Config, lang
|
|
|
42
51
|
use_for: rule.use_for,
|
|
43
52
|
...levelsOf(rule, genre),
|
|
44
53
|
levels_you_can_set: definedLevels(rule),
|
|
45
|
-
your_setting:
|
|
54
|
+
your_setting: yourSetting(rule, config),
|
|
46
55
|
now: now(rule, config, genre),
|
|
47
56
|
})),
|
|
48
57
|
how_to_change: {
|
package/src/run.ts
CHANGED
|
@@ -43,9 +43,9 @@ const hasTokens = (doc: ProseDocument): boolean => doc.sentences.length === 0 ||
|
|
|
43
43
|
/** 要求を満たさない rule は動かせない。満たさないまま動かすと「指摘 0 件」が保証に見える。 */
|
|
44
44
|
const unmet = (rule: RuleDefinition, doc: ProseDocument): string | undefined => {
|
|
45
45
|
if (rule.languages !== undefined && !rule.languages.includes(doc.language)) return `${doc.language} 向けの rule ではないため`;
|
|
46
|
-
const missing = rule.requires.find((need) => !has(doc.capabilities, need));
|
|
46
|
+
const missing = rule.requires.filter((need) => need !== "structure").find((need) => !has(doc.capabilities, need));
|
|
47
47
|
if (missing !== undefined) return `この言語では${CAPABILITY_NAME[missing] ?? missing}が使えないため`;
|
|
48
|
-
if (rule.requires.
|
|
48
|
+
if (rule.requires.some((need) => need === "pos" || need === "lemma") && !hasTokens(doc)) return "アダプタが品詞を返さなかったため";
|
|
49
49
|
return undefined;
|
|
50
50
|
};
|
|
51
51
|
|
|
@@ -92,7 +92,19 @@ const compositeOf = (rule: RuleDefinition, found: readonly Finding[], limit: num
|
|
|
92
92
|
];
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
/** chaff.yaml の rules に数値で書いた上限。段階の表より先に効く。 */
|
|
96
|
+
export type Limits = Readonly<Record<string, number>>;
|
|
97
|
+
|
|
98
|
+
const limitFor = (rule: RuleDefinition, level: Level, genre: string, limits: Limits): number => limits[rule.id] ?? resolve(rule, level, genre).limit;
|
|
99
|
+
|
|
100
|
+
export const runRules = (
|
|
101
|
+
doc: ProseDocument,
|
|
102
|
+
rules: readonly RuleDefinition[],
|
|
103
|
+
settings: Settings,
|
|
104
|
+
experimental: boolean,
|
|
105
|
+
genre: string,
|
|
106
|
+
limits: Limits = {},
|
|
107
|
+
): RunResult => {
|
|
96
108
|
const starts = lineStarts(doc.source);
|
|
97
109
|
const applicable = forGenre(rules, genre);
|
|
98
110
|
const forced = applicable
|
|
@@ -111,12 +123,17 @@ export const runRules = (doc: ProseDocument, rules: readonly RuleDefinition[], s
|
|
|
111
123
|
const why = rule.status === "experimental" && settings[rule.id] === undefined ? "まだ試験中のため" : "設定で止めているため";
|
|
112
124
|
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why }] };
|
|
113
125
|
}
|
|
126
|
+
// 木は capability ではなく、adapter が structure を持つかで決まる。持たない言語で動かすと「参照先が無い」が 0 件に見える。
|
|
127
|
+
// 段階を見た後で聞く。doc.structure は触れたときに木を作るので、止めている rule のために作らない。
|
|
128
|
+
if (rule.requires.includes("structure") && doc.structure === undefined) {
|
|
129
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: `${doc.language} のパッケージは文書の構造を読めないため` }] };
|
|
130
|
+
}
|
|
114
131
|
// 複合シグナルは二段目で扱う。一段目では「検出器が無い」と言わせない。
|
|
115
132
|
if (rule.from.length > 0) return acc;
|
|
116
133
|
const detector = DETECTORS[rule.how_to_find];
|
|
117
134
|
if (detector === undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: `検出器 ${rule.how_to_find} がないため` }] };
|
|
118
135
|
const options = {
|
|
119
|
-
limit:
|
|
136
|
+
limit: limitFor(rule, level, genre, limits),
|
|
120
137
|
lexicon: rule.word_list === undefined ? undefined : doc.lexicons[rule.word_list],
|
|
121
138
|
where: rule.where,
|
|
122
139
|
};
|
|
@@ -131,7 +148,7 @@ export const runRules = (doc: ProseDocument, rules: readonly RuleDefinition[], s
|
|
|
131
148
|
);
|
|
132
149
|
const composites = applicable
|
|
133
150
|
.filter((rule) => rule.from.length > 0 && levelFor(rule, settings, experimental) !== "off")
|
|
134
|
-
.flatMap((rule) => compositeOf(rule, outcome.findings,
|
|
151
|
+
.flatMap((rule) => compositeOf(rule, outcome.findings, limitFor(rule, levelFor(rule, settings, experimental), genre, limits), starts));
|
|
135
152
|
const all = [...outcome.findings, ...composites];
|
|
136
153
|
return { ...outcome, findings: [...all].sort((left, right) => left.line - right.line), forcedExperimental: forced };
|
|
137
154
|
};
|