@sushichan044/kg-core 0.4.1 → 0.6.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/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,12 +1543,194 @@ function defineMatchRule(options) {
1538
1543
  });
1539
1544
  }
1540
1545
  //#endregion
1541
- //#region src/proofreading/rules/even-dash.ts
1542
- const evenDashRule = () => defineMatchRule({
1543
- id: "kg/even-dash",
1544
- pattern: /―+/gu,
1545
- accept: (match) => match[0].length % 2 === 1,
1546
- message: "連続するダッシュの数は偶数にしてください"
1546
+ //#region src/proofreading/rules/consistent-kanji-opening.ts
1547
+ const RULE_ID$5 = "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$3(pairs) {
1601
+ return defineParsedRule(RULE_ID$5, 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$3(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$5,
1623
+ option: "pairs",
1624
+ issues: ValidationIssue.from(pairs.issues)
1625
+ });
1626
+ return ManuscriptResult.succeed(build$3(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.ts
1667
+ const RULE_ID$4 = "kg/dash";
1668
+ const DEFAULT_PREFERRED = "―";
1669
+ const DashCharacterSchema = v.picklist([
1670
+ "―",
1671
+ "—",
1672
+ "─"
1673
+ ]);
1674
+ const DASH_LIKE_RUN = /[—–―─━﹣--]+/gu;
1675
+ const EVEN_COUNT_MESSAGE = "連続するダッシュの数は偶数にしてください";
1676
+ function messageIdFor(run, preferred) {
1677
+ if (run === "-") return void 0;
1678
+ if (run !== preferred.repeat(run.length)) return "character";
1679
+ return run.length % 2 === 1 ? "even-count" : void 0;
1680
+ }
1681
+ function build$2(preferred) {
1682
+ const messages = {
1683
+ character: `ダッシュには「${preferred}」を使ってください`,
1684
+ "even-count": EVEN_COUNT_MESSAGE
1685
+ };
1686
+ return {
1687
+ kind: "parsed",
1688
+ meta: {
1689
+ id: RULE_ID$4,
1690
+ messages
1691
+ },
1692
+ check: (manuscript, context) => {
1693
+ for (const line of splitDisplayLines(manuscript.displayText)) {
1694
+ DASH_LIKE_RUN.lastIndex = 0;
1695
+ let match = DASH_LIKE_RUN.exec(line.text);
1696
+ while (match !== null) {
1697
+ const run = match[0];
1698
+ const messageId = messageIdFor(run, preferred);
1699
+ if (messageId !== void 0) {
1700
+ const start = line.start + match.index;
1701
+ context.report({
1702
+ range: displayRange(manuscript, start, start + run.length),
1703
+ messageId
1704
+ });
1705
+ }
1706
+ match = DASH_LIKE_RUN.exec(line.text);
1707
+ }
1708
+ }
1709
+ }
1710
+ };
1711
+ }
1712
+ const dashRule = () => build$2(DEFAULT_PREFERRED);
1713
+ function createDashRule(options = {}) {
1714
+ if (options.preferred === void 0) return ManuscriptResult.succeed(dashRule());
1715
+ const preferred = v.safeParse(DashCharacterSchema, options.preferred);
1716
+ if (!preferred.success) return ManuscriptResult.fail({
1717
+ kind: "InvalidRuleOptions",
1718
+ ruleId: RULE_ID$4,
1719
+ option: "preferred",
1720
+ issues: ValidationIssue.from(preferred.issues)
1721
+ });
1722
+ return ManuscriptResult.succeed(build$2(preferred.output));
1723
+ }
1724
+ //#endregion
1725
+ //#region src/proofreading/rules/ellipsis-character.ts
1726
+ /**
1727
+ * Stand-ins for `…`. Runs of `。` and `・` are left to the rules that already count them, so one
1728
+ * substitute is never reported twice.
1729
+ */
1730
+ const ellipsisCharacterRule = () => defineMatchRule({
1731
+ id: "kg/ellipsis-character",
1732
+ pattern: /\.{3,}|⋯+/gu,
1733
+ message: "三点リーダーには「…」を使ってください"
1547
1734
  });
1548
1735
  //#endregion
1549
1736
  //#region src/proofreading/rules/even-ellipsis.ts
@@ -1554,13 +1741,78 @@ const evenEllipsisRule = () => defineMatchRule({
1554
1741
  message: "連続する三点リーダーの数は偶数にしてください"
1555
1742
  });
1556
1743
  //#endregion
1744
+ //#region src/proofreading/rules/fullwidth-japanese-punctuation.ts
1745
+ const RULE_ID$3 = "kg/fullwidth-japanese-punctuation";
1746
+ const MESSAGES$2 = {
1747
+ "halfwidth-japanese-punctuation": "半角の「{{ character }}」があります。日本語の句読点・括弧・感嘆符・疑問符は全角にしてください",
1748
+ "halfwidth-near-japanese": "日本語の前後に半角の「{{ character }}」があります。全角にすべきか確認してください"
1749
+ };
1750
+ const JAPANESE_CHARACTER = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}々〆ヵヶ]/u;
1751
+ const CHECKS = [
1752
+ {
1753
+ pattern: /[。、「」]/gu,
1754
+ messageId: "halfwidth-japanese-punctuation",
1755
+ severity: "error",
1756
+ requiresJapanese: false
1757
+ },
1758
+ {
1759
+ pattern: /[!?]/gu,
1760
+ messageId: "halfwidth-japanese-punctuation",
1761
+ severity: "error",
1762
+ requiresJapanese: true
1763
+ },
1764
+ {
1765
+ pattern: /[,.()[\]{}]/gu,
1766
+ messageId: "halfwidth-near-japanese",
1767
+ severity: "warning",
1768
+ requiresJapanese: true
1769
+ }
1770
+ ];
1771
+ function isBesideJapanese(text, index) {
1772
+ const before = text[index - 1];
1773
+ const after = text[index + 1];
1774
+ return before !== void 0 && JAPANESE_CHARACTER.test(before) || after !== void 0 && JAPANESE_CHARACTER.test(after);
1775
+ }
1776
+ /**
1777
+ * A period inside a run of three or more is a stand-in for `…`, which `kg/ellipsis-character`
1778
+ * already reports.
1779
+ */
1780
+ function isEllipsisSubstitute(text, index) {
1781
+ if (text[index] !== ".") return false;
1782
+ let start = index;
1783
+ while (text[start - 1] === ".") start -= 1;
1784
+ let end = index;
1785
+ while (text[end + 1] === ".") end += 1;
1786
+ return end - start + 1 >= 3;
1787
+ }
1788
+ const fullwidthJapanesePunctuationRule = () => ({
1789
+ kind: "parsed",
1790
+ meta: {
1791
+ id: RULE_ID$3,
1792
+ messages: MESSAGES$2
1793
+ },
1794
+ check: (manuscript, context) => {
1795
+ for (const line of splitDisplayLines(manuscript.displayText)) for (const { pattern, messageId, severity, requiresJapanese } of CHECKS) for (const match of line.text.matchAll(pattern)) {
1796
+ if (requiresJapanese && !isBesideJapanese(line.text, match.index)) continue;
1797
+ if (isEllipsisSubstitute(line.text, match.index)) continue;
1798
+ const start = line.start + match.index;
1799
+ context.report({
1800
+ range: displayRange(manuscript, start, start + match[0].length),
1801
+ messageId,
1802
+ data: { character: match[0] },
1803
+ severity
1804
+ });
1805
+ }
1806
+ }
1807
+ });
1808
+ //#endregion
1557
1809
  //#region src/proofreading/rules/max-arabic-numeral-digits.ts
1558
- const RULE_ID$1 = "kg/max-arabic-numeral-digits";
1810
+ const RULE_ID$2 = "kg/max-arabic-numeral-digits";
1559
1811
  const DEFAULT_MAX_DIGITS = 2;
1560
1812
  const MaxDigitsSchema = v.pipe(v.number(), v.finite(), v.integer(), v.minValue(1));
1561
1813
  function build$1(maxDigits) {
1562
1814
  return defineMatchRule({
1563
- id: RULE_ID$1,
1815
+ id: RULE_ID$2,
1564
1816
  pattern: /([0-90-9]+)(?:[..]([0-90-9]+))?/gu,
1565
1817
  accept: (match) => (match[1]?.length ?? 0) > maxDigits || (match[2]?.length ?? 0) > maxDigits,
1566
1818
  message: `${maxDigits}桁を超えるアラビア数字が使われています`
@@ -1572,7 +1824,7 @@ function createMaxArabicNumeralDigitsRule(options = {}) {
1572
1824
  const maxDigits = v.safeParse(MaxDigitsSchema, options.maxDigits);
1573
1825
  if (!maxDigits.success) return ManuscriptResult.fail({
1574
1826
  kind: "InvalidRuleOptions",
1575
- ruleId: RULE_ID$1,
1827
+ ruleId: RULE_ID$2,
1576
1828
  option: "maxDigits",
1577
1829
  issues: ValidationIssue.from(maxDigits.issues)
1578
1830
  });
@@ -1586,13 +1838,6 @@ const minusBeforeNumberRule = () => defineMatchRule({
1586
1838
  message: "マイナス記号の直後には数字が必要です"
1587
1839
  });
1588
1840
  //#endregion
1589
- //#region src/proofreading/rules/no-consecutive-choonpu.ts
1590
- const noConsecutiveChoonpuRule = () => defineMatchRule({
1591
- id: "kg/no-consecutive-choonpu",
1592
- pattern: /ーー+/gu,
1593
- message: "長音符が連続しています"
1594
- });
1595
- //#endregion
1596
1841
  //#region src/proofreading/rules/no-consecutive-interpunct.ts
1597
1842
  const noConsecutiveInterpunctRule = () => defineMatchRule({
1598
1843
  id: "kg/no-consecutive-interpunct",
@@ -1607,46 +1852,151 @@ const noConsecutivePunctuationRule = () => defineMatchRule({
1607
1852
  message: "句読点が連続しています"
1608
1853
  });
1609
1854
  //#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));
1855
+ //#region src/proofreading/rules/internal/brackets.ts
1856
+ /**
1857
+ * Brackets and quotes a Japanese paragraph may open with. Halfwidth forms are included because a
1858
+ * manuscript that uses them is still opening a paragraph, not writing prose.
1859
+ */
1860
+ const OPENING_BRACKETS = "「『〖〈《((“\"‘'[[〔{{<<";
1861
+ /**
1862
+ * The closing counterparts, in the same order.
1863
+ */
1864
+ const CLOSING_BRACKETS = "」』〗〉》))”\"’']]〕}}>>";
1865
+ /**
1866
+ * Escapes the characters a regular expression character class would otherwise read as syntax, so a
1867
+ * set declared above can be dropped into a pattern.
1868
+ */
1869
+ function characterClass(characters) {
1870
+ return characters.replaceAll(/[\\\]^-]/gu, "\\$&");
1871
+ }
1872
+ //#endregion
1873
+ //#region src/proofreading/rules/internal/decoration-line.ts
1874
+ const DECORATION_LINE = /^[**◆◇■□●○▽▼△▲☆★・…―─━ー\-==~~\s]+$/u;
1875
+ /**
1876
+ * A line made only of symbols and spaces: a scene break or a section marker rather than a
1877
+ * paragraph. Indentation rules skip these, because their layout is deliberate.
1878
+ */
1879
+ function isDecorationLine(text) {
1880
+ return DECORATION_LINE.test(text);
1881
+ }
1882
+ //#endregion
1883
+ //#region src/proofreading/rules/internal/spaces.ts
1884
+ const LEADING_SPACES = /^[  \t]+/u;
1885
+ /**
1886
+ * The run of spaces the text begins with, or an empty string. Halfwidth spaces and tabs count: a
1887
+ * rule has to see them to say they are the wrong space.
1888
+ */
1889
+ function leadingSpaces(text) {
1890
+ return LEADING_SPACES.exec(text)?.[0] ?? "";
1891
+ }
1892
+ //#endregion
1893
+ //#region src/proofreading/rules/paragraph-opening.ts
1894
+ const RULE_ID$1 = "kg/paragraph-opening";
1895
+ const MESSAGES$1 = {
1896
+ "leading-character": "段落の先頭には全角スペースまたは開き括弧が必要です",
1897
+ "indent-width": "段落冒頭の字下げは全角スペース1字にしてください",
1898
+ "unexpected-indent": "括弧から始まる段落を字下げすることはできません"
1899
+ };
1900
+ const OpeningBracketsSchema = v.pipe(v.string(), v.nonEmpty());
1901
+ /**
1902
+ * How a paragraph opens, judged as one decision: a paragraph that opens with a bracket carries no
1903
+ * indent, and prose carries exactly one ideographic space. The three findings are branches of that
1904
+ * one decision rather than separate rules, so a line can never collect two of them at once.
1905
+ */
1906
+ function build(openingBrackets) {
1907
+ return {
1908
+ kind: "parsed",
1909
+ meta: {
1910
+ id: RULE_ID$1,
1911
+ messages: MESSAGES$1
1912
+ },
1913
+ check: (manuscript, context) => {
1914
+ const report = (start, end, messageId) => {
1915
+ context.report({
1916
+ range: displayRange(manuscript, start, end),
1917
+ messageId
1918
+ });
1919
+ };
1920
+ for (const line of splitDisplayLines(manuscript.displayText)) {
1921
+ if (isDecorationLine(line.text)) continue;
1922
+ const indent = leadingSpaces(line.text);
1923
+ const opening = line.text[indent.length];
1924
+ if (opening === void 0) continue;
1925
+ if (openingBrackets.includes(opening)) {
1926
+ if (indent !== "") report(line.start, line.start + indent.length, "unexpected-indent");
1927
+ } else if (!line.text.startsWith(" ")) {
1928
+ const length = (line.text.codePointAt(0) ?? 0) > 65535 ? 2 : 1;
1929
+ report(line.start, line.start + length, "leading-character");
1930
+ } else if (indent.length !== 1) report(line.start, line.start + indent.length, "indent-width");
1931
+ }
1622
1932
  }
1623
- });
1933
+ };
1624
1934
  }
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({
1935
+ const paragraphOpeningRule = () => build(OPENING_BRACKETS);
1936
+ function createParagraphOpeningRule(options = {}) {
1937
+ if (options.openingBrackets === void 0) return ManuscriptResult.succeed(paragraphOpeningRule());
1938
+ const brackets = v.safeParse(OpeningBracketsSchema, options.openingBrackets);
1939
+ if (!brackets.success) return ManuscriptResult.fail({
1630
1940
  kind: "InvalidRuleOptions",
1631
- ruleId: RULE_ID,
1632
- option: "allowedCharacters",
1633
- issues: ValidationIssue.from(allowed.issues)
1941
+ ruleId: RULE_ID$1,
1942
+ option: "openingBrackets",
1943
+ issues: ValidationIssue.from(brackets.issues)
1634
1944
  });
1635
- return ManuscriptResult.succeed(build(allowed.output));
1945
+ return ManuscriptResult.succeed(build(brackets.output));
1636
1946
  }
1637
1947
  //#endregion
1638
1948
  //#region src/proofreading/rules/punctuation-before-closing-quote.ts
1949
+ const PATTERN = new RegExp(`[。、]+(?=[${characterClass(CLOSING_BRACKETS)}])`, "gu");
1639
1950
  const punctuationBeforeClosingQuoteRule = () => defineMatchRule({
1640
1951
  id: "kg/punctuation-before-closing-quote",
1641
- pattern: /[。、]+(?=[」』〗〉》))”"’']\]〕}}>>])/gu,
1952
+ pattern: PATTERN,
1642
1953
  message: "閉じ括弧の直前に句読点を置くことはできません"
1643
1954
  });
1644
1955
  //#endregion
1645
1956
  //#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: "感嘆符または疑問符の直後には空白か閉じ括弧が必要です"
1957
+ const RULE_ID = "kg/space-after-question-or-exclamation";
1958
+ const MESSAGES = {
1959
+ default: "感嘆符または疑問符の直後には全角スペースか閉じ括弧が必要です",
1960
+ "space-width": "感嘆符または疑問符の直後の空白は全角スペース1字にしてください",
1961
+ "before-closing-bracket": "閉じ括弧の直前に空白を置くことはできません"
1962
+ };
1963
+ /**
1964
+ * Runs, not single marks: `!?` is one mark and takes one gap, so the gap belongs after the run.
1965
+ */
1966
+ const MARKS = /[?!]+/gu;
1967
+ /**
1968
+ * Checks the gap that follows every run of ! or ?. A run at the end of a line or right before a
1969
+ * closing bracket needs no gap; anywhere else it takes exactly one ideographic space.
1970
+ */
1971
+ const spaceAfterQuestionOrExclamationRule = () => ({
1972
+ kind: "parsed",
1973
+ meta: {
1974
+ id: RULE_ID,
1975
+ messages: MESSAGES
1976
+ },
1977
+ check: (manuscript, context) => {
1978
+ const report = (start, end, messageId) => {
1979
+ context.report({
1980
+ range: displayRange(manuscript, start, end),
1981
+ messageId
1982
+ });
1983
+ };
1984
+ for (const line of splitDisplayLines(manuscript.displayText)) for (const marks of line.text.matchAll(MARKS)) {
1985
+ const marksEnd = marks.index + marks[0].length;
1986
+ const next = line.text[marksEnd];
1987
+ if (next === void 0 || "」』〗〉》))”\"’']]〕}}>>".includes(next)) continue;
1988
+ const spaces = leadingSpaces(line.text.slice(marksEnd));
1989
+ if (spaces === "") {
1990
+ report(line.start + marks.index, line.start + marksEnd, "default");
1991
+ continue;
1992
+ }
1993
+ const following = line.text[marksEnd + spaces.length];
1994
+ const spacesStart = line.start + marksEnd;
1995
+ const spacesEnd = spacesStart + spaces.length;
1996
+ if (following !== void 0 && "」』〗〉》))”\"’']]〕}}>>".includes(following)) report(spacesStart, spacesEnd, "before-closing-bracket");
1997
+ else if (spaces !== " ") report(spacesStart, spacesEnd, "space-width");
1998
+ }
1999
+ }
1650
2000
  });
1651
2001
  //#endregion
1652
2002
  //#region src/proofreading/rules/variant-character.ts
@@ -1672,21 +2022,25 @@ const variantCharacterRule = () => defineParsedRule("kg/variant-character", "異
1672
2022
  /**
1673
2023
  * The built-in rule set at its default configuration. Infallible by construction: every rule is
1674
2024
  * built from constants, so there is no options error for a caller to handle.
2025
+ *
2026
+ * Rules whose answer depends on the work's own conventions — which width numerals take, whether a
2027
+ * word is written in kanji or kana — are exported individually instead of included here.
1675
2028
  */
1676
2029
  function createDefaultProofreadingRules() {
1677
2030
  return [
1678
- paragraphLeadingCharacterRule(),
2031
+ paragraphOpeningRule(),
1679
2032
  punctuationBeforeClosingQuoteRule(),
1680
2033
  spaceAfterQuestionOrExclamationRule(),
1681
2034
  evenEllipsisRule(),
1682
- evenDashRule(),
2035
+ dashRule(),
2036
+ ellipsisCharacterRule(),
1683
2037
  noConsecutivePunctuationRule(),
1684
2038
  noConsecutiveInterpunctRule(),
1685
- noConsecutiveChoonpuRule(),
1686
2039
  minusBeforeNumberRule(),
1687
2040
  maxArabicNumeralDigitsRule(),
2041
+ fullwidthJapanesePunctuationRule(),
1688
2042
  variantCharacterRule()
1689
2043
  ];
1690
2044
  }
1691
2045
  //#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 };
2046
+ 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, createDashRule, createDefaultProofreadingRules, createMaxArabicNumeralDigitsRule, createParagraphOpeningRule, dashRule, ellipsisCharacterRule, evenEllipsisRule, fullwidthJapanesePunctuationRule, kakuyomuParser, manuscriptGridComposer, maxArabicNumeralDigitsRule, minusBeforeNumberRule, mmToPt, 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.6.0",
4
4
  "description": "Framework-independent parsing, composition, and proofreading for Japanese manuscripts",
5
5
  "keywords": [
6
6
  "japanese",