@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 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: pixivParser });
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 Pixiv
50
- parser normalizes ruby, bold, italic, and emphasis notation into a closed
51
- annotation union. Malformed or unknown notation remains visible and produces a
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
- The default proofreading rules report common Japanese novel-style issues and
91
- Unicode variation sequences. Diagnostics do not modify the source and do not
92
- contain automatic fixes.
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-leading-character.d.ts
14649
- type ParagraphLeadingCharacterOptions = Readonly<{
14650
- allowedCharacters?: string;
14712
+ //#region src/proofreading/rules/paragraph-opening.d.ts
14713
+ type ParagraphOpeningOptions = Readonly<{
14714
+ openingBrackets?: string;
14651
14715
  }>;
14652
- declare const paragraphLeadingCharacterRule: () => ParsedProofreadingRule;
14653
- declare function createParagraphLeadingCharacterRule(options?: ParagraphLeadingCharacterOptions): ManuscriptResult<ParsedProofreadingRule, InvalidRuleOptions>;
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 ParagraphLeadingCharacterOptions, 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, createDefaultProofreadingRules, createMaxArabicNumeralDigitsRule, createParagraphLeadingCharacterRule, evenDashRule, evenEllipsisRule, manuscriptGridComposer, maxArabicNumeralDigitsRule, minusBeforeNumberRule, mmToPt, noConsecutiveChoonpuRule, noConsecutiveInterpunctRule, noConsecutivePunctuationRule, paragraphLeadingCharacterRule, parseManuscript, pixivParser, plainTextParser, proofreadManuscript, ptToMm, punctuationBeforeClosingQuoteRule, spaceAfterQuestionOrExclamationRule, variantCharacterRule };
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/pixiv-notation.ts
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
- * Tags never span lines, so a candidate closing delimiter past the line end does not count.
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, unrecognized.map((span, order) => unrecognizedNotationWarning(source, manuscript, span, order)));
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$1 = "kg/max-arabic-numeral-digits";
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$1,
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$1,
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/paragraph-leading-character.ts
1419
- const RULE_ID = "kg/paragraph-leading-character";
1420
- const MESSAGE = "段落の先頭には全角スペースまたは開き括弧が必要です";
1421
- const DEFAULT_ALLOWED_CHARACTERS = " 「『〖〈《((“\"‘'[[〔{{<<";
1422
- const AllowedCharactersSchema = v.pipe(v.string(), v.nonEmpty());
1423
- function build(allowedCharacters) {
1424
- return defineParsedRule(RULE_ID, MESSAGE, (manuscript, report) => {
1425
- for (const line of splitDisplayLines(manuscript.displayText)) {
1426
- const leading = line.text[0];
1427
- if (leading === void 0 || allowedCharacters.includes(leading)) continue;
1428
- const length = (line.text.codePointAt(0) ?? 0) > 65535 ? 2 : 1;
1429
- report(displayRange(manuscript, line.start, line.start + length));
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 paragraphLeadingCharacterRule = () => build(DEFAULT_ALLOWED_CHARACTERS);
1434
- function createParagraphLeadingCharacterRule(options = {}) {
1435
- if (options.allowedCharacters === void 0) return ManuscriptResult.succeed(paragraphLeadingCharacterRule());
1436
- const allowed = v.safeParse(AllowedCharactersSchema, options.allowedCharacters);
1437
- if (!allowed.success) return ManuscriptResult.fail({
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: "allowedCharacters",
1441
- issues: ValidationIssue.from(allowed.issues)
1908
+ ruleId: RULE_ID$1,
1909
+ option: "openingBrackets",
1910
+ issues: ValidationIssue.from(brackets.issues)
1442
1911
  });
1443
- return ManuscriptResult.succeed(build(allowed.output));
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: /[。、]+(?=[」』〗〉》))”"’']\]〕}}>>])/gu,
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 spaceAfterQuestionOrExclamationRule = () => defineMatchRule({
1455
- id: "kg/space-after-question-or-exclamation",
1456
- pattern: /[?!](?![  ?!」』〗〉》))”"’']\]〕}}>>]|$)/gu,
1457
- message: "感嘆符または疑問符の直後には空白か閉じ括弧が必要です"
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
- paragraphLeadingCharacterRule(),
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, createParagraphLeadingCharacterRule, evenDashRule, evenEllipsisRule, manuscriptGridComposer, maxArabicNumeralDigitsRule, minusBeforeNumberRule, mmToPt, noConsecutiveChoonpuRule, noConsecutiveInterpunctRule, noConsecutivePunctuationRule, paragraphLeadingCharacterRule, parseManuscript, pixivParser, plainTextParser, proofreadManuscript, ptToMm, punctuationBeforeClosingQuoteRule, spaceAfterQuestionOrExclamationRule, variantCharacterRule };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sushichan044/kg-core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Framework-independent parsing, composition, and proofreading for Japanese manuscripts",
5
5
  "keywords": [
6
6
  "japanese",