@sushichan044/kg-core 0.11.1 → 0.11.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/lint.d.mts CHANGED
@@ -440,13 +440,11 @@ export declare function createParagraphOpeningRule(options?: ParagraphOpeningOpt
440
440
  export declare const punctuationBeforeClosingQuoteRule: () => ParsedProofreadingRule;
441
441
  //#endregion
442
442
  //#region src/proofreading/rules/space-after-question-or-exclamation.d.ts
443
- /**
444
- * The gap that follows every run of ! or ? (区切り約物, JLReq 3.1.6). A dividing punctuation mark ends a
445
- * sentence without a full stop, so a full em of white stands in for the one a 句点 would have
446
- * carried. A run at the end of a line or right before a closing bracket is already separated from
447
- * what follows and needs no gap; anywhere else it takes exactly one ideographic space.
448
- */
449
443
  export declare const spaceAfterQuestionOrExclamationRule: () => ParsedProofreadingRule;
444
+ type SpaceAfterQuestionOrExclamationOptions = Readonly<{
445
+ additionalSentenceEndingMarks?: string;
446
+ }>;
447
+ export declare function createSpaceAfterQuestionOrExclamationRule(options?: SpaceAfterQuestionOrExclamationOptions): ManuscriptResult<ParsedProofreadingRule, InvalidRuleOptions>;
450
448
  //#endregion
451
449
  //#region src/proofreading/rules/variant-character.d.ts
452
450
  /**
@@ -458,4 +456,4 @@ export declare const spaceAfterQuestionOrExclamationRule: () => ParsedProofreadi
458
456
  */
459
457
  export declare const variantCharacterRule: () => ParsedProofreadingRule;
460
458
  //#endregion
461
- export type { ComposedProofreadingRule, ConsistentKanjiOpeningOptions, DashOptions, KanjiOpeningPair, MaxArabicNumeralDigitsOptions, ParagraphOpeningOptions, ParsedProofreadingRule, ProofreadOptions, ProofreadingReport, ProofreadingRule, ProofreadingRuleContext, ProofreadingRuleMessages };
459
+ export type { ComposedProofreadingRule, ConsistentKanjiOpeningOptions, DashOptions, KanjiOpeningPair, MaxArabicNumeralDigitsOptions, ParagraphOpeningOptions, ParsedProofreadingRule, ProofreadOptions, ProofreadingReport, ProofreadingRule, ProofreadingRuleContext, ProofreadingRuleMessages, SpaceAfterQuestionOrExclamationOptions };
package/dist/lint.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as CLOSING_BRACKETS, c as assertNever, d as ManuscriptResult, f as ManuscriptDiagnostic, h as ManuscriptRange, l as graphemeSegmenter, m as NamespacedId, n as leadingSpaces, o as OPENING_BRACKETS, p as DiagnosticSeverity, s as characterClass, t as questionOrExclamationSpacings, u as ValidationIssue, v as readonlyArray, y as readonlyObject } from "./question-or-exclamation-spacing-C5cFLeKr.mjs";
1
+ import { _ as ManuscriptRange, a as characterClass, b as readonlyArray, d as graphemeSegmenter, f as ValidationIssue, g as NamespacedId, h as DiagnosticSeverity, i as OPENING_BRACKETS, l as validAdditionalSentenceEndingMarks, m as ManuscriptDiagnostic, n as leadingSpaces, p as ManuscriptResult, r as CLOSING_BRACKETS, t as questionOrExclamationSpacings, u as assertNever, x as readonlyObject } from "./question-or-exclamation-spacing-D7TSDUG8.mjs";
2
2
  import { t as describeCause } from "./describe-cause-SABI_1ex.mjs";
3
3
  import * as v from "valibot";
4
4
  //#region src/proofreading/invalid-rule-options.ts
