From 11631cde12505b7659918e7537d0c6d42acf5f04 Mon Sep 17 00:00:00 2001 From: aiongg Date: Sun, 11 Oct 2026 00:42:47 +0000 Subject: [PATCH] CJK: custom mojikumi tables without a base set apply their rules --- crates/doc/src/mojikumi.rs | 17 +++++++++++++++-- crates/engine/src/cmd/preflight.rs | 11 +++++++++-- crates/engine/src/tests.rs | 18 ++++++++++++++++++ crates/idml/src/tests.rs | 30 ++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/crates/doc/src/mojikumi.rs b/crates/doc/src/mojikumi.rs index fbb8a13df..0b95d2b71 100644 --- a/crates/doc/src/mojikumi.rs +++ b/crates/doc/src/mojikumi.rs @@ -23,6 +23,7 @@ impl Aki { pub struct Rules<'a> { chinese: bool, rows: &'a [MojikumiAki], + assumed_base: bool, } impl<'a> Rules<'a> { pub fn resolve(styles: &'a Styles, name: &str) -> Result, String> { @@ -31,7 +32,14 @@ impl<'a> Rules<'a> { } let key = name.strip_prefix("MojikumiTable/").unwrap_or(name); let table = styles.mojikumi_tables.iter().find(|t| t.name == key); - let base = table.map_or(key, |t| if t.based_on.is_empty() { t.name.as_str() } else { t.based_on.as_str() }); + // A custom table whose base set is missing (IDML leaves `BasedOnMojikumiSet` + // out when it cannot name the base) keeps its rows on the half-em default set. + let assumed_base = table.is_some_and(|t| t.based_on.is_empty()); + let base = match table { + Some(t) if t.based_on.is_empty() => "LineEndAllOneHalfEmEnum", + Some(t) => t.based_on.as_str(), + None => key, + }; let chinese = match base.trim_start_matches("$ID/") { "SimpChineseDefault" | "TradChineseDefault" => true, "LineEndAllOneHalfEmEnum" | "kMojikumiDefaultName1" => false, @@ -50,7 +58,12 @@ impl<'a> Rules<'a> { return Err("invalid spacing range or priority".into()); } } - Ok(Some(Self { chinese, rows })) + Ok(Some(Self { chinese, rows, assumed_base })) + } + + /// True when the table names no base set and the default one was used. + pub fn assumed_base(&self) -> bool { + self.assumed_base } fn override_pair(&self, left: i16, right: i16) -> Option<&MojikumiAki> { diff --git a/crates/engine/src/cmd/preflight.rs b/crates/engine/src/cmd/preflight.rs index 5a69ff1bf..1681ff89d 100644 --- a/crates/engine/src/cmd/preflight.rs +++ b/crates/engine/src/cmd/preflight.rs @@ -158,8 +158,15 @@ pub fn check(s: &Session, min_ppi: f64) -> Vec { .insert("Paragraph Kashida width preset is preserved; automatic elongation uses the engine's bounded allocation".into()); } } - if let Err(reason) = designcraft_doc::mojikumi::Rules::resolve(&d.styles, ¶.mojikumi) { - unsupported_typography.insert(format!("Mojikumi `{}` is not applied: {reason}", para.mojikumi)); + match designcraft_doc::mojikumi::Rules::resolve(&d.styles, ¶.mojikumi) { + Err(reason) => { + unsupported_typography.insert(format!("Mojikumi `{}` is not applied: {reason}", para.mojikumi)); + } + Ok(Some(rules)) if rules.assumed_base() => { + unsupported_typography + .insert(format!("Mojikumi `{}` names no base set; its rules apply on the default half-em set", para.mojikumi)); + } + Ok(_) => {} } if !matches!( para.kinsoku_type.as_str(), diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index f29462053..65a6644d6 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -579,3 +579,21 @@ fn preflight_mojikumi_uses_the_same_supported_rules_as_composition() { assert_eq!(!warnings.is_empty(), unsupported, "{name}: {warnings:?}"); } } + +#[test] +fn preflight_notes_an_assumed_mojikumi_base_set_without_calling_the_table_unapplied() { + let mut s = session(); + s.execute("frame.create", &json!({"rect": [36, 36, 300, 300], "content": "text", "text": "中文 A1"})).unwrap(); + let story = s.doc().unwrap().doc.stories.keys().next().unwrap().0; + let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc); + std::sync::Arc::make_mut(&mut d.styles) + .mojikumi_tables + .push(designcraft_doc::cjk::MojikumiTable { name: "Baseless".into(), ..Default::default() }); + s.execute("text.select", &json!({"story": story, "anchor": 0, "focus": 1})).unwrap(); + s.execute("type.para", &json!({"attrs": {"mojikumi": "MojikumiTable/Baseless"}})).unwrap(); + let result = s.execute("preflight.run", &json!({})).unwrap(); + let messages: Vec<_> = + result["issues"].as_array().unwrap().iter().filter(|i| i["kind"] == "unsupportedTypography").filter_map(|i| i["message"].as_str()).collect(); + assert!(messages.iter().any(|m| m.contains("Baseless") && m.contains("no base set")), "{messages:?}"); + assert!(!messages.iter().any(|m| m.contains("not applied")), "{messages:?}"); +} diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 11fb11b8b..4ecd9facb 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -815,6 +815,36 @@ fn cjk_mojikumi_overrides_and_priority_round_trip() { assert!(back.stories.values().flat_map(|s| &s.paras).any(|p| p.para.mojikumi.as_deref() == Some("MojikumiTable/Spacing"))); } +/// A custom table with no `BasedOnMojikumiSet` keeps its own rows on the +/// default half-em set, and the attribute stays absent on export. +#[test] +fn cjk_mojikumi_table_without_base_set_applies_its_rows() { + let map = DESIGNMAP.replace("", r#" + + "#); + let story = STORY.replace("