From be9b8f360e4c3fef273ef9de65437ce2bcff0259 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 28 Oct 2026 11:39:26 +0000 Subject: [PATCH] Refactor idml for strict Clippy checks --- crates/idml/src/export/common.rs | 2 +- crates/idml/src/export/designmap.rs | 2 +- crates/idml/src/export/fonts_prefs.rs | 2 +- crates/idml/src/export/graphics.rs | 2 +- crates/idml/src/export/item.rs | 105 ++--- crates/idml/src/export/spread.rs | 2 +- crates/idml/src/export/story.rs | 584 +++++++++++++------------ crates/idml/src/export/styles.rs | 263 +++++------ crates/idml/src/export/table.rs | 2 +- crates/idml/src/export/text_attrs.rs | 456 ++++++++++--------- crates/idml/src/import.rs | 15 +- crates/idml/src/import/document.rs | 2 +- crates/idml/src/import/frame.rs | 2 +- crates/idml/src/import/item.rs | 28 +- crates/idml/src/import/object_style.rs | 2 +- crates/idml/src/import/package.rs | 2 +- crates/idml/src/import/spread.rs | 4 +- crates/idml/src/import/story.rs | 5 +- crates/idml/src/import/styles.rs | 5 +- crates/idml/src/import/swatches.rs | 5 +- crates/idml/src/import/table.rs | 2 +- crates/idml/src/import/text_attrs.rs | 133 +++--- crates/idml/src/import/values.rs | 2 +- crates/idml/src/tests.rs | 84 +++- 24 files changed, 953 insertions(+), 758 deletions(-) diff --git a/crates/idml/src/export/common.rs b/crates/idml/src/export/common.rs index 5c9470fc2..4e2d42e2c 100644 --- a/crates/idml/src/export/common.rs +++ b/crates/idml/src/export/common.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Affine, Color, DOM_VERSION, El, Node, PKG_NS, escape_id, num}; pub(crate) fn uid(n: u64) -> String { format!("u{n:x}") diff --git a/crates/idml/src/export/designmap.rs b/crates/idml/src/export/designmap.rs index adce1cbd0..1c1c720b1 100644 --- a/crates/idml/src/export/designmap.rs +++ b/crates/idml/src/export/designmap.rs @@ -1,5 +1,5 @@ use super::common::{anchor_self, bool_s, p, uid, with_props, xref_format_self}; -use super::*; +use super::{CHAR_BUILTINS, DOM_VERSION, Document, El, Ex, HashSet, PARA_BUILTINS, PKG_NS, document_with_pi, escape_id, names, num}; impl<'a> Ex<'a> { // ---------- designmap ---------- diff --git a/crates/idml/src/export/fonts_prefs.rs b/crates/idml/src/export/fonts_prefs.rs index 8bc435f16..fac86af33 100644 --- a/crates/idml/src/export/fonts_prefs.rs +++ b/crates/idml/src/export/fonts_prefs.rs @@ -1,6 +1,6 @@ use super::common::{bool_s, p, pct, pkg_root}; use super::spread::margin_el; -use super::*; +use super::{El, Ex, PARA_BUILTINS, document, escape_id, names, num, pt, st}; impl<'a> Ex<'a> { // ---------- Fonts.xml ---------- diff --git a/crates/idml/src/export/graphics.rs b/crates/idml/src/export/graphics.rs index 91949307b..3730166a6 100644 --- a/crates/idml/src/export/graphics.rs +++ b/crates/idml/src/export/graphics.rs @@ -1,5 +1,5 @@ use super::common::{bool_s, color_value, pct, pkg_root}; -use super::*; +use super::{Color, ColorType, El, Ex, GradientKind, SwatchValue, document, escape_id, num, swatch}; // Retained IDML attributes have unprefixed ASCII names. Native JSON can be // edited independently: never let its map keys become XML syntax or Self IDs. diff --git a/crates/idml/src/export/item.rs b/crates/idml/src/export/item.rs index 46b94fd65..5ed93879b 100644 --- a/crates/idml/src/export/item.rs +++ b/crates/idml/src/export/item.rs @@ -1,5 +1,8 @@ use super::common::{affine_s, bool_s, p, pct, uid, with_props}; -use super::*; +use super::{ + Affine, ColumnsKind, Content, El, Ex, HashSet, Item, Node, OBJECT_BUILTINS, PathData, Shape, TextFrameOptions, base64_encode, escape_id, names, + num, pt, +}; impl<'a> Ex<'a> { pub(super) fn item_el(&mut self, it: &Item, xf: Affine) -> El { @@ -99,54 +102,7 @@ impl<'a> Ex<'a> { props.push(path_geometry(&it.path)); } let mut el = with_props(el, props); - // Transparency. - // An item whose style sets effects writes the effects it has turned off too, so that on - // import it doesn't take the style's. - let styled = self.d.styles.object_effects(os).is_some(); - let ds = &it.effects.drop_shadow; - let (gf, df) = (&it.effects.gradient_feather, &it.effects.directional_feather); - if it.opacity < 1.0 || it.blend != designcraft_color::BlendMode::Normal || ds.on || it.effects.feather > 0.0 || gf.on || df.on || styled { - let mut t = El::new("TransparencySetting"); - t.push(El::new("BlendingSetting").attr("BlendMode", names::blend_out(it.blend)).attr("Opacity", pct(it.opacity as f64))); - for c in self.effect_els(&it.effects, styled) { - t.push(c); - } - if df.on { - let [top, left, bottom, right] = df.widths; - t.push( - El::new("DirectionalFeatherSetting") - .attr("Applied", "true") - .attr("LeftWidth", num(left)) - .attr("RightWidth", num(right)) - .attr("TopWidth", num(top)) - .attr("BottomWidth", num(bottom)), - ); - } - if gf.on { - let mut g = El::new("GradientFeatherSetting").attr("Applied", "true").attr("Type", if gf.radial { "Radial" } else { "Linear" }); - if let Some([x0, y0, x1, y1]) = gf.vector { - // Gradient Feather tool vector: start, length and angle in the item's space. - g.set("Angle", num((-(y1 - y0)).atan2(x1 - x0).to_degrees())); - g.set("Length", num((x1 - x0).hypot(y1 - y0))); - g.set("GradientStart", format!("{} {}", num(x0), num(y0))); - } else { - g.set("Angle", num(gf.angle)); - } - let id = uid(it.id.0); - for (i, (opacity, location)) in [(gf.start, 0.0), (gf.end, 100.0)].into_iter().enumerate() { - let mut s = El::new("OpacityGradientStop") - .attr("Self", format!("{id}TransparencySetting1GradientFeatherSetting1OpacityGradientStop{i}")) - .attr("Opacity", pct(opacity as f64)) - .attr("Location", num(location)); - if i > 0 { - s.set("Midpoint", "50"); - } - g.push(s); - } - t.push(g); - } - el.push(t); - } + self.append_transparency(&mut el, it, os); if let Content::Text(t) = &it.content { el.push(text_frame_pref(&t.options, &self.sw(&t.options.column_rule_color))); if let Some(g) = t.options.baseline_grid { @@ -198,6 +154,57 @@ impl<'a> Ex<'a> { el } + fn append_transparency(&self, el: &mut El, it: &Item, os: &str) { + // Transparency. + // An item whose style sets effects writes the effects it has turned off too, so that on + // import it doesn't take the style's. + let styled = self.d.styles.object_effects(os).is_some(); + let ds = &it.effects.drop_shadow; + let (gf, df) = (&it.effects.gradient_feather, &it.effects.directional_feather); + if it.opacity < 1.0 || it.blend != designcraft_color::BlendMode::Normal || ds.on || it.effects.feather > 0.0 || gf.on || df.on || styled { + let mut t = El::new("TransparencySetting"); + t.push(El::new("BlendingSetting").attr("BlendMode", names::blend_out(it.blend)).attr("Opacity", pct(it.opacity as f64))); + for c in self.effect_els(&it.effects, styled) { + t.push(c); + } + if df.on { + let [top, left, bottom, right] = df.widths; + t.push( + El::new("DirectionalFeatherSetting") + .attr("Applied", "true") + .attr("LeftWidth", num(left)) + .attr("RightWidth", num(right)) + .attr("TopWidth", num(top)) + .attr("BottomWidth", num(bottom)), + ); + } + if gf.on { + let mut g = El::new("GradientFeatherSetting").attr("Applied", "true").attr("Type", if gf.radial { "Radial" } else { "Linear" }); + if let Some([x0, y0, x1, y1]) = gf.vector { + // Gradient Feather tool vector: start, length and angle in the item's space. + g.set("Angle", num((-(y1 - y0)).atan2(x1 - x0).to_degrees())); + g.set("Length", num((x1 - x0).hypot(y1 - y0))); + g.set("GradientStart", format!("{} {}", num(x0), num(y0))); + } else { + g.set("Angle", num(gf.angle)); + } + let id = uid(it.id.0); + for (i, (opacity, location)) in [(gf.start, 0.0), (gf.end, 100.0)].into_iter().enumerate() { + let mut s = El::new("OpacityGradientStop") + .attr("Self", format!("{id}TransparencySetting1GradientFeatherSetting1OpacityGradientStop{i}")) + .attr("Opacity", pct(opacity as f64)) + .attr("Location", num(location)); + if i > 0 { + s.set("Midpoint", "50"); + } + g.push(s); + } + t.push(g); + } + el.push(t); + } + } + /// The `TransparencySetting` children for `e`'s drop shadow and feather; `all` also writes the /// effects that are off (`Mode="None"`), as an object style that sets effects does. pub(super) fn effect_els(&self, e: &designcraft_doc::Effects, all: bool) -> Vec { diff --git a/crates/idml/src/export/spread.rs b/crates/idml/src/export/spread.rs index 952439ad7..0610980d1 100644 --- a/crates/idml/src/export/spread.rs +++ b/crates/idml/src/export/spread.rs @@ -1,5 +1,5 @@ use super::common::{bool_s, uid}; -use super::*; +use super::{Affine, Document, El, Ex, PageSide, Spread, names, num}; /// The IDML spread origin in our spread coordinates. pub(crate) fn spread_origin(sp: &Spread, facing: bool) -> (f64, f64) { diff --git a/crates/idml/src/export/story.rs b/crates/idml/src/export/story.rs index 61555fcc8..97c2837ec 100644 --- a/crates/idml/src/export/story.rs +++ b/crates/idml/src/export/story.rs @@ -1,6 +1,9 @@ use super::common::{anchor_self, bool_s, change_type, topic_self, uid, with_props, xref_format_self}; use super::graphics::graphic_layer_attribute_name; -use super::*; +use super::{ + BTreeMap, CHAR_BUILTINS, CharFormat, El, Ex, ListType, NO_CHAR_ID, Node, PARA_BUILTINS, ParaAttrs, ParaFormat, Story, TOC_STYLE_ID, names, num, + st, +}; impl<'a> Ex<'a> { /// `` of a placed graphic: its layer attributes and the nested layer tree. @@ -189,295 +192,318 @@ impl<'a> Ex<'a> { el } - /// `ParagraphStyleRange`s of a story (or a table cell's story). - pub(super) fn story_paras(&mut self, s: &Story) -> Vec { - let mut paras = Vec::new(); - let ranges = s.para_ranges(); - let n = ranges.len(); - for (pi, r) in ranges.into_iter().enumerate() { - let pf = s.paras.get(pi).cloned().unwrap_or_default(); - let mut psr = self.psr_el(&pf); - let last = pi + 1 == n; - let ends_with_break = s.text.get(r.clone()).and_then(|t| t.chars().next_back()).is_some_and(st::is_break_char); - // Character runs intersecting the paragraph. - let mut segs: Vec<(std::ops::Range, CharFormat)> = Vec::new(); - for (rr, f) in s.runs() { - let a = rr.start.max(r.start); - let b = rr.end.min(r.end); - if a < b { - segs.push((a..b, f.clone())); - } + /// Write an index reference and record any cross-reference it declares. + fn index_reference(&mut self, s: &Story, k: usize) -> Option { + if let Some(r) = s.index_refs.get(k).filter(|r| !r.topics.is_empty()) { + use designcraft_doc::index::IndexRange as R; + for n in 1..=r.topics.len() { + // A shared topic has one key: keep the first reference's + // value (even empty), rather than inventing a merged key. + self.index_topics.entry(r.topics[..n].to_vec()).or_insert_with(|| r.sort.get(n - 1).cloned().unwrap_or_default()); } - if segs.is_empty() { - let f = s.char_format_at(r.start).clone(); - segs.push((r.start..r.start, f)); + let (kind, limit) = match &r.range { + R::CurrentPage => (Some("CurrentPage"), None), + R::ToEndOfStory => (Some("ToEndOfStory"), None), + R::NextParagraphs(n) => (Some("ForNextNParagraphs"), Some(*n)), + R::SuppressPageRange => (Some("SuppressPageNumbers"), None), + R::See(t) => { + self.index_xrefs.push((r.topics.clone(), "See", t.clone())); + (None, None) + } + R::SeeAlso(t) => { + self.index_xrefs.push((r.topics.clone(), "SeeAlso", t.clone())); + (Some("CurrentPage"), None) + } + }; + if let Some(kind) = kind { + let mut el = El::new("PageReference") + .attr("Self", self.fresh()) + .attr("PageReferenceType", kind) + .attr("ReferencedTopic", topic_self(&r.topics)) + .attr("Id", k + 1); + if let Some(n) = limit { + el.set("PageReferenceLimit", n); + } + return Some(el); } - let nseg = segs.len(); - for (k, (rr, f)) in segs.into_iter().enumerate() { - let seg_start = rr.start; - let text = &s.text[rr]; - // Split at break characters. - let mut cur = String::new(); - let mut pending: Vec = Vec::new(); - let mut csrs: Vec = Vec::new(); - let flush_text = |cur: &mut String, pending: &mut Vec| { - if !cur.is_empty() { - pending.push(Node::Text(std::mem::take(cur))); - } - }; - let flush_content = |pending: &mut Vec, out: &mut Vec| { - if !pending.is_empty() { - let mut c = El::new("Content"); - c.children = std::mem::take(pending); - out.push(Node::El(c)); - } - }; - let mut out: Vec = Vec::new(); - for (ci, ch) in text.char_indices() { - let ace = match ch { - st::PAGE_NUMBER | st::NEXT_PAGE_NUMBER | st::PREV_PAGE_NUMBER => Some("18"), - st::SECTION_MARKER => Some("19"), - st::INDENT_HERE => Some("7"), - st::RIGHT_INDENT_TAB => Some("8"), - _ => None, - }; - let brk = match ch { - st::COLUMN_BREAK => Some("NextColumn"), - st::FRAME_BREAK => Some("NextFrame"), - st::PAGE_BREAK => Some("NextPage"), - st::ODD_PAGE_BREAK => Some("NextOddPage"), - st::EVEN_PAGE_BREAK => Some("NextEvenPage"), - _ => None, - }; - if let Some(code) = ace { - flush_text(&mut cur, &mut pending); - pending.push(Node::Pi("ACE".into(), code.into())); - } else if ch == st::TABLE_ANCHOR { - flush_text(&mut cur, &mut pending); - flush_content(&mut pending, &mut out); - if let Some(t) = s.para_table(pi) { - out.push(Node::El(self.table_el(t))); - } - } else if ch == designcraft_doc::ANCHOR_MARK { - flush_text(&mut cur, &mut pending); - flush_content(&mut pending, &mut out); - let k = s.text[..seg_start + ci].matches(designcraft_doc::ANCHOR_MARK).count(); - if let Some(a) = s.anchors.get(k) { - out.push(Node::El( - El::new("HyperlinkTextDestination") - .attr("Self", anchor_self(a.id)) - .attr("Name", &a.name) - .attr("Hidden", "false") - .attr("DestinationUniqueKey", a.id & 0xFFFF_FFFF), - )); - } - } else if ch == designcraft_doc::OBJECT_MARK { - // An anchored object: the item inside the range with its anchoring settings. - flush_text(&mut cur, &mut pending); - flush_content(&mut pending, &mut out); - let k = s.text[..seg_start + ci].matches(designcraft_doc::OBJECT_MARK).count(); - if let Some(o) = s.objects.get(k) { - let mut e = self.item_el(&o.item, o.item.xf); - use designcraft_doc::anchored::AnchorAlign as A; - let setting = match &o.position { - designcraft_doc::AnchorPosition::Inline { y_offset } => { - El::new("AnchoredObjectSetting").attr("AnchoredPosition", "InlinePosition").attr("AnchorYoffset", num(*y_offset)) - } - designcraft_doc::AnchorPosition::AboveLine { align, space_before, space_after } => El::new("AnchoredObjectSetting") - .attr("AnchoredPosition", "AboveLine") - .attr( - "HorizontalAlignment", - match align { - A::Left => "LeftAlign", - A::Center => "CenterAlign", - A::Right => "RightAlign", - }, - ) - .attr("AnchorSpaceAbove", num(*space_before)) - .attr("AnchorYoffset", num(*space_after)), - designcraft_doc::AnchorPosition::Custom { - x_relative, - y_relative, - x_offset, - y_offset, - object_point, - ref_point, - keep_within_column, - spine_relative, - } => El::new("AnchoredObjectSetting") - .attr("AnchoredPosition", "Anchored") - .attr("AnchorPoint", names::anchor_out(*object_point)) - .attr("HorizontalReferencePoint", names::anchor_rel_out(*x_relative, false)) - .attr("VerticalReferencePoint", names::anchor_rel_out(*y_relative, true)) - .attr("HorizontalAlignment", ["LeftAlign", "CenterAlign", "RightAlign"][(*ref_point % 3) as usize]) - .attr("VerticalAlignment", ["TopAlign", "CenterAlign", "BottomAlign"][(*ref_point / 3).min(2) as usize]) - // IDML measures a left-aligned object's X offset leftwards. - .attr("AnchorXoffset", num(if *ref_point % 3 == 0 { 0.0 - *x_offset } else { *x_offset })) - .attr("AnchorYoffset", num(*y_offset)) - .attr("PinPosition", bool_s(*keep_within_column)) - .attr("SpineRelative", bool_s(*spine_relative)), - }; - e.push(setting); - out.push(Node::El(e)); - } - } else if ch == designcraft_doc::INDEX_MARK { - flush_text(&mut cur, &mut pending); - flush_content(&mut pending, &mut out); - let k = s.text[..seg_start + ci].matches(designcraft_doc::INDEX_MARK).count(); - if let Some(r) = s.index_refs.get(k).filter(|r| !r.topics.is_empty()) { - use designcraft_doc::index::IndexRange as R; - for n in 1..=r.topics.len() { - // A shared topic has one key: keep the first reference's - // value (even empty), rather than inventing a merged key. - self.index_topics.entry(r.topics[..n].to_vec()).or_insert_with(|| r.sort.get(n - 1).cloned().unwrap_or_default()); - } - let (kind, limit) = match &r.range { - R::CurrentPage => (Some("CurrentPage"), None), - R::ToEndOfStory => (Some("ToEndOfStory"), None), - R::NextParagraphs(n) => (Some("ForNextNParagraphs"), Some(*n)), - R::SuppressPageRange => (Some("SuppressPageNumbers"), None), - R::See(t) => { - self.index_xrefs.push((r.topics.clone(), "See", t.clone())); - (None, None) - } - R::SeeAlso(t) => { - self.index_xrefs.push((r.topics.clone(), "SeeAlso", t.clone())); - (Some("CurrentPage"), None) - } - }; - if let Some(kind) = kind { - let mut el = El::new("PageReference") - .attr("Self", self.fresh()) - .attr("PageReferenceType", kind) - .attr("ReferencedTopic", topic_self(&r.topics)) - .attr("Id", k + 1); - if let Some(n) = limit { - el.set("PageReferenceLimit", n); - } - out.push(Node::El(el)); - } - } - } else if ch == designcraft_doc::XREF_MARK { - // A source wrapping its current text (InDesign regenerates it on update). - flush_text(&mut cur, &mut pending); - flush_content(&mut pending, &mut out); - if !out.is_empty() { - let mut c = self.csr_el(&f); - c.children.append(&mut out); - csrs.push(c); - } - let k = s.text[..seg_start + ci].matches(designcraft_doc::XREF_MARK).count(); - if let Some(x) = s.xrefs.get(k) { - let me = self.fresh(); - let fmt = self.d.xref_formats.iter().position(|ff| ff.name == x.format).map_or("n".to_string(), xref_format_self); - let text = self.d.xref_values(x.target).map_or("??".into(), |mut v| { - v.page = "?".into(); - designcraft_doc::xref::expand(self.d.xref_format(&x.format).map_or("", |ff| ff.definition.as_str()), &v) - }); - let mut c = self.csr_el(&f); - let mut content = El::new("Content"); - content.children.push(Node::Text(text)); - c.push(content); - let mut src = El::new("CrossReferenceSource") - .attr("Self", &me) - .attr("AppliedFormat", fmt) - .attr("Name", format!("Cross-Reference {}", self.xref_sources.len() + 1)) - .attr("Hidden", "false"); - src.push(c); - csrs.push(src); - self.xref_sources.push((me, x.target)); - } - } else if let Some(vi) = designcraft_doc::vars::var_index(ch) { - flush_text(&mut cur, &mut pending); - flush_content(&mut pending, &mut out); - if let Some(v) = self.d.text_variables.get(vi) { - let result = self.d.variable_value(vi, None).unwrap_or_else(|| format!("{}{}", v.before, v.after)); - out.push(Node::El( - El::new("TextVariableInstance") - .attr("Self", self.fresh()) - .attr("Name", &v.name) - .attr("ResultText", result) - .attr("AssociatedTextVariable", format!("dTextVariablen{}", v.name)), - )); - } - } else if ch == designcraft_doc::NOTE_MARK { - // Editorial notes aren't written to IDML yet. - } else if ch == designcraft_doc::ENDNOTE_REF { - // Endnotes aren't written to IDML yet: the endnote frame keeps their text - // (numbers included) and the references are left out. - } else if ch == designcraft_doc::FOOTNOTE_REF { - // The reference: its own range carrying the reference position. - flush_text(&mut cur, &mut pending); - flush_content(&mut pending, &mut out); - if !out.is_empty() { - let mut c = self.csr_el(&f); - c.children.append(&mut out); - csrs.push(c); - } - if let Some(n) = s.notes.get(s.notes_before(seg_start + ci)) { - let mut c = self.csr_el(&f); - let pos = self.d.footnote_options.ref_position; - if pos != designcraft_doc::Position::Normal { - c.set("Position", names::position_out(pos)); - } - c.push(self.footnote_el(&n.text)); - csrs.push(c); - } - } else if let Some(bt) = brk { - flush_text(&mut cur, &mut pending); - flush_content(&mut pending, &mut out); - if !out.is_empty() { - let mut c = self.csr_el(&f); - c.children.append(&mut out); - csrs.push(c); - } - let mut c = self.csr_el(&f); - c.set("ParagraphBreakType", bt); - c.push(El::new("Br")); - csrs.push(c); - } else { - cur.push(ch); - } + } + None + } + + fn anchored_object(&mut self, s: &Story, k: usize) -> Option { + if let Some(o) = s.objects.get(k) { + let mut e = self.item_el(&o.item, o.item.xf); + use designcraft_doc::anchored::AnchorAlign as A; + let setting = match &o.position { + designcraft_doc::AnchorPosition::Inline { y_offset } => { + El::new("AnchoredObjectSetting").attr("AnchoredPosition", "InlinePosition").attr("AnchorYoffset", num(*y_offset)) } - flush_text(&mut cur, &mut pending); - flush_content(&mut pending, &mut out); - let is_last_seg = k + 1 == nseg; - // A break character that ends the paragraph was written as its `Br`. - let para_br = is_last_seg && !last && !ends_with_break; - if !out.is_empty() || csrs.is_empty() || para_br { - let mut c = self.csr_el(&f); - c.children.extend(out); - csrs.push(c); - } - // Tracked changes wrap the text they mark. - if let Some(kind) = change_type(f.over.change) { - for c in csrs.iter_mut().filter(|c| c.name == "CharacterStyleRange" && !c.children.is_empty()) { + designcraft_doc::AnchorPosition::AboveLine { align, space_before, space_after } => El::new("AnchoredObjectSetting") + .attr("AnchoredPosition", "AboveLine") + .attr( + "HorizontalAlignment", + match align { + A::Left => "LeftAlign", + A::Center => "CenterAlign", + A::Right => "RightAlign", + }, + ) + .attr("AnchorSpaceAbove", num(*space_before)) + .attr("AnchorYoffset", num(*space_after)), + designcraft_doc::AnchorPosition::Custom { + x_relative, + y_relative, + x_offset, + y_offset, + object_point, + ref_point, + keep_within_column, + spine_relative, + } => El::new("AnchoredObjectSetting") + .attr("AnchoredPosition", "Anchored") + .attr("AnchorPoint", names::anchor_out(*object_point)) + .attr("HorizontalReferencePoint", names::anchor_rel_out(*x_relative, false)) + .attr("VerticalReferencePoint", names::anchor_rel_out(*y_relative, true)) + .attr("HorizontalAlignment", ["LeftAlign", "CenterAlign", "RightAlign"][(*ref_point % 3) as usize]) + .attr("VerticalAlignment", ["TopAlign", "CenterAlign", "BottomAlign"][(*ref_point / 3).min(2) as usize]) + // IDML measures a left-aligned object's X offset leftwards. + .attr("AnchorXoffset", num(if *ref_point % 3 == 0 { 0.0 - *x_offset } else { *x_offset })) + .attr("AnchorYoffset", num(*y_offset)) + .attr("PinPosition", bool_s(*keep_within_column)) + .attr("SpineRelative", bool_s(*spine_relative)), + }; + e.push(setting); + return Some(e); + } + None + } + + fn apply_tracked_changes(&mut self, s: &Story, f: &CharFormat, csrs: &mut [El], para_br: bool, para_end: usize) { + // Tracked changes wrap the text they mark. + if let Some(kind) = change_type(f.over.change) { + for c in csrs.iter_mut().filter(|c| c.name == "CharacterStyleRange" && !c.children.is_empty()) { + let mut ch = El::new("Change").attr("Self", self.fresh()).attr("ChangeType", kind); + ch.children = std::mem::take(&mut c.children); + c.push(ch); + } + } + if para_br && let Some(c) = csrs.last_mut() { + let nl = s.runs().find(|(rr, _)| rr.contains(¶_end)).and_then(|(_, nf)| change_type(nf.over.change)); + match nl { + None => c.push(El::new("Br")), + Some(kind) => match c.children.last_mut() { + Some(Node::El(ch)) if ch.name == "Change" && ch.get("ChangeType") == Some(kind) => ch.push(El::new("Br")), + _ => { let mut ch = El::new("Change").attr("Self", self.fresh()).attr("ChangeType", kind); - ch.children = std::mem::take(&mut c.children); + ch.push(El::new("Br")); c.push(ch); } + }, + } + } + } + + /// `ParagraphStyleRange`s of a story (or a table cell's story). + pub(super) fn story_paras(&mut self, s: &Story) -> Vec { + let ranges = s.para_ranges(); + let n = ranges.len(); + ranges.into_iter().enumerate().map(|(pi, r)| self.story_para(s, pi, r, pi + 1 == n)).collect() + } + + fn paragraph_runs(s: &Story, r: &std::ops::Range) -> Vec<(std::ops::Range, CharFormat)> { + // Character runs intersecting the paragraph. + let mut segs: Vec<(std::ops::Range, CharFormat)> = Vec::new(); + for (rr, f) in s.runs() { + let a = rr.start.max(r.start); + let b = rr.end.min(r.end); + if a < b { + segs.push((a..b, f.clone())); + } + } + if segs.is_empty() { + let f = s.char_format_at(r.start).clone(); + segs.push((r.start..r.start, f)); + } + segs + } + + fn story_para(&mut self, s: &Story, pi: usize, r: std::ops::Range, last: bool) -> El { + let pf = s.paras.get(pi).cloned().unwrap_or_default(); + let mut psr = self.psr_el(&pf); + let ends_with_break = s.text.get(r.clone()).and_then(|t| t.chars().next_back()).is_some_and(st::is_break_char); + let segs = Self::paragraph_runs(s, &r); + let nseg = segs.len(); + for (k, (rr, f)) in segs.into_iter().enumerate() { + let seg_start = rr.start; + let text = &s.text[rr]; + // Split at break characters. + let mut cur = String::new(); + let mut pending: Vec = Vec::new(); + let mut csrs: Vec = Vec::new(); + let flush_text = |cur: &mut String, pending: &mut Vec| { + if !cur.is_empty() { + pending.push(Node::Text(std::mem::take(cur))); } - if para_br && let Some(c) = csrs.last_mut() { - let nl = s.runs().find(|(rr, _)| rr.contains(&r.end)).and_then(|(_, nf)| change_type(nf.over.change)); - match nl { - None => c.push(El::new("Br")), - Some(kind) => match c.children.last_mut() { - Some(Node::El(ch)) if ch.name == "Change" && ch.get("ChangeType") == Some(kind) => ch.push(El::new("Br")), - _ => { - let mut ch = El::new("Change").attr("Self", self.fresh()).attr("ChangeType", kind); - ch.push(El::new("Br")); - c.push(ch); - } - }, + }; + let flush_content = |pending: &mut Vec, out: &mut Vec| { + if !pending.is_empty() { + let mut c = El::new("Content"); + c.children = std::mem::take(pending); + out.push(Node::El(c)); + } + }; + let mut out: Vec = Vec::new(); + for (ci, ch) in text.char_indices() { + let ace = match ch { + st::PAGE_NUMBER | st::NEXT_PAGE_NUMBER | st::PREV_PAGE_NUMBER => Some("18"), + st::SECTION_MARKER => Some("19"), + st::INDENT_HERE => Some("7"), + st::RIGHT_INDENT_TAB => Some("8"), + _ => None, + }; + let brk = match ch { + st::COLUMN_BREAK => Some("NextColumn"), + st::FRAME_BREAK => Some("NextFrame"), + st::PAGE_BREAK => Some("NextPage"), + st::ODD_PAGE_BREAK => Some("NextOddPage"), + st::EVEN_PAGE_BREAK => Some("NextEvenPage"), + _ => None, + }; + if let Some(code) = ace { + flush_text(&mut cur, &mut pending); + pending.push(Node::Pi("ACE".into(), code.into())); + } else if ch == st::TABLE_ANCHOR { + flush_text(&mut cur, &mut pending); + flush_content(&mut pending, &mut out); + if let Some(t) = s.para_table(pi) { + out.push(Node::El(self.table_el(t))); } - } - for c in csrs { - psr.push(c); + } else if ch == designcraft_doc::ANCHOR_MARK { + flush_text(&mut cur, &mut pending); + flush_content(&mut pending, &mut out); + let k = s.text[..seg_start + ci].matches(designcraft_doc::ANCHOR_MARK).count(); + if let Some(a) = s.anchors.get(k) { + out.push(Node::El( + El::new("HyperlinkTextDestination") + .attr("Self", anchor_self(a.id)) + .attr("Name", &a.name) + .attr("Hidden", "false") + .attr("DestinationUniqueKey", a.id & 0xFFFF_FFFF), + )); + } + } else if ch == designcraft_doc::OBJECT_MARK { + // An anchored object: the item inside the range with its anchoring settings. + flush_text(&mut cur, &mut pending); + flush_content(&mut pending, &mut out); + let k = s.text[..seg_start + ci].matches(designcraft_doc::OBJECT_MARK).count(); + if let Some(el) = self.anchored_object(s, k) { + out.push(Node::El(el)); + } + } else if ch == designcraft_doc::INDEX_MARK { + flush_text(&mut cur, &mut pending); + flush_content(&mut pending, &mut out); + let k = s.text[..seg_start + ci].matches(designcraft_doc::INDEX_MARK).count(); + if let Some(el) = self.index_reference(s, k) { + out.push(Node::El(el)); + } + } else if ch == designcraft_doc::XREF_MARK { + // A source wrapping its current text (InDesign regenerates it on update). + flush_text(&mut cur, &mut pending); + flush_content(&mut pending, &mut out); + if !out.is_empty() { + let mut c = self.csr_el(&f); + c.children.append(&mut out); + csrs.push(c); + } + let k = s.text[..seg_start + ci].matches(designcraft_doc::XREF_MARK).count(); + if let Some(x) = s.xrefs.get(k) { + let me = self.fresh(); + let fmt = self.d.xref_formats.iter().position(|ff| ff.name == x.format).map_or("n".to_string(), xref_format_self); + let text = self.d.xref_values(x.target).map_or("??".into(), |mut v| { + v.page = "?".into(); + designcraft_doc::xref::expand(self.d.xref_format(&x.format).map_or("", |ff| ff.definition.as_str()), &v) + }); + let mut c = self.csr_el(&f); + let mut content = El::new("Content"); + content.children.push(Node::Text(text)); + c.push(content); + let mut src = El::new("CrossReferenceSource") + .attr("Self", &me) + .attr("AppliedFormat", fmt) + .attr("Name", format!("Cross-Reference {}", self.xref_sources.len() + 1)) + .attr("Hidden", "false"); + src.push(c); + csrs.push(src); + self.xref_sources.push((me, x.target)); + } + } else if let Some(vi) = designcraft_doc::vars::var_index(ch) { + flush_text(&mut cur, &mut pending); + flush_content(&mut pending, &mut out); + if let Some(v) = self.d.text_variables.get(vi) { + let result = self.d.variable_value(vi, None).unwrap_or_else(|| format!("{}{}", v.before, v.after)); + out.push(Node::El( + El::new("TextVariableInstance") + .attr("Self", self.fresh()) + .attr("Name", &v.name) + .attr("ResultText", result) + .attr("AssociatedTextVariable", format!("dTextVariablen{}", v.name)), + )); + } + } else if ch == designcraft_doc::NOTE_MARK { + // Editorial notes aren't written to IDML yet. + } else if ch == designcraft_doc::ENDNOTE_REF { + // Endnotes aren't written to IDML yet: the endnote frame keeps their text + // (numbers included) and the references are left out. + } else if ch == designcraft_doc::FOOTNOTE_REF { + // The reference: its own range carrying the reference position. + flush_text(&mut cur, &mut pending); + flush_content(&mut pending, &mut out); + if !out.is_empty() { + let mut c = self.csr_el(&f); + c.children.append(&mut out); + csrs.push(c); + } + if let Some(n) = s.notes.get(s.notes_before(seg_start + ci)) { + let mut c = self.csr_el(&f); + let pos = self.d.footnote_options.ref_position; + if pos != designcraft_doc::Position::Normal { + c.set("Position", names::position_out(pos)); + } + c.push(self.footnote_el(&n.text)); + csrs.push(c); + } + } else if let Some(bt) = brk { + flush_text(&mut cur, &mut pending); + flush_content(&mut pending, &mut out); + if !out.is_empty() { + let mut c = self.csr_el(&f); + c.children.append(&mut out); + csrs.push(c); + } + let mut c = self.csr_el(&f); + c.set("ParagraphBreakType", bt); + c.push(El::new("Br")); + csrs.push(c); + } else { + cur.push(ch); } } - paras.push(psr); + flush_text(&mut cur, &mut pending); + flush_content(&mut pending, &mut out); + let is_last_seg = k + 1 == nseg; + // A break character that ends the paragraph was written as its `Br`. + let para_br = is_last_seg && !last && !ends_with_break; + if !out.is_empty() || csrs.is_empty() || para_br { + let mut c = self.csr_el(&f); + c.children.extend(out); + csrs.push(c); + } + self.apply_tracked_changes(s, &f, &mut csrs, para_br, r.end); + for c in csrs { + psr.push(c); + } } - paras + psr } pub(super) fn psr_el(&mut self, pf: &ParaFormat) -> El { diff --git a/crates/idml/src/export/styles.rs b/crates/idml/src/export/styles.rs index 281d76712..9435b533c 100644 --- a/crates/idml/src/export/styles.rs +++ b/crates/idml/src/export/styles.rs @@ -1,7 +1,9 @@ use super::common::{bool_s, p, pct, pkg_root, with_props}; use super::item::{Group, text_frame_pref}; use super::story::with_list_tab; -use super::*; +use super::{ + CHAR_BUILTINS, El, Ex, NO_PARA_ID, OBJECT_BUILTINS, PARA_BUILTINS, Rule, TOC_STYLE_ID, TOC_STYLE_NAME, document, escape_id, names, num, st, +}; impl<'a> Ex<'a> { // ---------- Styles.xml ---------- @@ -72,132 +74,8 @@ impl<'a> Ex<'a> { pg.emit(&mut rp, "ParagraphStyleGroup", ""); root.push(rp); - // Cell and table styles. - let mut cells = El::new("RootCellStyleGroup") - .attr("Self", self.fresh()) - .child(El::new("CellStyle").attr("Self", "CellStyle/$ID/[None]").attr("AppliedParagraphStyle", NO_PARA_ID).attr("Name", "$ID/[None]")); - for cs in styles.cell.iter().filter(|c| c.name != designcraft_doc::NO_CELL_STYLE) { - let mut el = El::new("CellStyle") - .attr("Self", names::style_self("CellStyle", names::CELL_BUILTINS, &cs.name)) - .attr("Name", names::style_name_out(names::CELL_BUILTINS, &cs.name)); - if let Some(f) = &cs.fill { - el.set("FillColor", self.sw(f)); - } - if let Some(t) = cs.fill_tint { - el.set("FillTint", pct(t as f64)); - } - if let Some(i) = cs.insets { - for (k, v) in ["TopInset", "LeftInset", "BottomInset", "RightInset"].iter().zip(i) { - el.set(k, num(v)); - } - } - for (key, value) in ["TopInset", "LeftInset", "BottomInset", "RightInset"].iter().zip(cs.inset_overrides) { - if let Some(value) = value { - el.set(key, num(value)); - } - } - if let Some(vj) = cs.vj { - el.set("VerticalJustification", names::vj_out(vj)); - } - if let Some(fb) = cs.first_baseline { - el.set("FirstBaselineOffset", names::first_baseline_out(fb)); - } - if let Some(min) = cs.first_baseline_min { - el.set("MinimumFirstBaselineOffset", num(min)); - } - if let Some(st) = &cs.stroke { - for side in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"] { - self.cell_stroke_attrs(&mut el, side, st); - } - } - for (side, attrs) in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"].iter().zip(&cs.strokes) { - self.partial_cell_stroke_attrs(&mut el, side, attrs); - } - if let Some(ps) = &cs.paragraph_style { - el.set("AppliedParagraphStyle", names::style_self("ParagraphStyle", PARA_BUILTINS, ps)); - } - let base = cs.based_on.as_deref().unwrap_or(designcraft_doc::NO_CELL_STYLE); - let based_on = if base == designcraft_doc::NO_CELL_STYLE { - p("BasedOn", "string", "$ID/[None]") - } else { - p("BasedOn", "object", names::style_self("CellStyle", names::CELL_BUILTINS, base)) - }; - cells = cells.child(with_props(el, vec![based_on])); - } - root.push(cells); - let mut tables = El::new("RootTableStyleGroup").attr("Self", self.fresh()); - if !styles.table.iter().any(|s| s.name == designcraft_doc::NO_TABLE_STYLE) { - tables.push(El::new("TableStyle").attr("Self", "TableStyle/$ID/[No table style]").attr("Name", "$ID/[No table style]")); - } - if !styles.table.iter().any(|s| s.name == designcraft_doc::BASIC_TABLE) { - tables.push(with_props( - El::new("TableStyle").attr("Self", "TableStyle/$ID/[Basic Table]").attr("Name", "$ID/[Basic Table]"), - vec![p("BasedOn", "string", "$ID/[No table style]")], - )); - } - for ts in &styles.table { - let mut el = El::new("TableStyle") - .attr("Self", names::style_self("TableStyle", names::TABLE_BUILTINS, &ts.name)) - .attr("Name", names::style_name_out(names::TABLE_BUILTINS, &ts.name)); - for (k, v) in [ - ("HeaderRegionCellStyle", &ts.header), - ("BodyRegionCellStyle", &ts.body), - ("FooterRegionCellStyle", &ts.footer), - ("LeftColumnRegionCellStyle", &ts.left_column), - ("RightColumnRegionCellStyle", &ts.right_column), - ] { - if let Some(n) = v { - el.set(k, names::style_self("CellStyle", names::CELL_BUILTINS, n)); - } - } - for (key, value) in [ - ("HeaderRegionSameAsBodyRegion", ts.header_same_as_body), - ("FooterRegionSameAsBodyRegion", ts.footer_same_as_body), - ("LeftColumnRegionSameAsBodyRegion", ts.left_column_same_as_body), - ("RightColumnRegionSameAsBodyRegion", ts.right_column_same_as_body), - ] { - if let Some(value) = value { - el.set(key, value); - } - } - if let Some(b) = &ts.border { - for side in ["Top", "Left", "Bottom", "Right"] { - self.cell_stroke_attrs(&mut el, &format!("{side}Border"), b); - } - } - for (side, attrs) in ["TopBorder", "LeftBorder", "BottomBorder", "RightBorder"].iter().zip(&ts.borders) { - self.partial_cell_stroke_attrs(&mut el, side, attrs); - } - if let Some(a) = &ts.alt_rows { - el.set("StartRowFillColor", self.sw(&a.first_color)); - el.set("StartRowFillCount", a.first); - el.set("StartRowFillTint", pct(a.first_tint as f64)); - el.set("EndRowFillColor", self.sw(&a.next_color)); - el.set("EndRowFillCount", a.next); - el.set("EndRowFillTint", pct(a.next_tint as f64)); - } - self.partial_alt_fill_attrs(&mut el, "Row", &ts.row_fills); - self.partial_alt_fill_attrs(&mut el, "Column", &ts.column_fills); - self.partial_alt_stroke_attrs(&mut el, "Row", &ts.row_strokes); - self.partial_alt_stroke_attrs(&mut el, "Column", &ts.column_strokes); - if let Some(v) = ts.space_before { - el.set("SpaceBefore", num(v)); - } - if let Some(v) = ts.space_after { - el.set("SpaceAfter", num(v)); - } - let mut props = Vec::new(); - if ts.name != designcraft_doc::NO_TABLE_STYLE { - let base = ts.based_on.as_deref().unwrap_or(designcraft_doc::NO_TABLE_STYLE); - props.push(if base == designcraft_doc::NO_TABLE_STYLE { - p("BasedOn", "string", "$ID/[No table style]") - } else { - p("BasedOn", "object", names::style_self("TableStyle", names::TABLE_BUILTINS, base)) - }); - } - tables = tables.child(with_props(el, props)); - } - root.push(tables); + self.append_cell_styles(&mut root, &styles); + self.append_table_styles(&mut root, &styles); // Named numbered lists. for l in &self.d.settings.lists { root.push( @@ -281,6 +159,137 @@ impl<'a> Ex<'a> { document(&root) } + fn append_cell_styles(&mut self, root: &mut El, styles: &designcraft_doc::Styles) { + let mut cells = El::new("RootCellStyleGroup") + .attr("Self", self.fresh()) + .child(El::new("CellStyle").attr("Self", "CellStyle/$ID/[None]").attr("AppliedParagraphStyle", NO_PARA_ID).attr("Name", "$ID/[None]")); + for cs in styles.cell.iter().filter(|c| c.name != designcraft_doc::NO_CELL_STYLE) { + let mut el = El::new("CellStyle") + .attr("Self", names::style_self("CellStyle", names::CELL_BUILTINS, &cs.name)) + .attr("Name", names::style_name_out(names::CELL_BUILTINS, &cs.name)); + if let Some(f) = &cs.fill { + el.set("FillColor", self.sw(f)); + } + if let Some(t) = cs.fill_tint { + el.set("FillTint", pct(t as f64)); + } + if let Some(i) = cs.insets { + for (k, v) in ["TopInset", "LeftInset", "BottomInset", "RightInset"].iter().zip(i) { + el.set(k, num(v)); + } + } + for (key, value) in ["TopInset", "LeftInset", "BottomInset", "RightInset"].iter().zip(cs.inset_overrides) { + if let Some(value) = value { + el.set(key, num(value)); + } + } + if let Some(vj) = cs.vj { + el.set("VerticalJustification", names::vj_out(vj)); + } + if let Some(fb) = cs.first_baseline { + el.set("FirstBaselineOffset", names::first_baseline_out(fb)); + } + if let Some(min) = cs.first_baseline_min { + el.set("MinimumFirstBaselineOffset", num(min)); + } + if let Some(st) = &cs.stroke { + for side in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"] { + self.cell_stroke_attrs(&mut el, side, st); + } + } + for (side, attrs) in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"].iter().zip(&cs.strokes) { + self.partial_cell_stroke_attrs(&mut el, side, attrs); + } + if let Some(ps) = &cs.paragraph_style { + el.set("AppliedParagraphStyle", names::style_self("ParagraphStyle", PARA_BUILTINS, ps)); + } + let base = cs.based_on.as_deref().unwrap_or(designcraft_doc::NO_CELL_STYLE); + let based_on = if base == designcraft_doc::NO_CELL_STYLE { + p("BasedOn", "string", "$ID/[None]") + } else { + p("BasedOn", "object", names::style_self("CellStyle", names::CELL_BUILTINS, base)) + }; + cells = cells.child(with_props(el, vec![based_on])); + } + root.push(cells); + } + + fn append_table_styles(&mut self, root: &mut El, styles: &designcraft_doc::Styles) { + let mut tables = El::new("RootTableStyleGroup").attr("Self", self.fresh()); + if !styles.table.iter().any(|s| s.name == designcraft_doc::NO_TABLE_STYLE) { + tables.push(El::new("TableStyle").attr("Self", "TableStyle/$ID/[No table style]").attr("Name", "$ID/[No table style]")); + } + if !styles.table.iter().any(|s| s.name == designcraft_doc::BASIC_TABLE) { + tables.push(with_props( + El::new("TableStyle").attr("Self", "TableStyle/$ID/[Basic Table]").attr("Name", "$ID/[Basic Table]"), + vec![p("BasedOn", "string", "$ID/[No table style]")], + )); + } + for ts in &styles.table { + let mut el = El::new("TableStyle") + .attr("Self", names::style_self("TableStyle", names::TABLE_BUILTINS, &ts.name)) + .attr("Name", names::style_name_out(names::TABLE_BUILTINS, &ts.name)); + for (k, v) in [ + ("HeaderRegionCellStyle", &ts.header), + ("BodyRegionCellStyle", &ts.body), + ("FooterRegionCellStyle", &ts.footer), + ("LeftColumnRegionCellStyle", &ts.left_column), + ("RightColumnRegionCellStyle", &ts.right_column), + ] { + if let Some(n) = v { + el.set(k, names::style_self("CellStyle", names::CELL_BUILTINS, n)); + } + } + for (key, value) in [ + ("HeaderRegionSameAsBodyRegion", ts.header_same_as_body), + ("FooterRegionSameAsBodyRegion", ts.footer_same_as_body), + ("LeftColumnRegionSameAsBodyRegion", ts.left_column_same_as_body), + ("RightColumnRegionSameAsBodyRegion", ts.right_column_same_as_body), + ] { + if let Some(value) = value { + el.set(key, value); + } + } + if let Some(b) = &ts.border { + for side in ["Top", "Left", "Bottom", "Right"] { + self.cell_stroke_attrs(&mut el, &format!("{side}Border"), b); + } + } + for (side, attrs) in ["TopBorder", "LeftBorder", "BottomBorder", "RightBorder"].iter().zip(&ts.borders) { + self.partial_cell_stroke_attrs(&mut el, side, attrs); + } + if let Some(a) = &ts.alt_rows { + el.set("StartRowFillColor", self.sw(&a.first_color)); + el.set("StartRowFillCount", a.first); + el.set("StartRowFillTint", pct(a.first_tint as f64)); + el.set("EndRowFillColor", self.sw(&a.next_color)); + el.set("EndRowFillCount", a.next); + el.set("EndRowFillTint", pct(a.next_tint as f64)); + } + self.partial_alt_fill_attrs(&mut el, "Row", &ts.row_fills); + self.partial_alt_fill_attrs(&mut el, "Column", &ts.column_fills); + self.partial_alt_stroke_attrs(&mut el, "Row", &ts.row_strokes); + self.partial_alt_stroke_attrs(&mut el, "Column", &ts.column_strokes); + if let Some(v) = ts.space_before { + el.set("SpaceBefore", num(v)); + } + if let Some(v) = ts.space_after { + el.set("SpaceAfter", num(v)); + } + let mut props = Vec::new(); + if ts.name != designcraft_doc::NO_TABLE_STYLE { + let base = ts.based_on.as_deref().unwrap_or(designcraft_doc::NO_TABLE_STYLE); + props.push(if base == designcraft_doc::NO_TABLE_STYLE { + p("BasedOn", "string", "$ID/[No table style]") + } else { + p("BasedOn", "object", names::style_self("TableStyle", names::TABLE_BUILTINS, base)) + }); + } + tables = tables.child(with_props(el, props)); + } + root.push(tables); + } + /// The document's table of contents as a TOC style; its story names it in `AppliedTOCStyle`. fn toc_style_el(&mut self, toc: &designcraft_doc::Toc) -> El { let para = |n: &str| names::style_self("ParagraphStyle", PARA_BUILTINS, n); diff --git a/crates/idml/src/export/table.rs b/crates/idml/src/export/table.rs index bf4f3ed25..1e81a811a 100644 --- a/crates/idml/src/export/table.rs +++ b/crates/idml/src/export/table.rs @@ -1,5 +1,5 @@ use super::common::{bool_s, pct, uid}; -use super::*; +use super::{Affine, CharFormat, Content, El, Ex, Item, ParaFormat, Shape, names, num}; impl<'a> Ex<'a> { pub(super) fn cell_stroke_attrs(&self, el: &mut El, prefix: &str, s: &designcraft_doc::CellStroke) { diff --git a/crates/idml/src/export/text_attrs.rs b/crates/idml/src/export/text_attrs.rs index 79d5940af..38acf0d2a 100644 --- a/crates/idml/src/export/text_attrs.rs +++ b/crates/idml/src/export/text_attrs.rs @@ -1,5 +1,26 @@ use super::common::{bool_s, p, pct}; -use super::*; +use super::{CHAR_BUILTINS, CharAttrs, El, Ex, ListType, ParaAttrs, SpanColumns, escape_id, names, num, st}; + +// Optional paragraph values are omitted when inherited from the style. +macro_rules! n { + ($a:expr, $el:expr, $f:ident, $k:literal) => { + if let Some(v) = $a.$f { + $el.set($k, num(v as f64)); + } + }; + ($a:expr, $el:expr, $f:ident, $k:literal, pct) => { + if let Some(v) = $a.$f { + $el.set($k, pct(v as f64)); + } + }; +} +macro_rules! b { + ($a:expr, $el:expr, $f:ident, $k:literal) => { + if let Some(v) = $a.$f { + $el.set($k, bool_s(v)); + } + }; +} impl<'a> Ex<'a> { pub(super) fn char_attrs(&mut self, el: &mut El, props: &mut Vec, a: &CharAttrs) { @@ -21,6 +42,93 @@ impl<'a> Ex<'a> { if let Some(v) = a.character_alignment { el.set("CharacterAlignment", crate::cjk::alignment_out(v)); } + self.char_font_attrs(el, props, a); + if let Some(v) = &a.fill { + el.set("FillColor", self.sw(v)); + } + if let Some(v) = a.fill_tint { + el.set("FillTint", pct(v as f64)); + } + if let Some(v) = &a.stroke { + el.set("StrokeColor", self.sw(v)); + } + if let Some(v) = a.stroke_tint { + el.set("StrokeTint", pct(v as f64)); + } + if let Some(v) = a.stroke_weight { + el.set("StrokeWeight", num(v)); + } + if let Some(v) = a.stroke_behind_fill { + el.set("StrokeAlignment", if v { "OutsideAlignment" } else { "CenterAlignment" }); + } + if let Some(v) = a.capitalization { + el.set("Capitalization", names::caps_out(v)); + } + if let Some(v) = a.position { + el.set("Position", names::position_out(v)); + } + if let Some(v) = a.underline { + el.set("Underline", bool_s(v)); + } + if let Some(v) = a.strikethrough { + el.set("StrikeThru", bool_s(v)); + } + if let Some(v) = a.ligatures { + el.set("Ligatures", bool_s(v)); + } + self.char_decoration_attrs(el, a); + if let Some(list) = &a.otf_features { + for (tag, _, attr) in designcraft_doc::otf::TOGGLES { + el.set(attr, bool_s(designcraft_doc::otf::is_on(list, tag))); + } + let fig = designcraft_doc::otf::figures(list); + if let Some(f) = designcraft_doc::otf::FIGURES.iter().find(|f| f.0 == fig) { + el.set("OTFFigureStyle", f.3); + } + el.set("OTFStylisticSets", designcraft_doc::otf::stylistic_sets(list).to_string()); + } + if let Some(v) = &a.glyph_form { + el.set("GlyphForm", v); + } + if let Some(v) = a.no_break { + el.set("NoBreak", bool_s(v)); + } + self.char_arabic_attrs(el, a); + self.char_cjk_layout_attrs(el, props, a); + self.cjk_settings_chars(el, props, a); + if let Some(list) = &a.conditions { + el.set("AppliedConditions", list.iter().map(|c| format!("Condition/{}", escape_id(c))).collect::>().join(" ")); + } + if let Some(v) = &a.language { + el.set("AppliedLanguage", format!("$ID/{v}")); + } + } + + fn char_arabic_attrs(&self, el: &mut El, a: &CharAttrs) { + if let Some(v) = a.digits { + el.set("DigitsType", names::digits_out(v)); + } + if let Some(v) = a.character_direction { + el.set("CharacterDirection", crate::arabic::direction_out(v)); + } + if let Some(v) = a.allow_kashidas { + el.set("Kashidas", if v { "DefaultKashidas" } else { "KashidasOff" }); + } + if let Some(v) = a.diacritic_position { + el.set("DiacriticPosition", crate::arabic::diacritic_out(v)); + } + if let Some(v) = a.diacritic_x_offset { + el.set("XOffsetDiacritic", num(v)); + } + if let Some(v) = a.diacritic_y_offset { + el.set("YOffsetDiacritic", num(v)); + } + if let Some(v) = &a.positional_form { + el.set("PositionalForm", v); + } + } + + fn char_font_attrs(&mut self, el: &mut El, props: &mut Vec, a: &CharAttrs) { if let Some(f) = &a.font_family { if let Some(composite) = self.d.styles.composite_font(f) { props.push(p("AppliedFont", "object", format!("CompositeFont/{}", escape_id(&composite.name)))); @@ -63,39 +171,9 @@ impl<'a> Ex<'a> { if let Some(v) = a.skew { el.set("Skew", num(v)); } - if let Some(v) = &a.fill { - el.set("FillColor", self.sw(v)); - } - if let Some(v) = a.fill_tint { - el.set("FillTint", pct(v as f64)); - } - if let Some(v) = &a.stroke { - el.set("StrokeColor", self.sw(v)); - } - if let Some(v) = a.stroke_tint { - el.set("StrokeTint", pct(v as f64)); - } - if let Some(v) = a.stroke_weight { - el.set("StrokeWeight", num(v)); - } - if let Some(v) = a.stroke_behind_fill { - el.set("StrokeAlignment", if v { "OutsideAlignment" } else { "CenterAlignment" }); - } - if let Some(v) = a.capitalization { - el.set("Capitalization", names::caps_out(v)); - } - if let Some(v) = a.position { - el.set("Position", names::position_out(v)); - } - if let Some(v) = a.underline { - el.set("Underline", bool_s(v)); - } - if let Some(v) = a.strikethrough { - el.set("StrikeThru", bool_s(v)); - } - if let Some(v) = a.ligatures { - el.set("Ligatures", bool_s(v)); - } + } + + fn char_decoration_attrs(&self, el: &mut El, a: &CharAttrs) { for (k, w, o, c, t) in [ ("Underline", a.underline_weight, a.underline_offset, &a.underline_color, a.underline_tint), ("StrikeThru", a.strikethrough_weight, a.strikethrough_offset, &a.strikethrough_color, a.strikethrough_tint), @@ -113,43 +191,9 @@ impl<'a> Ex<'a> { el.set(&format!("{k}Tint"), num(t as f64 * 100.0)); } } - if let Some(list) = &a.otf_features { - for (tag, _, attr) in designcraft_doc::otf::TOGGLES { - el.set(attr, bool_s(designcraft_doc::otf::is_on(list, tag))); - } - let fig = designcraft_doc::otf::figures(list); - if let Some(f) = designcraft_doc::otf::FIGURES.iter().find(|f| f.0 == fig) { - el.set("OTFFigureStyle", f.3); - } - el.set("OTFStylisticSets", designcraft_doc::otf::stylistic_sets(list).to_string()); - } - if let Some(v) = &a.glyph_form { - el.set("GlyphForm", v); - } - if let Some(v) = a.no_break { - el.set("NoBreak", bool_s(v)); - } - if let Some(v) = a.digits { - el.set("DigitsType", names::digits_out(v)); - } - if let Some(v) = a.character_direction { - el.set("CharacterDirection", crate::arabic::direction_out(v)); - } - if let Some(v) = a.allow_kashidas { - el.set("Kashidas", if v { "DefaultKashidas" } else { "KashidasOff" }); - } - if let Some(v) = a.diacritic_position { - el.set("DiacriticPosition", crate::arabic::diacritic_out(v)); - } - if let Some(v) = a.diacritic_x_offset { - el.set("XOffsetDiacritic", num(v)); - } - if let Some(v) = a.diacritic_y_offset { - el.set("YOffsetDiacritic", num(v)); - } - if let Some(v) = &a.positional_form { - el.set("PositionalForm", v); - } + } + + fn char_cjk_layout_attrs(&mut self, el: &mut El, props: &mut Vec, a: &CharAttrs) { if let Some(r) = a.ruby.as_ref() { el.set("RubyFlag", if r.is_empty() { "0" } else { "1" }); el.set("RubyString", r.as_str()); @@ -196,66 +240,9 @@ impl<'a> Ex<'a> { if let Some(v) = a.warichu_chars_after_break { el.set("WarichuCharsAfterBreak", v); } - self.cjk_settings_chars(el, props, a); - if let Some(list) = &a.conditions { - el.set("AppliedConditions", list.iter().map(|c| format!("Condition/{}", escape_id(c))).collect::>().join(" ")); - } - if let Some(v) = &a.language { - el.set("AppliedLanguage", format!("$ID/{v}")); - } } - /// Write paragraph attributes `a`, set over the resolved paragraph style `parent` (`None` for the - /// root style). - pub(super) fn para_attrs(&mut self, el: &mut El, props: &mut Vec, a: &ParaAttrs, parent: Option<&str>) { - if let Some(v) = &a.mojikumi { - props.push(p("Mojikumi", if v.starts_with("MojikumiTable/") { "object" } else { "enumeration" }, v)); - } - if let Some(v) = &a.kinsoku_type { - el.set("KinsokuType", v); - } - if let Some(v) = a.kinsoku_hang { - el.set( - "KinsokuHangType", - match v { - designcraft_doc::cjk::KinsokuHang::None => "None", - designcraft_doc::cjk::KinsokuHang::Regular => "KinsokuHangRegular", - designcraft_doc::cjk::KinsokuHang::Force => "KinsokuHangForce", - }, - ); - } - if let Some(v) = a.bunri_kinshi { - el.set("BunriKinshi", bool_s(v)); - } - if let Some(v) = a.rensuuji { - el.set("Rensuuji", bool_s(v)); - } - if let Some(v) = a.treat_ideographic_space_as_space { - el.set("TreatIdeographicSpaceAsSpace", bool_s(v)); - } - cjk_settings_para(el, a); - if let Some(Some(k)) = &a.kinsoku { - props.push(p( - "KinsokuSet", - if k.name.is_empty() { "enumeration" } else { "object" }, - if k.name.is_empty() { "Nothing".into() } else { format!("KinsokuTable/{}", escape_id(&k.name)) }, - )); - } - if let Some(n) = &a.list_name { - el.set( - "AppliedNumberingList", - if n.is_empty() { "NumberingList/$ID/[Default]".to_string() } else { format!("NumberingList/{}", escape_id(n)) }, - ); - } - if let Some(v) = a.start_at { - match v { - Some(n) => { - el.set("NumberingContinue", "false"); - el.set("NumberingStartAt", n); - } - None => el.set("NumberingContinue", "true"), - } - } + fn para_nested_styles(&self, props: &mut Vec, a: &ParaAttrs, parent: Option<&str>) { // Nested and GREP styles: property lists of records. The drop cap's character style is the // nested style list's leading "through 1 Dropcap" entry (IDML has no attribute for it): a // drop cap style set here (`[None]` included) restyles that entry, and a real one is @@ -319,25 +306,9 @@ impl<'a> Ex<'a> { } props.push(l); } - macro_rules! n { - ($f:ident, $k:literal) => { - if let Some(v) = a.$f { - el.set($k, num(v as f64)); - } - }; - ($f:ident, $k:literal, pct) => { - if let Some(v) = a.$f { - el.set($k, pct(v as f64)); - } - }; - } - macro_rules! b { - ($f:ident, $k:literal) => { - if let Some(v) = a.$f { - el.set($k, bool_s(v)); - } - }; - } + } + + fn para_alignment_attrs(&self, el: &mut El, a: &ParaAttrs, parent: Option<&str>) { if let Some(v) = a.align { el.set("Justification", names::align_out(v)); } @@ -356,20 +327,20 @@ impl<'a> Ex<'a> { if let Some(Some(v)) = a.paragraph_kashida_width { el.set("ParagraphKashidaWidth", num(v)); } - n!(left_indent, "LeftIndent"); - n!(right_indent, "RightIndent"); - n!(first_line_indent, "FirstLineIndent"); - n!(last_line_indent, "LastLineIndent"); - n!(space_before, "SpaceBefore"); - n!(space_after, "SpaceAfter"); + n!(a, el, left_indent, "LeftIndent"); + n!(a, el, right_indent, "RightIndent"); + n!(a, el, first_line_indent, "FirstLineIndent"); + n!(a, el, last_line_indent, "LastLineIndent"); + n!(a, el, space_before, "SpaceBefore"); + n!(a, el, space_after, "SpaceAfter"); match a.same_para_style_spacing { Some(Some(v)) => el.set("SameParaStyleSpacing", num(v)), // Explicit Ignore: must be written so it still overrides a based-on style's value. Some(None) => el.set("SameParaStyleSpacing", "Nothing"), None => {} } - n!(drop_cap_lines, "DropCapLines"); - n!(drop_cap_chars, "DropCapCharacters"); + n!(a, el, drop_cap_lines, "DropCapLines"); + n!(a, el, drop_cap_chars, "DropCapCharacters"); if a.drop_cap_align_left.is_some() || a.drop_cap_scale_descenders.is_some() { let d = parent.map(|name| self.d.styles.resolve_para_style(name).0).unwrap_or_default(); let bits = u32::from(a.drop_cap_align_left.unwrap_or(d.drop_cap_align_left)) @@ -403,40 +374,46 @@ impl<'a> Ex<'a> { }, ); } - b!(hyphenate, "Hyphenation"); - n!(hyph_min_word, "HyphenateWordsLongerThan"); - n!(hyph_after_first, "HyphenateAfterFirst"); - n!(hyph_before_last, "HyphenateBeforeLast"); - n!(hyph_limit, "HyphenateLadderLimit"); - n!(hyph_zone, "HyphenationZone"); - b!(hyph_capitalized, "HyphenateCapitalizedWords"); - b!(hyph_last_word, "HyphenateLastWord"); - b!(hyph_across_column, "HyphenateAcrossColumns"); + } + + fn para_flow_attrs(&self, el: &mut El, a: &ParaAttrs) { + b!(a, el, hyphenate, "Hyphenation"); + n!(a, el, hyph_min_word, "HyphenateWordsLongerThan"); + n!(a, el, hyph_after_first, "HyphenateAfterFirst"); + n!(a, el, hyph_before_last, "HyphenateBeforeLast"); + n!(a, el, hyph_limit, "HyphenateLadderLimit"); + n!(a, el, hyph_zone, "HyphenationZone"); + b!(a, el, hyph_capitalized, "HyphenateCapitalizedWords"); + b!(a, el, hyph_last_word, "HyphenateLastWord"); + b!(a, el, hyph_across_column, "HyphenateAcrossColumns"); if let Some(v) = a.hyph_weight { el.set("HyphenWeight", num((v * 10.0).round())); } - n!(word_space_min, "MinimumWordSpacing", pct); - n!(word_space_desired, "DesiredWordSpacing", pct); - n!(word_space_max, "MaximumWordSpacing", pct); - n!(letter_space_min, "MinimumLetterSpacing", pct); - n!(letter_space_desired, "DesiredLetterSpacing", pct); - n!(letter_space_max, "MaximumLetterSpacing", pct); - n!(glyph_scale_min, "MinimumGlyphScaling", pct); - n!(glyph_scale_desired, "DesiredGlyphScaling", pct); - n!(glyph_scale_max, "MaximumGlyphScaling", pct); - n!(auto_leading, "AutoLeading", pct); + n!(a, el, word_space_min, "MinimumWordSpacing", pct); + n!(a, el, word_space_desired, "DesiredWordSpacing", pct); + n!(a, el, word_space_max, "MaximumWordSpacing", pct); + n!(a, el, letter_space_min, "MinimumLetterSpacing", pct); + n!(a, el, letter_space_desired, "DesiredLetterSpacing", pct); + n!(a, el, letter_space_max, "MaximumLetterSpacing", pct); + n!(a, el, glyph_scale_min, "MinimumGlyphScaling", pct); + n!(a, el, glyph_scale_desired, "DesiredGlyphScaling", pct); + n!(a, el, glyph_scale_max, "MaximumGlyphScaling", pct); + n!(a, el, auto_leading, "AutoLeading", pct); if let Some(v) = a.single_word_justify { el.set("SingleWordJustification", names::align_out(v)); } - n!(keep_with_next, "KeepWithNext"); - b!(keep_lines_together, "KeepLinesTogether"); - b!(ignore_optical_margin, "IgnoreEdgeAlignment"); - b!(keep_all_lines, "KeepAllLinesTogether"); - n!(keep_first, "KeepFirstLines"); - n!(keep_last, "KeepLastLines"); + n!(a, el, keep_with_next, "KeepWithNext"); + b!(a, el, keep_lines_together, "KeepLinesTogether"); + b!(a, el, ignore_optical_margin, "IgnoreEdgeAlignment"); + b!(a, el, keep_all_lines, "KeepAllLinesTogether"); + n!(a, el, keep_first, "KeepFirstLines"); + n!(a, el, keep_last, "KeepLastLines"); if let Some(v) = a.start_paragraph { el.set("StartParagraph", names::start_para_out(v)); } + } + + fn para_columns_and_list_attrs(&mut self, el: &mut El, props: &mut Vec, a: &ParaAttrs) { if let Some(sc) = a.span_columns { match sc { SpanColumns::Single => el.set("SpanColumnType", "SingleColumn"), @@ -450,8 +427,8 @@ impl<'a> Ex<'a> { } } } - n!(split_inside_gutter, "SplitColumnInsideGutter"); - n!(split_outside_gutter, "SplitColumnOutsideGutter"); + n!(a, el, split_inside_gutter, "SplitColumnInsideGutter"); + n!(a, el, split_outside_gutter, "SplitColumnOutsideGutter"); if let Some(r) = &a.rule_above { self.rule(el, props, "RuleAbove", r); } @@ -497,22 +474,37 @@ impl<'a> Ex<'a> { if let Some(b) = a.balance_ragged { props.push(p("BalanceRaggedLines", "enumeration", if b { "FullyBalanced" } else { "NoBalancing" })); } - b!(shading_on, "ParagraphShadingOn"); + } + + fn para_box_corners(el: &mut El, a: &ParaAttrs) { + const CORNERS: [&str; 4] = ["TopLeft", "TopRight", "BottomRight", "BottomLeft"]; + for (prefix, corners) in [("ParagraphBorder", a.border_corners), ("ParagraphShading", a.shading_corners)] { + if let Some(cs) = corners { + for (c, k) in cs.iter().zip(CORNERS) { + el.set(&format!("{prefix}{k}CornerOption"), names::corner_out(c.shape)); + el.set(&format!("{prefix}{k}CornerRadius"), num(c.size)); + } + } + } + } + + fn para_box_attrs(&self, el: &mut El, props: &mut Vec, a: &ParaAttrs) { + b!(a, el, shading_on, "ParagraphShadingOn"); if let Some(c) = &a.shading_color { props.push(p("ParagraphShadingColor", "object", self.sw(c))); } - n!(shading_tint, "ParagraphShadingTint", pct); + n!(a, el, shading_tint, "ParagraphShadingTint", pct); const SIDES: [&str; 4] = ["Top", "Left", "Bottom", "Right"]; if let Some(o) = a.shading_offsets { for (i, side) in SIDES.iter().enumerate() { el.set(&format!("ParagraphShading{side}Offset"), num(o[i])); } } - b!(border_on, "ParagraphBorderOn"); + b!(a, el, border_on, "ParagraphBorderOn"); if let Some(c) = &a.border_color { props.push(p("ParagraphBorderColor", "object", self.sw(c))); } - n!(border_tint, "ParagraphBorderTint", pct); + n!(a, el, border_tint, "ParagraphBorderTint", pct); if let Some(w) = a.border_weights { for (i, side) in SIDES.iter().enumerate() { el.set(&format!("ParagraphBorder{side}LineWeight"), num(w[i])); @@ -532,15 +524,7 @@ impl<'a> Ex<'a> { if let Some(j) = a.border_join { el.set("ParagraphBorderStrokeEndJoin", names::join_out(j)); } - const CORNERS: [&str; 4] = ["TopLeft", "TopRight", "BottomRight", "BottomLeft"]; - for (prefix, corners) in [("ParagraphBorder", a.border_corners), ("ParagraphShading", a.shading_corners)] { - if let Some(cs) = corners { - for (c, k) in cs.iter().zip(CORNERS) { - el.set(&format!("{prefix}{k}CornerOption"), names::corner_out(c.shape)); - el.set(&format!("{prefix}{k}CornerRadius"), num(c.size)); - } - } - } + Self::para_box_corners(el, a); if let Some(w) = a.border_width { el.set("ParagraphBorderWidth", names::box_width_out(w)); } @@ -550,12 +534,12 @@ impl<'a> Ex<'a> { if let Some(b) = a.border_bottom { el.set("ParagraphBorderBottomOrigin", names::box_bottom_out(b)); } - b!(border_display_if_splits, "ParagraphBorderDisplayIfSplits"); - b!(border_merge, "MergeConsecutiveParaBorders"); + b!(a, el, border_display_if_splits, "ParagraphBorderDisplayIfSplits"); + b!(a, el, border_merge, "MergeConsecutiveParaBorders"); if let Some(c) = &a.border_gap_color { props.push(p("ParagraphBorderGapColor", "object", self.sw(c))); } - n!(border_gap_tint, "ParagraphBorderGapTint", pct); + n!(a, el, border_gap_tint, "ParagraphBorderGapTint", pct); if let Some(w) = a.shading_width { el.set("ParagraphShadingWidth", names::box_width_out(w)); } @@ -565,11 +549,14 @@ impl<'a> Ex<'a> { if let Some(b) = a.shading_bottom { el.set("ParagraphShadingBottomOrigin", names::box_bottom_out(b)); } - b!(shading_clip, "ParagraphShadingClipToFrame"); - b!(shading_nonprinting, "ParagraphShadingSuppressPrinting"); - n!(span_space_before, "SpanColumnMinSpaceBefore"); - n!(span_space_after, "SpanColumnMinSpaceAfter"); - n!(list_level, "NumberingLevel"); + b!(a, el, shading_clip, "ParagraphShadingClipToFrame"); + b!(a, el, shading_nonprinting, "ParagraphShadingSuppressPrinting"); + } + + fn para_list_attrs(&self, el: &mut El, props: &mut Vec, a: &ParaAttrs) { + n!(a, el, span_space_before, "SpanColumnMinSpaceBefore"); + n!(a, el, span_space_after, "SpanColumnMinSpaceAfter"); + n!(a, el, list_level, "NumberingLevel"); for (k, style) in [("BulletsCharacterStyle", &a.bullet_char_style), ("NumberingCharacterStyle", &a.number_char_style)] { if let Some(st) = style { props.push(p(k, "object", names::style_self("CharacterStyle", CHAR_BUILTINS, st))); @@ -581,7 +568,7 @@ impl<'a> Ex<'a> { if let Some(v) = a.number_align { el.set("NumberingAlignment", names::list_align_out(v)); } - b!(restart_numbers, "NumberingApplyRestartPolicy"); + b!(a, el, restart_numbers, "NumberingApplyRestartPolicy"); if let Some(level) = a.restart_after_level { let (policy, level) = if level == 0 { ("AnyPreviousLevel", 0) } else { ("SpecificPreviousLevel", level) }; props.push( @@ -598,6 +585,65 @@ impl<'a> Ex<'a> { props.push(p("BulletsFontStyle", "string", f.clone())); } } + + /// Write paragraph attributes `a`, set over the resolved paragraph style `parent` (`None` for the + /// root style). + pub(super) fn para_attrs(&mut self, el: &mut El, props: &mut Vec, a: &ParaAttrs, parent: Option<&str>) { + if let Some(v) = &a.mojikumi { + props.push(p("Mojikumi", if v.starts_with("MojikumiTable/") { "object" } else { "enumeration" }, v)); + } + if let Some(v) = &a.kinsoku_type { + el.set("KinsokuType", v); + } + if let Some(v) = a.kinsoku_hang { + el.set( + "KinsokuHangType", + match v { + designcraft_doc::cjk::KinsokuHang::None => "None", + designcraft_doc::cjk::KinsokuHang::Regular => "KinsokuHangRegular", + designcraft_doc::cjk::KinsokuHang::Force => "KinsokuHangForce", + }, + ); + } + if let Some(v) = a.bunri_kinshi { + el.set("BunriKinshi", bool_s(v)); + } + if let Some(v) = a.rensuuji { + el.set("Rensuuji", bool_s(v)); + } + if let Some(v) = a.treat_ideographic_space_as_space { + el.set("TreatIdeographicSpaceAsSpace", bool_s(v)); + } + cjk_settings_para(el, a); + if let Some(Some(k)) = &a.kinsoku { + props.push(p( + "KinsokuSet", + if k.name.is_empty() { "enumeration" } else { "object" }, + if k.name.is_empty() { "Nothing".into() } else { format!("KinsokuTable/{}", escape_id(&k.name)) }, + )); + } + if let Some(n) = &a.list_name { + el.set( + "AppliedNumberingList", + if n.is_empty() { "NumberingList/$ID/[Default]".to_string() } else { format!("NumberingList/{}", escape_id(n)) }, + ); + } + if let Some(v) = a.start_at { + match v { + Some(n) => { + el.set("NumberingContinue", "false"); + el.set("NumberingStartAt", n); + } + None => el.set("NumberingContinue", "true"), + } + } + self.para_nested_styles(props, a, parent); + self.para_alignment_attrs(el, a, parent); + self.para_flow_attrs(el, a); + self.para_columns_and_list_attrs(el, props, a); + self.para_box_attrs(el, props, a); + self.para_list_attrs(el, props, a); + } } /// Auto tate-chu-yoko, Japanese composition and grid settings of a paragraph range or style. diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index 73bed14d3..161594e1e 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -284,7 +284,7 @@ impl<'r> Importer<'r> { self.next_id } - fn run(&mut self, _root: &El, top: &[El]) -> Result<()> { + fn read_preferences(&mut self, top: &[El]) { // Preferences first (page size, facing pages). for e in top { match e.local() { @@ -341,6 +341,9 @@ impl<'r> Importer<'r> { _ => {} } } + } + + fn read_composite_fonts(&mut self, top: &[El]) { for e in top.iter().filter(|e| e.local() == "CompositeFont") { let name = e.get("Name").map(str::to_string).unwrap_or_else(|| unescape_id(e.get("Self").unwrap_or("").trim_start_matches("CompositeFont/"))); @@ -365,6 +368,9 @@ impl<'r> Importer<'r> { .collect(); self.styles.composite_fonts.push(designcraft_doc::cjk::CompositeFont { name, entries }); } + } + + fn read_cjk_tables(&mut self, top: &[El]) -> Result<()> { for e in top.iter().filter(|e| e.local() == "KinsokuTable") { if e.get("CantBeginLineChars").is_some() || e.get("CantEndLineChars").is_some() { self.kinsoku.insert( @@ -394,6 +400,13 @@ impl<'r> Importer<'r> { overrides, }); } + Ok(()) + } + + fn run(&mut self, _root: &El, top: &[El]) -> Result<()> { + self.read_preferences(top); + self.read_composite_fonts(top); + self.read_cjk_tables(top)?; self.graphics(top); self.styles(top); if let Some(e) = top.iter().find(|e| e.local() == "FootnoteOption") { diff --git a/crates/idml/src/import/document.rs b/crates/idml/src/import/document.rs index 70a16320f..dce466381 100644 --- a/crates/idml/src/import/document.rs +++ b/crates/idml/src/import/document.rs @@ -1,5 +1,5 @@ use super::values::tint; -use super::*; +use super::{BaselineGrid, El, GridRelative, Importer, PARA_BUILTINS, names}; /// IDML stores a sort key on each topic, rather than on its page references. #[derive(Clone, Default)] diff --git a/crates/idml/src/import/frame.rs b/crates/idml/src/import/frame.rs index dda28436f..396168127 100644 --- a/crates/idml/src/import/frame.rs +++ b/crates/idml/src/import/frame.rs @@ -1,5 +1,5 @@ use super::values::tint; -use super::*; +use super::{Columns, ColumnsKind, El, Margins, TextFrameOptions, TextWrap, names}; /// Drop-shadow angle (degrees) from IDML offsets (inverse of export's mapping). /// A `TextWrapPreference` element (of a page item or a placed graphic). diff --git a/crates/idml/src/import/item.rs b/crates/idml/src/import/item.rs index b9bcfdce7..d25044dc6 100644 --- a/crates/idml/src/import/item.rs +++ b/crates/idml/src/import/item.rs @@ -2,7 +2,10 @@ use super::frame::{Prefs, frame_grid_relative, text_frame_options, text_wrap}; use super::object_style::INDESIGN_CORNER_SIZE; use super::package::uri_to_path; use super::values::{nums, parse_affine, parse_point, tint}; -use super::*; +use super::{ + Affine, Anchor, Arc, Asset, AssetId, Content, Corner, CornerOptions, El, Fill, Importer, Item, ItemId, OBJECT_BUILTINS, PathData, Point, Shape, + Story, StoryId, Stroke, SubPath, TextFrame, TextFrameOptions, base64_decode, names, sniff_image, swatch, unescape_id, +}; /// The viewBox size of an SVG whose root has a viewBox but no width/height (percentages count as /// none): InDesign places such a file at one viewBox unit to the point. @@ -177,7 +180,11 @@ impl<'r> Importer<'r> { self.ctx.threads.push((id, story_self, "n".into(), "n".into())); } } - // Content. + self.read_content(e, tag, id, &mut it); + Some(it) + } + + fn read_content(&mut self, e: &El, tag: &str, id: ItemId, item: &mut Item) { match tag { "TextFrame" => { let story_self = e.get("ParentStory").unwrap_or("n").to_string(); @@ -206,7 +213,7 @@ impl<'r> Importer<'r> { relative_to: g.get("BaselineFrameGridRelativeOption").map(frame_grid_relative).unwrap_or_default(), }); } - it.content = Content::Text(TextFrame { story, options }); + item.content = Content::Text(TextFrame { story, options }); self.ctx.threads.push(( id, story_self, @@ -221,18 +228,18 @@ impl<'r> Importer<'r> { kids.push(Arc::new(k)); } } - it.content = Content::Group { items: kids }; - it.fill = Fill::none(); - it.stroke = Stroke::none(); + item.content = Content::Group { items: kids }; + item.fill = Fill::none(); + item.stroke = Stroke::none(); } _ => { if let Some(g) = e.elements().find(|c| matches!(c.local(), "Image" | "PDF" | "EPS" | "ImportedPage" | "WMF" | "PICT" | "SVG")) { - it.content = self.graphic(g); + item.content = self.graphic(g); let fill = self.image_fill(g); - if let Content::Graphic(gr) = &mut it.content { + if let Content::Graphic(gr) = &mut item.content { gr.fill = fill; } - if let (Some(ff), Content::Graphic(gr)) = (self.with_style(e, "FrameFittingOption"), &mut it.content) { + if let (Some(ff), Content::Graphic(gr)) = (self.with_style(e, "FrameFittingOption"), &mut item.content) { gr.crop = [ff.num("TopCrop"), ff.num("LeftCrop"), ff.num("BottomCrop"), ff.num("RightCrop")].map(|v| v.unwrap_or(0.0)); gr.fit_align = ff.get("FittingAlignment").map_or(4, names::anchor_in); if ff.get("AutoFit") == Some("true") { @@ -251,12 +258,11 @@ impl<'r> Importer<'r> { .map(Arc::new) .collect(); if !kids.is_empty() { - it.content = Content::Group { items: kids }; + item.content = Content::Group { items: kids }; } } } } - Some(it) } /// The swatch applied to a placed image (`FillColor`, `FillTint`): it colours a greyscale or diff --git a/crates/idml/src/import/object_style.rs b/crates/idml/src/import/object_style.rs index 102082069..bcdaedb66 100644 --- a/crates/idml/src/import/object_style.rs +++ b/crates/idml/src/import/object_style.rs @@ -1,5 +1,5 @@ use super::values::{finite, tint}; -use super::*; +use super::{Corner, DirectionalFeather, DropShadow, Effects, El, GradientFeather, HashMap, Importer, Node, Stroke, names}; /// InDesign's corner size where a file gives a corner shape without one. pub(super) const INDESIGN_CORNER_SIZE: f64 = 12.0; diff --git a/crates/idml/src/import/package.rs b/crates/idml/src/import/package.rs index 5a359ceb7..94cbe309a 100644 --- a/crates/idml/src/import/package.rs +++ b/crates/idml/src/import/package.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{HashMap, IdmlError, Read, Result}; /// Most entries an IDML package may hold. const MAX_ENTRIES: usize = 100_000; diff --git a/crates/idml/src/import/spread.rs b/crates/idml/src/import/spread.rs index 6c7d905ad..e2caa6b18 100644 --- a/crates/idml/src/import/spread.rs +++ b/crates/idml/src/import/spread.rs @@ -1,6 +1,8 @@ use super::frame::{fit_columns, margins_of}; use super::values::{nums, parse_affine}; -use super::*; +use super::{ + Affine, Arc, Columns, El, Guide, Importer, ItemId, Margins, Orientation, Page, PageId, PageSide, ParentInfo, Point, Rect, Spread, SpreadId, names, +}; /// View › Rotate Spread from a `Spread` element's `ItemTransform`: quarter turns clockwise on /// screen (IDML y points down, so `0 1 -1 0` is one clockwise turn). The spread's items and diff --git a/crates/idml/src/import/story.rs b/crates/idml/src/import/story.rs index 694a2dbbd..3d87fc35a 100644 --- a/crates/idml/src/import/story.rs +++ b/crates/idml/src/import/story.rs @@ -1,5 +1,8 @@ use super::text_attrs::{ace_char, clear_story_drop_cap_styles, inherits_drop_cap_style}; -use super::*; +use super::{ + Arc, BTreeMap, CharAttrs, CharFormat, CharRun, El, HashMap, HashSet, Importer, Item, ItemId, ParaFormat, Story, StoryId, Table, names, st, +}; +use crate::xml::Node; impl<'r> Importer<'r> { // ---------- stories ---------- diff --git a/crates/idml/src/import/styles.rs b/crates/idml/src/import/styles.rs index 0581feea0..5719393a2 100644 --- a/crates/idml/src/import/styles.rs +++ b/crates/idml/src/import/styles.rs @@ -1,6 +1,9 @@ use super::table::uniform_cell_stroke; use super::values::tint; -use super::*; +use super::{ + CHAR_BUILTINS, CharacterStyle, El, Fill, Importer, OBJECT_BUILTINS, ObjectStyle, PARA_BUILTINS, ParagraphStyle, Rule, names, st, swatch, + unescape_id, +}; impl<'r> Importer<'r> { // ---------- styles ---------- diff --git a/crates/idml/src/import/swatches.rs b/crates/idml/src/import/swatches.rs index d96c69919..bdf00d9cb 100644 --- a/crates/idml/src/import/swatches.rs +++ b/crates/idml/src/import/swatches.rs @@ -1,6 +1,9 @@ use super::frame::{Prefs, text_frame_options}; use super::values::{lab_to_color, nums}; -use super::*; +use super::{ + Color, ColorType, El, Gradient, GradientKind, GradientStop, HashMap, HashSet, Importer, Styles, Swatch, SwatchValue, TextFrameOptions, names, + swatch, +}; impl<'r> Importer<'r> { // ---------- swatches ---------- diff --git a/crates/idml/src/import/table.rs b/crates/idml/src/import/table.rs index 5e5a8685d..5b5ca744b 100644 --- a/crates/idml/src/import/table.rs +++ b/crates/idml/src/import/table.rs @@ -1,5 +1,5 @@ use super::values::{span, tint}; -use super::*; +use super::{CellRange, El, Importer, RowHeightMode, StoryId, Table, names, swatch}; /// Preserve the legacy uniform field only when all four sides explicitly specify every /// property. Partial records must stay sparse so they can inherit from a parent style. diff --git a/crates/idml/src/import/text_attrs.rs b/crates/idml/src/import/text_attrs.rs index 8921c9cd3..323c14b17 100644 --- a/crates/idml/src/import/text_attrs.rs +++ b/crates/idml/src/import/text_attrs.rs @@ -1,7 +1,10 @@ use super::object_style::corners; use super::story::MAX_STORY_NESTING; use super::values::tint; -use super::*; +use super::{ + Arc, CharAttrs, El, GridAlign, HashSet, IdmlError, Importer, Kerning, ListType, ParaAttrs, Result, SpanColumns, Story, Styles, TabStop, names, + st, unescape_id, +}; /// Whether sanitizing a file's tab list dropped, moved or shortened a stop. Sorting, and the `.` /// a char stop with no character aligns on, change nothing. @@ -213,49 +216,7 @@ impl<'r> Importer<'r> { Some("true") => a.start_at = Some(None), _ => {} } - // Nested and GREP styles. - if let Some(l) = e.prop_el("AllNestedStyles") { - let v: Vec = l - .find_all("ListItem") - .map(|it| { - let t = |k: &str| it.find(k).map(|x| x.text_content()).unwrap_or_default(); - let d = it.find("Delimiter"); - designcraft_doc::NestedStyle { - style: self.listed_char_style_ref(&t("AppliedCharacterStyle")), - through: t("Inclusive").trim() != "false", - count: t("Repetition").trim().parse().unwrap_or(1), - until: names::nested_until_in(d.and_then(|d| d.get("type")).unwrap_or("enumeration"), &t("Delimiter")), - } - }) - .collect(); - // IDML has no drop cap style attribute: the drop cap's character style is the list's - // leading "through 1 Dropcap" nested style. - a.drop_cap_style = v.first().filter(|ns| ns.is_drop_cap()).map(|ns| ns.style.clone()); - a.nested_styles = Some(v); - } - if let Some(l) = e.prop_el("AllNestedLineStyles") { - let v: Vec = l - .find_all("ListItem") - .map(|it| { - let t = |k: &str| it.find(k).map(|x| x.text_content()).unwrap_or_default(); - designcraft_doc::NestedLineStyle { - style: self.listed_char_style_ref(&t("AppliedCharacterStyle")), - lines: t("LineCount").trim().parse().unwrap_or(1), - } - }) - .collect(); - a.nested_line_styles = Some(v); - } - if let Some(l) = e.prop_el("AllGREPStyles") { - let v: Vec = l - .find_all("ListItem") - .map(|it| { - let t = |k: &str| it.find(k).map(|x| x.text_content()).unwrap_or_default(); - designcraft_doc::GrepStyle { style: self.listed_char_style_ref(&t("AppliedCharacterStyle")), pattern: t("GrepExpression") } - }) - .collect(); - a.grep_styles = Some(v); - } + self.read_nested_styles(e, &mut a); a.space_after = e.num("SpaceAfter"); // A number, or an enumeration (Ignore) that turns it off even over a based-on style's value. a.same_para_style_spacing = e.prop("SameParaStyleSpacing").map(|v| v.trim().parse::().ok().filter(|v| v.is_finite())); @@ -320,23 +281,7 @@ impl<'r> Importer<'r> { a.split_outside_gutter = e.num("SplitColumnOutsideGutter").filter(|v| v.is_finite()); a.rule_above = self.rule(e, "RuleAbove"); a.rule_below = self.rule(e, "RuleBelow"); - if let Some(tl) = e.prop_el("TabList") { - let mut tabs = Vec::new(); - for li in tl.find_all("ListItem") { - let get = |k: &str| li.find(k).map(|x| x.text_content()); - tabs.push(TabStop { - position: get("Position").and_then(|v| v.trim().parse::().ok()).filter(|v| v.is_finite()).unwrap_or(0.0), - align: names::tab_align_in(get("Alignment").unwrap_or_default().trim()), - leader: get("Leader").unwrap_or_default(), - align_on: get("AlignmentCharacter").filter(|c| c != ".").unwrap_or_default(), - }); - } - let sanitized = TabStop::sanitized_list(tabs.clone()); - if tab_list_repaired(&tabs, &sanitized) { - self.repaired_tab_lists = self.repaired_tab_lists.saturating_add(1); - } - a.tabs = Some(sanitized); - } + a.tabs = self.read_tabs(e); a.list_type = e.prop("BulletsAndNumberingListType").map(|v| match v.trim() { "BulletList" => ListType::Bullets, "NumberedList" => ListType::Numbers, @@ -417,6 +362,72 @@ impl<'r> Importer<'r> { } a } + + fn read_nested_styles(&self, e: &El, attrs: &mut ParaAttrs) { + if let Some(list) = e.prop_el("AllNestedStyles") { + let styles: Vec = list + .find_all("ListItem") + .map(|item| { + let text = |key: &str| item.find(key).map(|element| element.text_content()).unwrap_or_default(); + let delimiter = item.find("Delimiter"); + designcraft_doc::NestedStyle { + style: self.listed_char_style_ref(&text("AppliedCharacterStyle")), + through: text("Inclusive").trim() != "false", + count: text("Repetition").trim().parse().unwrap_or(1), + until: names::nested_until_in(delimiter.and_then(|element| element.get("type")).unwrap_or("enumeration"), &text("Delimiter")), + } + }) + .collect(); + // IDML has no drop cap style attribute: the drop cap's character style is the list's + // leading "through 1 Dropcap" nested style. + attrs.drop_cap_style = styles.first().filter(|style| style.is_drop_cap()).map(|style| style.style.clone()); + attrs.nested_styles = Some(styles); + } + if let Some(list) = e.prop_el("AllNestedLineStyles") { + let styles = list + .find_all("ListItem") + .map(|item| { + let text = |key: &str| item.find(key).map(|element| element.text_content()).unwrap_or_default(); + designcraft_doc::NestedLineStyle { + style: self.listed_char_style_ref(&text("AppliedCharacterStyle")), + lines: text("LineCount").trim().parse().unwrap_or(1), + } + }) + .collect(); + attrs.nested_line_styles = Some(styles); + } + if let Some(list) = e.prop_el("AllGREPStyles") { + let styles = list + .find_all("ListItem") + .map(|item| { + let text = |key: &str| item.find(key).map(|element| element.text_content()).unwrap_or_default(); + designcraft_doc::GrepStyle { style: self.listed_char_style_ref(&text("AppliedCharacterStyle")), pattern: text("GrepExpression") } + }) + .collect(); + attrs.grep_styles = Some(styles); + } + } + + fn read_tabs(&mut self, e: &El) -> Option> { + let list = e.prop_el("TabList")?; + let tabs: Vec<_> = list + .find_all("ListItem") + .map(|item| { + let get = |key: &str| item.find(key).map(|element| element.text_content()); + TabStop { + position: get("Position").and_then(|value| value.trim().parse::().ok()).filter(|value| value.is_finite()).unwrap_or(0.0), + align: names::tab_align_in(get("Alignment").unwrap_or_default().trim()), + leader: get("Leader").unwrap_or_default(), + align_on: get("AlignmentCharacter").filter(|character| character != ".").unwrap_or_default(), + } + }) + .collect(); + let sanitized = TabStop::sanitized_list(tabs.clone()); + if tab_list_repaired(&tabs, &sanitized) { + self.repaired_tab_lists = self.repaired_tab_lists.saturating_add(1); + } + Some(sanitized) + } } pub(super) fn ace_char(v: &str) -> Option { diff --git a/crates/idml/src/import/values.rs b/crates/idml/src/import/values.rs index dceb1ddff..41ad7bfe6 100644 --- a/crates/idml/src/import/values.rs +++ b/crates/idml/src/import/values.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Affine, Color, El, Point, lab}; /// An XMP date property, written as an element or an attribute. pub(super) fn xmp_date(xmp: &str, key: &str) -> Option { diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 53860a2f0..0d078c63c 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -733,6 +733,15 @@ fn kanji_page_numbers() { #[test] fn imports_hand_written_fixture() { let d = import_idml_with(&fixture(), &|_| None).unwrap(); + assert_fixture_settings_and_pages(&d); + assert_fixture_sections_and_layers(&d); + assert_fixture_swatches(&d); + assert_fixture_styles(&d); + assert_fixture_text(&d); + assert_fixture_items(&d); +} + +fn assert_fixture_settings_and_pages(d: &Document) { assert_eq!(d.title, "Fixture"); assert!(d.settings.facing_pages); assert_eq!((d.settings.page_width, d.settings.page_height), (500.0, 700.0)); @@ -748,12 +757,18 @@ fn imports_hand_written_fixture() { assert_eq!(sp.pages[0].parent, Some(d.parents[0].id)); let info = d.parents[0].parent.as_ref().unwrap(); assert_eq!((info.prefix.as_str(), info.name.as_str()), ("A", "Parent")); +} + +fn assert_fixture_sections_and_layers(d: &Document) { // Sections. assert_eq!(d.page_name(0), "v"); assert_eq!(d.page_name(1), "vi"); // Layers. assert_eq!(d.layers[0].name, "Art"); assert_eq!(d.layers[0].color, [255, 0, 0]); +} + +fn assert_fixture_swatches(d: &Document) { // Swatches. assert!(matches!(d.swatch("Brand").unwrap().value, SwatchValue::Color { color_type: designcraft_color::ColorType::Spot, .. })); assert!(matches!(&d.swatch("Brand 50%").unwrap().value, SwatchValue::Tint { base, tint } if base == "Brand" && *tint == 0.5)); @@ -767,6 +782,9 @@ fn imports_hand_written_fixture() { let mr = &d.parents[0].items[0]; assert_eq!(mr.fill.swatch, "C=10 M=20 Y=30 K=40"); assert_eq!(mr.bounds(), Rect::new(20.0, 20.0, 100.0, 50.0)); +} + +fn assert_fixture_styles(d: &Document) { // Styles. let body = d.styles.para("Text/Body").expect("grouped style"); assert_eq!(body.based_on.as_deref(), Some(st::BASIC_PARAGRAPH)); @@ -774,6 +792,9 @@ fn imports_hand_written_fixture() { assert_eq!(body.chars.leading, Some(designcraft_doc::Leading::Points(13.0))); assert_eq!(body.para.align, Some(designcraft_doc::Align::LeftJustified)); assert_eq!(d.styles.char_style("Strong").unwrap().chars.font_style.as_deref(), Some("Bold")); +} + +fn assert_fixture_text(d: &Document) { // Frames, threads, text. let story = d.stories.values().next().unwrap(); assert_eq!(story.text, format!("Hello & bold\nPage {}\tend{}\nafter", st::PAGE_NUMBER, st::FRAME_BREAK)); @@ -791,6 +812,10 @@ fn imports_hand_written_fixture() { let o = &f1.text_frame().unwrap().options; assert_eq!((o.columns, o.gutter, o.inset), (2, 8.0, [1.0, 2.0, 3.0, 4.0])); assert_eq!(d.item(story.frames[1]).unwrap().bounds(), Rect::new(550.0, 50.0, 650.0, 150.0)); +} + +fn assert_fixture_items(d: &Document) { + let sp = &d.spreads[0]; // Group child keeps its own transform relative to the group. let g = sp.items.iter().find(|i| i.shape == Shape::Group).unwrap(); assert_eq!(g.bounds(), Rect::new(510.0, 370.0, 560.0, 420.0)); @@ -2431,7 +2456,20 @@ fn fixture_with_table_styles(styles: &str, story: &str) -> Vec { fn table_style_cascade_preserves_sparse_edges_and_local_overrides() { let d = import_idml(&fixture_with_table_styles(CASCADE_STYLES, CASCADE_STORY)).unwrap(); d.check().unwrap(); - let table = d.stories.values().flat_map(|s| s.tables.values()).next().unwrap(); + let table = first_table(&d); + assert_cascade_table_options(table); + assert_cascade_header_cell(table); + assert_cascade_explicit_cell(table); + assert_cascade_local_cell(table); + assert_cascade_body_cells(table); + assert_cascade_round_trip(&d, table); +} + +fn first_table(doc: &Document) -> &designcraft_doc::Table { + doc.stories.values().flat_map(|s| s.tables.values()).next().unwrap() +} + +fn assert_cascade_table_options(table: &designcraft_doc::Table) { assert_eq!((table.nrows(), table.ncols()), (2, 3)); assert_eq!(table.style, "Table"); assert_eq!(table.options.space_before, 19.0); @@ -2440,6 +2478,9 @@ fn table_style_cascade_preserves_sparse_edges_and_local_overrides() { let fills = table.options.alt_rows.as_ref().unwrap(); assert_eq!((fills.first, fills.next, fills.first_tint), (1, 1, 0.2)); assert_eq!(fills.first_color, "Brand"); +} + +fn assert_cascade_header_cell(table: &designcraft_doc::Table) { let header = table.cell(0, 0).unwrap(); assert_eq!(header.fill, designcraft_color::swatch::PAPER, "header takes precedence over left region"); assert!(header.style.is_empty(), "a table region does not become AppliedCellStyle"); @@ -2453,6 +2494,9 @@ fn table_style_cascade_preserves_sparse_edges_and_local_overrides() { assert_eq!(header.border_overrides, [true; 4]); assert_eq!(header.stroke_defined, [true; 4]); assert_eq!(header.stroke_priorities, [0; 4]); +} + +fn assert_cascade_explicit_cell(table: &designcraft_doc::Table) { let explicit = table.cell(0, 1).unwrap(); assert_eq!(explicit.fill, "Brand"); assert_eq!(explicit.fill_tint, 0.75); @@ -2461,6 +2505,9 @@ fn table_style_cascade_preserves_sparse_edges_and_local_overrides() { assert_eq!(explicit.strokes[0].color, designcraft_color::swatch::NONE); assert_eq!(explicit.strokes[3].weight, 5.0); assert_eq!(explicit.strokes[3].tint, 0.3); +} + +fn assert_cascade_local_cell(table: &designcraft_doc::Table) { let local = table.cell(0, 2).unwrap(); assert_eq!(local.fill, designcraft_color::swatch::NONE); assert_eq!(local.fill_tint, 0.0); @@ -2470,13 +2517,18 @@ fn table_style_cascade_preserves_sparse_edges_and_local_overrides() { assert_eq!(local.strokes[3].color, designcraft_color::swatch::NONE); assert_eq!(local.stroke_priorities, [0, 0, 0, 7]); assert_eq!(local.text.paras[0].style, st::BASIC_PARAGRAPH, "explicit paragraph style wins"); +} + +fn assert_cascade_body_cells(table: &designcraft_doc::Table) { assert_eq!(table.cell(1, 0).unwrap().fill, designcraft_color::swatch::BLACK); assert_eq!(table.cell(1, 0).unwrap().strokes[1].weight, 0.0); assert_eq!(table.cell(1, 1).unwrap().fill_tint, 0.5); assert_eq!(table.cell(1, 2).unwrap().fill, "Brand"); +} - let back = import_idml(&export_idml(&d)).unwrap(); - let roundtrip = back.stories.values().flat_map(|s| s.tables.values()).next().unwrap(); +fn assert_cascade_round_trip(d: &Document, table: &designcraft_doc::Table) { + let back = import_idml(&export_idml(d)).unwrap(); + let roundtrip = first_table(&back); for (before, after) in table.cells.iter().zip(&roundtrip.cells) { assert_eq!(after.strokes, before.strokes); assert_eq!(after.border_overrides, before.border_overrides); @@ -3000,14 +3052,22 @@ fn kenten_shatai_and_grid_settings_import_and_round_trip() { #[test] fn ruby_settings_import_from_idml_and_round_trip() { - use designcraft_doc::cjk_settings::AdornmentOverprint; - use designcraft_doc::ruby::{RubyAlignment, RubyOverhang, RubyParentSpacing, RubyPosition, RubyType}; // InDesign's names: RubyFlag is a number, colours and fonts are properties. let story = r#" $ID/NothingText Color漢字 "#; - let doc = import_idml(&fixture_with_story(story)).unwrap(); + let imported = import_idml(&fixture_with_story(story)).unwrap(); + assert_imported_ruby_settings(&imported); + + let doc = ruby_round_trip_document(); + let back = import_idml(&export_idml(&doc)).unwrap(); + assert_ruby_round_trip(&doc, &back); +} + +fn assert_imported_ruby_settings(doc: &Document) { + use designcraft_doc::cjk_settings::AdornmentOverprint; + use designcraft_doc::ruby::{RubyAlignment, RubyOverhang, RubyParentSpacing, RubyPosition, RubyType}; let a = doc.stories.values().find(|s| s.text.contains("漢字")).unwrap().runs().next().unwrap().1.over.clone(); assert_eq!(a.ruby.as_deref(), Some("かん じ")); assert_eq!(a.ruby_type, Some(RubyType::PerCharacter)); @@ -3022,8 +3082,11 @@ fn ruby_settings_import_from_idml_and_round_trip() { assert_eq!(a.ruby_auto_tcy_digits, Some(2)); assert_eq!((a.ruby_font.as_deref(), a.ruby_font_style.as_deref(), a.ruby_fill.as_deref()), (Some(""), Some(""), Some(""))); assert_eq!((a.ruby_fill_tint, a.ruby_overprint_fill), (Some(None), Some(AdornmentOverprint::Auto))); +} - // Every setting survives export and import. +fn ruby_round_trip_document() -> Document { + use designcraft_doc::cjk_settings::AdornmentOverprint; + use designcraft_doc::ruby::{RubyAlignment, RubyOverhang, RubyParentSpacing, RubyPosition, RubyType}; let mut d = Document::new(&NewDocument::default()); let lid = d.default_layer(); let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(72.0, 72.0, 300.0, 200.0), lid, "漢字", ParaFormat::default()).unwrap(); @@ -3058,8 +3121,11 @@ fn ruby_settings_import_from_idml_and_round_trip() { o.ruby_overprint_fill = Some(AdornmentOverprint::On); o.ruby_overprint_stroke = Some(AdornmentOverprint::Off); }); - let a = d.stories[&sid].runs().next().unwrap().1.over.clone(); - let back = import_idml(&export_idml(&d)).unwrap(); + d +} + +fn assert_ruby_round_trip(d: &Document, back: &Document) { + let a = d.stories.values().find(|s| s.text.contains("漢字")).unwrap().runs().next().unwrap().1.over.clone(); let b = back.stories.values().find(|s| s.text.contains("漢字")).unwrap().runs().next().unwrap().1.over.clone(); macro_rules! same { ($($f:ident),*) => { $( assert_eq!(a.$f, b.$f, stringify!($f)); )* };