@sushichan044/kg-core 0.6.0 → 0.6.2

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
@@ -1062,6 +1062,83 @@ const GridCell = {
1062
1062
  annotations: []
1063
1063
  })
1064
1064
  };
1065
+ //#endregion
1066
+ //#region src/internal/japanese-brackets.ts
1067
+ /**
1068
+ * Brackets and quotes a Japanese paragraph may open with. Halfwidth forms are included because a
1069
+ * manuscript that uses them is still opening a paragraph, not writing prose.
1070
+ */
1071
+ const OPENING_BRACKETS = "「『〖〈《((“\"‘'[[〔{{<<";
1072
+ /**
1073
+ * The closing counterparts, in the same order.
1074
+ */
1075
+ const CLOSING_BRACKETS = "」』〗〉》))”\"’']]〕}}>>";
1076
+ /**
1077
+ * Escapes the characters a regular expression character class would otherwise read as syntax, so a
1078
+ * set declared above can be dropped into a pattern.
1079
+ */
1080
+ function characterClass(characters) {
1081
+ return characters.replaceAll(/[\\\]^-]/gu, "\\$&");
1082
+ }
1083
+ //#endregion
1084
+ //#region src/internal/japanese-spaces.ts
1085
+ const LEADING_SPACES = /^[  \t]+/u;
1086
+ /**
1087
+ * The run of spaces the text begins with, or an empty string. Halfwidth spaces and tabs count: a
1088
+ * rule has to see them to say they are the wrong space.
1089
+ */
1090
+ function leadingSpaces(text) {
1091
+ return LEADING_SPACES.exec(text)?.[0] ?? "";
1092
+ }
1093
+ //#endregion
1094
+ //#region src/internal/question-or-exclamation-spacing.ts
1095
+ /**
1096
+ * Runs, not single marks: `!?` is one mark and takes one gap, so the gap belongs after the run.
1097
+ */
1098
+ const MARKS = /[?!]+/gu;
1099
+ function analyzeMatch(line, marks) {
1100
+ const start = marks.index;
1101
+ if (start === void 0) return void 0;
1102
+ const marksRange = {
1103
+ start,
1104
+ end: start + marks[0].length
1105
+ };
1106
+ const next = line[marksRange.end];
1107
+ if (next === void 0 || "」』〗〉》))”\"’']]〕}}>>".includes(next)) return void 0;
1108
+ const spaces = leadingSpaces(line.slice(marksRange.end));
1109
+ if (spaces === "") return {
1110
+ kind: "missing",
1111
+ marks: marksRange
1112
+ };
1113
+ const gap = {
1114
+ start: marksRange.end,
1115
+ end: marksRange.end + spaces.length
1116
+ };
1117
+ const following = line[gap.end];
1118
+ if (following !== void 0 && "」』〗〉》))”\"’']]〕}}>>".includes(following)) return {
1119
+ kind: "before-closing-bracket",
1120
+ marks: marksRange,
1121
+ gap
1122
+ };
1123
+ if (spaces !== " ") return {
1124
+ kind: "invalid-space",
1125
+ marks: marksRange,
1126
+ gap
1127
+ };
1128
+ return {
1129
+ kind: "valid",
1130
+ marks: marksRange,
1131
+ gap
1132
+ };
1133
+ }
1134
+ /**
1135
+ * Classifies the spacing after each run of fullwidth question or exclamation marks in one display
1136
+ * line. Runs at the end of the line or immediately before a closing bracket need no gap and are
1137
+ * omitted from the result.
1138
+ */
1139
+ function questionOrExclamationSpacings(line) {
1140
+ return [...line.matchAll(MARKS)].map((marks) => analyzeMatch(line, marks)).filter((spacing) => spacing !== void 0);
1141
+ }
1065
1142
  const GridLine = {
1066
1143
  schema: readonlyObject({
1067
1144
  range: v.nullable(ManuscriptRange.schema),
@@ -1191,12 +1268,32 @@ function toCells(sourceLine, annotations) {
1191
1268
  annotations: ManuscriptAnnotation.overlapping(annotations, grapheme.range)
1192
1269
  }));
1193
1270
  }
1271
+ function hangingGapIndexes(sourceLine) {
1272
+ const line = sourceLine.map(({ value }) => value).join("");
1273
+ const gapStarts = new Set(questionOrExclamationSpacings(line).filter((spacing) => spacing.kind === "valid").map(({ gap }) => gap.start));
1274
+ const indexes = /* @__PURE__ */ new Set();
1275
+ let offset = 0;
1276
+ for (const [index, grapheme] of sourceLine.entries()) {
1277
+ if (gapStarts.has(offset)) indexes.add(index);
1278
+ offset += grapheme.value.length;
1279
+ }
1280
+ return indexes;
1281
+ }
1194
1282
  /**
1195
- * Wraps one source line across as many grid lines as its length requires.
1283
+ * Wraps one source line across as many grid lines as its length requires. A valid gap after ! or ?
1284
+ * that would start a wrapped line is consumed as hanging punctuation instead of occupying a cell.
1196
1285
  */
