chaffjs 0.6.0 → 0.8.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 +11 -4
- package/dist/adapter-load.d.ts +12 -1
- package/dist/adapter-load.d.ts.map +1 -1
- package/dist/adapter-load.js +37 -13
- package/dist/adapter-load.js.map +1 -1
- package/dist/cli-args.d.ts +8 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +8 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli-text.d.ts +26 -0
- package/dist/cli-text.d.ts.map +1 -0
- package/dist/cli-text.js +151 -0
- package/dist/cli-text.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +83 -86
- package/dist/cli.js.map +1 -1
- package/dist/commands/cite.d.ts +2 -1
- package/dist/commands/cite.d.ts.map +1 -1
- package/dist/commands/cite.js +39 -18
- package/dist/commands/cite.js.map +1 -1
- 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/skill.d.ts +22 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +68 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/tree.d.ts +11 -1
- package/dist/commands/tree.d.ts.map +1 -1
- package/dist/commands/tree.js +18 -6
- package/dist/commands/tree.js.map +1 -1
- 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 +11 -0
- package/dist/config/rule-problems.d.ts.map +1 -0
- package/dist/config/rule-problems.js +31 -0
- package/dist/config/rule-problems.js.map +1 -0
- package/dist/config/write.d.ts.map +1 -1
- package/dist/config/write.js +29 -9
- package/dist/config/write.js.map +1 -1
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +3 -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/document.d.ts +3 -0
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +13 -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 +10 -5
- package/dist/eval.js.map +1 -1
- package/dist/genre.d.ts +1 -1
- package/dist/genre.d.ts.map +1 -1
- package/dist/genre.js +3 -3
- package/dist/genre.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/init.d.ts +2 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +62 -13
- package/dist/init.js.map +1 -1
- 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 +6 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/reasons.d.ts +15 -0
- package/dist/reasons.d.ts.map +1 -0
- package/dist/reasons.js +29 -0
- package/dist/reasons.js.map +1 -0
- package/dist/render/explain.d.ts.map +1 -1
- package/dist/render/explain.js +36 -12
- package/dist/render/explain.js.map +1 -1
- package/dist/render/friendly.d.ts.map +1 -1
- package/dist/render/friendly.js +32 -9
- package/dist/render/friendly.js.map +1 -1
- package/dist/render/rules-json.d.ts.map +1 -1
- package/dist/render/rules-json.js +57 -27
- package/dist/render/rules-json.js.map +1 -1
- package/dist/render/summary.d.ts +3 -2
- package/dist/render/summary.d.ts.map +1 -1
- package/dist/render/summary.js +24 -9
- package/dist/render/summary.js.map +1 -1
- package/dist/render/suppressions.d.ts +2 -1
- package/dist/render/suppressions.d.ts.map +1 -1
- package/dist/render/suppressions.js +34 -10
- package/dist/render/suppressions.js.map +1 -1
- package/dist/run.d.ts +3 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +22 -13
- package/dist/run.js.map +1 -1
- package/dist/structure/build.d.ts.map +1 -1
- package/dist/structure/build.js +19 -10
- package/dist/structure/build.js.map +1 -1
- package/dist/structure/cite.d.ts.map +1 -1
- package/dist/structure/cite.js +3 -1
- package/dist/structure/cite.js.map +1 -1
- package/dist/structure/issues.d.ts +5 -2
- package/dist/structure/issues.d.ts.map +1 -1
- package/dist/structure/issues.js +27 -9
- package/dist/structure/issues.js.map +1 -1
- package/dist/structure/numbering.d.ts.map +1 -1
- package/dist/structure/numbering.js +11 -6
- package/dist/structure/numbering.js.map +1 -1
- package/dist/ui.d.ts +14 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +14 -0
- package/dist/ui.js.map +1 -0
- package/dist/watch.d.ts +1 -1
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +2 -2
- package/dist/watch.js.map +1 -1
- package/package.json +3 -2
- package/rules/latin-spacing.yaml +27 -0
- package/rules/preferred-term.yaml +27 -0
- package/skills/chaff/SKILL.md +61 -0
- package/src/adapter-load.ts +41 -10
- package/src/cli-args.ts +9 -0
- package/src/cli-text.ts +182 -0
- package/src/cli.ts +100 -89
- package/src/commands/cite.ts +51 -18
- package/src/commands/eval.ts +1 -0
- package/src/commands/skill.ts +74 -0
- package/src/commands/tree.ts +27 -7
- package/src/config/load.ts +43 -1
- package/src/config/rule-problems.ts +43 -0
- package/src/config/write.ts +33 -9
- package/src/detectors/index.ts +3 -0
- package/src/detectors/orthography.ts +55 -0
- package/src/document.ts +23 -4
- package/src/eval.ts +16 -5
- package/src/genre.ts +4 -4
- package/src/heading-text.ts +26 -0
- package/src/init.ts +74 -15
- package/src/orthography.ts +115 -0
- package/src/plugin.ts +6 -0
- package/src/reasons.ts +44 -0
- package/src/render/explain.ts +46 -12
- package/src/render/friendly.ts +42 -10
- package/src/render/rules-json.ts +45 -10
- package/src/render/summary.ts +34 -9
- package/src/render/suppressions.ts +45 -10
- package/src/run.ts +35 -12
- package/src/structure/build.ts +20 -10
- package/src/structure/cite.ts +3 -1
- package/src/structure/issues.ts +33 -14
- package/src/structure/numbering.ts +12 -7
- package/src/ui.ts +23 -0
- package/src/watch.ts +2 -2
package/src/run.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { DETECTORS } from "./detectors/index.ts";
|
|
|
2
2
|
import { resolve } from "./levels.ts";
|
|
3
3
|
import type { AdapterNeeds, Finding, Level, ProseDocument, RuleDefinition } from "./plugin.ts";
|
|
4
4
|
import { lineStarts, placeOf } from "./position.ts";
|
|
5
|
+
import { REASONS, type Reasons } from "./reasons.ts";
|
|
6
|
+
import { uiLanguageOf } from "./ui.ts";
|
|
5
7
|
|
|
6
8
|
export type Skipped = { readonly rule: string; readonly why: string };
|
|
7
9
|
|
|
@@ -22,7 +24,7 @@ const levelFor = (rule: RuleDefinition, settings: Settings, experimental: boolea
|
|
|
22
24
|
return "normal";
|
|
23
25
|
};
|
|
24
26
|
|
|
25
|
-
const
|
|
27
|
+
const reasonsFor = (doc: ProseDocument): Reasons => REASONS[uiLanguageOf(doc.language)];
|
|
26
28
|
|
|
27
29
|
/** 知らない要求は満たされていないものとして扱う。黙って無視すると、要求なしで動いてしまう。 */
|
|
28
30
|
const has = (capabilities: ProseDocument["capabilities"], need: string): boolean => {
|
|
@@ -42,13 +44,19 @@ const hasTokens = (doc: ProseDocument): boolean => doc.sentences.length === 0 ||
|
|
|
42
44
|
|
|
43
45
|
/** 要求を満たさない rule は動かせない。満たさないまま動かすと「指摘 0 件」が保証に見える。 */
|
|
44
46
|
const unmet = (rule: RuleDefinition, doc: ProseDocument): string | undefined => {
|
|
45
|
-
if (rule.languages !== undefined && !rule.languages.includes(doc.language)) return
|
|
47
|
+
if (rule.languages !== undefined && !rule.languages.includes(doc.language)) return reasonsFor(doc).otherLanguage(doc.language);
|
|
46
48
|
const missing = rule.requires.filter((need) => need !== "structure").find((need) => !has(doc.capabilities, need));
|
|
47
|
-
if (missing !== undefined) return
|
|
48
|
-
if (rule.requires.some((need) => need === "pos" || need === "lemma") && !hasTokens(doc)) return "アダプタが品詞を返さなかったため";
|
|
49
|
+
if (missing !== undefined) return reasonsFor(doc).noCapability(missing);
|
|
49
50
|
return undefined;
|
|
50
51
|
};
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 品詞は、動かす rule があるときだけ用意する(neededBy)。だから「品詞が無い」は、段階を見た後でしか言えない。
|
|
55
|
+
* 先に聞くと、止めている rule まで「アダプタが品詞を返さなかった」と、違う理由で出る。
|
|
56
|
+
*/
|
|
57
|
+
const untagged = (rule: RuleDefinition, doc: ProseDocument): string | undefined =>
|
|
58
|
+
rule.requires.some((need) => need === "pos" || need === "lemma") && !hasTokens(doc) ? reasonsFor(doc).noTags : undefined;
|
|
59
|
+
|
|
52
60
|
const forGenre = (rules: readonly RuleDefinition[], genre: string): RuleDefinition[] =>
|
|
53
61
|
rules.filter((rule) => rule.use_for.some((target) => genre.startsWith(target)));
|
|
54
62
|
|
|
@@ -92,7 +100,19 @@ const compositeOf = (rule: RuleDefinition, found: readonly Finding[], limit: num
|
|
|
92
100
|
];
|
|
93
101
|
};
|
|
94
102
|
|
|
95
|
-
|
|
103
|
+
/** chaff.yaml の rules に数値で書いた上限。段階の表より先に効く。 */
|
|
104
|
+
export type Limits = Readonly<Record<string, number>>;
|
|
105
|
+
|
|
106
|
+
const limitFor = (rule: RuleDefinition, level: Level, genre: string, limits: Limits): number => limits[rule.id] ?? resolve(rule, level, genre).limit;
|
|
107
|
+
|
|
108
|
+
export const runRules = (
|
|
109
|
+
doc: ProseDocument,
|
|
110
|
+
rules: readonly RuleDefinition[],
|
|
111
|
+
settings: Settings,
|
|
112
|
+
experimental: boolean,
|
|
113
|
+
genre: string,
|
|
114
|
+
limits: Limits = {},
|
|
115
|
+
): RunResult => {
|
|
96
116
|
const starts = lineStarts(doc.source);
|
|
97
117
|
const applicable = forGenre(rules, genre);
|
|
98
118
|
const forced = applicable
|
|
@@ -102,32 +122,35 @@ export const runRules = (doc: ProseDocument, rules: readonly RuleDefinition[], s
|
|
|
102
122
|
(acc, rule) => {
|
|
103
123
|
// L4 は意味を読む検査。chaff test が扱う。ここで「検出器が無い」と言わせない。
|
|
104
124
|
if (rule.layer === "L4") {
|
|
105
|
-
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why:
|
|
125
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).semantic }] };
|
|
106
126
|
}
|
|
107
127
|
const blocked = unmet(rule, doc);
|
|
108
128
|
if (blocked !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: blocked }] };
|
|
109
129
|
const level = levelFor(rule, settings, experimental);
|
|
110
130
|
if (level === "off") {
|
|
111
|
-
const why = rule.status === "experimental" && settings[rule.id] === undefined ?
|
|
131
|
+
const why = rule.status === "experimental" && settings[rule.id] === undefined ? reasonsFor(doc).experimental : reasonsFor(doc).turnedOff;
|
|
112
132
|
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why }] };
|
|
113
133
|
}
|
|
134
|
+
const noTags = untagged(rule, doc);
|
|
135
|
+
if (noTags !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: noTags }] };
|
|
114
136
|
// 木は capability ではなく、adapter が structure を持つかで決まる。持たない言語で動かすと「参照先が無い」が 0 件に見える。
|
|
115
137
|
// 段階を見た後で聞く。doc.structure は触れたときに木を作るので、止めている rule のために作らない。
|
|
116
138
|
if (rule.requires.includes("structure") && doc.structure === undefined) {
|
|
117
|
-
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why:
|
|
139
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).noStructure(doc.language) }] };
|
|
118
140
|
}
|
|
119
141
|
// 複合シグナルは二段目で扱う。一段目では「検出器が無い」と言わせない。
|
|
120
142
|
if (rule.from.length > 0) return acc;
|
|
121
143
|
const detector = DETECTORS[rule.how_to_find];
|
|
122
|
-
if (detector === undefined)
|
|
144
|
+
if (detector === undefined)
|
|
145
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).noDetector(rule.how_to_find) }] };
|
|
123
146
|
const options = {
|
|
124
|
-
limit:
|
|
147
|
+
limit: limitFor(rule, level, genre, limits),
|
|
125
148
|
lexicon: rule.word_list === undefined ? undefined : doc.lexicons[rule.word_list],
|
|
126
149
|
where: rule.where,
|
|
127
150
|
};
|
|
128
151
|
// 語彙表を要求する rule で、その言語に語彙表が無ければ動かせない。黙って通さない。
|
|
129
152
|
if (rule.word_list !== undefined && options.lexicon === undefined) {
|
|
130
|
-
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why:
|
|
153
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).noLexicon(doc.language, rule.word_list) }] };
|
|
131
154
|
}
|
|
132
155
|
const found = detector(doc, options).map((finding) => place(starts, { ...finding, rule: rule.id, severity: rule.severity }));
|
|
133
156
|
return { findings: [...acc.findings, ...found], skipped: acc.skipped };
|
|
@@ -136,7 +159,7 @@ export const runRules = (doc: ProseDocument, rules: readonly RuleDefinition[], s
|
|
|
136
159
|
);
|
|
137
160
|
const composites = applicable
|
|
138
161
|
.filter((rule) => rule.from.length > 0 && levelFor(rule, settings, experimental) !== "off")
|
|
139
|
-
.flatMap((rule) => compositeOf(rule, outcome.findings,
|
|
162
|
+
.flatMap((rule) => compositeOf(rule, outcome.findings, limitFor(rule, levelFor(rule, settings, experimental), genre, limits), starts));
|
|
140
163
|
const all = [...outcome.findings, ...composites];
|
|
141
164
|
return { ...outcome, findings: [...all].sort((left, right) => left.line - right.line), forcedExperimental: forced };
|
|
142
165
|
};
|
package/src/structure/build.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Heading } from "../document.ts";
|
|
2
|
+
import { atxHeadingText, headingText } from "../heading-text.ts";
|
|
2
3
|
import { maskSpans } from "../mask.ts";
|
|
3
4
|
import type { Mention, NumberedLine, NumberingContext, Span, StructureKind, StructureNode, StructurePatterns } from "../plugin.ts";
|
|
4
5
|
import { lineNumberAt, linesOf, type Line } from "./lines.ts";
|
|
@@ -14,6 +15,8 @@ type Draft = {
|
|
|
14
15
|
readonly attrs: Readonly<Record<string, string | number>>;
|
|
15
16
|
readonly children: Draft[];
|
|
16
17
|
readonly ordinal?: number | undefined;
|
|
18
|
+
readonly level?: number | undefined;
|
|
19
|
+
readonly ordinalTo?: number | undefined;
|
|
17
20
|
};
|
|
18
21
|
|
|
19
22
|
/** 開いている節点。rank の大きいものほど内側。 */
|
|
@@ -87,7 +90,12 @@ const openNumbered = (state: State, line: Line, numbered: NumberedLine, headingD
|
|
|
87
90
|
const rank = headingDepth ?? NUMBERED_RANK + numbered.depth;
|
|
88
91
|
// 番地は、自分と同じか内側を閉じてから決める。閉じる前だと、同じ深さの前の条を親と取り違える。
|
|
89
92
|
while (state.stack.length > 1 && top(state).rank >= rank) state.stack.pop();
|
|
90
|
-
const draft = {
|
|
93
|
+
const draft = {
|
|
94
|
+
...draftOf(numbered.kind, addressOf(state, numbered), line, attrs),
|
|
95
|
+
ordinal: numbered.ordinal,
|
|
96
|
+
level: numbered.depth,
|
|
97
|
+
ordinalTo: numbered.ordinalTo,
|
|
98
|
+
};
|
|
91
99
|
open(state, { draft, rank, numbered });
|
|
92
100
|
};
|
|
93
101
|
|
|
@@ -135,6 +143,8 @@ const freeze = (draft: Draft): StructureNode => ({
|
|
|
135
143
|
attrs: draft.attrs,
|
|
136
144
|
children: draft.children.map(freeze),
|
|
137
145
|
...(draft.ordinal === undefined ? {} : { ordinal: draft.ordinal }),
|
|
146
|
+
...(draft.level === undefined ? {} : { level: draft.level }),
|
|
147
|
+
...(draft.ordinalTo === undefined ? {} : { ordinalTo: draft.ordinalTo }),
|
|
138
148
|
});
|
|
139
149
|
|
|
140
150
|
/** Markdown から取った手がかり。見出しと、中を読まない範囲(コード)。.txt はどちらも空。 */
|
|
@@ -155,18 +165,18 @@ const headingsByLine = (lines: readonly Line[], headings: readonly Heading[]): M
|
|
|
155
165
|
}),
|
|
156
166
|
);
|
|
157
167
|
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return (
|
|
168
|
+
/**
|
|
169
|
+
* 見出しの行から見出しの言葉を取る。コードを覆った後の行から取るので、見出しの中のコードも読まない。
|
|
170
|
+
* ATX(行頭が #)なら印と閉じの # を外す。setext(下線で書く見出し)の末尾の # は言葉なので残す。
|
|
171
|
+
*/
|
|
172
|
+
const headingLineText = (text: string): string => {
|
|
173
|
+
const line = text.trim();
|
|
174
|
+
if (!line.startsWith("#")) return headingText(line);
|
|
175
|
+
return atxHeadingText(line.replace(/^#{1,6}(?=[ \t]|$)/u, "").trim());
|
|
166
176
|
};
|
|
167
177
|
|
|
168
178
|
const readLine = (state: State, patterns: StructurePatterns, line: Line, heading: Heading | undefined): void => {
|
|
169
|
-
const text = heading === undefined ? line.text :
|
|
179
|
+
const text = heading === undefined ? line.text : headingLineText(line.text);
|
|
170
180
|
const openNumbers = state.stack.flatMap((frame) => (frame.numbered === undefined ? [] : [frame.numbered]));
|
|
171
181
|
const context = { open: openNumbers, isHeading: heading !== undefined };
|
|
172
182
|
const numbered = patterns.numbered(text, context) ?? universalNumber(patterns, text, context);
|
package/src/structure/cite.ts
CHANGED
|
@@ -100,7 +100,9 @@ export const checkCitations = (source: string, tree: StructureNode, citations: r
|
|
|
100
100
|
const whole = normalize(source);
|
|
101
101
|
const starts = lineStarts(source);
|
|
102
102
|
return citations.map((citation): CitationResult => {
|
|
103
|
-
|
|
103
|
+
// 第 1 項に番号の無い書き方では、「12.1」は第12条そのもの。参照の fallback と同じに読む。
|
|
104
|
+
const firstParagraph = citation.address.endsWith(".1") ? byAddress.get(citation.address.slice(0, -2)) : undefined;
|
|
105
|
+
const candidates = byAddress.get(citation.address) ?? firstParagraph ?? [];
|
|
104
106
|
const first = candidates[0];
|
|
105
107
|
if (first === undefined) return { citation, status: "missing-address" };
|
|
106
108
|
const key = normalize(citation.quote).text;
|
package/src/structure/issues.ts
CHANGED
|
@@ -34,30 +34,49 @@ const resolves = (node: StructureNode, addresses: ReadonlySet<string>): boolean
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* 参照の番地が木に無い。他の文書の名前が付いた参照(民法第709条、Section 9 of the Master Agreement)は引かない。
|
|
37
|
-
*
|
|
37
|
+
* 条を 1 つも持たない文書も見ない。他の文書を指しているだけかもしれない。
|
|
38
38
|
*/
|
|
39
39
|
export const danglingReferences = (tree: StructureNode): StructureIssue[] => {
|
|
40
40
|
const nodes = inDocumentOrder(tree);
|
|
41
|
+
// 参照は条を指す。条を一つも持たない文書(契約書に付ける承諾書のひな形など)の「契約書第6条」は、別の文書の条。
|
|
42
|
+
if (!nodes.some((node) => node.kind === "article")) return [];
|
|
41
43
|
const addresses = new Set(nodes.filter((node) => NUMBERED.includes(node.kind)).map((node) => node.address));
|
|
42
|
-
if (addresses.size === 0) return [];
|
|
43
44
|
return nodes
|
|
44
45
|
.filter((node) => node.kind === "reference" && node.attrs["document"] === undefined)
|
|
45
46
|
.filter((node) => !resolves(node, addresses))
|
|
46
47
|
.map((node) => ({ offset: node.span.start, values: { label: textOf(node, "label"), target: textOf(node, "target") } }));
|
|
47
48
|
};
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
type Definition = { readonly node: StructureNode; readonly article: string };
|
|
51
|
+
|
|
52
|
+
/** 定義を文書の順に、それが置かれた条の番地と一緒に並べる。範囲を限った定義は、その条の中でだけ比べるため。 */
|
|
53
|
+
const definitionsInOrder = (tree: StructureNode): Definition[] => {
|
|
54
|
+
const found: Definition[] = [];
|
|
55
|
+
const pending: Definition[] = [{ node: tree, article: "" }];
|
|
56
|
+
while (pending.length > 0) {
|
|
57
|
+
const current = pending.pop();
|
|
58
|
+
if (current === undefined) break;
|
|
59
|
+
if (current.node.kind === "definition") found.push(current);
|
|
60
|
+
const article = current.node.kind === "article" ? current.node.address : current.article;
|
|
61
|
+
[...current.node.children].reverse().forEach((child) => pending.push({ node: child, article }));
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 同じ語の二度目以降の定義。どちらが正しいかは決めず、両方の場所を示す。
|
|
68
|
+
* 範囲を限った定義(この条において「X」とは)は、同じ条の中でだけ比べる。別の条で定義し直すのは正しい書き方。
|
|
69
|
+
*/
|
|
50
70
|
export const duplicateDefinitions = (tree: StructureNode): StructureIssue[] => {
|
|
51
71
|
const first = new Map<string, StructureNode>();
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
72
|
+
return definitionsInOrder(tree).flatMap(({ node, article }) => {
|
|
73
|
+
const term = textOf(node, "term");
|
|
74
|
+
const key = node.attrs["scope"] === "local" ? `${article}\u0000${term}` : term;
|
|
75
|
+
const earlier = first.get(key);
|
|
76
|
+
if (earlier === undefined) {
|
|
77
|
+
first.set(key, node);
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
return [{ offset: node.span.start, values: { term, first: earlier.line } }];
|
|
81
|
+
});
|
|
63
82
|
};
|
|
@@ -3,17 +3,21 @@ import { inDocumentOrder, type StructureIssue } from "./issues.ts";
|
|
|
3
3
|
|
|
4
4
|
const labelOf = (node: StructureNode): string => String(node.attrs["label"] ?? node.address);
|
|
5
5
|
|
|
6
|
-
type Ordered = { readonly node: StructureNode; readonly ordinal: number };
|
|
6
|
+
type Ordered = { readonly node: StructureNode; readonly ordinal: number; readonly last: number };
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* 同じ親の子のうち、番号の付いたものを種類と深さごとに分ける。条と項は別の並び、項と号も別の並び。
|
|
10
|
+
* 法令の第 1 項は番号が無いので、その号(一、二)と第 2 項(2)は同じ条の直下に並ぶ。深さで分けないと「二の次が2」に見える。
|
|
11
|
+
*/
|
|
9
12
|
const sequencesOf = (parent: StructureNode): Ordered[][] => {
|
|
10
13
|
const groups = new Map<string, Ordered[]>();
|
|
11
14
|
parent.children.forEach((node) => {
|
|
12
15
|
if (node.ordinal === undefined) return;
|
|
16
|
+
const key = `${node.kind}/${String(node.level ?? "")}`;
|
|
13
17
|
// 配列を作り直さずに足す。条が何千もある法令で二乗にしない。
|
|
14
|
-
const group = groups.get(
|
|
15
|
-
group.push({ node, ordinal: node.ordinal });
|
|
16
|
-
groups.set(
|
|
18
|
+
const group = groups.get(key) ?? [];
|
|
19
|
+
group.push({ node, ordinal: node.ordinal, last: node.ordinalTo ?? node.ordinal });
|
|
20
|
+
groups.set(key, group);
|
|
17
21
|
});
|
|
18
22
|
return [...groups.values()];
|
|
19
23
|
};
|
|
@@ -23,11 +27,12 @@ const breaksIn = (sequence: readonly Ordered[]): StructureIssue[] =>
|
|
|
23
27
|
sequence.flatMap((current, index) => {
|
|
24
28
|
const previous = sequence[index - 1];
|
|
25
29
|
// 1 に戻ったら新しい並び。一つの条に (a)(b) の箇条書きが二つある、附則が第1条から振り直す、のどちらも誤りではない。
|
|
26
|
-
|
|
30
|
+
// 範囲をまとめた行(第四十三条から第五十五条まで)の次は、範囲の最後から数える。
|
|
31
|
+
if (previous === undefined || current.ordinal === previous.last + 1 || current.ordinal === 1) return [];
|
|
27
32
|
return [
|
|
28
33
|
{
|
|
29
34
|
offset: current.node.span.start,
|
|
30
|
-
values: { previous: labelOf(previous.node), label: labelOf(current.node), expected: previous.
|
|
35
|
+
values: { previous: labelOf(previous.node), label: labelOf(current.node), expected: previous.last + 1, found: current.ordinal },
|
|
31
36
|
},
|
|
32
37
|
];
|
|
33
38
|
});
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The language chaff speaks in. Japanese for Japanese, English for everything else: English is the language a
|
|
3
|
+
* reader of any other language is likeliest to read, and every message has an English text.
|
|
4
|
+
*/
|
|
5
|
+
export type UiLanguage = "ja" | "en";
|
|
6
|
+
|
|
7
|
+
export const uiLanguageOf = (language: string | undefined): UiLanguage => (language === "ja" ? "ja" : "en");
|
|
8
|
+
|
|
9
|
+
/** Locale variables in the order POSIX gives them precedence. */
|
|
10
|
+
const LOCALE_VARIABLES: readonly string[] = ["LC_ALL", "LC_MESSAGES", "LANG"];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The language for output that is not about one document (--help, genres, init, explain, relax, warnings):
|
|
14
|
+
* the language `chaff.yaml` sets, else the terminal's locale, else English. Pure: `env` is passed in.
|
|
15
|
+
*/
|
|
16
|
+
export const hostLanguage = (configLanguage: string | undefined, env: Readonly<Record<string, string | undefined>>): UiLanguage => {
|
|
17
|
+
if (configLanguage !== undefined) return uiLanguageOf(configLanguage);
|
|
18
|
+
const locale = LOCALE_VARIABLES.map((name) => env[name]).find((value) => value !== undefined && value !== "");
|
|
19
|
+
return locale !== undefined && locale.toLowerCase().startsWith("ja") ? "ja" : "en";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Text in both languages. Each module keeps its own, next to where it is used. */
|
|
23
|
+
export type Texts<T> = Readonly<Record<UiLanguage, T>>;
|
package/src/watch.ts
CHANGED
|
@@ -15,7 +15,7 @@ const total = (snapshot: Snapshot): number => Object.values(snapshot).reduce((su
|
|
|
15
15
|
* 差分だけを出す。書いている最中に全件を出し直されると、何が変わったのか分からない。
|
|
16
16
|
* workflow spec §11。
|
|
17
17
|
*/
|
|
18
|
-
export const describeChange = (before: Snapshot, after: Snapshot): string | undefined => {
|
|
18
|
+
export const describeChange = (before: Snapshot, after: Snapshot, unit = " 件"): string | undefined => {
|
|
19
19
|
const rules = [...new Set([...Object.keys(before), ...Object.keys(after)])];
|
|
20
20
|
const moved = rules
|
|
21
21
|
.map((rule) => ({ rule, delta: (after[rule] ?? 0) - (before[rule] ?? 0) }))
|
|
@@ -24,7 +24,7 @@ export const describeChange = (before: Snapshot, after: Snapshot): string | unde
|
|
|
24
24
|
if (moved.length === 0) return undefined;
|
|
25
25
|
const detail = moved.map((entry) => `${entry.delta > 0 ? "+" : ""}${entry.delta} ${entry.rule}`).join(", ");
|
|
26
26
|
const mark = total(after) < total(before) ? "✓" : "✗";
|
|
27
|
-
return `${mark} ${total(before)} → ${total(after)}
|
|
27
|
+
return `${mark} ${total(before)} → ${total(after)}${unit} (${detail})`;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
export const clock = (): string => new Date().toTimeString().slice(0, 8);
|