@sushichan044/kg-core 0.6.5 → 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 +62 -13
- package/dist/composed-manuscript-QgIdae8p.d.mts +1740 -0
- package/dist/describe-cause-BV8mZ3Um.mjs +18 -0
- package/dist/index.d.mts +6481 -18298
- package/dist/index.mjs +50 -2475
- package/dist/lint.d.mts +461 -0
- package/dist/lint.mjs +812 -0
- package/dist/novel-composer-BCt9g172.d.mts +9781 -0
- package/dist/novel-composer-CROZLBqt.mjs +1929 -0
- package/dist/plugin.d.mts +3 -0
- package/dist/plugin.mjs +3 -0
- package/dist/question-or-exclamation-spacing-B5o23ZLf.mjs +251 -0
- package/package.json +11 -4
package/dist/lint.mjs
ADDED
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
import { a as characterClass, c as ManuscriptResult, d as NamespacedId, f as ManuscriptRange, h as readonlyObject, i as OPENING_BRACKETS, l as ManuscriptDiagnostic, m as readonlyArray, n as leadingSpaces, o as graphemeSegmenter, r as CLOSING_BRACKETS, s as ValidationIssue, t as questionOrExclamationSpacings, u as DiagnosticSeverity } from "./question-or-exclamation-spacing-B5o23ZLf.mjs";
|
|
2
|
+
import { n as assertNever, t as describeCause } from "./describe-cause-BV8mZ3Um.mjs";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
//#region src/proofreading/invalid-rule-options.ts
|
|
5
|
+
const InvalidRuleOptions = { describe: (error) => `ルール ${error.ruleId} のオプション "${error.option}" が不正です: ${error.issues.map(({ message }) => message).join(", ")}` };
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/proofreading/proofread-error.ts
|
|
8
|
+
const ProofreadError = {
|
|
9
|
+
/**
|
|
10
|
+
* Human-readable rendering. Branch on `kind` instead when the caller needs to react.
|
|
11
|
+
*/
|
|
12
|
+
describe: (error) => {
|
|
13
|
+
switch (error.kind) {
|
|
14
|
+
case "InvalidRuleId": return `ルール ID "${error.ruleId}" は namespace/name の形式ではありません`;
|
|
15
|
+
case "DuplicateRuleId": return `ルール ID ${error.ruleId} が重複しています`;
|
|
16
|
+
case "InvalidRuleMetadata": return `ルール ${error.ruleId} の metadata が不正です: ${joinIssues(error.issues)}`;
|
|
17
|
+
case "InvalidReport": return `ルール ${error.ruleId} の報告が契約を満たしていません: ${joinIssues(error.issues)}`;
|
|
18
|
+
case "UnknownMessageId": return `ルール ${error.ruleId} が未宣言のメッセージ ID "${error.messageId}" を報告しました`;
|
|
19
|
+
case "RuleThrew": return `ルール ${error.ruleId} が例外を投げました: ${describeCause(error.cause)}`;
|
|
20
|
+
default: return assertNever(error);
|
|
21
|
+
}
|
|
22
|
+
} };
|
|
23
|
+
function joinIssues(issues) {
|
|
24
|
+
return issues.map(({ message }) => message).join(", ");
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/proofreading/internal/interpolate.ts
|
|
28
|
+
/**
|
|
29
|
+
* Fills `{{ name }}` placeholders from the report's data; an absent key renders as empty.
|
|
30
|
+
*/
|
|
31
|
+
function interpolate(template, data) {
|
|
32
|
+
return template.replaceAll(/\{\{\s*([^}\s]+)\s*\}\}/gu, (_match, key) => String(data?.[key] ?? ""));
|
|
33
|
+
}
|
|
34
|
+
const ProofreadingReport = { schema: readonlyObject({
|
|
35
|
+
range: ManuscriptRange.schema,
|
|
36
|
+
messageId: v.string(),
|
|
37
|
+
data: v.optional(v.pipe(v.record(v.string(), v.union([v.string(), v.number()])), v.readonly())),
|
|
38
|
+
/**
|
|
39
|
+
* Omitted means `error`: a rule that says nothing is claiming it found a real defect.
|
|
40
|
+
*/
|
|
41
|
+
severity: v.optional(DiagnosticSeverity.schema)
|
|
42
|
+
}) };
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/proofreading/proofreading-rule.ts
|
|
45
|
+
const ProofreadingRuleMessagesSchema = v.pipe(v.record(v.string(), v.string()), v.readonly());
|
|
46
|
+
const ProofreadingRuleMeta = {
|
|
47
|
+
schema: readonlyObject({
|
|
48
|
+
id: v.string(),
|
|
49
|
+
messages: ProofreadingRuleMessagesSchema
|
|
50
|
+
}),
|
|
51
|
+
messagesSchema: ProofreadingRuleMessagesSchema
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/proofreading/proofread-manuscript.ts
|
|
55
|
+
const reportsSchema = readonlyArray(ProofreadingReport.schema);
|
|
56
|
+
function prepareRules(rules) {
|
|
57
|
+
const prepared = [];
|
|
58
|
+
const seen = /* @__PURE__ */ new Set();
|
|
59
|
+
for (const rule of rules) {
|
|
60
|
+
const id = NamespacedId.parse(rule.meta.id);
|
|
61
|
+
if (id === void 0) return ManuscriptResult.fail({
|
|
62
|
+
kind: "InvalidRuleId",
|
|
63
|
+
ruleId: rule.meta.id
|
|
64
|
+
});
|
|
65
|
+
if (seen.has(id)) return ManuscriptResult.fail({
|
|
66
|
+
kind: "DuplicateRuleId",
|
|
67
|
+
ruleId: id
|
|
68
|
+
});
|
|
69
|
+
const messages = v.safeParse(ProofreadingRuleMeta.messagesSchema, rule.meta.messages);
|
|
70
|
+
if (!messages.success) return ManuscriptResult.fail({
|
|
71
|
+
kind: "InvalidRuleMetadata",
|
|
72
|
+
ruleId: id,
|
|
73
|
+
issues: ValidationIssue.from(messages.issues)
|
|
74
|
+
});
|
|
75
|
+
seen.add(id);
|
|
76
|
+
prepared.push({
|
|
77
|
+
id,
|
|
78
|
+
messages: messages.output,
|
|
79
|
+
rule
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return ManuscriptResult.succeed(prepared);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Runs one rule and returns what it reported. A composed-only rule against a parsed manuscript is
|
|
86
|
+
* simply skipped: the caller asked for a stage this rule cannot see.
|
|
87
|
+
*/
|
|
88
|
+
function collectReports({ id, rule }, parsed, composed) {
|
|
89
|
+
const reports = [];
|
|
90
|
+
const context = { report: (report) => {
|
|
91
|
+
reports.push(report);
|
|
92
|
+
} };
|
|
93
|
+
try {
|
|
94
|
+
switch (rule.kind) {
|
|
95
|
+
case "parsed":
|
|
96
|
+
rule.check(parsed, context);
|
|
97
|
+
break;
|
|
98
|
+
case "composed":
|
|
99
|
+
if (composed !== null) rule.check(composed, context);
|
|
100
|
+
break;
|
|
101
|
+
default: assertNever(rule);
|
|
102
|
+
}
|
|
103
|
+
} catch (cause) {
|
|
104
|
+
return ManuscriptResult.fail({
|
|
105
|
+
kind: "RuleThrew",
|
|
106
|
+
ruleId: id,
|
|
107
|
+
cause
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const validated = v.safeParse(reportsSchema, reports);
|
|
111
|
+
return validated.success ? ManuscriptResult.succeed(validated.output) : ManuscriptResult.fail({
|
|
112
|
+
kind: "InvalidReport",
|
|
113
|
+
ruleId: id,
|
|
114
|
+
issues: ValidationIssue.from(validated.issues)
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function toDiagnostics({ id, messages }, reports, source) {
|
|
118
|
+
const diagnostics = [];
|
|
119
|
+
for (const report of reports) {
|
|
120
|
+
const template = messages[report.messageId];
|
|
121
|
+
if (template === void 0) return ManuscriptResult.fail({
|
|
122
|
+
kind: "UnknownMessageId",
|
|
123
|
+
ruleId: id,
|
|
124
|
+
messageId: report.messageId
|
|
125
|
+
});
|
|
126
|
+
diagnostics.push(ManuscriptDiagnostic.of({
|
|
127
|
+
source,
|
|
128
|
+
origin: {
|
|
129
|
+
kind: "rule",
|
|
130
|
+
id
|
|
131
|
+
},
|
|
132
|
+
severity: report.severity ?? "error",
|
|
133
|
+
code: report.messageId,
|
|
134
|
+
message: interpolate(template, report.data),
|
|
135
|
+
range: report.range
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
return ManuscriptResult.succeed(diagnostics);
|
|
139
|
+
}
|
|
140
|
+
function run(parsed, composed, rules) {
|
|
141
|
+
const prepared = prepareRules(rules);
|
|
142
|
+
if (!prepared.ok) return prepared;
|
|
143
|
+
const diagnostics = [];
|
|
144
|
+
for (const rule of prepared.value) {
|
|
145
|
+
const reports = collectReports(rule, parsed, composed);
|
|
146
|
+
if (!reports.ok) return reports;
|
|
147
|
+
const produced = toDiagnostics(rule, reports.value, parsed.source);
|
|
148
|
+
if (!produced.ok) return produced;
|
|
149
|
+
diagnostics.push(...produced.value);
|
|
150
|
+
}
|
|
151
|
+
return ManuscriptResult.succeed([...diagnostics].sort(ManuscriptDiagnostic.compare));
|
|
152
|
+
}
|
|
153
|
+
function proofreadManuscript(manuscript, options) {
|
|
154
|
+
return "composerId" in manuscript ? run(manuscript.parsed, manuscript, options.rules) : run(manuscript, null, options.rules);
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/proofreading/rules/internal/display-line.ts
|
|
158
|
+
/**
|
|
159
|
+
* Splits on CRLF, CR and LF. The text after the last break is always emitted, even when empty.
|
|
160
|
+
*/
|
|
161
|
+
function splitDisplayLines(text) {
|
|
162
|
+
const lines = [];
|
|
163
|
+
let start = 0;
|
|
164
|
+
let index = 0;
|
|
165
|
+
while (index < text.length) {
|
|
166
|
+
const character = text[index];
|
|
167
|
+
if (character !== "\n" && character !== "\r") {
|
|
168
|
+
index += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
lines.push({
|
|
172
|
+
text: text.slice(start, index),
|
|
173
|
+
start
|
|
174
|
+
});
|
|
175
|
+
index += character === "\r" && text[index + 1] === "\n" ? 2 : 1;
|
|
176
|
+
start = index;
|
|
177
|
+
}
|
|
178
|
+
lines.push({
|
|
179
|
+
text: text.slice(start),
|
|
180
|
+
start
|
|
181
|
+
});
|
|
182
|
+
return lines;
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/proofreading/rules/internal/rule-range.ts
|
|
186
|
+
const EMPTY_BOUNDS = {
|
|
187
|
+
start: 0,
|
|
188
|
+
end: 0
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Turns a span of display text into a full range. The source span is the smallest contiguous run
|
|
192
|
+
* that covers the display span, so it includes any notation the parser removed in between.
|
|
193
|
+
*/
|
|
194
|
+
function displayRange(manuscript, start, end) {
|
|
195
|
+
const covered = manuscript.graphemes.filter(({ range }) => range.display.start < end && range.display.end > start);
|
|
196
|
+
const first = covered[0];
|
|
197
|
+
const last = covered.at(-1);
|
|
198
|
+
if (first === void 0 || last === void 0) return ManuscriptRange.of({
|
|
199
|
+
source: EMPTY_BOUNDS,
|
|
200
|
+
display: {
|
|
201
|
+
start,
|
|
202
|
+
end
|
|
203
|
+
},
|
|
204
|
+
graphemes: EMPTY_BOUNDS
|
|
205
|
+
});
|
|
206
|
+
const merged = ManuscriptRange.merge(covered.map(({ range }) => range));
|
|
207
|
+
return ManuscriptRange.of({
|
|
208
|
+
source: {
|
|
209
|
+
start: first.range.source.start + clampWithin(start - first.range.display.start, first.range),
|
|
210
|
+
end: end === last.range.display.end ? last.range.source.end : last.range.source.start + clampWithin(end - last.range.display.start, last.range)
|
|
211
|
+
},
|
|
212
|
+
display: {
|
|
213
|
+
start,
|
|
214
|
+
end
|
|
215
|
+
},
|
|
216
|
+
graphemes: merged?.graphemes ?? EMPTY_BOUNDS
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* A display offset inside a grapheme cannot map past that grapheme's own source span.
|
|
221
|
+
*/
|
|
222
|
+
function clampWithin(offset, range) {
|
|
223
|
+
return Math.min(offset, range.source.end - range.source.start);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Turns a span of source text into a full range, for rules that inspect the raw source.
|
|
227
|
+
*/
|
|
228
|
+
function sourceRange(manuscript, source) {
|
|
229
|
+
const covered = manuscript.graphemes.filter(({ range }) => range.source.start < source.end && range.source.end > source.start);
|
|
230
|
+
const enclosing = manuscript.annotations.find(({ range }) => range.source.start <= source.start && range.source.end >= source.end);
|
|
231
|
+
const merged = ManuscriptRange.merge(covered.map(({ range }) => range)) ?? enclosing?.range ?? null;
|
|
232
|
+
return ManuscriptRange.of({
|
|
233
|
+
source,
|
|
234
|
+
display: merged?.display ?? EMPTY_BOUNDS,
|
|
235
|
+
graphemes: merged?.graphemes ?? EMPTY_BOUNDS
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/proofreading/rules/internal/define-rule.ts
|
|
240
|
+
const DEFAULT_MESSAGE_ID = "default";
|
|
241
|
+
/**
|
|
242
|
+
* Builds a single-message rule; the body reports ranges and the wrapper names the message.
|
|
243
|
+
*/
|
|
244
|
+
function defineParsedRule(id, message, check) {
|
|
245
|
+
return {
|
|
246
|
+
kind: "parsed",
|
|
247
|
+
meta: {
|
|
248
|
+
id,
|
|
249
|
+
messages: { [DEFAULT_MESSAGE_ID]: message }
|
|
250
|
+
},
|
|
251
|
+
check: (manuscript, context) => {
|
|
252
|
+
check(manuscript, (range, options) => {
|
|
253
|
+
context.report({
|
|
254
|
+
...options,
|
|
255
|
+
range,
|
|
256
|
+
messageId: DEFAULT_MESSAGE_ID
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Builds a rule that scans each display line with a sticky pattern. Matching per line rather than
|
|
264
|
+
* across the whole text keeps findings from spanning a line break.
|
|
265
|
+
*/
|
|
266
|
+
function defineMatchRule(options) {
|
|
267
|
+
return defineParsedRule(options.id, options.message, (manuscript, report) => {
|
|
268
|
+
for (const line of splitDisplayLines(manuscript.displayText)) {
|
|
269
|
+
options.pattern.lastIndex = 0;
|
|
270
|
+
let match = options.pattern.exec(line.text);
|
|
271
|
+
while (match !== null) {
|
|
272
|
+
if (options.accept?.(match) ?? true) {
|
|
273
|
+
const start = line.start + match.index;
|
|
274
|
+
report(displayRange(manuscript, start, start + match[0].length));
|
|
275
|
+
}
|
|
276
|
+
match = options.pattern.exec(line.text);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/proofreading/rules/consistent-kanji-opening.ts
|
|
283
|
+
const RULE_ID$5 = "kg/consistent-kanji-opening";
|
|
284
|
+
const MESSAGE = "「{{ closed }}」と「{{ opened }}」が混在しています。文脈上の使い分けか表記ゆれかを確認してください";
|
|
285
|
+
const KanjiOpeningPairSchema = v.pipe(readonlyObject({
|
|
286
|
+
closed: v.pipe(v.string(), v.nonEmpty()),
|
|
287
|
+
opened: v.pipe(v.string(), v.nonEmpty())
|
|
288
|
+
}), v.check(({ closed, opened }) => closed !== opened, "a pair must hold two different spellings"));
|
|
289
|
+
const PairsSchema = v.pipe(v.array(KanjiOpeningPairSchema), v.nonEmpty(), v.readonly());
|
|
290
|
+
const DEFAULT_PAIRS = [
|
|
291
|
+
{
|
|
292
|
+
closed: "出来る",
|
|
293
|
+
opened: "できる"
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
closed: "出来ない",
|
|
297
|
+
opened: "できない"
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
closed: "下さい",
|
|
301
|
+
opened: "ください"
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
closed: "頂く",
|
|
305
|
+
opened: "いただく"
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
closed: "致す",
|
|
309
|
+
opened: "いたす"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
closed: "何故",
|
|
313
|
+
opened: "なぜ"
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
closed: "為に",
|
|
317
|
+
opened: "ために"
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
closed: "時に",
|
|
321
|
+
opened: "ときに"
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
closed: "様に",
|
|
325
|
+
opened: "ように"
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
closed: "方が",
|
|
329
|
+
opened: "ほうが"
|
|
330
|
+
}
|
|
331
|
+
];
|
|
332
|
+
/**
|
|
333
|
+
* Reports the first kanji occurrence of every pair the manuscript also writes in kana. Whether the
|
|
334
|
+
* two spellings are a deliberate distinction is the author's call, so the finding is a warning.
|
|
335
|
+
*/
|
|
336
|
+
function build$3(pairs) {
|
|
337
|
+
return defineParsedRule(RULE_ID$5, MESSAGE, (manuscript, report) => {
|
|
338
|
+
for (const { closed, opened } of pairs) {
|
|
339
|
+
if (!manuscript.displayText.includes(opened)) continue;
|
|
340
|
+
const index = manuscript.displayText.indexOf(closed);
|
|
341
|
+
if (index === -1) continue;
|
|
342
|
+
report(displayRange(manuscript, index, index + closed.length), {
|
|
343
|
+
data: {
|
|
344
|
+
closed,
|
|
345
|
+
opened
|
|
346
|
+
},
|
|
347
|
+
severity: "warning"
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
const consistentKanjiOpeningRule = () => build$3(DEFAULT_PAIRS);
|
|
353
|
+
function createConsistentKanjiOpeningRule(options = {}) {
|
|
354
|
+
if (options.pairs === void 0) return ManuscriptResult.succeed(consistentKanjiOpeningRule());
|
|
355
|
+
const pairs = v.safeParse(PairsSchema, options.pairs);
|
|
356
|
+
if (!pairs.success) return ManuscriptResult.fail({
|
|
357
|
+
kind: "InvalidRuleOptions",
|
|
358
|
+
ruleId: RULE_ID$5,
|
|
359
|
+
option: "pairs",
|
|
360
|
+
issues: ValidationIssue.from(pairs.issues)
|
|
361
|
+
});
|
|
362
|
+
return ManuscriptResult.succeed(build$3(pairs.output));
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/proofreading/rules/internal/mixed-width.ts
|
|
366
|
+
/**
|
|
367
|
+
* Builds a rule that fires when a manuscript uses both widths of one character class. Which width
|
|
368
|
+
* is right is the author's decision, so the finding is a warning, and one finding at the first
|
|
369
|
+
* halfwidth occurrence says everything a list of every mixed character would.
|
|
370
|
+
*/
|
|
371
|
+
function defineMixedWidthRule({ id, message, halfwidth, fullwidth }) {
|
|
372
|
+
return defineParsedRule(id, message, (manuscript, report) => {
|
|
373
|
+
const found = halfwidth.exec(manuscript.displayText);
|
|
374
|
+
const counterpart = fullwidth.exec(manuscript.displayText);
|
|
375
|
+
if (found === null || counterpart === null) return;
|
|
376
|
+
report(displayRange(manuscript, found.index, found.index + found[0].length), {
|
|
377
|
+
data: {
|
|
378
|
+
halfwidth: found[0],
|
|
379
|
+
fullwidth: counterpart[0]
|
|
380
|
+
},
|
|
381
|
+
severity: "warning"
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/proofreading/rules/consistent-latin-width.ts
|
|
387
|
+
/**
|
|
388
|
+
* Halfwidth and fullwidth Latin letters (半角英字 and 全角英字) in one work. In vertical writing the two
|
|
389
|
+
* are set differently — a fullwidth letter stands upright on its own em, a halfwidth one joins a
|
|
390
|
+
* run that may turn sideways — so mixing them is 表記ゆれ the reader can see. Which one a work uses is
|
|
391
|
+
* its own choice, hence a warning.
|
|
392
|
+
*/
|
|
393
|
+
const consistentLatinWidthRule = () => defineMixedWidthRule({
|
|
394
|
+
id: "kg/consistent-latin-width",
|
|
395
|
+
message: "半角英字「{{ halfwidth }}」と全角英字「{{ fullwidth }}」が混在しています。作品内の方針を確認してください",
|
|
396
|
+
halfwidth: /[A-Za-z]/u,
|
|
397
|
+
fullwidth: /[A-Za-z]/u
|
|
398
|
+
});
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/proofreading/rules/consistent-numeral-width.ts
|
|
401
|
+
/**
|
|
402
|
+
* Halfwidth and fullwidth digits (半角数字 and 全角数字) in one work. A fullwidth digit stands upright on
|
|
403
|
+
* its own em; a halfwidth pair sets as 縦中横 inside one. Mixing them is 表記ゆれ, but which one a work
|
|
404
|
+
* uses is its own choice, hence a warning.
|
|
405
|
+
*/
|
|
406
|
+
const consistentNumeralWidthRule = () => defineMixedWidthRule({
|
|
407
|
+
id: "kg/consistent-numeral-width",
|
|
408
|
+
message: "半角数字「{{ halfwidth }}」と全角数字「{{ fullwidth }}」が混在しています。作品内の方針を確認してください",
|
|
409
|
+
halfwidth: /[0-9]/u,
|
|
410
|
+
fullwidth: /[0-9]/u
|
|
411
|
+
});
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/proofreading/rules/dash.ts
|
|
414
|
+
const RULE_ID$4 = "kg/dash";
|
|
415
|
+
const DEFAULT_PREFERRED = "―";
|
|
416
|
+
const DashCharacterSchema = v.picklist([
|
|
417
|
+
"―",
|
|
418
|
+
"—",
|
|
419
|
+
"─"
|
|
420
|
+
]);
|
|
421
|
+
const DASH_LIKE_RUN = /[—–―─━﹣--]+/gu;
|
|
422
|
+
const EVEN_COUNT_MESSAGE = "連続するダッシュの数は偶数にしてください";
|
|
423
|
+
function messageIdFor(run, preferred) {
|
|
424
|
+
if (run === "-") return void 0;
|
|
425
|
+
if (run !== preferred.repeat(run.length)) return "character";
|
|
426
|
+
return run.length % 2 === 1 ? "even-count" : void 0;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* The dash a novel sets is the 2倍ダッシュ: two copies of one character read as a single rule two ems
|
|
430
|
+
* long. That is why an odd run is a finding — half a dash is left over — and why the characters
|
|
431
|
+
* must match, since only a repeat of the same character joins without a seam. JLReq classes the
|
|
432
|
+
* dash as 分離禁止文字 (cl-08) for the same reason: the pair must not be split across lines.
|
|
433
|
+
*/
|
|
434
|
+
function build$2(preferred) {
|
|
435
|
+
const messages = {
|
|
436
|
+
character: `ダッシュには「${preferred}」を使ってください`,
|
|
437
|
+
"even-count": EVEN_COUNT_MESSAGE
|
|
438
|
+
};
|
|
439
|
+
return {
|
|
440
|
+
kind: "parsed",
|
|
441
|
+
meta: {
|
|
442
|
+
id: RULE_ID$4,
|
|
443
|
+
messages
|
|
444
|
+
},
|
|
445
|
+
check: (manuscript, context) => {
|
|
446
|
+
for (const line of splitDisplayLines(manuscript.displayText)) {
|
|
447
|
+
DASH_LIKE_RUN.lastIndex = 0;
|
|
448
|
+
let match = DASH_LIKE_RUN.exec(line.text);
|
|
449
|
+
while (match !== null) {
|
|
450
|
+
const run = match[0];
|
|
451
|
+
const messageId = messageIdFor(run, preferred);
|
|
452
|
+
if (messageId !== void 0) {
|
|
453
|
+
const start = line.start + match.index;
|
|
454
|
+
context.report({
|
|
455
|
+
range: displayRange(manuscript, start, start + run.length),
|
|
456
|
+
messageId
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
match = DASH_LIKE_RUN.exec(line.text);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
const dashRule = () => build$2(DEFAULT_PREFERRED);
|
|
466
|
+
function createDashRule(options = {}) {
|
|
467
|
+
if (options.preferred === void 0) return ManuscriptResult.succeed(dashRule());
|
|
468
|
+
const preferred = v.safeParse(DashCharacterSchema, options.preferred);
|
|
469
|
+
if (!preferred.success) return ManuscriptResult.fail({
|
|
470
|
+
kind: "InvalidRuleOptions",
|
|
471
|
+
ruleId: RULE_ID$4,
|
|
472
|
+
option: "preferred",
|
|
473
|
+
issues: ValidationIssue.from(preferred.issues)
|
|
474
|
+
});
|
|
475
|
+
return ManuscriptResult.succeed(build$2(preferred.output));
|
|
476
|
+
}
|
|
477
|
+
//#endregion
|
|
478
|
+
//#region src/proofreading/rules/ellipsis-character.ts
|
|
479
|
+
/**
|
|
480
|
+
* Stand-ins for the ellipsis (三点リーダー) `…`. Only the real character is set on its own em and pairs
|
|
481
|
+
* into a 2倍リーダ; a run of ASCII periods is neither. Runs of `。` and `・` are left to the rules that
|
|
482
|
+
* already count them, so one substitute is never reported twice.
|
|
483
|
+
*/
|
|
484
|
+
const ellipsisCharacterRule = () => defineMatchRule({
|
|
485
|
+
id: "kg/ellipsis-character",
|
|
486
|
+
pattern: /\.{3,}|⋯+/gu,
|
|
487
|
+
message: "三点リーダーには「…」を使ってください"
|
|
488
|
+
});
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/proofreading/rules/even-ellipsis.ts
|
|
491
|
+
/**
|
|
492
|
+
* The ellipsis a novel sets is the 2倍リーダ: two copies of `…` read as one mark six dots long. An odd
|
|
493
|
+
* run leaves half of it, the same way an odd dash run does.
|
|
494
|
+
*/
|
|
495
|
+
const evenEllipsisRule = () => defineMatchRule({
|
|
496
|
+
id: "kg/even-ellipsis",
|
|
497
|
+
pattern: /…+/gu,
|
|
498
|
+
accept: (match) => match[0].length % 2 === 1,
|
|
499
|
+
message: "連続する三点リーダーの数は偶数にしてください"
|
|
500
|
+
});
|
|
501
|
+
//#endregion
|
|
502
|
+
//#region src/proofreading/rules/fullwidth-japanese-punctuation.ts
|
|
503
|
+
const RULE_ID$3 = "kg/fullwidth-japanese-punctuation";
|
|
504
|
+
const MESSAGES$2 = {
|
|
505
|
+
"halfwidth-japanese-punctuation": "半角の「{{ character }}」があります。日本語の句読点・括弧・感嘆符・疑問符は全角にしてください",
|
|
506
|
+
"halfwidth-near-japanese": "日本語の前後に半角の「{{ character }}」があります。全角にすべきか確認してください"
|
|
507
|
+
};
|
|
508
|
+
const JAPANESE_CHARACTER = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}々〆ヵヶ]/u;
|
|
509
|
+
const CHECKS = [
|
|
510
|
+
{
|
|
511
|
+
pattern: /[。、「」]/gu,
|
|
512
|
+
messageId: "halfwidth-japanese-punctuation",
|
|
513
|
+
severity: "error",
|
|
514
|
+
requiresJapanese: false
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
pattern: /[!?]/gu,
|
|
518
|
+
messageId: "halfwidth-japanese-punctuation",
|
|
519
|
+
severity: "error",
|
|
520
|
+
requiresJapanese: true
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
pattern: /[,.()[\]{}]/gu,
|
|
524
|
+
messageId: "halfwidth-near-japanese",
|
|
525
|
+
severity: "warning",
|
|
526
|
+
requiresJapanese: true
|
|
527
|
+
}
|
|
528
|
+
];
|
|
529
|
+
function isBesideJapanese(text, index) {
|
|
530
|
+
const before = text[index - 1];
|
|
531
|
+
const after = text[index + 1];
|
|
532
|
+
return before !== void 0 && JAPANESE_CHARACTER.test(before) || after !== void 0 && JAPANESE_CHARACTER.test(after);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* A period inside a run of three or more is a stand-in for `…`, which `kg/ellipsis-character`
|
|
536
|
+
* already reports.
|
|
537
|
+
*/
|
|
538
|
+
function isEllipsisSubstitute(text, index) {
|
|
539
|
+
if (text[index] !== ".") return false;
|
|
540
|
+
let start = index;
|
|
541
|
+
while (text[start - 1] === ".") start -= 1;
|
|
542
|
+
let end = index;
|
|
543
|
+
while (text[end + 1] === ".") end += 1;
|
|
544
|
+
return end - start + 1 >= 3;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Halfwidth punctuation (約物) where Japanese prose calls for the fullwidth form. Japanese
|
|
548
|
+
* punctuation is designed to occupy one em, and the composer's spacing rules are stated in those
|
|
549
|
+
* terms, so a halfwidth form neither lands on the grid nor picks up the spacing its character class
|
|
550
|
+
* calls for.
|
|
551
|
+
*
|
|
552
|
+
* The three checks differ in how much the surroundings decide the answer, which is why only the
|
|
553
|
+
* first is decisive on its own.
|
|
554
|
+
*/
|
|
555
|
+
const fullwidthJapanesePunctuationRule = () => ({
|
|
556
|
+
kind: "parsed",
|
|
557
|
+
meta: {
|
|
558
|
+
id: RULE_ID$3,
|
|
559
|
+
messages: MESSAGES$2
|
|
560
|
+
},
|
|
561
|
+
check: (manuscript, context) => {
|
|
562
|
+
for (const line of splitDisplayLines(manuscript.displayText)) for (const { pattern, messageId, severity, requiresJapanese } of CHECKS) for (const match of line.text.matchAll(pattern)) {
|
|
563
|
+
if (requiresJapanese && !isBesideJapanese(line.text, match.index)) continue;
|
|
564
|
+
if (isEllipsisSubstitute(line.text, match.index)) continue;
|
|
565
|
+
const start = line.start + match.index;
|
|
566
|
+
context.report({
|
|
567
|
+
range: displayRange(manuscript, start, start + match[0].length),
|
|
568
|
+
messageId,
|
|
569
|
+
data: { character: match[0] },
|
|
570
|
+
severity
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region src/proofreading/rules/max-arabic-numeral-digits.ts
|
|
577
|
+
const RULE_ID$2 = "kg/max-arabic-numeral-digits";
|
|
578
|
+
const DEFAULT_MAX_DIGITS = 2;
|
|
579
|
+
const MaxDigitsSchema = v.pipe(v.number(), v.finite(), v.integer(), v.minValue(1));
|
|
580
|
+
/**
|
|
581
|
+
* Arabic numerals in vertical writing (縦組). A short run is set as 縦中横 (JLReq 3.2.5) and stays
|
|
582
|
+
* upright inside one em, but a longer one has nowhere to go: it either turns the line sideways or
|
|
583
|
+
* breaks across it. The usual limit is two digits, past which a novel spells the number in kanji.
|
|
584
|
+
*/
|
|
585
|
+
function build$1(maxDigits) {
|
|
586
|
+
return defineMatchRule({
|
|
587
|
+
id: RULE_ID$2,
|
|
588
|
+
pattern: /([0-90-9]+)(?:[..]([0-90-9]+))?/gu,
|
|
589
|
+
accept: (match) => (match[1]?.length ?? 0) > maxDigits || (match[2]?.length ?? 0) > maxDigits,
|
|
590
|
+
message: `${maxDigits}桁を超えるアラビア数字が使われています`
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
const maxArabicNumeralDigitsRule = () => build$1(DEFAULT_MAX_DIGITS);
|
|
594
|
+
function createMaxArabicNumeralDigitsRule(options = {}) {
|
|
595
|
+
if (options.maxDigits === void 0) return ManuscriptResult.succeed(maxArabicNumeralDigitsRule());
|
|
596
|
+
const maxDigits = v.safeParse(MaxDigitsSchema, options.maxDigits);
|
|
597
|
+
if (!maxDigits.success) return ManuscriptResult.fail({
|
|
598
|
+
kind: "InvalidRuleOptions",
|
|
599
|
+
ruleId: RULE_ID$2,
|
|
600
|
+
option: "maxDigits",
|
|
601
|
+
issues: ValidationIssue.from(maxDigits.issues)
|
|
602
|
+
});
|
|
603
|
+
return ManuscriptResult.succeed(build$1(maxDigits.output));
|
|
604
|
+
}
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region src/proofreading/rules/minus-before-number.ts
|
|
607
|
+
/**
|
|
608
|
+
* A minus sign (U+2212, JLReq 演算記号) with no number after it. It is almost always a mistyped dash or
|
|
609
|
+
* prolonged sound mark, which look alike but belong to different character classes and so are
|
|
610
|
+
* spaced and broken differently.
|
|
611
|
+
*/
|
|
612
|
+
const minusBeforeNumberRule = () => defineMatchRule({
|
|
613
|
+
id: "kg/minus-before-number",
|
|
614
|
+
pattern: /−(?![0-90-9〇一二三四五六七八九十])/gu,
|
|
615
|
+
message: "マイナス記号の直後には数字が必要です"
|
|
616
|
+
});
|
|
617
|
+
//#endregion
|
|
618
|
+
//#region src/proofreading/rules/no-consecutive-interpunct.ts
|
|
619
|
+
/**
|
|
620
|
+
* A repeated interpunct (中黒, JLReq 中点類 cl-05). The interpunct separates items; repeating it is
|
|
621
|
+
* almost always a stand-in for an ellipsis, which `kg/ellipsis-character` names properly.
|
|
622
|
+
*/
|
|
623
|
+
const noConsecutiveInterpunctRule = () => defineMatchRule({
|
|
624
|
+
id: "kg/no-consecutive-interpunct",
|
|
625
|
+
pattern: /・・+/gu,
|
|
626
|
+
message: "中黒が連続しています"
|
|
627
|
+
});
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region src/proofreading/rules/no-consecutive-punctuation.ts
|
|
630
|
+
/**
|
|
631
|
+
* A repeated full stop or comma (句読点 — JLReq 句点類 cl-06 and 読点類 cl-07). Each ends its clause once,
|
|
632
|
+
* so a run of them is not punctuation at all: it is a sentence trailing off, which Japanese prose
|
|
633
|
+
* writes with `…`.
|
|
634
|
+
*/
|
|
635
|
+
const noConsecutivePunctuationRule = () => defineMatchRule({
|
|
636
|
+
id: "kg/no-consecutive-punctuation",
|
|
637
|
+
pattern: /。。+|、、+/gu,
|
|
638
|
+
message: "句読点が連続しています"
|
|
639
|
+
});
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/proofreading/rules/internal/decoration-line.ts
|
|
642
|
+
const DECORATION_LINE = /^[**◆◇■□●○▽▼△▲☆★・…―─━ー\-==~~\s]+$/u;
|
|
643
|
+
/**
|
|
644
|
+
* A line made only of symbols and spaces: a scene break or a section marker rather than a
|
|
645
|
+
* paragraph. Indentation rules skip these, because their layout is deliberate.
|
|
646
|
+
*/
|
|
647
|
+
function isDecorationLine(text) {
|
|
648
|
+
return DECORATION_LINE.test(text);
|
|
649
|
+
}
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region src/proofreading/rules/paragraph-opening.ts
|
|
652
|
+
const RULE_ID$1 = "kg/paragraph-opening";
|
|
653
|
+
const MESSAGES$1 = {
|
|
654
|
+
"leading-character": "段落の先頭には全角スペースまたは開き括弧が必要です",
|
|
655
|
+
"indent-width": "段落冒頭の字下げは全角スペース1字にしてください",
|
|
656
|
+
"unexpected-indent": "括弧から始まる段落を字下げすることはできません"
|
|
657
|
+
};
|
|
658
|
+
const OpeningBracketsSchema = v.pipe(v.string(), v.nonEmpty());
|
|
659
|
+
/**
|
|
660
|
+
* Paragraph indentation (字下げ, JLReq 3.5.1). Japanese prose opens a paragraph with exactly one
|
|
661
|
+
* ideographic space (和字間隔), and a paragraph opening with a bracket (始め括弧類) carries none, because
|
|
662
|
+
* the bracket is already set on a half em and reads as the indent itself.
|
|
663
|
+
*
|
|
664
|
+
* The three findings are branches of that one decision rather than separate rules, so a line can
|
|
665
|
+
* never collect two of them at once.
|
|
666
|
+
*/
|
|
667
|
+
function build(openingBrackets) {
|
|
668
|
+
return {
|
|
669
|
+
kind: "parsed",
|
|
670
|
+
meta: {
|
|
671
|
+
id: RULE_ID$1,
|
|
672
|
+
messages: MESSAGES$1
|
|
673
|
+
},
|
|
674
|
+
check: (manuscript, context) => {
|
|
675
|
+
const report = (start, end, messageId) => {
|
|
676
|
+
context.report({
|
|
677
|
+
range: displayRange(manuscript, start, end),
|
|
678
|
+
messageId
|
|
679
|
+
});
|
|
680
|
+
};
|
|
681
|
+
for (const line of splitDisplayLines(manuscript.displayText)) {
|
|
682
|
+
if (isDecorationLine(line.text)) continue;
|
|
683
|
+
const indent = leadingSpaces(line.text);
|
|
684
|
+
const opening = line.text[indent.length];
|
|
685
|
+
if (opening === void 0) continue;
|
|
686
|
+
if (openingBrackets.includes(opening)) {
|
|
687
|
+
if (indent !== "") report(line.start, line.start + indent.length, "unexpected-indent");
|
|
688
|
+
} else if (!line.text.startsWith(" ")) {
|
|
689
|
+
const length = (line.text.codePointAt(0) ?? 0) > 65535 ? 2 : 1;
|
|
690
|
+
report(line.start, line.start + length, "leading-character");
|
|
691
|
+
} else if (indent.length !== 1) report(line.start, line.start + indent.length, "indent-width");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
const paragraphOpeningRule = () => build(OPENING_BRACKETS);
|
|
697
|
+
function createParagraphOpeningRule(options = {}) {
|
|
698
|
+
if (options.openingBrackets === void 0) return ManuscriptResult.succeed(paragraphOpeningRule());
|
|
699
|
+
const brackets = v.safeParse(OpeningBracketsSchema, options.openingBrackets);
|
|
700
|
+
if (!brackets.success) return ManuscriptResult.fail({
|
|
701
|
+
kind: "InvalidRuleOptions",
|
|
702
|
+
ruleId: RULE_ID$1,
|
|
703
|
+
option: "openingBrackets",
|
|
704
|
+
issues: ValidationIssue.from(brackets.issues)
|
|
705
|
+
});
|
|
706
|
+
return ManuscriptResult.succeed(build(brackets.output));
|
|
707
|
+
}
|
|
708
|
+
//#endregion
|
|
709
|
+
//#region src/proofreading/rules/punctuation-before-closing-quote.ts
|
|
710
|
+
const PATTERN = new RegExp(`[。、]+(?=[${characterClass(CLOSING_BRACKETS)}])`, "gu");
|
|
711
|
+
/**
|
|
712
|
+
* A full stop or comma immediately before a closing bracket (終わり括弧類). The bracket already closes
|
|
713
|
+
* the quotation, and both characters are set on a half em, so the pair reads as a gap in the line
|
|
714
|
+
* rather than as punctuation. Japanese practice drops the punctuation and keeps the bracket.
|
|
715
|
+
*/
|
|
716
|
+
const punctuationBeforeClosingQuoteRule = () => defineMatchRule({
|
|
717
|
+
id: "kg/punctuation-before-closing-quote",
|
|
718
|
+
pattern: PATTERN,
|
|
719
|
+
message: "閉じ括弧の直前に句読点を置くことはできません"
|
|
720
|
+
});
|
|
721
|
+
//#endregion
|
|
722
|
+
//#region src/proofreading/rules/space-after-question-or-exclamation.ts
|
|
723
|
+
const RULE_ID = "kg/space-after-question-or-exclamation";
|
|
724
|
+
const MESSAGES = {
|
|
725
|
+
default: "感嘆符または疑問符の直後には全角スペースか閉じ括弧が必要です",
|
|
726
|
+
"space-width": "感嘆符または疑問符の直後の空白は全角スペース1字にしてください",
|
|
727
|
+
"before-closing-bracket": "閉じ括弧の直前に空白を置くことはできません"
|
|
728
|
+
};
|
|
729
|
+
/**
|
|
730
|
+
* The gap that follows every run of ! or ? (区切り約物, JLReq 3.1.6). A dividing punctuation mark ends a
|
|
731
|
+
* sentence without a full stop, so a full em of white stands in for the one a 句点 would have
|
|
732
|
+
* carried. A run at the end of a line or right before a closing bracket is already separated from
|
|
733
|
+
* what follows and needs no gap; anywhere else it takes exactly one ideographic space.
|
|
734
|
+
*/
|
|
735
|
+
const spaceAfterQuestionOrExclamationRule = () => ({
|
|
736
|
+
kind: "parsed",
|
|
737
|
+
meta: {
|
|
738
|
+
id: RULE_ID,
|
|
739
|
+
messages: MESSAGES
|
|
740
|
+
},
|
|
741
|
+
check: (manuscript, context) => {
|
|
742
|
+
const report = (start, end, messageId) => {
|
|
743
|
+
context.report({
|
|
744
|
+
range: displayRange(manuscript, start, end),
|
|
745
|
+
messageId
|
|
746
|
+
});
|
|
747
|
+
};
|
|
748
|
+
for (const line of splitDisplayLines(manuscript.displayText)) for (const spacing of questionOrExclamationSpacings(line.text)) switch (spacing.kind) {
|
|
749
|
+
case "valid": break;
|
|
750
|
+
case "missing":
|
|
751
|
+
report(line.start + spacing.marks.start, line.start + spacing.marks.end, "default");
|
|
752
|
+
break;
|
|
753
|
+
case "invalid-space":
|
|
754
|
+
report(line.start + spacing.gap.start, line.start + spacing.gap.end, "space-width");
|
|
755
|
+
break;
|
|
756
|
+
case "before-closing-bracket":
|
|
757
|
+
report(line.start + spacing.gap.start, line.start + spacing.gap.end, "before-closing-bracket");
|
|
758
|
+
break;
|
|
759
|
+
default: assertNever(spacing);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
//#endregion
|
|
764
|
+
//#region src/proofreading/rules/variant-character.ts
|
|
765
|
+
/**
|
|
766
|
+
* Mongolian free variation selectors, variation selectors, CJK compatibility ideographs, IVSes.
|
|
767
|
+
*/
|
|
768
|
+
function isVariantCharacter(character) {
|
|
769
|
+
const codePoint = character.codePointAt(0);
|
|
770
|
+
if (codePoint === void 0) return false;
|
|
771
|
+
return codePoint >= 6155 && codePoint <= 6157 || codePoint === 6159 || codePoint >= 65024 && codePoint <= 65039 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 194560 && codePoint <= 195103 || codePoint >= 917760 && codePoint <= 917999;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Variant forms of a character (異体字) and the selectors that request them (字形選択子 / IVS). Whether
|
|
775
|
+
* such a form survives depends on the font and the platform, so a manuscript that relies on one
|
|
776
|
+
* will not read the same everywhere it is published.
|
|
777
|
+
*
|
|
778
|
+
* Scans the source rather than the display text: notation removal must not hide a variant.
|
|
779
|
+
*/
|
|
780
|
+
const variantCharacterRule = () => defineParsedRule("kg/variant-character", "異体字または字形選択子が使われています", (manuscript, report) => {
|
|
781
|
+
for (const { index, segment } of graphemeSegmenter.segment(manuscript.source)) if (Array.from(segment).some(isVariantCharacter)) report(sourceRange(manuscript, {
|
|
782
|
+
start: index,
|
|
783
|
+
end: index + segment.length
|
|
784
|
+
}));
|
|
785
|
+
});
|
|
786
|
+
//#endregion
|
|
787
|
+
//#region src/proofreading/rules/default-rules.ts
|
|
788
|
+
/**
|
|
789
|
+
* The built-in rule set at its default configuration. Infallible by construction: every rule is
|
|
790
|
+
* built from constants, so there is no options error for a caller to handle.
|
|
791
|
+
*
|
|
792
|
+
* Rules whose answer depends on the work's own conventions — which width numerals take, whether a
|
|
793
|
+
* word is written in kanji or kana — are exported individually instead of included here.
|
|
794
|
+
*/
|
|
795
|
+
function createDefaultProofreadingRules() {
|
|
796
|
+
return [
|
|
797
|
+
paragraphOpeningRule(),
|
|
798
|
+
punctuationBeforeClosingQuoteRule(),
|
|
799
|
+
spaceAfterQuestionOrExclamationRule(),
|
|
800
|
+
evenEllipsisRule(),
|
|
801
|
+
dashRule(),
|
|
802
|
+
ellipsisCharacterRule(),
|
|
803
|
+
noConsecutivePunctuationRule(),
|
|
804
|
+
noConsecutiveInterpunctRule(),
|
|
805
|
+
minusBeforeNumberRule(),
|
|
806
|
+
maxArabicNumeralDigitsRule(),
|
|
807
|
+
fullwidthJapanesePunctuationRule(),
|
|
808
|
+
variantCharacterRule()
|
|
809
|
+
];
|
|
810
|
+
}
|
|
811
|
+
//#endregion
|
|
812
|
+
export { InvalidRuleOptions, ProofreadError, ProofreadingRuleMeta, consistentKanjiOpeningRule, consistentLatinWidthRule, consistentNumeralWidthRule, createConsistentKanjiOpeningRule, createDashRule, createDefaultProofreadingRules, createMaxArabicNumeralDigitsRule, createParagraphOpeningRule, dashRule, ellipsisCharacterRule, evenEllipsisRule, fullwidthJapanesePunctuationRule, maxArabicNumeralDigitsRule, minusBeforeNumberRule, noConsecutiveInterpunctRule, noConsecutivePunctuationRule, paragraphOpeningRule, proofreadManuscript, punctuationBeforeClosingQuoteRule, spaceAfterQuestionOrExclamationRule, variantCharacterRule };
|