diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 92f24682b..db03b67d8 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -6169,6 +6169,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..54f3269c0 100644 --- a/crates/doc/src/mojikumi.rs +++ b/crates/doc/src/mojikumi.rs @@ -126,6 +126,23 @@ impl<'a> Rules<'a> { if right == 22 { 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. 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); + 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 { return Aki { min: 0.0, desired: punctuation, max: punctuation, priority: 1, discrete: false }; @@ -237,6 +254,36 @@ mod tests { assert_ne!(class('「'), class('(')); } + #[test] + fn consecutive_punctuation_touches_except_closing_before_opening() { + let styles = Styles::default(); + 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 + [('」', '。'), ('」', '、'), ('。', '」'), ('、', '」'), ('」', '』'), (')', '」'), (',', ')'), ('「', '『'), ('(', '「')] + { + assert_eq!(aki(a, b), 0.0, "{set}: {a}{b}"); + } + for (a, b) in [('」', '「'), ('。', '「'), ('、', '('), ('」', '漢'), ('漢', '「')] { + assert_eq!(aki(a, b), 0.5, "{set}: {a}{b}"); + } + // 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 [('」', '。'), (',', ')'), ('「', '『'), ('」', '「'), ('」', '・'), ('・', '「')] { + assert_eq!(rules.pair(class(a), class(b)).desired, 0.5, "{set}: {a}{b}"); + } + } + } + #[test] fn one_em_line_end_set_keeps_closing_punctuation_full_at_line_end_only() { let styles = Styles::default(); diff --git a/docs/cjk.md b/docs/cjk.md index 4ee48dc06..68839e0e8 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; 」・ 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 | @@ -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) |