1197
- function wrapLine(cells, charsPerLine) {
1286
+ function wrapLine(cells, charsPerLine, hangingGaps) {
1198
1287
  if (cells.length === 0) return [GridLine.padded([], charsPerLine)];
1199
- return Array.from({ length: Math.ceil(cells.length / charsPerLine) }, (_unused, index) => GridLine.padded(cells.slice(index * charsPerLine, (index + 1) * charsPerLine), charsPerLine));
1288
+ const lines = [];
1289
+ let cursor = 0;
1290
+ while (cursor < cells.length) {
1291
+ if (lines.length > 0 && hangingGaps.has(cursor)) cursor += 1;
1292
+ if (cursor >= cells.length) break;
1293
+ lines.push(GridLine.padded(cells.slice(cursor, cursor + charsPerLine), charsPerLine));
1294
+ cursor += charsPerLine;
1295
+ }
1296
+ return lines;
1200
1297
  }
1201
1298
  function blankLines(count, charsPerLine) {
1202
1299
  return Array.from({ length: count }, () => GridLine.padded([], charsPerLine));
@@ -1237,8 +1334,7 @@ function buildPages(contentLines, settings) {
1237
1334
  function composeGrid(manuscript, settings) {
1238
1335
  const { charsPerLine } = settings.grid;
1239
1336
  const sourceLines = displayedLines(manuscript.graphemes);
1240
- const cellLines = sourceLines.map((line) => toCells(line, manuscript.annotations));
1241
- const manuscriptLines = cellLines.flatMap((cells) => wrapLine(cells, charsPerLine));
1337
+ const manuscriptLines = sourceLines.flatMap((line) => wrapLine(toCells(line, manuscript.annotations), charsPerLine, hangingGapIndexes(line)));
1242
1338
  const pages = buildPages([
1243
1339
  ...blankLines(settings.offsets.document.leading, charsPerLine),
1244
1340
  ...manuscriptLines,
@@ -1248,7 +1344,7 @@ function composeGrid(manuscript, settings) {
1248
1344
  pages,
1249
1345
  geometry: ManuscriptGeometry.of(settings.grid, settings.appearance),
1250
1346
  stats: {
1251
- chars: cellLines.reduce((total, cells) => total + cells.length, 0),
1347
+ chars: sourceLines.reduce((total, line) => total + line.length, 0),
1252
1348
  sourceLines: sourceLines.length,
1253
1349
  pages: pages.length
1254
1350
  }
@@ -1852,24 +1948,6 @@ const noConsecutivePunctuationRule = () => defineMatchRule({
1852
1948
  message: "句読点が連続しています"
1853
1949
  });
1854
1950
  //#endregion
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
1951
  //#region src/proofreading/rules/internal/decoration-line.ts
1874
1952
  const DECORATION_LINE = /^[**◆◇■□●○▽▼△▲☆★・…―─━ー\-==~~\s]+$/u;
1875
1953
  /**
@@ -1880,16 +1958,6 @@ function isDecorationLine(text) {
1880
1958
  return DECORATION_LINE.test(text);
1881
1959
  }
1882
1960
  //#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
1961
  //#region src/proofreading/rules/paragraph-opening.ts
1894
1962
  const RULE_ID$1 = "kg/paragraph-opening";
1895
1963
  const MESSAGES$1 = {
@@ -1961,10 +2029,6 @@ const MESSAGES = {
1961
2029
  "before-closing-bracket": "閉じ括弧の直前に空白を置くことはできません"
1962
2030
  };
1963
2031
  /**
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
2032
  * Checks the gap that follows every run of ! or ?. A run at the end of a line or right before a
1969
2033
  * closing bracket needs no gap; anywhere else it takes exactly one ideographic space.
1970
2034
  */
@@ -1981,20 +2045,18 @@ const spaceAfterQuestionOrExclamationRule = () => ({
1981
2045
  messageId
1982
2046
  });
1983
2047
  };
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");
2048
+ for (const line of splitDisplayLines(manuscript.displayText)) for (const spacing of questionOrExclamationSpacings(line.text)) switch (spacing.kind) {
2049
+ case "valid": break;
2050
+ case "missing":
2051
+ report(line.start + spacing.marks.start, line.start + spacing.marks.end, "default");
2052
+ break;
2053
+ case "invalid-space":
2054
+ report(line.start + spacing.gap.start, line.start + spacing.gap.end, "space-width");
2055
+ break;
2056
+ case "before-closing-bracket":
2057
+ report(line.start + spacing.gap.start, line.start + spacing.gap.end, "before-closing-bracket");
2058
+ break;
2059
+ default: assertNever(spacing);
1998
2060
  }
1999
2061
  }
2000
2062
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sushichan044/kg-core",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Framework-independent parsing, composition, and proofreading for Japanese manuscripts",
5
5
  "keywords": [
6
6
  "japanese",