From aa55877b7af1f49dd3d3b2858c9b3e90133c541e Mon Sep 17 00:00:00 2001 From: con3code Date: Sat, 17 Oct 2026 22:51:34 +0900 Subject: [PATCH 1/5] Mojikumi: consecutive punctuation closes up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening brackets together, and closing brackets, commas and full stops together, now touch (JLREQ 3.1.4): 」。 and 「『 no longer keep half an em between them. A closing bracket and an opening one stay half an em apart, and other neighbours keep the larger blank, as before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013phQ3ekq6uGBEWD6FjFYG2 --- crates/compose/src/tests.rs | 22 ++++++++++++++++++++++ crates/doc/src/mojikumi.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 9d8f6be70..b11031c6c 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -5443,6 +5443,28 @@ fn mojikumi_one_em_line_end_set_sets_closing_punctuation_full_width_at_line_end( assert!((open - one.0 - em * 0.5).abs() < 0.01 && (stop - one.1).abs() < 0.01, "one-em set: {one:?} of {open}/{stop}"); } +/// Consecutive punctuation touches (JLREQ 3.1.4): in 漢」。漢(「漢 the closing bracket and +/// the full stop, and the two opening brackets, close up by half an em each. +#[test] +fn mojikumi_closes_up_consecutive_punctuation() { + let family = "Mojikumi Consecutive"; + let font = designcraft_fonts::testing::font_mapping(family, &[('漢', '‰'), ('」', '‰'), ('。', '‰'), ('(', '‰'), ('「', '‰')]).unwrap(); + designcraft_fonts::FontDb::global().add_font(font); + let text = "漢」。漢(「漢"; + let (mut d, sid, _) = doc_with(text, Rect::new(0.0, 0.0, 300.0, 100.0), ParaAttrs::default()); + d.story_mut(sid).unwrap().format_chars(0..text.len(), |f| f.over.font_family = Some(family.into())); + let mut width = |set: &str| { + d.story_mut(sid).unwrap().paras[0].para.mojikumi = Some(set.into()); + let cs = compose_story(&d, sid, &ComposeOptions::default()); + let line = all_lines(&cs)[0]; + let g = &line.glyphs[0]; + (line.end_x - line.x0, g.face.units_per_em() * g.sx) + }; + let (natural, em) = width("Nothing"); + let (set, _) = width("LineEndAllOneHalfEmEnum"); + assert!((natural - set - em).abs() < 0.01, "{natural} - {set} should be one em ({em})"); +} + fn ruled(text: &str, rect: Rect, columns: u32, edit: impl FnOnce(&mut Document, StoryId)) -> (Document, ItemId, Vec) { let (mut d, sid, fid) = doc_with(text, rect, ParaAttrs::default()); { diff --git a/crates/doc/src/mojikumi.rs b/crates/doc/src/mojikumi.rs index 415e26bc3..4e99900eb 100644 --- a/crates/doc/src/mojikumi.rs +++ b/crates/doc/src/mojikumi.rs @@ -126,6 +126,14 @@ impl<'a> Rules<'a> { if right == 22 { return Aki::fixed(if self.base == Base::LineEndHalfEm { 0.0 } else { trailing(left) }); } + // Consecutive punctuation (JLREQ 3.1.4): opening brackets together, and closing brackets, + // commas and full stops together, touch. Other neighbours keep the larger blank, so a + // closing bracket and an opening one are half an em apart. + let opening = |c| matches!(c, 1 | 26 | 27); + let closing = |c| matches!(c, 2 | 6 | 21 | 28..=31); + if (opening(left) && opening(right)) || (closing(left) && closing(right)) { + return Aki::default(); + } let punctuation = trailing(left).max(leading(right)); if punctuation > 0.0 { return Aki { min: 0.0, desired: punctuation, max: punctuation, priority: 1, discrete: false }; @@ -197,6 +205,24 @@ mod tests { use super::*; use crate::cjk::MojikumiTable; + #[test] + fn consecutive_punctuation_touches_except_closing_before_opening() { + let styles = Styles::default(); + for set in ["LineEndAllOneHalfEmEnum", "LineEndAllOneEmEnum", "SimpChineseDefault"] { + let rules = Rules::resolve(&styles, set).unwrap().unwrap(); + let aki = |a: char, b: char| rules.pair(class(a), class(b)).desired; + for (a, b) in + [('」', '。'), ('」', '、'), ('。', '」'), ('、', '」'), ('」', '』'), (')', '」'), (',', ')'), ('「', '『'), ('(', '「')] + { + assert_eq!(aki(a, b), 0.0, "{set}: {a}{b}"); + } + for (a, b) in [('」', '「'), ('。', '「'), ('、', '('), ('」', '漢'), ('漢', '「')] { + assert_eq!(aki(a, b), 0.5, "{set}: {a}{b}"); + } + assert_eq!(aki('」', '・'), 0.5, "{set}: the larger blank stays"); + } + } + #[test] fn overrides_are_directional_and_unknown_tables_are_not_silently_applied() { let mut styles = Styles::default(); From e06f4134d4d887626b371ce0f4e665e84b53caf4 Mon Sep 17 00:00:00 2001 From: con3code Date: Sat, 17 Oct 2026 23:08:18 +0900 Subject: [PATCH 2/5] Mojikumi: move the consecutive punctuation test clear of #1127's Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013phQ3ekq6uGBEWD6FjFYG2 --- crates/doc/src/mojikumi.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/doc/src/mojikumi.rs b/crates/doc/src/mojikumi.rs index 4e99900eb..e0a03d85a 100644 --- a/crates/doc/src/mojikumi.rs +++ b/crates/doc/src/mojikumi.rs @@ -205,24 +205,6 @@ mod tests { use super::*; use crate::cjk::MojikumiTable; - #[test] - fn consecutive_punctuation_touches_except_closing_before_opening() { - let styles = Styles::default(); - for set in ["LineEndAllOneHalfEmEnum", "LineEndAllOneEmEnum", "SimpChineseDefault"] { - let rules = Rules::resolve(&styles, set).unwrap().unwrap(); - let aki = |a: char, b: char| rules.pair(class(a), class(b)).desired; - for (a, b) in - [('」', '。'), ('」', '、'), ('。', '」'), ('、', '」'), ('」', '』'), (')', '」'), (',', ')'), ('「', '『'), ('(', '「')] - { - assert_eq!(aki(a, b), 0.0, "{set}: {a}{b}"); - } - for (a, b) in [('」', '「'), ('。', '「'), ('、', '('), ('」', '漢'), ('漢', '「')] { - assert_eq!(aki(a, b), 0.5, "{set}: {a}{b}"); - } - assert_eq!(aki('」', '・'), 0.5, "{set}: the larger blank stays"); - } - } - #[test] fn overrides_are_directional_and_unknown_tables_are_not_silently_applied() { let mut styles = Styles::default(); @@ -263,6 +245,24 @@ mod tests { assert_ne!(class('「'), class('(')); } + #[test] + fn consecutive_punctuation_touches_except_closing_before_opening() { + let styles = Styles::default(); + for set in ["LineEndAllOneHalfEmEnum", "LineEndAllOneEmEnum", "SimpChineseDefault"] { + let rules = Rules::resolve(&styles, set).unwrap().unwrap(); + let aki = |a: char, b: char| rules.pair(class(a), class(b)).desired; + for (a, b) in + [('」', '。'), ('」', '、'), ('。', '」'), ('、', '」'), ('」', '』'), (')', '」'), (',', ')'), ('「', '『'), ('(', '「')] + { + assert_eq!(aki(a, b), 0.0, "{set}: {a}{b}"); + } + for (a, b) in [('」', '「'), ('。', '「'), ('、', '('), ('」', '漢'), ('漢', '「')] { + assert_eq!(aki(a, b), 0.5, "{set}: {a}{b}"); + } + assert_eq!(aki('」', '・'), 0.5, "{set}: the larger blank stays"); + } + } + #[test] fn one_em_line_end_set_keeps_closing_punctuation_full_at_line_end_only() { let styles = Styles::default(); From 46596a9d19f21a12f94053ef2cb94b3e5024bda4 Mon Sep 17 00:00:00 2001 From: con3code Date: Sat, 17 Oct 2026 23:42:21 +0900 Subject: [PATCH 3/5] Mojikumi: close up consecutive punctuation in the Japanese sets only The Chinese sets keep the larger blank between two marks, as before: JLREQ's rule is Japanese, and Chinese styles (Taiwan's centred, full-width marks among them) set consecutive punctuation their own way. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013phQ3ekq6uGBEWD6FjFYG2 --- crates/doc/src/mojikumi.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/doc/src/mojikumi.rs b/crates/doc/src/mojikumi.rs index e0a03d85a..daabe34d7 100644 --- a/crates/doc/src/mojikumi.rs +++ b/crates/doc/src/mojikumi.rs @@ -126,12 +126,13 @@ impl<'a> Rules<'a> { if right == 22 { return Aki::fixed(if self.base == Base::LineEndHalfEm { 0.0 } else { trailing(left) }); } - // Consecutive punctuation (JLREQ 3.1.4): opening brackets together, and closing brackets, - // commas and full stops together, touch. Other neighbours keep the larger blank, so a - // closing bracket and an opening one are half an em apart. + // Consecutive punctuation in the Japanese sets (JLREQ 3.1.4): opening brackets together, + // and closing brackets, commas and full stops together, touch. Other neighbours keep the + // larger blank, so a closing bracket and an opening one are half an em apart. The Chinese + // sets keep the larger blank throughout. let opening = |c| matches!(c, 1 | 26 | 27); let closing = |c| matches!(c, 2 | 6 | 21 | 28..=31); - if (opening(left) && opening(right)) || (closing(left) && closing(right)) { + if self.base != Base::Chinese && ((opening(left) && opening(right)) || (closing(left) && closing(right))) { return Aki::default(); } let punctuation = trailing(left).max(leading(right)); @@ -248,7 +249,7 @@ mod tests { #[test] fn consecutive_punctuation_touches_except_closing_before_opening() { let styles = Styles::default(); - for set in ["LineEndAllOneHalfEmEnum", "LineEndAllOneEmEnum", "SimpChineseDefault"] { + for set in ["LineEndAllOneHalfEmEnum", "LineEndAllOneEmEnum"] { let rules = Rules::resolve(&styles, set).unwrap().unwrap(); let aki = |a: char, b: char| rules.pair(class(a), class(b)).desired; for (a, b) in @@ -261,6 +262,13 @@ mod tests { } assert_eq!(aki('」', '・'), 0.5, "{set}: the larger blank stays"); } + // The Chinese sets are unchanged: the larger blank stays between any two marks. + for set in ["SimpChineseDefault", "TradChineseDefault"] { + let rules = Rules::resolve(&styles, set).unwrap().unwrap(); + for (a, b) in [('」', '。'), (',', ')'), ('「', '『'), ('」', '「')] { + assert_eq!(rules.pair(class(a), class(b)).desired, 0.5, "{set}: {a}{b}"); + } + } } #[test] From 91d4c66b242bea390e866769ca59cc7ad40844ce Mon Sep 17 00:00:00 2001 From: con3code Date: Sun, 18 Oct 2026 01:12:41 +0900 Subject: [PATCH 4/5] docs/cjk.md: mojikumi is applied; consecutive punctuation The status table still said mojikumi tables were kept but not applied, which #129 changed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013phQ3ekq6uGBEWD6FjFYG2 --- docs/cjk.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/cjk.md b/docs/cjk.md index 4ee48dc06..d37e9c29c 100644 --- a/docs/cjk.md +++ b/docs/cjk.md @@ -1,6 +1,6 @@ # CJK typesetting plan -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** trivial (status line and revision history added; content unchanged in the 2026-10-10 review) · **Target:** Adobe InDesign 2026 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (mojikumi applied; consecutive punctuation) · **Target:** Adobe InDesign 2026 Goal: the Chinese, Japanese and Korean typesetting that InDesign offers with its East Asian feature set (the "Japanese/CJK" feature set, `Feature Set Locale Setting` 257), so that a @@ -37,7 +37,7 @@ presets or tables are copied. Our default mojikumi and kinsoku tables are built | Vertical metrics and shaping | done: upright runs shaped top to bottom (`vert`; `vkrn`/`vpal` when asked) with `vmtx`/`VORG` advances and origins, centred on the ideographic em box (`BASE`, else OS/2); turned runs shaped horizontally, without `vrt2`; Horizontal Kana (`hkna`) takes the vertical kana (`vkna`) in upright runs. Default location of variable fonts only (no `VVAR`) | | Kinsoku | named (hard, soft, Chinese, Korean) and custom kinsoku sets, bunri-kinshi, rensuuji, hanging punctuation (regular, force); without a set, fixed no-start / no-end lists and a break anywhere between CJK characters; kinsoku type (push in / push out priorities) kept but not applied | | Korean line breaking | at spaces (Hangul everywhere, hanja in Korean text); per-paragraph character-based breaking (`koreanCharBreaks`); DesignCraft-only, not in IDML | -| Mojikumi | tables and paragraph references kept through IDML; not applied (Preflight says so) | +| Mojikumi | applied in composition: the built-in base sets (line end half-width, line end one em, Simplified and Traditional Chinese) and the document's tables with their override rows, kept through IDML; Preflight notes tables it can't apply. In the Japanese sets consecutive punctuation closes up (JLREQ 3.1.4: 」。, 「『 touch; 」「 half an em apart) | | Aki, tsume, jidori | done ([cjk-typography.md](cjk-typography.md)) | | Character alignment, leading model | em box top / centre / bottom, ICF top / bottom (`BASE` `icft`/`icfb`, else the em box inset by 5%), roman baseline; aki above / below, centre (centre down as centre); under aki below a line's leading is the space to the line below it; with the Japanese composers (`Composer::Japanese`, `JapaneseSingleLine`, IDML `HL Composer J` / `HL Single J`), the others set lines baseline to baseline on the Roman baseline | | Tate-chu-yoko | manual, with offsets; auto tate-chu-yoko of short digit runs (with roman letters when asked) in vertical text, narrowed to fit the em (`type.autoTateChuYoko`, IDML `AutoTcy`, `AutoTcyIncludeRoman`); manual groups of 3+ digits overflow the em | @@ -75,7 +75,7 @@ punctuation centred vs corner-placed). A *mojikumi set* is a table indexed by (class before, class after) giving the space between them as a min/optimum/max triple in em fractions, plus line-start and line-end rules. Sets are document resources (`Styles::mojikumi_tables`, read from and written to IDML with their override -rows), referenced by name from the paragraph's `mojikumi`; what remains is applying them. We +rows), referenced by name from the paragraph's `mojikumi`, and applied in composition. We ship a few built-in sets derived from JLREQ (no line-start indent, one-em indent, half-width punctuation, full-width punctuation…), all original data, for the IDML built-in names. @@ -292,6 +292,7 @@ Each follows what an InDesign user expects. | Date | Change | Summary | |---|---|---| +| 2026-10-18 | minor | Mojikumi is applied (since #129); consecutive punctuation closes up in the Japanese sets | | 2026-10-10 | trivial | Reviewed in the full re-measure; status line and this table added; summarized in [typography-parity.md](typography-parity.md) and [localization-parity.md](localization-parity.md) | | 2026-10-10 | minor | Updated (see git history) | | 2026-10-06 | major | Created: CJK typesetting plan and status (the Japanese/CJK feature set) | From 5248ad683cd2e11adf889d90e5d3a13c958fcf36 Mon Sep 17 00:00:00 2001 From: con3code Date: Tue, 20 Oct 2026 19:33:13 +0900 Subject: [PATCH 5/5] Mojikumi: a middle dot beside a bracket keeps only its quarter em (JLREQ 3.1.4) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013phQ3ekq6uGBEWD6FjFYG2 --- crates/doc/src/mojikumi.rs | 27 ++++++++++++++++++++------- docs/cjk.md | 2 +- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/doc/src/mojikumi.rs b/crates/doc/src/mojikumi.rs index daabe34d7..54f3269c0 100644 --- a/crates/doc/src/mojikumi.rs +++ b/crates/doc/src/mojikumi.rs @@ -127,13 +127,21 @@ impl<'a> Rules<'a> { return Aki::fixed(if self.base == Base::LineEndHalfEm { 0.0 } else { trailing(left) }); } // Consecutive punctuation in the Japanese sets (JLREQ 3.1.4): opening brackets together, - // and closing brackets, commas and full stops together, touch. Other neighbours keep the - // larger blank, so a closing bracket and an opening one are half an em apart. The Chinese - // sets keep the larger blank throughout. + // and closing brackets, commas and full stops together, touch. A middle dot (・:;) after + // a closing bracket or before an opening one keeps only its own quarter em. Other + // neighbours keep the larger blank, so a closing bracket and an opening one are half an + // em apart. The Chinese sets keep the larger blank throughout. let opening = |c| matches!(c, 1 | 26 | 27); let closing = |c| matches!(c, 2 | 6 | 21 | 28..=31); - if self.base != Base::Chinese && ((opening(left) && opening(right)) || (closing(left) && closing(right))) { - return Aki::default(); + let closing_bracket = |c| matches!(c, 2 | 28 | 29); + let middle_dot = |c| matches!(c, 5 | 32); + if self.base != Base::Chinese { + if (opening(left) && opening(right)) || (closing(left) && closing(right)) { + return Aki::default(); + } + if (closing_bracket(left) && middle_dot(right)) || (middle_dot(left) && opening(right)) { + return Aki { min: 0.0, desired: 0.25, max: 0.25, priority: 1, discrete: false }; + } } let punctuation = trailing(left).max(leading(right)); if punctuation > 0.0 { @@ -260,12 +268,17 @@ mod tests { for (a, b) in [('」', '「'), ('。', '「'), ('、', '('), ('」', '漢'), ('漢', '「')] { assert_eq!(aki(a, b), 0.5, "{set}: {a}{b}"); } - assert_eq!(aki('」', '・'), 0.5, "{set}: the larger blank stays"); + // A middle dot keeps its quarter em beside a bracket (JLREQ 3.1.4, Figure 69 ⑦). + for (a, b) in [('」', '・'), (')', ':'), ('』', ';'), ('・', '「'), (';', '('), (':', '〈')] { + assert_eq!(aki(a, b), 0.25, "{set}: {a}{b}"); + } + // After a comma or full stop the larger blank stays. + assert_eq!(aki('、', '・'), 0.5, "{set}: 、・"); } // The Chinese sets are unchanged: the larger blank stays between any two marks. for set in ["SimpChineseDefault", "TradChineseDefault"] { let rules = Rules::resolve(&styles, set).unwrap().unwrap(); - for (a, b) in [('」', '。'), (',', ')'), ('「', '『'), ('」', '「')] { + for (a, b) in [('」', '。'), (',', ')'), ('「', '『'), ('」', '「'), ('」', '・'), ('・', '「')] { assert_eq!(rules.pair(class(a), class(b)).desired, 0.5, "{set}: {a}{b}"); } } diff --git a/docs/cjk.md b/docs/cjk.md index d37e9c29c..68839e0e8 100644 --- a/docs/cjk.md +++ b/docs/cjk.md @@ -37,7 +37,7 @@ presets or tables are copied. Our default mojikumi and kinsoku tables are built | Vertical metrics and shaping | done: upright runs shaped top to bottom (`vert`; `vkrn`/`vpal` when asked) with `vmtx`/`VORG` advances and origins, centred on the ideographic em box (`BASE`, else OS/2); turned runs shaped horizontally, without `vrt2`; Horizontal Kana (`hkna`) takes the vertical kana (`vkna`) in upright runs. Default location of variable fonts only (no `VVAR`) | | Kinsoku | named (hard, soft, Chinese, Korean) and custom kinsoku sets, bunri-kinshi, rensuuji, hanging punctuation (regular, force); without a set, fixed no-start / no-end lists and a break anywhere between CJK characters; kinsoku type (push in / push out priorities) kept but not applied | | Korean line breaking | at spaces (Hangul everywhere, hanja in Korean text); per-paragraph character-based breaking (`koreanCharBreaks`); DesignCraft-only, not in IDML | -| Mojikumi | applied in composition: the built-in base sets (line end half-width, line end one em, Simplified and Traditional Chinese) and the document's tables with their override rows, kept through IDML; Preflight notes tables it can't apply. In the Japanese sets consecutive punctuation closes up (JLREQ 3.1.4: 」。, 「『 touch; 」「 half an em apart) | +| Mojikumi | applied in composition: the built-in base sets (line end half-width, line end one em, Simplified and Traditional Chinese) and the document's tables with their override rows, kept through IDML; Preflight notes tables it can't apply. In the Japanese sets consecutive punctuation closes up (JLREQ 3.1.4: 」。, 「『 touch; 」・ a quarter em, 」「 half an em apart). Still open: 、・ and ・・ take the sum of both blanks in JLREQ (Table 1 notes) | | Aki, tsume, jidori | done ([cjk-typography.md](cjk-typography.md)) | | Character alignment, leading model | em box top / centre / bottom, ICF top / bottom (`BASE` `icft`/`icfb`, else the em box inset by 5%), roman baseline; aki above / below, centre (centre down as centre); under aki below a line's leading is the space to the line below it; with the Japanese composers (`Composer::Japanese`, `JapaneseSingleLine`, IDML `HL Composer J` / `HL Single J`), the others set lines baseline to baseline on the Roman baseline | | Tate-chu-yoko | manual, with offsets; auto tate-chu-yoko of short digit runs (with roman letters when asked) in vertical text, narrowed to fit the em (`type.autoTateChuYoko`, IDML `AutoTcy`, `AutoTcyIncludeRoman`); manual groups of 3+ digits overflow the em |