@@ -333,7 +333,7 @@ const DEFAULT_PAIRS = [
333
333
  * Reports the first kanji occurrence of every pair the manuscript also writes in kana. Whether the
334
334
  * two spellings are a deliberate distinction is the author's call, so the finding is a warning.
335
335
  */
336
- function build$3(pairs) {
336
+ function build$4(pairs) {
337
337
  return defineParsedRule(RULE_ID$5, MESSAGE, (manuscript, report) => {
338
338
  for (const { closed, opened } of pairs) {
339
339
  if (!manuscript.displayText.includes(opened)) continue;
@@ -349,7 +349,7 @@ function build$3(pairs) {
349
349
  }
350
350
  });
351
351
  }
352
- const consistentKanjiOpeningRule = () => build$3(DEFAULT_PAIRS);
352
+ const consistentKanjiOpeningRule = () => build$4(DEFAULT_PAIRS);
353
353
  function createConsistentKanjiOpeningRule(options = {}) {
354
354
  if (options.pairs === void 0) return ManuscriptResult.succeed(consistentKanjiOpeningRule());
355
355
  const pairs = v.safeParse(PairsSchema, options.pairs);
@@ -359,7 +359,7 @@ function createConsistentKanjiOpeningRule(options = {}) {
359
359
  option: "pairs",
360
360
  issues: ValidationIssue.from(pairs.issues)
361
361
  });
362
- return ManuscriptResult.succeed(build$3(pairs.output));
362
+ return ManuscriptResult.succeed(build$4(pairs.output));
363
363
  }
364
364
  //#endregion
365
365
  //#region src/proofreading/rules/internal/mixed-width.ts
@@ -431,7 +431,7 @@ function messageIdFor(run, preferred) {
431
431
  * must match, since only a repeat of the same character joins without a seam. JLReq classes the
432
432
  * dash as 分離禁止文字 (cl-08) for the same reason: the pair must not be split across lines.
433
433
  */
434
- function build$2(preferred) {
434
+ function build$3(preferred) {
435
435
  const messages = {
436
436
  character: `ダッシュには「${preferred}」を使ってください`,
437
437
  "even-count": EVEN_COUNT_MESSAGE
@@ -462,7 +462,7 @@ function build$2(preferred) {
462
462
  }
463
463
  };
464
464
  }
465
- const dashRule = () => build$2(DEFAULT_PREFERRED);
465
+ const dashRule = () => build$3(DEFAULT_PREFERRED);
466
466
  function createDashRule(options = {}) {
467
467
  if (options.preferred === void 0) return ManuscriptResult.succeed(dashRule());
468
468
  const preferred = v.safeParse(DashCharacterSchema, options.preferred);
@@ -472,7 +472,7 @@ function createDashRule(options = {}) {
472
472
  option: "preferred",
473
473
  issues: ValidationIssue.from(preferred.issues)
474
474
  });
475
- return ManuscriptResult.succeed(build$2(preferred.output));
475
+ return ManuscriptResult.succeed(build$3(preferred.output));
476
476
  }
477
477
  //#endregion
478
478
  //#region src/proofreading/rules/ellipsis-character.ts
@@ -582,7 +582,7 @@ const MaxDigitsSchema = v.pipe(v.number(), v.finite(), v.integer(), v.minValue(1
582
582
  * upright inside one em, but a longer one has nowhere to go: it either turns the line sideways or
583
583
  * breaks across it. The usual limit is two digits, past which a novel spells the number in kanji.
584
584
  */
585
- function build$1(maxDigits) {
585
+ function build$2(maxDigits) {
586
586
  return defineMatchRule({
587
587
  id: RULE_ID$2,
588
588
  pattern: /([0-90-9]+)(?:[..]([0-90-9]+))?/gu,
@@ -590,7 +590,7 @@ function build$1(maxDigits) {
590
590
  message: `${maxDigits}桁を超えるアラビア数字が使われています`
591
591
  });
592
592
  }
593
- const maxArabicNumeralDigitsRule = () => build$1(DEFAULT_MAX_DIGITS);
593
+ const maxArabicNumeralDigitsRule = () => build$2(DEFAULT_MAX_DIGITS);
594
594
  function createMaxArabicNumeralDigitsRule(options = {}) {
595
595
  if (options.maxDigits === void 0) return ManuscriptResult.succeed(maxArabicNumeralDigitsRule());
596
596
  const maxDigits = v.safeParse(MaxDigitsSchema, options.maxDigits);
@@ -600,7 +600,7 @@ function createMaxArabicNumeralDigitsRule(options = {}) {
600
600
  option: "maxDigits",
601
601
  issues: ValidationIssue.from(maxDigits.issues)
602
602
  });
603
- return ManuscriptResult.succeed(build$1(maxDigits.output));
603
+ return ManuscriptResult.succeed(build$2(maxDigits.output));
604
604
  }
605
605
  //#endregion
606
606
  //#region src/proofreading/rules/minus-before-number.ts
@@ -664,7 +664,7 @@ const OpeningBracketsSchema = v.pipe(v.string(), v.nonEmpty());
664
664
  * The three findings are branches of that one decision rather than separate rules, so a line can
665
665
  * never collect two of them at once.
666
666
  */
667
- function build(openingBrackets) {
667
+ function build$1(openingBrackets) {
668
668
  return {
669
669
  kind: "parsed",
670
670
  meta: {
@@ -693,7 +693,7 @@ function build(openingBrackets) {
693
693
  }
694
694
  };
695
695
  }
696
- const paragraphOpeningRule = () => build(OPENING_BRACKETS);
696
+ const paragraphOpeningRule = () => build$1(OPENING_BRACKETS);
697
697
  function createParagraphOpeningRule(options = {}) {
698
698
  if (options.openingBrackets === void 0) return ManuscriptResult.succeed(paragraphOpeningRule());
699
699
  const brackets = v.safeParse(OpeningBracketsSchema, options.openingBrackets);
@@ -703,7 +703,7 @@ function createParagraphOpeningRule(options = {}) {
703
703
  option: "openingBrackets",
704
704
  issues: ValidationIssue.from(brackets.issues)
705
705
  });
706
- return ManuscriptResult.succeed(build(brackets.output));
706
+ return ManuscriptResult.succeed(build$1(brackets.output));
707
707
  }
708
708
  //#endregion
709
709
  //#region src/proofreading/rules/punctuation-before-closing-quote.ts
@@ -722,8 +722,8 @@ const punctuationBeforeClosingQuoteRule = () => defineMatchRule({
722
722
  //#region src/proofreading/rules/space-after-question-or-exclamation.ts
723
723
  const RULE_ID = "kg/space-after-question-or-exclamation";
724
724
  const MESSAGES = {
725
- default: "感嘆符または疑問符の直後には全角スペースか閉じ括弧が必要です",
726
- "space-width": "感嘆符または疑問符の直後の空白は全角スペース1字にしてください",
725
+ default: "文末記号の直後には全角スペースか閉じ括弧が必要です",
726
+ "space-width": "文末記号の直後の空白は全角スペース1字にしてください",
727
727
  "before-closing-bracket": "閉じ括弧の直前に空白を置くことはできません"
728
728
  };
729
729
  /**
@@ -732,7 +732,7 @@ const MESSAGES = {
732
732
  * carried. A run at the end of a line or right before a closing bracket is already separated from
733
733
  * what follows and needs no gap; anywhere else it takes exactly one ideographic space.
734
734
  */
735
- const spaceAfterQuestionOrExclamationRule = () => ({
735
+ const build = (additionalSentenceEndingMarks = "") => ({
736
736
  kind: "parsed",
737
737
  meta: {
738
738
  id: RULE_ID,
@@ -745,7 +745,7 @@ const spaceAfterQuestionOrExclamationRule = () => ({
745
745
  messageId
746
746
  });
747
747
  };
748
- for (const line of splitDisplayLines(manuscript.displayText)) for (const spacing of questionOrExclamationSpacings(line.text)) switch (spacing.kind) {
748
+ for (const line of splitDisplayLines(manuscript.displayText)) for (const spacing of questionOrExclamationSpacings(line.text, additionalSentenceEndingMarks)) switch (spacing.kind) {
749
749
  case "valid": break;
750
750
  case "missing":
751
751
  report(line.start + spacing.marks.start, line.start + spacing.marks.end, "default");
@@ -760,6 +760,17 @@ const spaceAfterQuestionOrExclamationRule = () => ({
760
760
  }
761
761
  }
762
762
  });
763
+ const spaceAfterQuestionOrExclamationRule = () => build();
764
+ function createSpaceAfterQuestionOrExclamationRule(options = {}) {
765
+ const result = v.safeParse(v.pipe(v.string(), v.check(validAdditionalSentenceEndingMarks)), options.additionalSentenceEndingMarks === void 0 ? "" : options.additionalSentenceEndingMarks);
766
+ if (!result.success) return ManuscriptResult.fail({
767
+ kind: "InvalidRuleOptions",
768
+ ruleId: RULE_ID,
769
+ option: "additionalSentenceEndingMarks",
770
+ issues: ValidationIssue.from(result.issues)
771
+ });
772
+ return ManuscriptResult.succeed(build(result.output));
773
+ }
763
774
  //#endregion
764
775
  //#region src/proofreading/rules/variant-character.ts
765
776
  /**
@@ -809,4 +820,4 @@ function createDefaultProofreadingRules() {
809
820
  ];
810
821
  }
811
822
  //#endregion
812
- export { InvalidRuleOptions, ProofreadError, ProofreadingRuleMeta, consistentKanjiOpeningRule, consistentLatinWidthRule, consistentNumeralWidthRule, createConsistentKanjiOpeningRule, createDashRule, createDefaultProofreadingRules, createMaxArabicNumeralDigitsRule, createParagraphOpeningRule, dashRule, ellipsisCharacterRule, evenEllipsisRule, fullwidthJapanesePunctuationRule, maxArabicNumeralDigitsRule, minusBeforeNumberRule, noConsecutiveInterpunctRule, noConsecutivePunctuationRule, paragraphOpeningRule, proofreadManuscript, punctuationBeforeClosingQuoteRule, spaceAfterQuestionOrExclamationRule, variantCharacterRule };
823
+ export { InvalidRuleOptions, ProofreadError, ProofreadingRuleMeta, consistentKanjiOpeningRule, consistentLatinWidthRule, consistentNumeralWidthRule, createConsistentKanjiOpeningRule, createDashRule, createDefaultProofreadingRules, createMaxArabicNumeralDigitsRule, createParagraphOpeningRule, createSpaceAfterQuestionOrExclamationRule, dashRule, ellipsisCharacterRule, evenEllipsisRule, fullwidthJapanesePunctuationRule, maxArabicNumeralDigitsRule, minusBeforeNumberRule, noConsecutiveInterpunctRule, noConsecutivePunctuationRule, paragraphOpeningRule, proofreadManuscript, punctuationBeforeClosingQuoteRule, spaceAfterQuestionOrExclamationRule, variantCharacterRule };
@@ -1,4 +1,4 @@
1
- import { _ as nonNegativeInteger, a as CLOSING_BRACKETS, c as assertNever, d as ManuscriptResult, g as TextRange, h as ManuscriptRange, i as TATE_CHU_YOKO_MARKS, l as graphemeSegmenter, m as NamespacedId, o as OPENING_BRACKETS, r as DIVIDING_PUNCTUATION, s as characterClass, t as questionOrExclamationSpacings, v as readonlyArray, y as readonlyObject } from "./question-or-exclamation-spacing-C5cFLeKr.mjs";
1
+ import { _ as ManuscriptRange, a as characterClass, b as readonlyArray, c as isSentenceEndingMark, d as graphemeSegmenter, g as NamespacedId, i as OPENING_BRACKETS, l as validAdditionalSentenceEndingMarks, o as SENTENCE_ENDING_MARKS, p as ManuscriptResult, r as CLOSING_BRACKETS, s as TATE_CHU_YOKO_MARKS, t as questionOrExclamationSpacings, u as assertNever, v as TextRange, x as readonlyObject, y as nonNegativeInteger } from "./question-or-exclamation-spacing-D7TSDUG8.mjs";
2
2
  import * as v from "valibot";
3
3
  import { eastAsianWidth } from "get-east-asian-width";
4
4
  const BoldAnnotation = { schema: readonlyObject({
@@ -116,6 +116,22 @@ const A5_PAGE_FORMAT = {
116
116
  outerMm: 13
117
117
  }
118
118
  };
119
+ const A6_PAGE_FORMAT = {
120
+ fontSizeQ: 12,
121
+ flow: {
122
+ lineLengthEm: 41,
123
+ linesPerStage: 17,
124
+ stagesPerPage: 1
125
+ },
126
+ page: {
127
+ lineGapMm: 7 / 4,
128
+ stageGapMm: 0,
129
+ topMm: 12.5,
130
+ bottomMm: 12.5,
131
+ innerMm: 16,
132
+ outerMm: 10
133
+ }
134
+ };
119
135
  //#endregion
120
136
  //#region src/appearance/font-preset-id.ts
121
137
  const FontPresetIdSchema = v.picklist(["mincho", "gothic"]);
@@ -331,6 +347,7 @@ const NovelFlowSettings = {
331
347
  //#endregion
332
348
  //#region src/composer/composition-settings.ts
333
349
  const NovelCompositionSettingsSchema = v.pipe(readonlyObject({
350
+ additionalSentenceEndingMarks: v.optional(v.pipe(v.string(), v.check(validAdditionalSentenceEndingMarks))),
334
351
  flow: NovelFlowSettings.schema,
335
352
  offsets: ManuscriptOffsets.schema,
336
353
  appearance: ManuscriptAppearanceSettings.schema,
@@ -338,7 +355,10 @@ const NovelCompositionSettingsSchema = v.pipe(readonlyObject({
338
355
  }), v.check(({ flow, offsets }) => LineOffset.total(offsets.stage) <= ManuscriptOffsets.maxStageTotal(flow) && LineOffset.total(offsets.page) <= ManuscriptOffsets.maxPageTotal(flow, offsets.stage), "page or stage offsets leave no usable novel lines"), v.check(({ flow, appearance, page }) => {
339
356
  const geometry = ManuscriptGeometry.of(flow, appearance, page);
340
357
  return Math.abs(geometry.contentHeightMm + page.topMm + page.bottomMm - geometry.paperHeightMm) <= 1e-6 && Math.abs(geometry.contentWidthMm + page.innerMm + page.outerMm - geometry.paperWidthMm) <= 1e-6;
341
- }, "text dimensions and margins must match the paper dimensions"), v.check(({ appearance, page }) => page.lineGapMm + 1e-6 >= appearance.fontSizeQ / 8, "line gap must leave room for half-size annotations"), v.check(({ flow, appearance, page }) => appearance.paperSize !== "a5" || appearance.fontSizeQ === A5_PAGE_FORMAT.fontSizeQ && flow.lineLengthEm === A5_PAGE_FORMAT.flow.lineLengthEm && flow.linesPerStage === A5_PAGE_FORMAT.flow.linesPerStage && flow.stagesPerPage === A5_PAGE_FORMAT.flow.stagesPerPage && page.lineGapMm === A5_PAGE_FORMAT.page.lineGapMm && page.stageGapMm === A5_PAGE_FORMAT.page.stageGapMm && page.topMm === A5_PAGE_FORMAT.page.topMm && page.bottomMm === A5_PAGE_FORMAT.page.bottomMm && page.innerMm === A5_PAGE_FORMAT.page.innerMm && page.outerMm === A5_PAGE_FORMAT.page.outerMm, "A5 uses the fixed 12.7Q, 27-character, 23-line, two-stage format"));
358
+ }, "text dimensions and margins must match the paper dimensions"), v.check(({ appearance, page }) => page.lineGapMm + 1e-6 >= appearance.fontSizeQ / 8, "line gap must leave room for half-size annotations"), v.check(({ flow, appearance, page }) => {
359
+ const format = appearance.paperSize === "a5" ? A5_PAGE_FORMAT : appearance.paperSize === "a6" ? A6_PAGE_FORMAT : null;
360
+ return format === null || appearance.fontSizeQ === format.fontSizeQ && flow.lineLengthEm === format.flow.lineLengthEm && flow.linesPerStage === format.flow.linesPerStage && flow.stagesPerPage === format.flow.stagesPerPage && page.lineGapMm === format.page.lineGapMm && page.stageGapMm === format.page.stageGapMm && page.topMm === format.page.topMm && page.bottomMm === format.page.bottomMm && page.innerMm === format.page.innerMm && page.outerMm === format.page.outerMm;
361
+ }, "A5 and A6 use their fixed page formats"));
342
362
  const NovelCompositionSettings = {
343
363
  schema: NovelCompositionSettingsSchema,
344
364
  defaults: {
@@ -348,21 +368,22 @@ const NovelCompositionSettings = {
348
368
  page: ManuscriptPageSettings.defaults
349
369
  },
350
370
  /**
351
- * Select the fixed A5 format, or centre the current grid on another paper size.
371
+ * Select the fixed A5 or A6 format, or centre the current grid on another paper size.
352
372
  */
353
373
  forPaper: (paperSize, settings) => {
354
374
  const appearance = {
355
375
  ...settings.appearance,
356
376
  paperSize
357
377
  };
358
- if (paperSize === "a5") return {
378
+ const format = paperSize === "a5" ? A5_PAGE_FORMAT : paperSize === "a6" ? A6_PAGE_FORMAT : null;
379
+ if (format !== null) return {
359
380
  ...settings,
360
- flow: A5_PAGE_FORMAT.flow,
381
+ flow: format.flow,
361
382
  appearance: {
362
383
  ...appearance,
363
- fontSizeQ: A5_PAGE_FORMAT.fontSizeQ
384
+ fontSizeQ: format.fontSizeQ
364
385
  },
365
- page: A5_PAGE_FORMAT.page
386
+ page: format.page
366
387
  };
367
388
  return {
368
389
  ...settings,
@@ -524,7 +545,7 @@ function presentationOf(text) {
524
545
  * Recognize sentence-wide presentation while keeping words as separate line-break units. Standalone
525
546
  * dotted abbreviations retain the existing per-token presentation.
526
547
  */
527
- function recognize(graphemes) {
548
+ function recognizeText(graphemes) {
528
549
  const runs = [];
529
550
  const push = (members, presentation) => {
530
551
  const range = ManuscriptRange.merge(members.map((member) => member.range));
@@ -569,6 +590,25 @@ function recognize(graphemes) {
569
590
  }
570
591
  return runs;
571
592
  }
593
+ /**
594
+ * Split configured symbols before recognizing western words and presentation runs.
595
+ */
596
+ function recognize(graphemes, additionalSentenceEndingMarks = "") {
597
+ const runs = [];
598
+ let start = 0;
599
+ for (const [index, grapheme] of graphemes.entries()) {
600
+ if (!isSentenceEndingMark(grapheme.value, additionalSentenceEndingMarks) || isSentenceEndingMark(grapheme.value)) continue;
601
+ runs.push(...recognizeText(graphemes.slice(start, index)));
602
+ runs.push({
603
+ range: grapheme.range,
604
+ members: [grapheme],
605
+ presentation: "mixed"
606
+ });
607
+ start = index + 1;
608
+ }
609
+ runs.push(...recognizeText(graphemes.slice(start)));
610
+ return runs;
611
+ }
572
612
  const CompositionRun = { recognize };
573
613
  //#endregion
574
614
  //#region src/composer/internal/emphasis-placement.ts
@@ -1096,7 +1136,7 @@ const boundaryClasses = [
1096
1136
  const OPENING = /* @__PURE__ */ new Set(`${OPENING_BRACKETS}【〘〝⦅«`);
1097
1137
  const CLOSING = /* @__PURE__ */ new Set(`${CLOSING_BRACKETS}】〙〟⦆»`);
1098
1138
  const HYPHENS = /* @__PURE__ */ new Set("‐‑⁃–");
1099
- const DIVIDING_MARKS = new Set(DIVIDING_PUNCTUATION);
1139
+ const DIVIDING_MARKS = new Set(SENTENCE_ENDING_MARKS);
1100
1140
  const COMBINED_DIVIDING_MARKS = new Set(TATE_CHU_YOKO_MARKS);
1101
1141
  const MIDDLE_DOTS = /* @__PURE__ */ new Set("・:;");
1102
1142
  const SMALL_KANA = /* @__PURE__ */ new Set("ァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖ");
@@ -2688,8 +2728,8 @@ const RubyLineLayout = {
2688
2728
  };
2689
2729
  //#endregion
2690
2730
  //#region src/composer/internal/source-space.ts
2691
- const SourceSpace = { collect: (graphemes, advancesEm) => {
2692
- const gaps = new Set(questionOrExclamationSpacings(graphemes.map(({ value }) => value).join("")).filter((spacing) => spacing.kind === "valid").map(({ gap }) => gap.start));
2731
+ const SourceSpace = { collect: (graphemes, advancesEm, additionalSentenceEndingMarks = "") => {
2732
+ const gaps = new Set(questionOrExclamationSpacings(graphemes.map(({ value }) => value).join(""), additionalSentenceEndingMarks).filter((spacing) => spacing.kind === "valid").map(({ gap }) => gap.start));
2693
2733
  let offset = 0;
2694
2734
  return graphemes.flatMap((grapheme, index) => {
2695
2735
  const separator = gaps.has(offset);
@@ -2854,7 +2894,7 @@ function graphemeWidth(grapheme) {
2854
2894
  for (const character of grapheme) {
2855
2895
  const codePoint = character.codePointAt(0);
2856
2896
  if (codePoint !== void 0) {
2857
- const characterWidth = "!?‼⁇⁈⁉".includes(character) ? 2 : eastAsianWidth(codePoint, { ambiguousAsWide: true });
2897
+ const characterWidth = SENTENCE_ENDING_MARKS.includes(character) ? 2 : eastAsianWidth(codePoint, { ambiguousAsWide: true });
2858
2898
  width = Math.max(width, characterWidth);
2859
2899
  }
2860
2900
  }
@@ -2889,8 +2929,8 @@ function displayedLines(graphemes) {
2889
2929
  if (lines.length > 1 && endedWithLineBreak) lines.pop();
2890
2930
  return lines;
2891
2931
  }
2892
- function measureSourceLine(sourceLine, annotations, measurements) {
2893
- const runs = CompositionRun.recognize(sourceLine);
2932
+ function measureSourceLine(sourceLine, annotations, measurements, additionalSentenceEndingMarks) {
2933
+ const runs = CompositionRun.recognize(sourceLine, additionalSentenceEndingMarks);
2894
2934
  const rubyAssociations = RubyAssociation.collect(annotations, sourceLine);
2895
2935
  const atoms = [];
2896
2936
  for (const [runIndex, run] of runs.entries()) {
@@ -2907,11 +2947,12 @@ function measureSourceLine(sourceLine, annotations, measurements) {
2907
2947
  const first = memberIndex === 0;
2908
2948
  const renderAdvanceEm = first ? piece.renderSpan.advanceEm : 0;
2909
2949
  const measuredAdvanceEm = first ? piece.layoutSpan.advanceEm : 0;
2910
- const characterClass = TYPESETTING_PROFILE.classify({
2950
+ const sentenceEndingMark = isSentenceEndingMark(grapheme.value, additionalSentenceEndingMarks);
2951
+ const characterClass = sentenceEndingMark ? "cl-04" : TYPESETTING_PROFILE.classify({
2911
2952
  value: grapheme.value,
2912
2953
  presentation: run.presentation
2913
2954
  });
2914
- const metrics = TYPESETTING_PROFILE.boxMetrics(characterClass, measuredAdvanceEm);
2955
+ const metrics = TYPESETTING_PROFILE.boxMetrics(characterClass, sentenceEndingMark && !combineRun ? 1 : measuredAdvanceEm);
2915
2956
  atoms.push({
2916
2957
  grapheme,
2917
2958
  runIndex,
@@ -2965,7 +3006,7 @@ function measureSourceLine(sourceLine, annotations, measurements) {
2965
3006
  });
2966
3007
  }
2967
3008
  }
2968
- const sourceSpaces = SourceSpace.collect(sourceLine, atoms.map(({ boxAdvanceEm }) => boxAdvanceEm));
3009
+ const sourceSpaces = SourceSpace.collect(sourceLine, atoms.map(({ boxAdvanceEm }) => boxAdvanceEm), additionalSentenceEndingMarks);
2969
3010
  return {
2970
3011
  atoms,
2971
3012
  runs,
@@ -3408,7 +3449,7 @@ function createCompose(provider) {
3408
3449
  return (manuscript, settings) => {
3409
3450
  const measurements = MeasurementSession.create(provider, settings);
3410
3451
  const sourceLines = displayedLines(manuscript.graphemes);
3411
- const measured = sourceLines.map((line) => measureSourceLine(line, manuscript.annotations, measurements));
3452
+ const measured = sourceLines.map((line) => measureSourceLine(line, manuscript.annotations, measurements, settings.additionalSentenceEndingMarks ?? ""));
3412
3453
  if (measured.some((line) => line === void 0)) return ManuscriptResult.fail({ reason: "run measurer returned invalid metrics or cluster mappings" });
3413
3454
  const manuscriptLines = [];
3414
3455
  for (const line of measured) {
package/dist/plugin.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as ManuscriptResult, i as ParsedManuscript, n as ValidationIssue, r as NamespacedId } from "./composed-manuscript-C0f3mxKs.mjs";
2
- import { a as RunMeasurer, c as MeasurementRequest, f as ManuscriptComposer, m as Rejection, o as logicalRunMeasurer, p as ManuscriptParser, r as createNovelComposer, s as RunMeasurement, t as PresentationKind } from "./presentation-kind-CvlmQ3AY.mjs";
2
+ import { a as RunMeasurer, c as MeasurementRequest, f as ManuscriptComposer, m as Rejection, o as logicalRunMeasurer, p as ManuscriptParser, r as createNovelComposer, s as RunMeasurement, t as PresentationKind } from "./presentation-kind-DnJkvxHU.mjs";
3
3
  import * as v from "valibot";
4
4
  //#region src/composer/measurement-text-range.d.ts
5
5
  declare const MeasurementTextRangeSchema: v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.StrictObjectSchema<{
package/dist/plugin.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { d as ManuscriptResult, m as NamespacedId, u as ValidationIssue, y as readonlyObject } from "./question-or-exclamation-spacing-C5cFLeKr.mjs";
2
- import { _ as ParsedManuscript, a as MeasuredCluster, g as FontPresetId, h as FontSizeQ, i as RunMeasurement, o as MeasurementTextRange, r as logicalRunMeasurer, t as createNovelComposer } from "./novel-composer-z0cv8OHb.mjs";
1
+ import { f as ValidationIssue, g as NamespacedId, p as ManuscriptResult, x as readonlyObject } from "./question-or-exclamation-spacing-D7TSDUG8.mjs";
2
+ import { _ as ParsedManuscript, a as MeasuredCluster, g as FontPresetId, h as FontSizeQ, i as RunMeasurement, o as MeasurementTextRange, r as logicalRunMeasurer, t as createNovelComposer } from "./novel-composer-tcseYHc8.mjs";
3
3
  import * as v from "valibot";
4
4
  const PresentationKind = { schema: v.picklist([
5
5
  "mixed",