mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
CJK-REVERT2: withdraw speculative spacing rules and narrow import rejection
Restore the previous preset and character-class coverage. Remove regional punctuation and Tsume policy changes while retaining cluster, tracking, and spacing-allocation repairs. Preserve finite unsupported rows for diagnostics instead of rejecting whole documents.
This commit is contained in:
+3
-4
@@ -1,10 +1,9 @@
|
||||
# DesignCraft roadmap
|
||||
|
||||
- CJK-MOJI8: preserve shaped narrow punctuation and manual Tsume instead of restoring nonexistent full-width blank space.
|
||||
- CJK-MOJI7: reject malformed Mojikumi interchange values and check nested note/cell paragraph rules in preflight.
|
||||
- CJK-MOJI6: apply spacing at shaped-cluster boundaries and distinguish centered Traditional Chinese punctuation from Simplified Chinese side bearings.
|
||||
- CJK-MOJI8: preserve shaped narrow punctuation when tracking changes its advance.
|
||||
- CJK-MOJI7: prevent numeric coercion in Mojikumi import, preserve finite unsupported rules, and check nested note/cell rules in preflight.
|
||||
- CJK-MOJI6: apply spacing at shaped-cluster boundaries without moving attached marks.
|
||||
- CJK-MOJI5: share equal-priority boundary spacing and honor discrete compression endpoints, including leading-only ragged-line fitting.
|
||||
- CJK-MOJI4: resolve all public Mojikumi preset names and their distinct paragraph/line-edge policies; correct consecutive punctuation defaults and small-kana classes.
|
||||
|
||||
- CJK-MOJI3: use the composition rule resolver in preflight, report unknown Mojikumi presets and retain regression coverage for supported and unsupported settings.
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ pub struct Metrics {
|
||||
pub active: bool,
|
||||
/// Shaped body width before tracking and manual character spacing.
|
||||
pub body: f64,
|
||||
pub tsume_before: f64,
|
||||
pub tsume_after: f64,
|
||||
pub blank_before: f64,
|
||||
pub blank_after: f64,
|
||||
/// Desired internal gap attached to this glyph's advance.
|
||||
@@ -71,8 +69,8 @@ pub fn apply(glyphs: &mut [Glyph], styles: &Styles, name: &str) {
|
||||
// Tracking must not make a proportional or half-width glyph appear to
|
||||
// contain a removable half-em body. Shaping features already affect body.
|
||||
if body >= unit * 0.8 {
|
||||
let before = if cluster[0].moji.explicit_before { 0.0 } else { (table.leading(class) * unit - cluster[0].moji.tsume_before).max(0.0) };
|
||||
let after = if cluster[0].moji.explicit_after { 0.0 } else { (table.trailing(class) * unit - cluster[0].moji.tsume_after).max(0.0) };
|
||||
let before = if cluster[0].moji.explicit_before { 0.0 } else { rules::leading(class) * unit };
|
||||
let after = if cluster[0].moji.explicit_after { 0.0 } else { rules::trailing(class) * unit };
|
||||
let trim = before + after;
|
||||
if let Some(owner) = cluster.iter().position(|g| g.adv >= trim) {
|
||||
cluster[0].moji.blank_before = before / unit.max(1e-9);
|
||||
|
||||
@@ -477,8 +477,6 @@ fn shape_run(
|
||||
let tsume = p.tsume.clamp(0.0, 1.0);
|
||||
let before = p.leading_aki.unwrap_or(0.0).clamp(0.0, 100.0) * em;
|
||||
let after = p.trailing_aki.unwrap_or(0.0).clamp(0.0, 100.0) * em;
|
||||
g.moji.tsume_before = left * tsume;
|
||||
g.moji.tsume_after = right * tsume;
|
||||
g.dx += before - left * tsume;
|
||||
g.adv += before + after - (left + right) * tsume;
|
||||
}
|
||||
|
||||
@@ -706,8 +706,8 @@ impl Document {
|
||||
}
|
||||
}
|
||||
for t in &self.styles.mojikumi_tables {
|
||||
if t.overrides.iter().any(|r| !mojikumi::valid_range(r)) {
|
||||
return Err(DocError::Invalid(format!("invalid mojikumi spacing: {}", t.name)));
|
||||
if t.overrides.iter().any(|r| ![r.minimum, r.desired, r.maximum].iter().all(|v| v.is_finite())) {
|
||||
return Err(DocError::Invalid(format!("non-finite mojikumi spacing: {}", t.name)));
|
||||
}
|
||||
}
|
||||
let mut ids = std::collections::HashSet::new();
|
||||
|
||||
+26
-191
@@ -19,92 +19,9 @@ impl Aki {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adobe's public 16-preset enumeration. Japanese preset geometry is described
|
||||
/// in the Adobe Mojikumi guide, sections 3.1–3.14 (see docs/mojikumi.md).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Preset(u8);
|
||||
impl Preset {
|
||||
fn parse(name: &str) -> Option<Self> {
|
||||
let name = name.trim_start_matches("$ID/");
|
||||
let names = [
|
||||
"LineEndAllOneHalfEmEnum",
|
||||
"OneEmIndentLineEndUkeOneHalfEmEnum",
|
||||
"OneOrOneHalfEmIndentLineEndUkeOneHalfEmEnum",
|
||||
"OneOrOneHalfEmIndentLineEndAllOneEmEnum",
|
||||
"OneEmIndentLineEndAllOneEmEnum",
|
||||
"OneEmIndentLineEndAllNoFloatEnum",
|
||||
"OneEmIndentLineEndUkeNoFloatEnum",
|
||||
"OneOrOneHalfEmIndentLineEndUkeNoFloatEnum",
|
||||
"OneEmIndentLineEndAllOneHalfEmEnum",
|
||||
"LineEndAllOneEmEnum",
|
||||
"LineEndUkeNoFloatEnum",
|
||||
"OneOrOneHalfEmIndentLineEndPeriodOneEmEnum",
|
||||
"OneEmIndentLineEndPeriodOneEmEnum",
|
||||
"LineEndPeriodOneEmEnum",
|
||||
"TradChineseDefault",
|
||||
"SimpChineseDefault",
|
||||
];
|
||||
names.iter().position(|n| *n == name).map(|i| Self(i as u8 + 1)).or_else(|| {
|
||||
let n = name.strip_prefix("kMojikumiDefaultName")?.parse::<u8>().ok()?;
|
||||
(1..=16).contains(&n).then_some(Self(n))
|
||||
})
|
||||
}
|
||||
fn leading(self, c: i16) -> f64 {
|
||||
if self.0 == 15 && matches!(c, 6 | 21 | 30 | 31) {
|
||||
0.25
|
||||
} else if self.0 == 16 && c == 32 {
|
||||
0.0
|
||||
} else {
|
||||
leading(c)
|
||||
}
|
||||
}
|
||||
fn trailing(self, c: i16) -> f64 {
|
||||
if self.0 == 15 && matches!(c, 6 | 21 | 30 | 31) {
|
||||
0.25
|
||||
} else if self.0 == 16 && c == 32 {
|
||||
0.5
|
||||
} else {
|
||||
trailing(c)
|
||||
}
|
||||
}
|
||||
fn boundary(self, left: i16, right: i16) -> Option<Aki> {
|
||||
let n = self.0;
|
||||
let open = |c| matches!(c, 1 | 26 | 27);
|
||||
if left == 23 {
|
||||
if n >= 15 {
|
||||
return Some(Aki::fixed(self.leading(right)));
|
||||
}
|
||||
let indent = if matches!(n, 1 | 10 | 11 | 14) { 0.0 } else { 1.0 };
|
||||
let opening = match n {
|
||||
2 | 5 | 7 | 13 => 1.5,
|
||||
3 | 8 | 10 => 0.5,
|
||||
4 | 6 | 9 | 12 => 1.0,
|
||||
_ => 0.0,
|
||||
};
|
||||
return Some(Aki::fixed(if open(right) { opening } else { indent + self.leading(right) }));
|
||||
}
|
||||
if left == 22 {
|
||||
return Some(Aki::fixed(if n >= 15 || matches!(n, 4 | 5 | 10) { self.leading(right) } else { 0.0 }));
|
||||
}
|
||||
if right == 22 {
|
||||
let width = self.trailing(left);
|
||||
let full = n >= 15 || matches!(n, 4 | 5 | 10) || (matches!(n, 12..=14) && matches!(left, 6 | 31));
|
||||
let discrete = n == 6 || (matches!(n, 7 | 8 | 11) && matches!(left, 2 | 6 | 21 | 28..=31));
|
||||
return Some(if full {
|
||||
Aki::fixed(width)
|
||||
} else if discrete {
|
||||
Aki { min: 0.0, desired: width, max: width, priority: 1, discrete: true }
|
||||
} else {
|
||||
Aki::fixed(0.0)
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolved table; custom rows override the base preset in document order.
|
||||
pub struct Rules<'a> {
|
||||
preset: Preset,
|
||||
chinese: bool,
|
||||
rows: &'a [MojikumiAki],
|
||||
}
|
||||
impl<'a> Rules<'a> {
|
||||
@@ -115,25 +32,25 @@ 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() });
|
||||
let preset = Preset::parse(base).ok_or_else(|| format!("unknown base preset `{base}`"))?;
|
||||
let chinese = match base.trim_start_matches("$ID/") {
|
||||
"SimpChineseDefault" | "TradChineseDefault" => true,
|
||||
"LineEndAllOneHalfEmEnum" | "kMojikumiDefaultName1" => false,
|
||||
_ => return Err(format!("unknown base preset `{base}`")),
|
||||
};
|
||||
let rows = table.map_or(&[][..], |t| t.overrides.as_slice());
|
||||
for r in rows {
|
||||
if !valid_class(r.target_class) || !valid_class(r.side_class) {
|
||||
return Err(format!("unsupported character classes {}/{}", r.target_class, r.side_class));
|
||||
}
|
||||
if !valid_range(r) {
|
||||
if ![r.minimum, r.desired, r.maximum].iter().all(|v| v.is_finite() && (-1.0..=100.0).contains(v))
|
||||
|| r.minimum > r.desired
|
||||
|| r.desired > r.maximum
|
||||
|| !(0..=9).contains(&r.priority)
|
||||
{
|
||||
return Err("invalid spacing range or priority".into());
|
||||
}
|
||||
}
|
||||
Ok(Some(Self { preset, rows }))
|
||||
}
|
||||
|
||||
/// Blank body portions for the preset's regional punctuation convention.
|
||||
pub fn leading(&self, class: i16) -> f64 {
|
||||
self.preset.leading(class)
|
||||
}
|
||||
pub fn trailing(&self, class: i16) -> f64 {
|
||||
self.preset.trailing(class)
|
||||
Ok(Some(Self { chinese, rows }))
|
||||
}
|
||||
|
||||
fn override_pair(&self, left: i16, right: i16) -> Option<&MojikumiAki> {
|
||||
@@ -149,11 +66,11 @@ impl<'a> Rules<'a> {
|
||||
let mut aki = self.pair(left, right);
|
||||
if self.override_pair(left, right).is_none() {
|
||||
let missing = if matches!(left, 22 | 23) {
|
||||
self.leading(right) - before_right
|
||||
leading(right) - before_right
|
||||
} else if right == 22 {
|
||||
self.trailing(left) - after_left
|
||||
trailing(left) - after_left
|
||||
} else {
|
||||
self.trailing(left).max(self.leading(right)) - after_left.max(before_right)
|
||||
trailing(left).max(leading(right)) - after_left.max(before_right)
|
||||
}
|
||||
.max(0.0);
|
||||
aki.min = (aki.min - missing).max(0.0);
|
||||
@@ -173,17 +90,15 @@ impl<'a> Rules<'a> {
|
||||
discrete: row.does_not_float,
|
||||
};
|
||||
}
|
||||
if let Some(aki) = self.preset.boundary(left, right) {
|
||||
return aki;
|
||||
// Boundaries: Chinese keeps full punctuation; the half-em Japanese set
|
||||
// trims opening punctuation at line start and closing punctuation at end.
|
||||
if matches!(left, 22 | 23) {
|
||||
return Aki::fixed(if self.chinese { leading(right) } else { 0.0 });
|
||||
}
|
||||
// Adjacent opening brackets and adjacent closing punctuation share a
|
||||
// half-body, rather than restoring both glyphs to their full widths.
|
||||
if (matches!(left, 1 | 26 | 27) && matches!(right, 1 | 26 | 27))
|
||||
|| (matches!(left, 2 | 6 | 21 | 28..=31) && matches!(right, 2 | 6 | 21 | 28..=31))
|
||||
{
|
||||
return Aki::fixed(0.0);
|
||||
if right == 22 {
|
||||
return Aki::fixed(if self.chinese { trailing(left) } else { 0.0 });
|
||||
}
|
||||
let punctuation = self.trailing(left).max(self.leading(right));
|
||||
let punctuation = trailing(left).max(leading(right));
|
||||
if punctuation > 0.0 {
|
||||
return Aki { min: 0.0, desired: punctuation, max: punctuation, priority: 1, discrete: false };
|
||||
}
|
||||
@@ -199,15 +114,6 @@ impl<'a> Rules<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Structural validation shared by native document loading and interchange.
|
||||
/// Unknown integer classes remain preservable and are diagnosed by the resolver.
|
||||
pub fn valid_range(r: &MojikumiAki) -> bool {
|
||||
[r.minimum, r.desired, r.maximum].iter().all(|v| v.is_finite() && (-1.0..=100.0).contains(v))
|
||||
&& r.minimum <= r.desired
|
||||
&& r.desired <= r.maximum
|
||||
&& (0..=9).contains(&r.priority)
|
||||
}
|
||||
|
||||
pub fn valid_class(c: i16) -> bool {
|
||||
matches!(c, 1..=12 | 18 | 21..=33)
|
||||
}
|
||||
@@ -247,42 +153,13 @@ pub fn class(c: char) -> i16 {
|
||||
'¥' | '$' | '£' | '¥' => 8,
|
||||
'%' | '‰' | '℃' | '°' => 9,
|
||||
'\u{3000}' => 10,
|
||||
'ゕ'
|
||||
| 'ゖ'
|
||||
| 'ヵ'
|
||||
| 'ヶ'
|
||||
| '\u{31f0}'..='\u{31ff}'
|
||||
| 'ぁ'
|
||||
| 'ぃ'
|
||||
| 'ぅ'
|
||||
| 'ぇ'
|
||||
| 'ぉ'
|
||||
| 'っ'
|
||||
| 'ゃ'
|
||||
| 'ゅ'
|
||||
| 'ょ'
|
||||
| 'ゎ'
|
||||
| 'ァ'
|
||||
| 'ィ'
|
||||
| 'ゥ'
|
||||
| 'ェ'
|
||||
| 'ォ'
|
||||
| 'ッ'
|
||||
| 'ャ'
|
||||
| 'ュ'
|
||||
| 'ョ'
|
||||
| 'ヮ'
|
||||
| 'ー'
|
||||
| '々'
|
||||
| 'ゝ'
|
||||
| 'ゞ'
|
||||
| 'ヽ'
|
||||
| 'ヾ' => 3,
|
||||
'ぁ' | 'ぃ' | 'ぅ' | 'ぇ' | 'ぉ' | 'っ' | 'ゃ' | 'ゅ' | 'ょ' | 'ゎ' | 'ァ' | 'ィ' | 'ゥ' | 'ェ' | 'ォ' | 'ッ' | 'ャ' | 'ュ' | 'ョ' | 'ヮ'
|
||||
| 'ー' | '々' | 'ゝ' | 'ゞ' | 'ヽ' | 'ヾ' => 3,
|
||||
'\u{3040}'..='\u{309f}' => 11,
|
||||
'\u{30a0}'..='\u{30ff}' | '\u{ff66}'..='\u{ff9d}' => 33,
|
||||
'\u{30a0}'..='\u{30ff}' | '\u{31f0}'..='\u{31ff}' => 33,
|
||||
'0'..='9' => 24,
|
||||
'0'..='9' => 25,
|
||||
'\u{2e80}'..='\u{a4cf}' | '\u{ac00}'..='\u{d7af}' | '\u{f900}'..='\u{faff}' | '\u{ff01}'..='\u{ff60}' | '\u{20000}'..='\u{3347f}' => 12,
|
||||
'\u{2e80}'..='\u{a4cf}' | '\u{ac00}'..='\u{d7af}' | '\u{f900}'..='\u{faff}' | '\u{ff01}'..='\u{ff60}' | '\u{20000}'..='\u{323af}' => 12,
|
||||
_ => 18,
|
||||
}
|
||||
}
|
||||
@@ -292,47 +169,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::cjk::MojikumiTable;
|
||||
|
||||
#[test]
|
||||
fn regional_punctuation_retains_the_correct_side_bearings() {
|
||||
let styles = Styles::default();
|
||||
let traditional = Rules::resolve(&styles, "TradChineseDefault").unwrap().unwrap();
|
||||
let simplified = Rules::resolve(&styles, "SimpChineseDefault").unwrap().unwrap();
|
||||
assert_eq!(traditional.leading(class('。')), 0.25);
|
||||
assert_eq!(traditional.trailing(class('。')), 0.25);
|
||||
assert_eq!(simplified.leading(class('。')), 0.0);
|
||||
assert_eq!(simplified.trailing(class('。')), 0.5);
|
||||
assert_eq!(traditional.pair(12, class('。')).desired, 0.25);
|
||||
assert_eq!(simplified.pair(12, class('。')).desired, 0.0);
|
||||
assert_eq!(simplified.leading(class(':')), 0.0);
|
||||
assert_eq!(simplified.trailing(class(':')), 0.5);
|
||||
assert!((simplified.pair_with_bodies(6, 12, 0.1, 0.0).desired - 0.1).abs() < 1e-9);
|
||||
assert_eq!(simplified.pair_with_bodies(22, 27, 0.0, 0.0).desired, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_public_presets_have_distinct_documented_edges() {
|
||||
let styles = Styles::default();
|
||||
let openings = [0.0, 1.5, 0.5, 1.0, 1.5, 1.0, 1.5, 0.5, 1.0, 0.5, 0.0, 1.0, 1.5, 0.0];
|
||||
for (i, expected) in openings.into_iter().enumerate() {
|
||||
let rules = Rules::resolve(&styles, &format!("$ID/kMojikumiDefaultName{}", i + 1)).unwrap().unwrap();
|
||||
assert_eq!(rules.pair(23, 26).desired, expected, "preset {}", i + 1);
|
||||
assert_eq!(rules.pair(23, 12).desired, if matches!(i + 1, 1 | 10 | 11 | 14) { 0.0 } else { 1.0 });
|
||||
}
|
||||
let discrete = Rules::resolve(&styles, "OneEmIndentLineEndUkeNoFloatEnum").unwrap().unwrap();
|
||||
assert!(discrete.pair(6, 22).discrete);
|
||||
assert!(!discrete.pair(5, 22).discrete);
|
||||
let period = Rules::resolve(&styles, "LineEndPeriodOneEmEnum").unwrap().unwrap();
|
||||
assert_eq!(period.pair(6, 22).desired, 0.5);
|
||||
assert_eq!(period.pair(21, 22).desired, 0.0);
|
||||
for name in ["$ID/kMojikumiDefaultName15", "$ID/kMojikumiDefaultName16"] {
|
||||
assert!(Rules::resolve(&styles, name).unwrap().is_some());
|
||||
}
|
||||
assert_eq!(period.pair(26, 26).desired, 0.0);
|
||||
assert_eq!(period.pair(28, 6).desired, 0.0);
|
||||
assert_eq!(class('ㇰ'), 3);
|
||||
assert_eq!(class('カ'), 33);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overrides_are_directional_and_unknown_tables_are_not_silently_applied() {
|
||||
let mut styles = Styles::default();
|
||||
@@ -352,7 +188,6 @@ mod tests {
|
||||
});
|
||||
let rule = Rules::resolve(&styles, "MojikumiTable/Pair").unwrap().unwrap();
|
||||
assert_eq!(rule.pair(18, 12).desired, 0.2);
|
||||
assert_eq!(rule.pair_with_bodies(18, 12, 0.0, 0.0).desired, 0.2);
|
||||
assert_eq!(rule.pair(12, 18).desired, 0.25);
|
||||
assert!(Rules::resolve(&styles, "Unknown").is_err());
|
||||
assert!(Rules::resolve(&styles, "Nothing").unwrap().is_none());
|
||||
|
||||
+10
-13
@@ -144,14 +144,14 @@ fn lab_to_color(l: f32, a: f32, b: f32) -> Color {
|
||||
}
|
||||
|
||||
fn mojikumi_row(e: &El) -> Result<designcraft_doc::cjk::MojikumiAki> {
|
||||
let number = |key: &str, required: bool| -> Result<f64> {
|
||||
let number = |key: &str| -> Result<f64> {
|
||||
let Some(raw) = e.prop(key) else {
|
||||
return if required { Err(IdmlError::Invalid(format!("Mojikumi row is missing {key}"))) } else { Ok(0.0) };
|
||||
return Ok(0.0);
|
||||
};
|
||||
raw.trim().parse::<f64>().ok().filter(|v| v.is_finite()).ok_or_else(|| IdmlError::Invalid(format!("Mojikumi row has invalid {key}")))
|
||||
};
|
||||
let integer = |key: &str, required: bool| -> Result<i16> {
|
||||
let v = number(key, required)?;
|
||||
let integer = |key: &str| -> Result<i16> {
|
||||
let v = number(key)?;
|
||||
if v.fract() != 0.0 || v < f64::from(i16::MIN) || v > f64::from(i16::MAX) {
|
||||
return Err(IdmlError::Invalid(format!("Mojikumi {key} must be an in-range integer")));
|
||||
}
|
||||
@@ -165,18 +165,15 @@ fn mojikumi_row(e: &El) -> Result<designcraft_doc::cjk::MojikumiAki> {
|
||||
}
|
||||
};
|
||||
let row = designcraft_doc::cjk::MojikumiAki {
|
||||
target_class: integer("TargetMojikumiClass", true)?,
|
||||
side_class: integer("SideMojikumiClass", true)?,
|
||||
target_class: integer("TargetMojikumiClass")?,
|
||||
side_class: integer("SideMojikumiClass")?,
|
||||
after: boolean("SideIsAfterTarget")?,
|
||||
minimum: number("Minimum", false)?,
|
||||
desired: number("Desired", false)?,
|
||||
maximum: number("Maximum", false)?,
|
||||
priority: integer("CompressionPriority", false)?,
|
||||
minimum: number("Minimum")?,
|
||||
desired: number("Desired")?,
|
||||
maximum: number("Maximum")?,
|
||||
priority: integer("CompressionPriority")?,
|
||||
does_not_float: boolean("AkiDoesNotFloat")?,
|
||||
};
|
||||
if !designcraft_doc::mojikumi::valid_range(&row) {
|
||||
return Err(IdmlError::Invalid("Mojikumi row has an invalid spacing range or priority".into()));
|
||||
}
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
|
||||
@@ -947,6 +947,7 @@ fn malformed_mojikumi_rows_are_not_silently_coerced() {
|
||||
("Desired", "oops"),
|
||||
("Minimum", "0.4"),
|
||||
("Maximum", "0.1"),
|
||||
("Maximum", "101"),
|
||||
("CompressionPriority", "10"),
|
||||
("AkiDoesNotFloat", "maybe"),
|
||||
] {
|
||||
@@ -964,7 +965,17 @@ fn malformed_mojikumi_rows_are_not_silently_coerced() {
|
||||
("Spreads/Spread_sp1.xml", SPREAD),
|
||||
("Stories/Story_s1.xml", STORY),
|
||||
]);
|
||||
let err = import_idml(&bytes).unwrap_err().to_string();
|
||||
assert!(err.contains("Mojikumi"), "{key}={value}: {err}");
|
||||
if matches!((key, value), ("Minimum", "0.4") | ("Maximum", "0.1" | "101") | ("CompressionPriority", "10")) {
|
||||
// Finite, representable rules survive import even if composition
|
||||
// cannot apply them. The resolver supplies a diagnostic instead.
|
||||
let document = import_idml(&bytes).unwrap();
|
||||
document.check().unwrap();
|
||||
assert!(designcraft_doc::mojikumi::Rules::resolve(&document.styles, "MojikumiTable/Bad").is_err());
|
||||
let restored = import_idml(&export_idml(&document)).unwrap();
|
||||
assert_eq!(restored.styles.mojikumi_tables, document.styles.mojikumi_tables);
|
||||
} else {
|
||||
let err = import_idml(&bytes).unwrap_err().to_string();
|
||||
assert!(err.contains("Mojikumi"), "{key}={value}: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-29
@@ -1,33 +1,18 @@
|
||||
# Mojikumi implementation and public references
|
||||
# Mojikumi repair scope
|
||||
|
||||
This is a clean-room implementation of public spacing behavior. Mojikumi is an
|
||||
Adobe feature, not a single conformance standard. W3C JLReq and CLReq describe
|
||||
Japanese and Chinese layout requirements; they do not specify Adobe's private
|
||||
composer or its complete preset pair matrices.
|
||||
The retained changes address reproducible implementation defects: equal-priority
|
||||
spacing allocation, discrete compression endpoints, leading-only compression,
|
||||
spacing after complete shaped clusters, and tracking that made narrow punctuation
|
||||
look full-width. Preflight also checks nested note and table-cell paragraph rules.
|
||||
|
||||
References:
|
||||
IDML import rejects malformed/non-finite numbers and integer coercion. Finite,
|
||||
representable rules outside the composer's supported range remain preserved;
|
||||
the rule resolver reports unsupported settings instead of rejecting the document.
|
||||
|
||||
- [Adobe preset enumeration](https://developer.adobe.com/indesign/uxp/dom/api/m/mojikumi-table-defaults/)
|
||||
- [Adobe Mojikumi guide, sections 3–5](https://wwwimages.adobe.com/www.adobe.com/jp/joc/design/guides/pdf/mojikumi.pdf)
|
||||
- [Adobe spacing priorities](https://helpx.adobe.com/indesign/desktop/language-and-proofing/chinese-japanese-and-korean/set-spacing-priorities-in-mojikumi-character-classes.html)
|
||||
- [W3C JLReq](https://www.w3.org/TR/jlreq/), especially character classes and line adjustment
|
||||
- [W3C CLReq](https://www.w3.org/TR/clreq/), especially regional punctuation placement
|
||||
The broad preset expansion, regional side-bearing changes, Tsume combination
|
||||
policy, and CID preference/diagnostic extension were withdrawn after review.
|
||||
The original supported preset set and Unicode classification remain in place.
|
||||
No claim of full Adobe Mojikumi conformance or exact output parity is made.
|
||||
|
||||
## Presets and interchange
|
||||
|
||||
The resolver recognizes the 14 Japanese presets and two Chinese presets from the
|
||||
public enumeration, including their IDML resource aliases. Paragraph indentation,
|
||||
opening-bracket indentation, ordinary line starts, full/half/discrete line ends,
|
||||
and period-only line-end policies are represented separately. Custom IDML rows
|
||||
replace the corresponding directional pair. Adjacent opening and closing marks
|
||||
must not regain two full blank half-bodies.
|
||||
|
||||
Regression tests cover preset boundary differences, custom override precedence,
|
||||
IDML reference round trips, and unsupported names. The implementation must not be
|
||||
represented as a byte-for-byte reproduction of Adobe's full preset matrices.
|
||||
|
||||
## Audit work
|
||||
|
||||
Further audit areas are regional punctuation metrics, shaped cluster boundaries,
|
||||
equal-priority allocation, discrete line fitting, input validation, and diagnostics
|
||||
for compatibility modes whose glyph classification cannot yet be reproduced.
|
||||
Reference for priority semantics:
|
||||
[Adobe spacing priorities](https://helpx.adobe.com/indesign/desktop/language-and-proofing/chinese-japanese-and-korean/set-spacing-priorities-in-mojikumi-character-classes.html).
|
||||
|
||||
Reference in New Issue
Block a user