@sushichan044/kg-core 0.4.1 → 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
@@ -63,7 +63,8 @@ element has source, display, and grapheme ranges; empty placement elements use
63
63
  Parsers implement `ManuscriptParser`. Composers implement `ManuscriptComposer`
64
64
  and provide Valibot schemas for their settings and layout. Proofreading rules
65
65
  carry `kind: "parsed"` or `kind: "composed"` to declare which manuscript they
66
- 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`.
67
68
 
68
69
  A plugin signals its own failure with a `Rejection` — a plain reason string.
69
70
  Core turns that into a variant of the stage's error union, so plugins never
@@ -88,9 +89,40 @@ Grapheme ranges index the normalized grapheme array. Diagnostics include
88
89
  one-based source line and column positions, so renderers do not need to search
89
90
  or recalculate locations.
90
91
 
91
- The default proofreading rules report common Japanese novel-style issues and
92
- Unicode variation sequences. Diagnostics do not modify the source and do not
93
- 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.
94
126
 
95
127
  ## Browser support
96
128
 
package/dist/index.d.mts CHANGED
@@ -14432,6 +14432,10 @@ declare const ProofreadingReportSchema: v.SchemaWithPipe<readonly [v.StrictObjec
14432
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<{
14433
14433
  [x: string]: string | number;
14434
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>;
14435
14439
  }, undefined>, v.ReadonlyAction<{
14436
14440
  readonly range: Readonly<{
14437
14441
  source: Readonly<{
@@ -14451,6 +14455,7 @@ declare const ProofreadingReportSchema: v.SchemaWithPipe<readonly [v.StrictObjec
14451
14455
  data?: Readonly<{
14452
14456
  [x: string]: string | number;
14453
14457
  }> | undefined;
14458
+ severity?: "error" | "warning" | undefined;
14454
14459
  }>]>;
14455
14460
  /**
14456
14461
  * What a rule hands back for one finding. The rule names a message it declared rather than
@@ -14517,6 +14522,10 @@ declare const ProofreadingReport: {
14517
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<{
14518
14523
  [x: string]: string | number;
14519
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>;
14520
14529
  }, undefined>, v.ReadonlyAction<{
14521
14530
  readonly range: Readonly<{
14522
14531
  source: Readonly<{
@@ -14536,6 +14545,7 @@ declare const ProofreadingReport: {
14536
14545
  data?: Readonly<{
14537
14546
  [x: string]: string | number;
14538
14547
  }> | undefined;
14548
+ severity?: "error" | "warning" | undefined;
14539
14549
  }>]>;
14540
14550
  };
14541
14551
  //#endregion
@@ -14620,19 +14630,66 @@ declare function proofreadManuscript(manuscript: ParsedManuscript, options: Proo
14620
14630
  */
14621
14631
  declare function proofreadManuscript<TComposed extends ComposedManuscript>(manuscript: TComposed, options: ProofreadOptions<ParsedProofreadingRule | ComposedProofreadingRule<TComposed>>): ProofreadOutcome;
14622
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
14623
14667
  //#region src/proofreading/rules/default-rules.d.ts
14624
14668
  /**
14625
14669
  * The built-in rule set at its default configuration. Infallible by construction: every rule is
14626
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.
14627
14674
  */
14628
14675
  declare function createDefaultProofreadingRules(): readonly ParsedProofreadingRule[];
14629
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
14630
14684
  //#region src/proofreading/rules/even-dash.d.ts
14631
14685
  declare const evenDashRule: () => ParsedProofreadingRule;
14632
14686
  //#endregion
14633
14687
  //#region src/proofreading/rules/even-ellipsis.d.ts
14634
14688
  declare const evenEllipsisRule: () => ParsedProofreadingRule;
14635
14689
  //#endregion
14690
+ //#region src/proofreading/rules/fullwidth-japanese-punctuation.d.ts
14691
+ declare const fullwidthJapanesePunctuationRule: () => ParsedProofreadingRule;
14692
+ //#endregion
14636
14693
  //#region src/proofreading/rules/max-arabic-numeral-digits.d.ts
14637
14694
  type MaxArabicNumeralDigitsOptions = Readonly<{
14638
14695
  maxDigits?: number;
@@ -14652,17 +14709,21 @@ declare const noConsecutiveInterpunctRule: () => ParsedProofreadingRule;
14652
14709
  //#region src/proofreading/rules/no-consecutive-punctuation.d.ts
14653
14710
  declare const noConsecutivePunctuationRule: () => ParsedProofreadingRule;
14654
14711
  //#endregion
14655
- //#region src/proofreading/rules/paragraph-leading-character.d.ts
14656
- type ParagraphLeadingCharacterOptions = Readonly<{
14657
- allowedCharacters?: string;
14712
+ //#region src/proofreading/rules/paragraph-opening.d.ts
14713
+ type ParagraphOpeningOptions = Readonly<{
14714
+ openingBrackets?: string;
14658
14715
  }>;
14659
- declare const paragraphLeadingCharacterRule: () => ParsedProofreadingRule;
14660
- declare function createParagraphLeadingCharacterRule(options?: ParagraphLeadingCharacterOptions): ManuscriptResult<ParsedProofreadingRule, InvalidRuleOptions>;
14716
+ declare const paragraphOpeningRule: () => ParsedProofreadingRule;
14717
+ declare function createParagraphOpeningRule(options?: ParagraphOpeningOptions): ManuscriptResult<ParsedProofreadingRule, InvalidRuleOptions>;
14661
14718
  //#endregion
14662
14719
  //#region src/proofreading/rules/punctuation-before-closing-quote.d.ts
14663
14720
  declare const punctuationBeforeClosingQuoteRule: () => ParsedProofreadingRule;
14664
14721
  //#endregion
14665
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
+ */
14666
14727
  declare const spaceAfterQuestionOrExclamationRule: () => ParsedProofreadingRule;
14667
14728
  //#endregion
14668
14729
  //#region src/proofreading/rules/variant-character.d.ts
@@ -14671,4 +14732,4 @@ declare const spaceAfterQuestionOrExclamationRule: () => ParsedProofreadingRule;
14671
14732
  */
14672
14733
  declare const variantCharacterRule: () => ParsedProofreadingRule;
14673
14734
  //#endregion
14674
- 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, kakuyomuParser, 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
@@ -1298,7 +1298,11 @@ function interpolate(template, data) {
1298
1298
  const ProofreadingReport = { schema: readonlyObject({
1299
1299
  range: ManuscriptRange.schema,
1300
1300
  messageId: v.string(),
1301
- 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)
1302
1306
  }) };
1303
1307
  //#endregion
1304
1308
  //#region src/proofreading/proofreading-rule.ts
@@ -1389,7 +1393,7 @@ function toDiagnostics({ id, messages }, reports, source) {
1389
1393
  kind: "rule",
1390
1394
  id
1391
1395
  },
1392
- severity: "error",
1396
+ severity: report.severity ?? "error",
1393
1397
  code: report.messageId,
1394
1398
  message: interpolate(template, report.data),
1395
1399
  range: report.range
@@ -1509,8 +1513,9 @@ function defineParsedRule(id, message, check) {
1509
1513
  messages: { [DEFAULT_MESSAGE_ID]: message }
1510
1514
  },
1511
1515
  check: (manuscript, context) => {
1512
- check(manuscript, (range) => {
1516
+ check(manuscript, (range, options) => {
1513
1517
  context.report({
1518
+ ...options,
1514
1519
  range,
1515
1520
  messageId: DEFAULT_MESSAGE_ID
1516
1521
  });
@@ -1538,6 +1543,148 @@ function defineMatchRule(options) {
1538
1543
  });
1539
1544
  }
1540
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
1541
1688
  //#region src/proofreading/rules/even-dash.ts
1542
1689
  const evenDashRule = () => defineMatchRule({
1543
1690
  id: "kg/even-dash",
@@ -1554,13 +1701,78 @@ const evenEllipsisRule = () => defineMatchRule({
1554
1701
  message: "連続する三点リーダーの数は偶数にしてください"
1555
1702
  });
1556
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
1557
1769
  //#region src/proofreading/rules/max-arabic-numeral-digits.ts
1558
- const RULE_ID$1 = "kg/max-arabic-numeral-digits";
1770
+ const RULE_ID$2 = "kg/max-arabic-numeral-digits";
1559
1771
  const DEFAULT_MAX_DIGITS = 2;
1560
1772
  const MaxDigitsSchema = v.pipe(v.number(), v.finite(), v.integer(), v.minValue(1));
1561
1773
  function build$1(maxDigits) {
1562
1774
  return defineMatchRule({
1563
- id: RULE_ID$1,
1775
+ id: RULE_ID$2,
1564
1776
  pattern: /([0-90-9]+)(?:[..]([0-90-9]+))?/gu,
1565
1777
  accept: (match) => (match[1]?.length ?? 0) > maxDigits || (match[2]?.length ?? 0) > maxDigits,
1566
1778
  message: `${maxDigits}桁を超えるアラビア数字が使われています`
@@ -1572,7 +1784,7 @@ function createMaxArabicNumeralDigitsRule(options = {}) {
1572
1784
  const maxDigits = v.safeParse(MaxDigitsSchema, options.maxDigits);
1573
1785
  if (!maxDigits.success) return ManuscriptResult.fail({
1574
1786
  kind: "InvalidRuleOptions",
1575
- ruleId: RULE_ID$1,
1787
+ ruleId: RULE_ID$2,
1576
1788
  option: "maxDigits",
1577
1789
  issues: ValidationIssue.from(maxDigits.issues)
1578
1790
  });
@@ -1607,46 +1819,151 @@ const noConsecutivePunctuationRule = () => defineMatchRule({
1607
1819
  message: "句読点が連続しています"
1608
1820
  });
1609
1821
  //#endregion
1610
- //#region src/proofreading/rules/paragraph-leading-character.ts
1611
- const RULE_ID = "kg/paragraph-leading-character";
1612
- const MESSAGE = "段落の先頭には全角スペースまたは開き括弧が必要です";
1613
- const DEFAULT_ALLOWED_CHARACTERS = " 「『〖〈《((“\"‘'[[〔{{<<";
1614
- const AllowedCharactersSchema = v.pipe(v.string(), v.nonEmpty());
1615
- function build(allowedCharacters) {
1616
- return defineParsedRule(RULE_ID, MESSAGE, (manuscript, report) => {
1617
- for (const line of splitDisplayLines(manuscript.displayText)) {
1618
- const leading = line.text[0];
1619
- if (leading === void 0 || allowedCharacters.includes(leading)) continue;
1620
- const length = (line.text.codePointAt(0) ?? 0) > 65535 ? 2 : 1;
1621
- 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
+ }
1622
1899
  }
1623
- });
1900
+ };
1624
1901
  }
1625
- const paragraphLeadingCharacterRule = () => build(DEFAULT_ALLOWED_CHARACTERS);
1626
- function createParagraphLeadingCharacterRule(options = {}) {
1627
- if (options.allowedCharacters === void 0) return ManuscriptResult.succeed(paragraphLeadingCharacterRule());
1628
- const allowed = v.safeParse(AllowedCharactersSchema, options.allowedCharacters);
1629
- 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({
1630
1907
  kind: "InvalidRuleOptions",
1631
- ruleId: RULE_ID,
1632
- option: "allowedCharacters",
1633
- issues: ValidationIssue.from(allowed.issues)
1908
+ ruleId: RULE_ID$1,
1909
+ option: "openingBrackets",
1910
+ issues: ValidationIssue.from(brackets.issues)
1634
1911
  });
1635
- return ManuscriptResult.succeed(build(allowed.output));
1912
+ return ManuscriptResult.succeed(build(brackets.output));
1636
1913
  }
1637
1914
  //#endregion
1638
1915
  //#region src/proofreading/rules/punctuation-before-closing-quote.ts
1916
+ const PATTERN = new RegExp(`[。、]+(?=[${characterClass(CLOSING_BRACKETS)}])`, "gu");
1639
1917
  const punctuationBeforeClosingQuoteRule = () => defineMatchRule({
1640
1918
  id: "kg/punctuation-before-closing-quote",
1641
- pattern: /[。、]+(?=[」』〗〉》))”"’']\]〕}}>>])/gu,
1919
+ pattern: PATTERN,
1642
1920
  message: "閉じ括弧の直前に句読点を置くことはできません"
1643
1921
  });
1644
1922
  //#endregion
1645
1923
  //#region src/proofreading/rules/space-after-question-or-exclamation.ts
1646
- const spaceAfterQuestionOrExclamationRule = () => defineMatchRule({
1647
- id: "kg/space-after-question-or-exclamation",
1648
- pattern: /[?!](?![  ?!」』〗〉》))”"’']\]〕}}>>]|$)/gu,
1649
- 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
+ }
1650
1967
  });
1651
1968
  //#endregion
1652
1969
  //#region src/proofreading/rules/variant-character.ts
@@ -1672,21 +1989,27 @@ const variantCharacterRule = () => defineParsedRule("kg/variant-character", "異
1672
1989
  /**
1673
1990
  * The built-in rule set at its default configuration. Infallible by construction: every rule is
1674
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.
1675
1995
  */
1676
1996
  function createDefaultProofreadingRules() {
1677
1997
  return [
1678
- paragraphLeadingCharacterRule(),
1998
+ paragraphOpeningRule(),
1679
1999
  punctuationBeforeClosingQuoteRule(),
1680
2000
  spaceAfterQuestionOrExclamationRule(),
1681
2001
  evenEllipsisRule(),
1682
2002
  evenDashRule(),
2003
+ ellipsisCharacterRule(),
2004
+ dashCharacterRule(),
1683
2005
  noConsecutivePunctuationRule(),
1684
2006
  noConsecutiveInterpunctRule(),
1685
2007
  noConsecutiveChoonpuRule(),
1686
2008
  minusBeforeNumberRule(),
1687
2009
  maxArabicNumeralDigitsRule(),
2010
+ fullwidthJapanesePunctuationRule(),
1688
2011
  variantCharacterRule()
1689
2012
  ];
1690
2013
  }
1691
2014
  //#endregion
1692
- 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, kakuyomuParser, 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.1",
3
+ "version": "0.5.0",
4
4
  "description": "Framework-independent parsing, composition, and proofreading for Japanese manuscripts",
5
5
  "keywords": [
6
6
  "japanese",