Merge mojikumi-base: CJK: custom mojikumi tables without a base set apply their rules

# Conflicts:
#	crates/engine/src/tests.rs
This commit is contained in:
aiongg
2026-10-11 00:47:10 +00:00
4 changed files with 72 additions and 4 deletions
+15 -2
View File
@@ -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<Option<Self>, 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> {
+9 -2
View File
@@ -158,8 +158,15 @@ pub fn check(s: &Session, min_ppi: f64) -> Vec<Issue> {
.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, &para.mojikumi) {
unsupported_typography.insert(format!("Mojikumi `{}` is not applied: {reason}", para.mojikumi));
match designcraft_doc::mojikumi::Rules::resolve(&d.styles, &para.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(),
+18
View File
@@ -880,3 +880,21 @@ fn direct_selection_drag_and_arrow_keys_move_placed_content() {
assert_eq!(frame2, frame0);
assert_eq!(g2 - g0, designcraft_geom::Vec2::new(1.0, 10.0));
}
#[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:?}");
}
+30
View File
@@ -1437,6 +1437,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("</Document>", r#"<MojikumiTable Self="uMojikumi2" Name="Baseless"><Properties><OverrideMojikumiAkiList>
<OverrideMojikumiAkiType TargetMojikumiClass="12" SideMojikumiClass="18" SideIsAfterTarget="true" Minimum="0.25" Desired="0.75" Maximum="1" CompressionPriority="2" AkiDoesNotFloat="false"/>
</OverrideMojikumiAkiList></Properties></MojikumiTable></Document>"#);
let story = STORY.replace("<ParagraphStyleRange ", "<ParagraphStyleRange Mojikumi=\"uMojikumi2\" ");
let bytes = zip_files(&[
("designmap.xml", &map),
("Resources/Graphic.xml", GRAPHIC),
("Resources/Styles.xml", STYLES),
("Resources/Preferences.xml", PREFS),
("MasterSpreads/MasterSpread_m1.xml", MASTER),
("Spreads/Spread_sp1.xml", SPREAD),
("Stories/Story_s1.xml", &story),
]);
let d = import_idml(&bytes).unwrap();
let name = d.stories.values().flat_map(|s| &s.paras).find_map(|p| p.para.mojikumi.clone()).unwrap();
assert_eq!(name, "MojikumiTable/Baseless");
let rules = designcraft_doc::mojikumi::Rules::resolve(&d.styles, &name).unwrap().unwrap();
assert!(rules.assumed_base());
let pair = rules.pair(12, 18);
assert_eq!((pair.min, pair.desired, pair.max, pair.priority), (0.25, 0.75, 1.0, 2));
// Pairs the table does not list use the half-em set: no blank at line start.
assert_eq!(rules.pair(22, designcraft_doc::mojikumi::class('(')).desired, 0.0);
let back = import_idml(&export_idml(&d)).unwrap();
assert_eq!(back.styles.mojikumi_tables, d.styles.mojikumi_tables);
}
#[test]
fn arabic_controls_import_independent_xml_and_round_trip() {
let story = r#"<idPkg:Story xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging"><Story Self="s1">