@sushichan044/kg-core 0.4.0 → 0.5.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 +42 -9
- package/dist/index.d.mts +74 -6
- package/dist/index.mjs +604 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,12 +24,13 @@ import {
|
|
|
24
24
|
manuscriptGridComposer,
|
|
25
25
|
ParseError,
|
|
26
26
|
parseManuscript,
|
|
27
|
+
kakuyomuParser,
|
|
27
28
|
pixivParser,
|
|
28
29
|
ProofreadError,
|
|
29
30
|
proofreadManuscript,
|
|
30
31
|
} from "@sushichan044/kg-core";
|
|
31
32
|
|
|
32
|
-
const parsed = parseManuscript(source, { parser:
|
|
33
|
+
const parsed = parseManuscript(source, { parser: kakuyomuParser });
|
|
33
34
|
if (!parsed.ok) throw new Error(ParseError.describe(parsed.error));
|
|
34
35
|
|
|
35
36
|
const composed = composeManuscript(parsed.value, {
|
|
@@ -46,10 +47,10 @@ if (!proofread.ok) throw new Error(ProofreadError.describe(proofread.error));
|
|
|
46
47
|
const diagnostics = [...parsed.warnings, ...proofread.value];
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
`parseManuscript` uses `plainTextParser` when no parser is supplied. The
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
parser warning.
|
|
50
|
+
`parseManuscript` uses `plainTextParser` when no parser is supplied. The built-in
|
|
51
|
+
`pixivParser` normalizes ruby, bold, italic, and emphasis notation; `kakuyomuParser`
|
|
52
|
+
normalizes Kakuyomu ruby and emphasis marks. Malformed or unknown notation remains
|
|
53
|
+
visible and produces a parser warning.
|
|
53
54
|
|
|
54
55
|
`composeManuscript` requires a composer. The built-in grid composer produces a
|
|
55
56
|
self-contained snapshot containing the parsed manuscript, accepted settings,
|
|
@@ -62,7 +63,8 @@ element has source, display, and grapheme ranges; empty placement elements use
|
|
|
62
63
|
Parsers implement `ManuscriptParser`. Composers implement `ManuscriptComposer`
|
|
63
64
|
and provide Valibot schemas for their settings and layout. Proofreading rules
|
|
64
65
|
carry `kind: "parsed"` or `kind: "composed"` to declare which manuscript they
|
|
65
|
-
need, and report findings through `context.report`.
|
|
66
|
+
need, and report findings through `context.report`. A report may name a
|
|
67
|
+
`severity`; omitting it means `error`.
|
|
66
68
|
|
|
67
69
|
A plugin signals its own failure with a `Rejection` — a plain reason string.
|
|
68
70
|
Core turns that into a variant of the stage's error union, so plugins never
|
|
@@ -87,9 +89,40 @@ Grapheme ranges index the normalized grapheme array. Diagnostics include
|
|
|
87
89
|
one-based source line and column positions, so renderers do not need to search
|
|
88
90
|
or recalculate locations.
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
## Proofreading rules
|
|
93
|
+
|
|
94
|
+
`createDefaultProofreadingRules()` returns the rules whose answer does not
|
|
95
|
+
depend on the work: how a paragraph opens and how far it is indented,
|
|
96
|
+
punctuation before a closing bracket, spacing after `!` and `?`, ellipsis and
|
|
97
|
+
dash forms and counts, repeated punctuation, repeated interpuncts and choonpu, a
|
|
98
|
+
minus sign that no number follows, halfwidth Japanese punctuation, Arabic
|
|
99
|
+
numeral length, and Unicode variation sequences.
|
|
100
|
+
|
|
101
|
+
Rules that depend on the work's own conventions are exported individually and
|
|
102
|
+
report `warning` instead of `error`, so a caller opts into them:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import {
|
|
106
|
+
consistentKanjiOpeningRule,
|
|
107
|
+
consistentLatinWidthRule,
|
|
108
|
+
consistentNumeralWidthRule,
|
|
109
|
+
createConsistentKanjiOpeningRule,
|
|
110
|
+
createDefaultProofreadingRules,
|
|
111
|
+
} from "@sushichan044/kg-core";
|
|
112
|
+
|
|
113
|
+
const rules = [
|
|
114
|
+
...createDefaultProofreadingRules(),
|
|
115
|
+
consistentNumeralWidthRule(),
|
|
116
|
+
consistentLatinWidthRule(),
|
|
117
|
+
consistentKanjiOpeningRule(),
|
|
118
|
+
];
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`createConsistentKanjiOpeningRule({ pairs })` replaces the built-in kanji and
|
|
122
|
+
kana pairs; like every configurable rule it returns a `ManuscriptResult` and
|
|
123
|
+
fails with `InvalidRuleOptions` rather than dropping bad options.
|
|
124
|
+
|
|
125
|
+
Diagnostics do not modify the source and do not contain automatic fixes.
|
|
93
126
|
|
|
94
127
|
## Browser support
|
|
95
128
|
|
package/dist/index.d.mts
CHANGED
|
@@ -3330,6 +3330,13 @@ declare const ParsedGrapheme: {
|
|
|
3330
3330
|
}>]>;
|
|
3331
3331
|
};
|
|
3332
3332
|
//#endregion
|
|
3333
|
+
//#region src/parser/kakuyomu-parser.d.ts
|
|
3334
|
+
/**
|
|
3335
|
+
* Understands Kakuyomu's ruby and emphasis-mark notation. Invalid notation stays literal so source
|
|
3336
|
+
* ranges remain inspectable, while clear notation candidates receive a parser warning.
|
|
3337
|
+
*/
|
|
3338
|
+
declare const kakuyomuParser: ManuscriptParser;
|
|
3339
|
+
//#endregion
|
|
3333
3340
|
//#region src/parser/pixiv-parser.d.ts
|
|
3334
3341
|
/**
|
|
3335
3342
|
* Understands Pixiv's ruby, bold, italic and emphasis-mark notation. Anything bracket-shaped that
|
|
@@ -14425,6 +14432,10 @@ declare const ProofreadingReportSchema: v.SchemaWithPipe<readonly [v.StrictObjec
|
|
|
14425
14432
|
readonly data: v.OptionalSchema<v.SchemaWithPipe<readonly [v.RecordSchema<v.StringSchema<undefined>, v.UnionSchema<[v.StringSchema<undefined>, v.NumberSchema<undefined>], undefined>, undefined>, v.ReadonlyAction<{
|
|
14426
14433
|
[x: string]: string | number;
|
|
14427
14434
|
}>]>, undefined>;
|
|
14435
|
+
/**
|
|
14436
|
+
* Omitted means `error`: a rule that says nothing is claiming it found a real defect.
|
|
14437
|
+
*/
|
|
14438
|
+
readonly severity: v.OptionalSchema<v.PicklistSchema<["warning", "error"], undefined>, undefined>;
|
|
14428
14439
|
}, undefined>, v.ReadonlyAction<{
|
|
14429
14440
|
readonly range: Readonly<{
|
|
14430
14441
|
source: Readonly<{
|
|
@@ -14444,6 +14455,7 @@ declare const ProofreadingReportSchema: v.SchemaWithPipe<readonly [v.StrictObjec
|
|
|
14444
14455
|
data?: Readonly<{
|
|
14445
14456
|
[x: string]: string | number;
|
|
14446
14457
|
}> | undefined;
|
|
14458
|
+
severity?: "error" | "warning" | undefined;
|
|
14447
14459
|
}>]>;
|
|
14448
14460
|
/**
|
|
14449
14461
|
* What a rule hands back for one finding. The rule names a message it declared rather than
|
|
@@ -14510,6 +14522,10 @@ declare const ProofreadingReport: {
|
|
|
14510
14522
|
readonly data: v.OptionalSchema<v.SchemaWithPipe<readonly [v.RecordSchema<v.StringSchema<undefined>, v.UnionSchema<[v.StringSchema<undefined>, v.NumberSchema<undefined>], undefined>, undefined>, v.ReadonlyAction<{
|
|
14511
14523
|
[x: string]: string | number;
|
|
14512
14524
|
}>]>, undefined>;
|
|
14525
|
+
/**
|
|
14526
|
+
* Omitted means `error`: a rule that says nothing is claiming it found a real defect.
|
|
14527
|
+
*/
|
|
14528
|
+
readonly severity: v.OptionalSchema<v.PicklistSchema<["warning", "error"], undefined>, undefined>;
|
|
14513
14529
|
}, undefined>, v.ReadonlyAction<{
|
|
14514
14530
|
readonly range: Readonly<{
|
|
14515
14531
|
source: Readonly<{
|
|
@@ -14529,6 +14545,7 @@ declare const ProofreadingReport: {
|
|
|
14529
14545
|
data?: Readonly<{
|
|
14530
14546
|
[x: string]: string | number;
|
|
14531
14547
|
}> | undefined;
|
|
14548
|
+
severity?: "error" | "warning" | undefined;
|
|
14532
14549
|
}>]>;
|
|
14533
14550
|
};
|
|
14534
14551
|
//#endregion
|
|
@@ -14613,19 +14630,66 @@ declare function proofreadManuscript(manuscript: ParsedManuscript, options: Proo
|
|
|
14613
14630
|
*/
|
|
14614
14631
|
declare function proofreadManuscript<TComposed extends ComposedManuscript>(manuscript: TComposed, options: ProofreadOptions<ParsedProofreadingRule | ComposedProofreadingRule<TComposed>>): ProofreadOutcome;
|
|
14615
14632
|
//#endregion
|
|
14633
|
+
//#region src/proofreading/rules/consistent-kanji-opening.d.ts
|
|
14634
|
+
declare const KanjiOpeningPairSchema: v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.StrictObjectSchema<{
|
|
14635
|
+
readonly closed: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.NonEmptyAction<string, undefined>]>;
|
|
14636
|
+
readonly opened: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.NonEmptyAction<string, undefined>]>;
|
|
14637
|
+
}, undefined>, v.ReadonlyAction<{
|
|
14638
|
+
closed: string;
|
|
14639
|
+
opened: string;
|
|
14640
|
+
}>]>, v.CheckAction<Readonly<{
|
|
14641
|
+
closed: string;
|
|
14642
|
+
opened: string;
|
|
14643
|
+
}>, "a pair must hold two different spellings">]>;
|
|
14644
|
+
/**
|
|
14645
|
+
* A word written in kanji, paired with the kana form of the same word.
|
|
14646
|
+
*/
|
|
14647
|
+
type KanjiOpeningPair = v.InferOutput<typeof KanjiOpeningPairSchema>;
|
|
14648
|
+
type ConsistentKanjiOpeningOptions = Readonly<{
|
|
14649
|
+
pairs?: readonly KanjiOpeningPair[];
|
|
14650
|
+
}>;
|
|
14651
|
+
declare const consistentKanjiOpeningRule: () => ParsedProofreadingRule;
|
|
14652
|
+
declare function createConsistentKanjiOpeningRule(options?: ConsistentKanjiOpeningOptions): ManuscriptResult<ParsedProofreadingRule, InvalidRuleOptions>;
|
|
14653
|
+
//#endregion
|
|
14654
|
+
//#region src/proofreading/rules/consistent-latin-width.d.ts
|
|
14655
|
+
declare const consistentLatinWidthRule: () => ParsedProofreadingRule;
|
|
14656
|
+
//#endregion
|
|
14657
|
+
//#region src/proofreading/rules/consistent-numeral-width.d.ts
|
|
14658
|
+
declare const consistentNumeralWidthRule: () => ParsedProofreadingRule;
|
|
14659
|
+
//#endregion
|
|
14660
|
+
//#region src/proofreading/rules/dash-character.d.ts
|
|
14661
|
+
/**
|
|
14662
|
+
* Stand-ins for `―`. Repeated `ー` belongs to `kg/no-consecutive-choonpu` and `−` to
|
|
14663
|
+
* `kg/minus-before-number`, so neither is reported twice.
|
|
14664
|
+
*/
|
|
14665
|
+
declare const dashCharacterRule: () => ParsedProofreadingRule;
|
|
14666
|
+
//#endregion
|
|
14616
14667
|
//#region src/proofreading/rules/default-rules.d.ts
|
|
14617
14668
|
/**
|
|
14618
14669
|
* The built-in rule set at its default configuration. Infallible by construction: every rule is
|
|
14619
14670
|
* built from constants, so there is no options error for a caller to handle.
|
|
14671
|
+
*
|
|
14672
|
+
* Rules whose answer depends on the work's own conventions — which width numerals take, whether a
|
|
14673
|
+
* word is written in kanji or kana — are exported individually instead of included here.
|
|
14620
14674
|
*/
|
|
14621
14675
|
declare function createDefaultProofreadingRules(): readonly ParsedProofreadingRule[];
|
|
14622
14676
|
//#endregion
|
|
14677
|
+
//#region src/proofreading/rules/ellipsis-character.d.ts
|
|
14678
|
+
/**
|
|
14679
|
+
* Stand-ins for `…`. Runs of `。` and `・` are left to the rules that already count them, so one
|
|
14680
|
+
* substitute is never reported twice.
|
|
14681
|
+
*/
|
|
14682
|
+
declare const ellipsisCharacterRule: () => ParsedProofreadingRule;
|
|
14683
|
+
//#endregion
|
|
14623
14684
|
//#region src/proofreading/rules/even-dash.d.ts
|
|
14624
14685
|
declare const evenDashRule: () => ParsedProofreadingRule;
|
|
14625
14686
|
//#endregion
|
|
14626
14687
|
//#region src/proofreading/rules/even-ellipsis.d.ts
|
|
14627
14688
|
declare const evenEllipsisRule: () => ParsedProofreadingRule;
|
|
14628
14689
|
//#endregion
|
|
14690
|
+
//#region src/proofreading/rules/fullwidth-japanese-punctuation.d.ts
|
|
14691
|
+
declare const fullwidthJapanesePunctuationRule: () => ParsedProofreadingRule;
|
|
14692
|
+
//#endregion
|
|
14629
14693
|
//#region src/proofreading/rules/max-arabic-numeral-digits.d.ts
|
|
14630
14694
|
type MaxArabicNumeralDigitsOptions = Readonly<{
|
|
14631
14695
|
maxDigits?: number;
|
|
@@ -14645,17 +14709,21 @@ declare const noConsecutiveInterpunctRule: () => ParsedProofreadingRule;
|
|
|
14645
14709
|
//#region src/proofreading/rules/no-consecutive-punctuation.d.ts
|
|
14646
14710
|
declare const noConsecutivePunctuationRule: () => ParsedProofreadingRule;
|
|
14647
14711
|
//#endregion
|
|
14648
|
-
//#region src/proofreading/rules/paragraph-
|
|
14649
|
-
type
|
|
14650
|
-
|
|
14712
|
+
//#region src/proofreading/rules/paragraph-opening.d.ts
|
|
14713
|
+
type ParagraphOpeningOptions = Readonly<{
|
|
14714
|
+
openingBrackets?: string;
|
|
14651
14715
|
}>;
|
|
14652
|
-
declare const
|
|
14653
|
-
declare function
|
|
14716
|
+
declare const paragraphOpeningRule: () => ParsedProofreadingRule;
|
|
14717
|
+
declare function createParagraphOpeningRule(options?: ParagraphOpeningOptions): ManuscriptResult<ParsedProofreadingRule, InvalidRuleOptions>;
|
|
14654
14718
|
//#endregion
|
|
14655
14719
|
//#region src/proofreading/rules/punctuation-before-closing-quote.d.ts
|
|
14656
14720
|
declare const punctuationBeforeClosingQuoteRule: () => ParsedProofreadingRule;
|
|
14657
14721
|
//#endregion
|
|
14658
14722
|
//#region src/proofreading/rules/space-after-question-or-exclamation.d.ts
|
|
14723
|
+
/**
|
|
14724
|
+
* Checks the gap that follows every run of ! or ?. A run at the end of a line or right before a
|
|
14725
|
+
* closing bracket needs no gap; anywhere else it takes exactly one ideographic space.
|
|
14726
|
+
*/
|
|
14659
14727
|
declare const spaceAfterQuestionOrExclamationRule: () => ParsedProofreadingRule;
|
|
14660
14728
|
//#endregion
|
|
14661
14729
|
//#region src/proofreading/rules/variant-character.d.ts
|
|
@@ -14664,4 +14732,4 @@ declare const spaceAfterQuestionOrExclamationRule: () => ParsedProofreadingRule;
|
|
|
14664
14732
|
*/
|
|
14665
14733
|
declare const variantCharacterRule: () => ParsedProofreadingRule;
|
|
14666
14734
|
//#endregion
|
|
14667
|
-
export { BoldAnnotation, ComposeError, type ComposeManuscriptOptions, type ComposedManuscript, type ComposedProofreadingRule, CompositionStatistics, DiagnosticOrigin, DiagnosticSeverity, DisplayRange, EmphasisAnnotation, FontPreset, FontPresetId, FontSizePt, GraphemeRange, GridCell, type GridComposedManuscript, GridLine, GridPage, GridSettings, GridStage, InvalidRuleOptions, ItalicAnnotation, LineOffset, ManuscriptAnnotation, ManuscriptAppearanceSettings, type ManuscriptComposer, ManuscriptCompositionSettings, ManuscriptDiagnostic, type ManuscriptDiagnosticInput, ManuscriptGeometry, ManuscriptGridLayout, ManuscriptOffsets, type ManuscriptParser, ManuscriptRange, type ManuscriptRangeInput, ManuscriptResult, type MaxArabicNumeralDigitsOptions, NamespacedId, PaperSize, PaperSizeId, type
|
|
14735
|
+
export { BoldAnnotation, ComposeError, type ComposeManuscriptOptions, type ComposedManuscript, type ComposedProofreadingRule, CompositionStatistics, type ConsistentKanjiOpeningOptions, DiagnosticOrigin, DiagnosticSeverity, DisplayRange, EmphasisAnnotation, FontPreset, FontPresetId, FontSizePt, GraphemeRange, GridCell, type GridComposedManuscript, GridLine, GridPage, GridSettings, GridStage, InvalidRuleOptions, ItalicAnnotation, type KanjiOpeningPair, LineOffset, ManuscriptAnnotation, ManuscriptAppearanceSettings, type ManuscriptComposer, ManuscriptCompositionSettings, ManuscriptDiagnostic, type ManuscriptDiagnosticInput, ManuscriptGeometry, ManuscriptGridLayout, ManuscriptOffsets, type ManuscriptParser, ManuscriptRange, type ManuscriptRangeInput, ManuscriptResult, type MaxArabicNumeralDigitsOptions, NamespacedId, PaperSize, PaperSizeId, type ParagraphOpeningOptions, ParseError, type ParseManuscriptOptions, ParsedGrapheme, ParsedManuscript, type ParsedProofreadingRule, ProofreadError, type ProofreadOptions, ProofreadingReport, type ProofreadingRule, type ProofreadingRuleContext, type ProofreadingRuleMessages, ProofreadingRuleMeta, Rejection, RubyAnnotation, SourcePosition, SourceRange, TextRange, ValidationIssue, assertNever, composeManuscript, consistentKanjiOpeningRule, consistentLatinWidthRule, consistentNumeralWidthRule, createConsistentKanjiOpeningRule, createDefaultProofreadingRules, createMaxArabicNumeralDigitsRule, createParagraphOpeningRule, dashCharacterRule, ellipsisCharacterRule, evenDashRule, evenEllipsisRule, fullwidthJapanesePunctuationRule, kakuyomuParser, manuscriptGridComposer, maxArabicNumeralDigitsRule, minusBeforeNumberRule, mmToPt, noConsecutiveChoonpuRule, noConsecutiveInterpunctRule, noConsecutivePunctuationRule, paragraphOpeningRule, parseManuscript, pixivParser, plainTextParser, proofreadManuscript, ptToMm, punctuationBeforeClosingQuoteRule, spaceAfterQuestionOrExclamationRule, variantCharacterRule };
|
package/dist/index.mjs
CHANGED
|
@@ -513,17 +513,9 @@ function parseManuscript(source, options = {}) {
|
|
|
513
513
|
});
|
|
514
514
|
}
|
|
515
515
|
//#endregion
|
|
516
|
-
//#region src/parser/internal/
|
|
517
|
-
function trimContent(text, sourceStart) {
|
|
518
|
-
const trimmedStart = text.trimStart();
|
|
519
|
-
const leadingLength = text.length - trimmedStart.length;
|
|
520
|
-
return {
|
|
521
|
-
text: trimmedStart.trimEnd(),
|
|
522
|
-
sourceStart: sourceStart + leadingLength
|
|
523
|
-
};
|
|
524
|
-
}
|
|
516
|
+
//#region src/parser/internal/line-end.ts
|
|
525
517
|
/**
|
|
526
|
-
*
|
|
518
|
+
* Returns the first line terminator at or after start. Both halves of CRLF remain outside the line.
|
|
527
519
|
*/
|
|
528
520
|
function lineEnd(source, start) {
|
|
529
521
|
const lf = source.indexOf("\n", start);
|
|
@@ -532,6 +524,249 @@ function lineEnd(source, start) {
|
|
|
532
524
|
if (cr === -1) return lf;
|
|
533
525
|
return Math.min(lf, cr);
|
|
534
526
|
}
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/parser/internal/notation-warning.ts
|
|
529
|
+
function firstCoveredIndex(manuscript, start) {
|
|
530
|
+
let low = 0;
|
|
531
|
+
let high = manuscript.graphemes.length;
|
|
532
|
+
while (low < high) {
|
|
533
|
+
const middle = Math.floor((low + high) / 2);
|
|
534
|
+
const grapheme = manuscript.graphemes[middle];
|
|
535
|
+
if (grapheme === void 0) return manuscript.graphemes.length;
|
|
536
|
+
if (grapheme.range.source.end <= start) low = middle + 1;
|
|
537
|
+
else high = middle;
|
|
538
|
+
}
|
|
539
|
+
return low;
|
|
540
|
+
}
|
|
541
|
+
function lastCoveredIndex(manuscript, end) {
|
|
542
|
+
let low = 0;
|
|
543
|
+
let high = manuscript.graphemes.length;
|
|
544
|
+
while (low < high) {
|
|
545
|
+
const middle = Math.floor((low + high) / 2);
|
|
546
|
+
const grapheme = manuscript.graphemes[middle];
|
|
547
|
+
if (grapheme === void 0) return -1;
|
|
548
|
+
if (grapheme.range.source.start < end) low = middle + 1;
|
|
549
|
+
else high = middle;
|
|
550
|
+
}
|
|
551
|
+
return low - 1;
|
|
552
|
+
}
|
|
553
|
+
function spanRange(manuscript, span) {
|
|
554
|
+
const first = manuscript.graphemes[firstCoveredIndex(manuscript, span.start)];
|
|
555
|
+
const last = manuscript.graphemes[lastCoveredIndex(manuscript, span.end)];
|
|
556
|
+
if (first === void 0 || last === void 0 || first.range.source.start >= span.end || last.range.source.end <= span.start) return ManuscriptRange.of({
|
|
557
|
+
source: span,
|
|
558
|
+
display: {
|
|
559
|
+
start: 0,
|
|
560
|
+
end: 0
|
|
561
|
+
},
|
|
562
|
+
graphemes: {
|
|
563
|
+
start: 0,
|
|
564
|
+
end: 0
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
return ManuscriptRange.of({
|
|
568
|
+
source: span,
|
|
569
|
+
display: {
|
|
570
|
+
start: first.range.display.start,
|
|
571
|
+
end: last.range.display.end
|
|
572
|
+
},
|
|
573
|
+
graphemes: {
|
|
574
|
+
start: first.range.graphemes.start,
|
|
575
|
+
end: last.range.graphemes.end
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
function unrecognizedNotationWarnings(source, parserId, manuscript, spans) {
|
|
580
|
+
return spans.map((span, order) => ManuscriptDiagnostic.of({
|
|
581
|
+
source,
|
|
582
|
+
origin: {
|
|
583
|
+
kind: "parser",
|
|
584
|
+
id: parserId
|
|
585
|
+
},
|
|
586
|
+
severity: "warning",
|
|
587
|
+
code: `unrecognized-notation-${order}`,
|
|
588
|
+
message: "認識できない記法を原文として扱いました",
|
|
589
|
+
range: spanRange(manuscript, span)
|
|
590
|
+
}));
|
|
591
|
+
}
|
|
592
|
+
//#endregion
|
|
593
|
+
//#region src/parser/kakuyomu-parser.ts
|
|
594
|
+
const PARSER_ID$1 = NamespacedId.of("kg/kakuyomu");
|
|
595
|
+
const MAX_BASE_GRAPHEMES = 20;
|
|
596
|
+
const MAX_READING_GRAPHEMES = 50;
|
|
597
|
+
const HAN = /^\p{Script=Han}$/u;
|
|
598
|
+
function graphemeLength(text) {
|
|
599
|
+
return [...graphemeSegmenter.segment(text)].length;
|
|
600
|
+
}
|
|
601
|
+
function validRuby(parent, reading) {
|
|
602
|
+
return parent.length > 0 && reading.length > 0 && !parent.includes("《") && !parent.includes("》") && !reading.includes("《") && !reading.includes("》") && graphemeLength(parent) <= MAX_BASE_GRAPHEMES && graphemeLength(reading) <= MAX_READING_GRAPHEMES;
|
|
603
|
+
}
|
|
604
|
+
function parseRuby(source, parentStart, parentEnd, opening) {
|
|
605
|
+
const endOfLine = lineEnd(source, opening);
|
|
606
|
+
const closing = source.indexOf("》", opening + 1);
|
|
607
|
+
if (closing === -1 || closing >= endOfLine) return void 0;
|
|
608
|
+
const parent = source.slice(parentStart, parentEnd);
|
|
609
|
+
const reading = source.slice(opening + 1, closing);
|
|
610
|
+
if (!validRuby(parent, reading)) return void 0;
|
|
611
|
+
return {
|
|
612
|
+
parentStart,
|
|
613
|
+
parentEnd,
|
|
614
|
+
reading,
|
|
615
|
+
end: closing + 1
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function parseExplicitRuby(source, start) {
|
|
619
|
+
const endOfLine = lineEnd(source, start + 1);
|
|
620
|
+
const opening = source.indexOf("《", start + 1);
|
|
621
|
+
if (opening === -1 || opening >= endOfLine) return void 0;
|
|
622
|
+
return parseRuby(source, start + 1, opening, opening);
|
|
623
|
+
}
|
|
624
|
+
function parseEmphasis(source, start) {
|
|
625
|
+
const endOfLine = lineEnd(source, start + 2);
|
|
626
|
+
const closing = source.indexOf("》》", start + 2);
|
|
627
|
+
if (closing === -1 || closing >= endOfLine) return void 0;
|
|
628
|
+
const content = source.slice(start + 2, closing);
|
|
629
|
+
if (content.length === 0 || content.includes("《") || content.includes("》")) return void 0;
|
|
630
|
+
return {
|
|
631
|
+
contentStart: start + 2,
|
|
632
|
+
contentEnd: closing,
|
|
633
|
+
end: closing + 2
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
function codePointLength(source, index) {
|
|
637
|
+
const codePoint = source.codePointAt(index);
|
|
638
|
+
return codePoint === void 0 || codePoint <= 65535 ? 1 : 2;
|
|
639
|
+
}
|
|
640
|
+
function isHanAt(source, index) {
|
|
641
|
+
return HAN.test(source.slice(index, index + codePointLength(source, index)));
|
|
642
|
+
}
|
|
643
|
+
function invalidNotationEnd(source, start) {
|
|
644
|
+
return lineEnd(source, start + 1);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Understands Kakuyomu's ruby and emphasis-mark notation. Invalid notation stays literal so source
|
|
648
|
+
* ranges remain inspectable, while clear notation candidates receive a parser warning.
|
|
649
|
+
*/
|
|
650
|
+
const kakuyomuParser = {
|
|
651
|
+
id: PARSER_ID$1,
|
|
652
|
+
parse: (source) => {
|
|
653
|
+
const draft = createManuscriptDraft(source);
|
|
654
|
+
const unrecognized = [];
|
|
655
|
+
let index = 0;
|
|
656
|
+
let literalStart = 0;
|
|
657
|
+
let hanRunStart = -1;
|
|
658
|
+
const appendBefore = (end) => {
|
|
659
|
+
if (literalStart < end) draft.append(source.slice(literalStart, end), literalStart);
|
|
660
|
+
};
|
|
661
|
+
const appendNotationWarning = (start, end) => {
|
|
662
|
+
unrecognized.push({
|
|
663
|
+
start,
|
|
664
|
+
end
|
|
665
|
+
});
|
|
666
|
+
};
|
|
667
|
+
while (index < source.length) {
|
|
668
|
+
const character = source[index];
|
|
669
|
+
if (character === "|" || character === "|") {
|
|
670
|
+
if (source[index + 1] === "《") {
|
|
671
|
+
appendBefore(index);
|
|
672
|
+
draft.append("《", index + 1);
|
|
673
|
+
index += 2;
|
|
674
|
+
literalStart = index;
|
|
675
|
+
hanRunStart = -1;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
const ruby = parseExplicitRuby(source, index);
|
|
679
|
+
if (ruby !== void 0) {
|
|
680
|
+
appendBefore(index);
|
|
681
|
+
const range = draft.append(source.slice(ruby.parentStart, ruby.parentEnd), ruby.parentStart);
|
|
682
|
+
draft.annotate({
|
|
683
|
+
kind: "ruby",
|
|
684
|
+
reading: ruby.reading,
|
|
685
|
+
range: {
|
|
686
|
+
...range,
|
|
687
|
+
source: {
|
|
688
|
+
start: index,
|
|
689
|
+
end: ruby.end
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
index = ruby.end;
|
|
694
|
+
literalStart = index;
|
|
695
|
+
hanRunStart = -1;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const opening = source.indexOf("《", index + 1);
|
|
699
|
+
if (opening !== -1 && opening < lineEnd(source, index + 1)) {
|
|
700
|
+
const end = invalidNotationEnd(source, index);
|
|
701
|
+
appendNotationWarning(index, end);
|
|
702
|
+
index = end;
|
|
703
|
+
hanRunStart = -1;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (character === "《") {
|
|
708
|
+
const emphasis = source.startsWith("《《", index) ? parseEmphasis(source, index) : void 0;
|
|
709
|
+
if (emphasis !== void 0) {
|
|
710
|
+
appendBefore(index);
|
|
711
|
+
const range = draft.append(source.slice(emphasis.contentStart, emphasis.contentEnd), emphasis.contentStart);
|
|
712
|
+
draft.annotate({
|
|
713
|
+
kind: "emphasis",
|
|
714
|
+
mark: "・",
|
|
715
|
+
range: {
|
|
716
|
+
...range,
|
|
717
|
+
source: {
|
|
718
|
+
start: index,
|
|
719
|
+
end: emphasis.end
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
index = emphasis.end;
|
|
724
|
+
literalStart = index;
|
|
725
|
+
hanRunStart = -1;
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const ruby = hanRunStart === -1 ? void 0 : parseRuby(source, hanRunStart, index, index);
|
|
729
|
+
if (ruby !== void 0) {
|
|
730
|
+
appendBefore(hanRunStart);
|
|
731
|
+
const range = draft.append(source.slice(ruby.parentStart, ruby.parentEnd), ruby.parentStart);
|
|
732
|
+
draft.annotate({
|
|
733
|
+
kind: "ruby",
|
|
734
|
+
reading: ruby.reading,
|
|
735
|
+
range: {
|
|
736
|
+
...range,
|
|
737
|
+
source: {
|
|
738
|
+
start: ruby.parentStart,
|
|
739
|
+
end: ruby.end
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
index = ruby.end;
|
|
744
|
+
literalStart = index;
|
|
745
|
+
hanRunStart = -1;
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (source.startsWith("《《", index) || hanRunStart !== -1) appendNotationWarning(hanRunStart === -1 ? index : hanRunStart, invalidNotationEnd(source, index));
|
|
749
|
+
hanRunStart = -1;
|
|
750
|
+
} else if (isHanAt(source, index)) {
|
|
751
|
+
if (hanRunStart === -1) hanRunStart = index;
|
|
752
|
+
} else hanRunStart = -1;
|
|
753
|
+
index += codePointLength(source, index);
|
|
754
|
+
}
|
|
755
|
+
appendBefore(source.length);
|
|
756
|
+
const manuscript = draft.build();
|
|
757
|
+
return ManuscriptResult.succeed(manuscript, unrecognizedNotationWarnings(source, PARSER_ID$1, manuscript, unrecognized));
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
//#endregion
|
|
761
|
+
//#region src/parser/internal/pixiv-notation.ts
|
|
762
|
+
function trimContent(text, sourceStart) {
|
|
763
|
+
const trimmedStart = text.trimStart();
|
|
764
|
+
const leadingLength = text.length - trimmedStart.length;
|
|
765
|
+
return {
|
|
766
|
+
text: trimmedStart.trimEnd(),
|
|
767
|
+
sourceStart: sourceStart + leadingLength
|
|
768
|
+
};
|
|
769
|
+
}
|
|
535
770
|
function closingIndex(source, start, closing) {
|
|
536
771
|
const end = source.indexOf(closing, start);
|
|
537
772
|
return end !== -1 && end < lineEnd(source, start) ? end : void 0;
|
|
@@ -607,49 +842,6 @@ function literalEnd(source, start) {
|
|
|
607
842
|
//#region src/parser/pixiv-parser.ts
|
|
608
843
|
const PARSER_ID = NamespacedId.of("kg/pixiv");
|
|
609
844
|
/**
|
|
610
|
-
* Locates an unrecognised source span in display and grapheme coordinates for the warning.
|
|
611
|
-
*/
|
|
612
|
-
function spanRange(manuscript, span) {
|
|
613
|
-
const covered = manuscript.graphemes.filter(({ range }) => range.source.start < span.end && range.source.end > span.start);
|
|
614
|
-
const first = covered[0];
|
|
615
|
-
const last = covered.at(-1);
|
|
616
|
-
if (first === void 0 || last === void 0) return ManuscriptRange.of({
|
|
617
|
-
source: span,
|
|
618
|
-
display: {
|
|
619
|
-
start: 0,
|
|
620
|
-
end: 0
|
|
621
|
-
},
|
|
622
|
-
graphemes: {
|
|
623
|
-
start: 0,
|
|
624
|
-
end: 0
|
|
625
|
-
}
|
|
626
|
-
});
|
|
627
|
-
return ManuscriptRange.of({
|
|
628
|
-
source: span,
|
|
629
|
-
display: {
|
|
630
|
-
start: first.range.display.start,
|
|
631
|
-
end: last.range.display.end
|
|
632
|
-
},
|
|
633
|
-
graphemes: {
|
|
634
|
-
start: first.range.graphemes.start,
|
|
635
|
-
end: last.range.graphemes.end
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
function unrecognizedNotationWarning(source, manuscript, span, index) {
|
|
640
|
-
return ManuscriptDiagnostic.of({
|
|
641
|
-
source,
|
|
642
|
-
origin: {
|
|
643
|
-
kind: "parser",
|
|
644
|
-
id: PARSER_ID
|
|
645
|
-
},
|
|
646
|
-
severity: "warning",
|
|
647
|
-
code: `unrecognized-notation-${index}`,
|
|
648
|
-
message: "認識できない記法を原文として扱いました",
|
|
649
|
-
range: spanRange(manuscript, span)
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
845
|
* Understands Pixiv's ruby, bold, italic and emphasis-mark notation. Anything bracket-shaped that
|
|
654
846
|
* does not parse is kept as literal text and reported as a warning rather than dropped.
|
|
655
847
|
*/
|
|
@@ -692,7 +884,7 @@ const pixivParser = {
|
|
|
692
884
|
index = end;
|
|
693
885
|
}
|
|
694
886
|
const manuscript = draft.build();
|
|
695
|
-
return ManuscriptResult.succeed(manuscript,
|
|
887
|
+
return ManuscriptResult.succeed(manuscript, unrecognizedNotationWarnings(source, PARSER_ID, manuscript, unrecognized));
|
|
696
888
|
}
|
|
697
889
|
};
|
|
698
890
|
//#endregion
|
|
@@ -1106,7 +1298,11 @@ function interpolate(template, data) {
|
|
|
1106
1298
|
const ProofreadingReport = { schema: readonlyObject({
|
|
1107
1299
|
range: ManuscriptRange.schema,
|
|
1108
1300
|
messageId: v.string(),
|
|
1109
|
-
data: v.optional(v.pipe(v.record(v.string(), v.union([v.string(), v.number()])), v.readonly()))
|
|
1301
|
+
data: v.optional(v.pipe(v.record(v.string(), v.union([v.string(), v.number()])), v.readonly())),
|
|
1302
|
+
/**
|
|
1303
|
+
* Omitted means `error`: a rule that says nothing is claiming it found a real defect.
|
|
1304
|
+
*/
|
|
1305
|
+
severity: v.optional(DiagnosticSeverity.schema)
|
|
1110
1306
|
}) };
|
|
1111
1307
|
//#endregion
|
|
1112
1308
|
//#region src/proofreading/proofreading-rule.ts
|
|
@@ -1197,7 +1393,7 @@ function toDiagnostics({ id, messages }, reports, source) {
|
|
|
1197
1393
|
kind: "rule",
|
|
1198
1394
|
id
|
|
1199
1395
|
},
|
|
1200
|
-
severity: "error",
|
|
1396
|
+
severity: report.severity ?? "error",
|
|
1201
1397
|
code: report.messageId,
|
|
1202
1398
|
message: interpolate(template, report.data),
|
|
1203
1399
|
range: report.range
|
|
@@ -1317,8 +1513,9 @@ function defineParsedRule(id, message, check) {
|
|
|
1317
1513
|
messages: { [DEFAULT_MESSAGE_ID]: message }
|
|
1318
1514
|
},
|
|
1319
1515
|
check: (manuscript, context) => {
|
|
1320
|
-
check(manuscript, (range) => {
|
|
1516
|
+
check(manuscript, (range, options) => {
|
|
1321
1517
|
context.report({
|
|
1518
|
+
...options,
|
|
1322
1519
|
range,
|
|
1323
1520
|
messageId: DEFAULT_MESSAGE_ID
|
|
1324
1521
|
});
|
|
@@ -1346,6 +1543,148 @@ function defineMatchRule(options) {
|
|
|
1346
1543
|
});
|
|
1347
1544
|
}
|
|
1348
1545
|
//#endregion
|
|
1546
|
+
//#region src/proofreading/rules/consistent-kanji-opening.ts
|
|
1547
|
+
const RULE_ID$4 = "kg/consistent-kanji-opening";
|
|
1548
|
+
const MESSAGE = "「{{ closed }}」と「{{ opened }}」が混在しています。文脈上の使い分けか表記ゆれかを確認してください";
|
|
1549
|
+
const KanjiOpeningPairSchema = v.pipe(readonlyObject({
|
|
1550
|
+
closed: v.pipe(v.string(), v.nonEmpty()),
|
|
1551
|
+
opened: v.pipe(v.string(), v.nonEmpty())
|
|
1552
|
+
}), v.check(({ closed, opened }) => closed !== opened, "a pair must hold two different spellings"));
|
|
1553
|
+
const PairsSchema = v.pipe(v.array(KanjiOpeningPairSchema), v.nonEmpty(), v.readonly());
|
|
1554
|
+
const DEFAULT_PAIRS = [
|
|
1555
|
+
{
|
|
1556
|
+
closed: "出来る",
|
|
1557
|
+
opened: "できる"
|
|
1558
|
+
},
|
|
1559
|
+
{
|
|
1560
|
+
closed: "出来ない",
|
|
1561
|
+
opened: "できない"
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
closed: "下さい",
|
|
1565
|
+
opened: "ください"
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
closed: "頂く",
|
|
1569
|
+
opened: "いただく"
|
|
1570
|
+
},
|
|
1571
|
+
{
|
|
1572
|
+
closed: "致す",
|
|
1573
|
+
opened: "いたす"
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
closed: "何故",
|
|
1577
|
+
opened: "なぜ"
|
|
1578
|
+
},
|
|
1579
|
+
{
|
|
1580
|
+
closed: "為に",
|
|
1581
|
+
opened: "ために"
|
|
1582
|
+
},
|
|
1583
|
+
{
|
|
1584
|
+
closed: "時に",
|
|
1585
|
+
opened: "ときに"
|
|
1586
|
+
},
|
|
1587
|
+
{
|
|
1588
|
+
closed: "様に",
|
|
1589
|
+
opened: "ように"
|
|
1590
|
+
},
|
|
1591
|
+
{
|
|
1592
|
+
closed: "方が",
|
|
1593
|
+
opened: "ほうが"
|
|
1594
|
+
}
|
|
1595
|
+
];
|
|
1596
|
+
/**
|
|
1597
|
+
* Reports the first kanji occurrence of every pair the manuscript also writes in kana. Whether the
|
|
1598
|
+
* two spellings are a deliberate distinction is the author's call, so the finding is a warning.
|
|
1599
|
+
*/
|
|
1600
|
+
function build$2(pairs) {
|
|
1601
|
+
return defineParsedRule(RULE_ID$4, MESSAGE, (manuscript, report) => {
|
|
1602
|
+
for (const { closed, opened } of pairs) {
|
|
1603
|
+
if (!manuscript.displayText.includes(opened)) continue;
|
|
1604
|
+
const index = manuscript.displayText.indexOf(closed);
|
|
1605
|
+
if (index === -1) continue;
|
|
1606
|
+
report(displayRange(manuscript, index, index + closed.length), {
|
|
1607
|
+
data: {
|
|
1608
|
+
closed,
|
|
1609
|
+
opened
|
|
1610
|
+
},
|
|
1611
|
+
severity: "warning"
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
const consistentKanjiOpeningRule = () => build$2(DEFAULT_PAIRS);
|
|
1617
|
+
function createConsistentKanjiOpeningRule(options = {}) {
|
|
1618
|
+
if (options.pairs === void 0) return ManuscriptResult.succeed(consistentKanjiOpeningRule());
|
|
1619
|
+
const pairs = v.safeParse(PairsSchema, options.pairs);
|
|
1620
|
+
if (!pairs.success) return ManuscriptResult.fail({
|
|
1621
|
+
kind: "InvalidRuleOptions",
|
|
1622
|
+
ruleId: RULE_ID$4,
|
|
1623
|
+
option: "pairs",
|
|
1624
|
+
issues: ValidationIssue.from(pairs.issues)
|
|
1625
|
+
});
|
|
1626
|
+
return ManuscriptResult.succeed(build$2(pairs.output));
|
|
1627
|
+
}
|
|
1628
|
+
//#endregion
|
|
1629
|
+
//#region src/proofreading/rules/internal/mixed-width.ts
|
|
1630
|
+
/**
|
|
1631
|
+
* Builds a rule that fires when a manuscript uses both widths of one character class. Which width
|
|
1632
|
+
* is right is the author's decision, so the finding is a warning, and one finding at the first
|
|
1633
|
+
* halfwidth occurrence says everything a list of every mixed character would.
|
|
1634
|
+
*/
|
|
1635
|
+
function defineMixedWidthRule({ id, message, halfwidth, fullwidth }) {
|
|
1636
|
+
return defineParsedRule(id, message, (manuscript, report) => {
|
|
1637
|
+
const found = halfwidth.exec(manuscript.displayText);
|
|
1638
|
+
const counterpart = fullwidth.exec(manuscript.displayText);
|
|
1639
|
+
if (found === null || counterpart === null) return;
|
|
1640
|
+
report(displayRange(manuscript, found.index, found.index + found[0].length), {
|
|
1641
|
+
data: {
|
|
1642
|
+
halfwidth: found[0],
|
|
1643
|
+
fullwidth: counterpart[0]
|
|
1644
|
+
},
|
|
1645
|
+
severity: "warning"
|
|
1646
|
+
});
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
//#endregion
|
|
1650
|
+
//#region src/proofreading/rules/consistent-latin-width.ts
|
|
1651
|
+
const consistentLatinWidthRule = () => defineMixedWidthRule({
|
|
1652
|
+
id: "kg/consistent-latin-width",
|
|
1653
|
+
message: "半角英字「{{ halfwidth }}」と全角英字「{{ fullwidth }}」が混在しています。作品内の方針を確認してください",
|
|
1654
|
+
halfwidth: /[A-Za-z]/u,
|
|
1655
|
+
fullwidth: /[A-Za-z]/u
|
|
1656
|
+
});
|
|
1657
|
+
//#endregion
|
|
1658
|
+
//#region src/proofreading/rules/consistent-numeral-width.ts
|
|
1659
|
+
const consistentNumeralWidthRule = () => defineMixedWidthRule({
|
|
1660
|
+
id: "kg/consistent-numeral-width",
|
|
1661
|
+
message: "半角数字「{{ halfwidth }}」と全角数字「{{ fullwidth }}」が混在しています。作品内の方針を確認してください",
|
|
1662
|
+
halfwidth: /[0-9]/u,
|
|
1663
|
+
fullwidth: /[0-9]/u
|
|
1664
|
+
});
|
|
1665
|
+
//#endregion
|
|
1666
|
+
//#region src/proofreading/rules/dash-character.ts
|
|
1667
|
+
/**
|
|
1668
|
+
* Stand-ins for `―`. Repeated `ー` belongs to `kg/no-consecutive-choonpu` and `−` to
|
|
1669
|
+
* `kg/minus-before-number`, so neither is reported twice.
|
|
1670
|
+
*/
|
|
1671
|
+
const dashCharacterRule = () => defineMatchRule({
|
|
1672
|
+
id: "kg/dash-character",
|
|
1673
|
+
pattern: /[—–─━﹣-]+|-{2,}/gu,
|
|
1674
|
+
message: "ダッシュには「―」を使ってください"
|
|
1675
|
+
});
|
|
1676
|
+
//#endregion
|
|
1677
|
+
//#region src/proofreading/rules/ellipsis-character.ts
|
|
1678
|
+
/**
|
|
1679
|
+
* Stand-ins for `…`. Runs of `。` and `・` are left to the rules that already count them, so one
|
|
1680
|
+
* substitute is never reported twice.
|
|
1681
|
+
*/
|
|
1682
|
+
const ellipsisCharacterRule = () => defineMatchRule({
|
|
1683
|
+
id: "kg/ellipsis-character",
|
|
1684
|
+
pattern: /\.{3,}|⋯+/gu,
|
|
1685
|
+
message: "三点リーダーには「…」を使ってください"
|
|
1686
|
+
});
|
|
1687
|
+
//#endregion
|
|
1349
1688
|
//#region src/proofreading/rules/even-dash.ts
|
|
1350
1689
|
const evenDashRule = () => defineMatchRule({
|
|
1351
1690
|
id: "kg/even-dash",
|
|
@@ -1362,13 +1701,78 @@ const evenEllipsisRule = () => defineMatchRule({
|
|
|
1362
1701
|
message: "連続する三点リーダーの数は偶数にしてください"
|
|
1363
1702
|
});
|
|
1364
1703
|
//#endregion
|
|
1704
|
+
//#region src/proofreading/rules/fullwidth-japanese-punctuation.ts
|
|
1705
|
+
const RULE_ID$3 = "kg/fullwidth-japanese-punctuation";
|
|
1706
|
+
const MESSAGES$2 = {
|
|
1707
|
+
"halfwidth-japanese-punctuation": "半角の「{{ character }}」があります。日本語の句読点・括弧・感嘆符・疑問符は全角にしてください",
|
|
1708
|
+
"halfwidth-near-japanese": "日本語の前後に半角の「{{ character }}」があります。全角にすべきか確認してください"
|
|
1709
|
+
};
|
|
1710
|
+
const JAPANESE_CHARACTER = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}々〆ヵヶ]/u;
|
|
1711
|
+
const CHECKS = [
|
|
1712
|
+
{
|
|
1713
|
+
pattern: /[。、「」]/gu,
|
|
1714
|
+
messageId: "halfwidth-japanese-punctuation",
|
|
1715
|
+
severity: "error",
|
|
1716
|
+
requiresJapanese: false
|
|
1717
|
+
},
|
|
1718
|
+
{
|
|
1719
|
+
pattern: /[!?]/gu,
|
|
1720
|
+
messageId: "halfwidth-japanese-punctuation",
|
|
1721
|
+
severity: "error",
|
|
1722
|
+
requiresJapanese: true
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
pattern: /[,.()[\]{}]/gu,
|
|
1726
|
+
messageId: "halfwidth-near-japanese",
|
|
1727
|
+
severity: "warning",
|
|
1728
|
+
requiresJapanese: true
|
|
1729
|
+
}
|
|
1730
|
+
];
|
|
1731
|
+
function isBesideJapanese(text, index) {
|
|
1732
|
+
const before = text[index - 1];
|
|
1733
|
+
const after = text[index + 1];
|
|
1734
|
+
return before !== void 0 && JAPANESE_CHARACTER.test(before) || after !== void 0 && JAPANESE_CHARACTER.test(after);
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* A period inside a run of three or more is a stand-in for `…`, which `kg/ellipsis-character`
|
|
1738
|
+
* already reports.
|
|
1739
|
+
*/
|
|
1740
|
+
function isEllipsisSubstitute(text, index) {
|
|
1741
|
+
if (text[index] !== ".") return false;
|
|
1742
|
+
let start = index;
|
|
1743
|
+
while (text[start - 1] === ".") start -= 1;
|
|
1744
|
+
let end = index;
|
|
1745
|
+
while (text[end + 1] === ".") end += 1;
|
|
1746
|
+
return end - start + 1 >= 3;
|
|
1747
|
+
}
|
|
1748
|
+
const fullwidthJapanesePunctuationRule = () => ({
|
|
1749
|
+
kind: "parsed",
|
|
1750
|
+
meta: {
|
|
1751
|
+
id: RULE_ID$3,
|
|
1752
|
+
messages: MESSAGES$2
|
|
1753
|
+
},
|
|
1754
|
+
check: (manuscript, context) => {
|
|
1755
|
+
for (const line of splitDisplayLines(manuscript.displayText)) for (const { pattern, messageId, severity, requiresJapanese } of CHECKS) for (const match of line.text.matchAll(pattern)) {
|
|
1756
|
+
if (requiresJapanese && !isBesideJapanese(line.text, match.index)) continue;
|
|
1757
|
+
if (isEllipsisSubstitute(line.text, match.index)) continue;
|
|
1758
|
+
const start = line.start + match.index;
|
|
1759
|
+
context.report({
|
|
1760
|
+
range: displayRange(manuscript, start, start + match[0].length),
|
|
1761
|
+
messageId,
|
|
1762
|
+
data: { character: match[0] },
|
|
1763
|
+
severity
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
});
|
|
1768
|
+
//#endregion
|
|
1365
1769
|
//#region src/proofreading/rules/max-arabic-numeral-digits.ts
|
|
1366
|
-
const RULE_ID$
|
|
1770
|
+
const RULE_ID$2 = "kg/max-arabic-numeral-digits";
|
|
1367
1771
|
const DEFAULT_MAX_DIGITS = 2;
|
|
1368
1772
|
const MaxDigitsSchema = v.pipe(v.number(), v.finite(), v.integer(), v.minValue(1));
|
|
1369
1773
|
function build$1(maxDigits) {
|
|
1370
1774
|
return defineMatchRule({
|
|
1371
|
-
id: RULE_ID$
|
|
1775
|
+
id: RULE_ID$2,
|
|
1372
1776
|
pattern: /([0-90-9]+)(?:[..]([0-90-9]+))?/gu,
|
|
1373
1777
|
accept: (match) => (match[1]?.length ?? 0) > maxDigits || (match[2]?.length ?? 0) > maxDigits,
|
|
1374
1778
|
message: `${maxDigits}桁を超えるアラビア数字が使われています`
|
|
@@ -1380,7 +1784,7 @@ function createMaxArabicNumeralDigitsRule(options = {}) {
|
|
|
1380
1784
|
const maxDigits = v.safeParse(MaxDigitsSchema, options.maxDigits);
|
|
1381
1785
|
if (!maxDigits.success) return ManuscriptResult.fail({
|
|
1382
1786
|
kind: "InvalidRuleOptions",
|
|
1383
|
-
ruleId: RULE_ID$
|
|
1787
|
+
ruleId: RULE_ID$2,
|
|
1384
1788
|
option: "maxDigits",
|
|
1385
1789
|
issues: ValidationIssue.from(maxDigits.issues)
|
|
1386
1790
|
});
|
|
@@ -1415,46 +1819,151 @@ const noConsecutivePunctuationRule = () => defineMatchRule({
|
|
|
1415
1819
|
message: "句読点が連続しています"
|
|
1416
1820
|
});
|
|
1417
1821
|
//#endregion
|
|
1418
|
-
//#region src/proofreading/rules/
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1822
|
+
//#region src/proofreading/rules/internal/brackets.ts
|
|
1823
|
+
/**
|
|
1824
|
+
* Brackets and quotes a Japanese paragraph may open with. Halfwidth forms are included because a
|
|
1825
|
+
* manuscript that uses them is still opening a paragraph, not writing prose.
|
|
1826
|
+
*/
|
|
1827
|
+
const OPENING_BRACKETS = "「『〖〈《((“\"‘'[[〔{{<<";
|
|
1828
|
+
/**
|
|
1829
|
+
* The closing counterparts, in the same order.
|
|
1830
|
+
*/
|
|
1831
|
+
const CLOSING_BRACKETS = "」』〗〉》))”\"’']]〕}}>>";
|
|
1832
|
+
/**
|
|
1833
|
+
* Escapes the characters a regular expression character class would otherwise read as syntax, so a
|
|
1834
|
+
* set declared above can be dropped into a pattern.
|
|
1835
|
+
*/
|
|
1836
|
+
function characterClass(characters) {
|
|
1837
|
+
return characters.replaceAll(/[\\\]^-]/gu, "\\$&");
|
|
1838
|
+
}
|
|
1839
|
+
//#endregion
|
|
1840
|
+
//#region src/proofreading/rules/internal/decoration-line.ts
|
|
1841
|
+
const DECORATION_LINE = /^[**◆◇■□●○▽▼△▲☆★・…―─━ー\-==~~\s]+$/u;
|
|
1842
|
+
/**
|
|
1843
|
+
* A line made only of symbols and spaces: a scene break or a section marker rather than a
|
|
1844
|
+
* paragraph. Indentation rules skip these, because their layout is deliberate.
|
|
1845
|
+
*/
|
|
1846
|
+
function isDecorationLine(text) {
|
|
1847
|
+
return DECORATION_LINE.test(text);
|
|
1848
|
+
}
|
|
1849
|
+
//#endregion
|
|
1850
|
+
//#region src/proofreading/rules/internal/spaces.ts
|
|
1851
|
+
const LEADING_SPACES = /^[ \t]+/u;
|
|
1852
|
+
/**
|
|
1853
|
+
* The run of spaces the text begins with, or an empty string. Halfwidth spaces and tabs count: a
|
|
1854
|
+
* rule has to see them to say they are the wrong space.
|
|
1855
|
+
*/
|
|
1856
|
+
function leadingSpaces(text) {
|
|
1857
|
+
return LEADING_SPACES.exec(text)?.[0] ?? "";
|
|
1858
|
+
}
|
|
1859
|
+
//#endregion
|
|
1860
|
+
//#region src/proofreading/rules/paragraph-opening.ts
|
|
1861
|
+
const RULE_ID$1 = "kg/paragraph-opening";
|
|
1862
|
+
const MESSAGES$1 = {
|
|
1863
|
+
"leading-character": "段落の先頭には全角スペースまたは開き括弧が必要です",
|
|
1864
|
+
"indent-width": "段落冒頭の字下げは全角スペース1字にしてください",
|
|
1865
|
+
"unexpected-indent": "括弧から始まる段落を字下げすることはできません"
|
|
1866
|
+
};
|
|
1867
|
+
const OpeningBracketsSchema = v.pipe(v.string(), v.nonEmpty());
|
|
1868
|
+
/**
|
|
1869
|
+
* How a paragraph opens, judged as one decision: a paragraph that opens with a bracket carries no
|
|
1870
|
+
* indent, and prose carries exactly one ideographic space. The three findings are branches of that
|
|
1871
|
+
* one decision rather than separate rules, so a line can never collect two of them at once.
|
|
1872
|
+
*/
|
|
1873
|
+
function build(openingBrackets) {
|
|
1874
|
+
return {
|
|
1875
|
+
kind: "parsed",
|
|
1876
|
+
meta: {
|
|
1877
|
+
id: RULE_ID$1,
|
|
1878
|
+
messages: MESSAGES$1
|
|
1879
|
+
},
|
|
1880
|
+
check: (manuscript, context) => {
|
|
1881
|
+
const report = (start, end, messageId) => {
|
|
1882
|
+
context.report({
|
|
1883
|
+
range: displayRange(manuscript, start, end),
|
|
1884
|
+
messageId
|
|
1885
|
+
});
|
|
1886
|
+
};
|
|
1887
|
+
for (const line of splitDisplayLines(manuscript.displayText)) {
|
|
1888
|
+
if (isDecorationLine(line.text)) continue;
|
|
1889
|
+
const indent = leadingSpaces(line.text);
|
|
1890
|
+
const opening = line.text[indent.length];
|
|
1891
|
+
if (opening === void 0) continue;
|
|
1892
|
+
if (openingBrackets.includes(opening)) {
|
|
1893
|
+
if (indent !== "") report(line.start, line.start + indent.length, "unexpected-indent");
|
|
1894
|
+
} else if (!line.text.startsWith(" ")) {
|
|
1895
|
+
const length = (line.text.codePointAt(0) ?? 0) > 65535 ? 2 : 1;
|
|
1896
|
+
report(line.start, line.start + length, "leading-character");
|
|
1897
|
+
} else if (indent.length !== 1) report(line.start, line.start + indent.length, "indent-width");
|
|
1898
|
+
}
|
|
1430
1899
|
}
|
|
1431
|
-
}
|
|
1900
|
+
};
|
|
1432
1901
|
}
|
|
1433
|
-
const
|
|
1434
|
-
function
|
|
1435
|
-
if (options.
|
|
1436
|
-
const
|
|
1437
|
-
if (!
|
|
1902
|
+
const paragraphOpeningRule = () => build(OPENING_BRACKETS);
|
|
1903
|
+
function createParagraphOpeningRule(options = {}) {
|
|
1904
|
+
if (options.openingBrackets === void 0) return ManuscriptResult.succeed(paragraphOpeningRule());
|
|
1905
|
+
const brackets = v.safeParse(OpeningBracketsSchema, options.openingBrackets);
|
|
1906
|
+
if (!brackets.success) return ManuscriptResult.fail({
|
|
1438
1907
|
kind: "InvalidRuleOptions",
|
|
1439
|
-
ruleId: RULE_ID,
|
|
1440
|
-
option: "
|
|
1441
|
-
issues: ValidationIssue.from(
|
|
1908
|
+
ruleId: RULE_ID$1,
|
|
1909
|
+
option: "openingBrackets",
|
|
1910
|
+
issues: ValidationIssue.from(brackets.issues)
|
|
1442
1911
|
});
|
|
1443
|
-
return ManuscriptResult.succeed(build(
|
|
1912
|
+
return ManuscriptResult.succeed(build(brackets.output));
|
|
1444
1913
|
}
|
|
1445
1914
|
//#endregion
|
|
1446
1915
|
//#region src/proofreading/rules/punctuation-before-closing-quote.ts
|
|
1916
|
+
const PATTERN = new RegExp(`[。、]+(?=[${characterClass(CLOSING_BRACKETS)}])`, "gu");
|
|
1447
1917
|
const punctuationBeforeClosingQuoteRule = () => defineMatchRule({
|
|
1448
1918
|
id: "kg/punctuation-before-closing-quote",
|
|
1449
|
-
pattern:
|
|
1919
|
+
pattern: PATTERN,
|
|
1450
1920
|
message: "閉じ括弧の直前に句読点を置くことはできません"
|
|
1451
1921
|
});
|
|
1452
1922
|
//#endregion
|
|
1453
1923
|
//#region src/proofreading/rules/space-after-question-or-exclamation.ts
|
|
1454
|
-
const
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1924
|
+
const RULE_ID = "kg/space-after-question-or-exclamation";
|
|
1925
|
+
const MESSAGES = {
|
|
1926
|
+
default: "感嘆符または疑問符の直後には全角スペースか閉じ括弧が必要です",
|
|
1927
|
+
"space-width": "感嘆符または疑問符の直後の空白は全角スペース1字にしてください",
|
|
1928
|
+
"before-closing-bracket": "閉じ括弧の直前に空白を置くことはできません"
|
|
1929
|
+
};
|
|
1930
|
+
/**
|
|
1931
|
+
* Runs, not single marks: `!?` is one mark and takes one gap, so the gap belongs after the run.
|
|
1932
|
+
*/
|
|
1933
|
+
const MARKS = /[?!]+/gu;
|
|
1934
|
+
/**
|
|
1935
|
+
* Checks the gap that follows every run of ! or ?. A run at the end of a line or right before a
|
|
1936
|
+
* closing bracket needs no gap; anywhere else it takes exactly one ideographic space.
|
|
1937
|
+
*/
|
|
1938
|
+
const spaceAfterQuestionOrExclamationRule = () => ({
|
|
1939
|
+
kind: "parsed",
|
|
1940
|
+
meta: {
|
|
1941
|
+
id: RULE_ID,
|
|
1942
|
+
messages: MESSAGES
|
|
1943
|
+
},
|
|
1944
|
+
check: (manuscript, context) => {
|
|
1945
|
+
const report = (start, end, messageId) => {
|
|
1946
|
+
context.report({
|
|
1947
|
+
range: displayRange(manuscript, start, end),
|
|
1948
|
+
messageId
|
|
1949
|
+
});
|
|
1950
|
+
};
|
|
1951
|
+
for (const line of splitDisplayLines(manuscript.displayText)) for (const marks of line.text.matchAll(MARKS)) {
|
|
1952
|
+
const marksEnd = marks.index + marks[0].length;
|
|
1953
|
+
const next = line.text[marksEnd];
|
|
1954
|
+
if (next === void 0 || "」』〗〉》))”\"’']]〕}}>>".includes(next)) continue;
|
|
1955
|
+
const spaces = leadingSpaces(line.text.slice(marksEnd));
|
|
1956
|
+
if (spaces === "") {
|
|
1957
|
+
report(line.start + marks.index, line.start + marksEnd, "default");
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
const following = line.text[marksEnd + spaces.length];
|
|
1961
|
+
const spacesStart = line.start + marksEnd;
|
|
1962
|
+
const spacesEnd = spacesStart + spaces.length;
|
|
1963
|
+
if (following !== void 0 && "」』〗〉》))”\"’']]〕}}>>".includes(following)) report(spacesStart, spacesEnd, "before-closing-bracket");
|
|
1964
|
+
else if (spaces !== " ") report(spacesStart, spacesEnd, "space-width");
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1458
1967
|
});
|
|
1459
1968
|
//#endregion
|
|
1460
1969
|
//#region src/proofreading/rules/variant-character.ts
|
|
@@ -1480,21 +1989,27 @@ const variantCharacterRule = () => defineParsedRule("kg/variant-character", "異
|
|
|
1480
1989
|
/**
|
|
1481
1990
|
* The built-in rule set at its default configuration. Infallible by construction: every rule is
|
|
1482
1991
|
* built from constants, so there is no options error for a caller to handle.
|
|
1992
|
+
*
|
|
1993
|
+
* Rules whose answer depends on the work's own conventions — which width numerals take, whether a
|
|
1994
|
+
* word is written in kanji or kana — are exported individually instead of included here.
|
|
1483
1995
|
*/
|
|
1484
1996
|
function createDefaultProofreadingRules() {
|
|
1485
1997
|
return [
|
|
1486
|
-
|
|
1998
|
+
paragraphOpeningRule(),
|
|
1487
1999
|
punctuationBeforeClosingQuoteRule(),
|
|
1488
2000
|
spaceAfterQuestionOrExclamationRule(),
|
|
1489
2001
|
evenEllipsisRule(),
|
|
1490
2002
|
evenDashRule(),
|
|
2003
|
+
ellipsisCharacterRule(),
|
|
2004
|
+
dashCharacterRule(),
|
|
1491
2005
|
noConsecutivePunctuationRule(),
|
|
1492
2006
|
noConsecutiveInterpunctRule(),
|
|
1493
2007
|
noConsecutiveChoonpuRule(),
|
|
1494
2008
|
minusBeforeNumberRule(),
|
|
1495
2009
|
maxArabicNumeralDigitsRule(),
|
|
2010
|
+
fullwidthJapanesePunctuationRule(),
|
|
1496
2011
|
variantCharacterRule()
|
|
1497
2012
|
];
|
|
1498
2013
|
}
|
|
1499
2014
|
//#endregion
|
|
1500
|
-
export { BoldAnnotation, ComposeError, CompositionStatistics, DiagnosticOrigin, DiagnosticSeverity, DisplayRange, EmphasisAnnotation, FontPreset, FontPresetId, FontSizePt, GraphemeRange, GridCell, GridLine, GridPage, GridSettings, GridStage, InvalidRuleOptions, ItalicAnnotation, LineOffset, ManuscriptAnnotation, ManuscriptAppearanceSettings, ManuscriptCompositionSettings, ManuscriptDiagnostic, ManuscriptGeometry, ManuscriptGridLayout, ManuscriptOffsets, ManuscriptRange, ManuscriptResult, NamespacedId, PaperSize, PaperSizeId, ParseError, ParsedGrapheme, ParsedManuscript, ProofreadError, ProofreadingReport, ProofreadingRuleMeta, Rejection, RubyAnnotation, SourcePosition, SourceRange, TextRange, ValidationIssue, assertNever, composeManuscript, createDefaultProofreadingRules, createMaxArabicNumeralDigitsRule,
|
|
2015
|
+
export { BoldAnnotation, ComposeError, CompositionStatistics, DiagnosticOrigin, DiagnosticSeverity, DisplayRange, EmphasisAnnotation, FontPreset, FontPresetId, FontSizePt, GraphemeRange, GridCell, GridLine, GridPage, GridSettings, GridStage, InvalidRuleOptions, ItalicAnnotation, LineOffset, ManuscriptAnnotation, ManuscriptAppearanceSettings, ManuscriptCompositionSettings, ManuscriptDiagnostic, ManuscriptGeometry, ManuscriptGridLayout, ManuscriptOffsets, ManuscriptRange, ManuscriptResult, NamespacedId, PaperSize, PaperSizeId, ParseError, ParsedGrapheme, ParsedManuscript, ProofreadError, ProofreadingReport, ProofreadingRuleMeta, Rejection, RubyAnnotation, SourcePosition, SourceRange, TextRange, ValidationIssue, assertNever, composeManuscript, consistentKanjiOpeningRule, consistentLatinWidthRule, consistentNumeralWidthRule, createConsistentKanjiOpeningRule, createDefaultProofreadingRules, createMaxArabicNumeralDigitsRule, createParagraphOpeningRule, dashCharacterRule, ellipsisCharacterRule, evenDashRule, evenEllipsisRule, fullwidthJapanesePunctuationRule, kakuyomuParser, manuscriptGridComposer, maxArabicNumeralDigitsRule, minusBeforeNumberRule, mmToPt, noConsecutiveChoonpuRule, noConsecutiveInterpunctRule, noConsecutivePunctuationRule, paragraphOpeningRule, parseManuscript, pixivParser, plainTextParser, proofreadManuscript, ptToMm, punctuationBeforeClosingQuoteRule, spaceAfterQuestionOrExclamationRule, variantCharacterRule };
|