mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-02 06:36:51 -06:00
Bullets and numbering: the tab position survives IDML
IDML has no attribute for the list Tab Position. Export writes it as the left stop the composer adds for it, in the TabList of each style or paragraph that sets the list type, the tab position or the tabs; a level that ends an inherited list writes its tabs without the stop. Import reads it back as an ordinary tab stop, so the composed text is the same.
This commit is contained in:
@@ -859,6 +859,11 @@ impl<'a> Ex<'a> {
|
||||
}
|
||||
(ps.para.clone(), ps.chars.clone())
|
||||
};
|
||||
let parent = match ps.based_on.as_deref() {
|
||||
Some(b) if ps.name != designcraft_doc::NO_PARA_STYLE => styles.resolve_para_style(b).0,
|
||||
_ => designcraft_doc::ParaProps::default(),
|
||||
};
|
||||
let pa = with_list_tab(pa, || (styles.resolve_para_style(&ps.name).0, parent));
|
||||
self.para_attrs(&mut el, &mut props, &pa);
|
||||
self.char_attrs(&mut el, &mut props, &ca);
|
||||
pg.insert(&ps.name, with_props(el, props));
|
||||
@@ -2469,7 +2474,9 @@ impl<'a> Ex<'a> {
|
||||
fn psr_el(&mut self, pf: &ParaFormat) -> El {
|
||||
let mut el = El::new("ParagraphStyleRange").attr("AppliedParagraphStyle", names::style_self("ParagraphStyle", PARA_BUILTINS, &pf.style));
|
||||
let mut props = Vec::new();
|
||||
self.para_attrs(&mut el, &mut props, &pf.para);
|
||||
let styles = &self.d.styles;
|
||||
let pa = with_list_tab(pf.para.clone(), || (styles.resolve_para(pf).0, styles.resolve_para_style(&pf.style).0));
|
||||
self.para_attrs(&mut el, &mut props, &pa);
|
||||
self.char_attrs(&mut el, &mut props, &pf.chars);
|
||||
with_props(el, props)
|
||||
}
|
||||
@@ -2487,6 +2494,37 @@ impl<'a> Ex<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The left stop the composer adds for a list's Tab Position, if it adds one.
|
||||
fn list_tab_stop(pp: &designcraft_doc::ParaProps) -> Option<f64> {
|
||||
if !matches!(pp.list_type, ListType::Bullets | ListType::Numbers) {
|
||||
return None;
|
||||
}
|
||||
pp.list_tab.filter(|t| t.is_finite() && *t >= 0.0 && !pp.tabs.iter().any(|s| (s.position - t).abs() < 1e-6))
|
||||
}
|
||||
|
||||
/// IDML has no attribute for the bullets-and-numbering Tab Position, so it is written as the left
|
||||
/// stop the composer adds for it, in the TabList of each level (style or paragraph) that sets the
|
||||
/// list type, the tab position or the tabs. `resolved` gives this level's resolved attributes and
|
||||
/// those it inherits; the stop comes back on import as an ordinary tab stop.
|
||||
fn with_list_tab(mut a: ParaAttrs, resolved: impl FnOnce() -> (designcraft_doc::ParaProps, designcraft_doc::ParaProps)) -> ParaAttrs {
|
||||
if a.list_tab.is_none() && a.tabs.is_none() && a.list_type.is_none() {
|
||||
return a;
|
||||
}
|
||||
let (here, parent) = resolved();
|
||||
let stop = list_tab_stop(&here);
|
||||
// An inherited TabList holds the parent's stop; a level that ends the list writes its tabs without it.
|
||||
if stop.is_none() && list_tab_stop(&parent).is_none() {
|
||||
return a;
|
||||
}
|
||||
let mut tabs = here.tabs;
|
||||
if let Some(t) = stop {
|
||||
tabs.push(designcraft_doc::TabStop { position: t, align: designcraft_doc::TabAlign::Left, leader: String::new(), align_on: String::new() });
|
||||
tabs.sort_by(|a, b| a.position.total_cmp(&b.position));
|
||||
}
|
||||
a.tabs = Some(tabs);
|
||||
a
|
||||
}
|
||||
|
||||
/// Style folder tree (names `A/B/C` → groups A, B).
|
||||
#[derive(Default)]
|
||||
struct Group {
|
||||
|
||||
@@ -1460,3 +1460,41 @@ fn list_numbering_format_expression_and_bullet_import_and_round_trip() {
|
||||
check(&d);
|
||||
check(&import_idml(&export_idml(&d)).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_tab_position_round_trips_as_a_tab_stop() {
|
||||
use designcraft_doc::{ListType, ParaAttrs, ParagraphStyle, TabAlign, TabStop};
|
||||
let stop = |position, align| TabStop { position, align, leader: String::new(), align_on: String::new() };
|
||||
let mut d = Document::new(&NewDocument::default());
|
||||
std::sync::Arc::make_mut(&mut d.styles).paragraph.push(ParagraphStyle {
|
||||
name: "List".into(),
|
||||
based_on: None,
|
||||
next_style: None,
|
||||
para: ParaAttrs { list_type: Some(ListType::Bullets), list_tab: Some(Some(24.0)), ..Default::default() },
|
||||
chars: CharAttrs::default(),
|
||||
shortcut: String::new(),
|
||||
});
|
||||
let local = ParaAttrs {
|
||||
list_type: Some(ListType::Bullets),
|
||||
list_tab: Some(Some(36.0)),
|
||||
tabs: Some(vec![stop(100.0, TabAlign::Right)]),
|
||||
..Default::default()
|
||||
};
|
||||
let ended = ParaAttrs { list_type: Some(ListType::None), ..Default::default() };
|
||||
let lid = d.default_layer();
|
||||
for (i, (text, style, para)) in
|
||||
[("Local\tx", "", local), ("Styled\ty", "List", ParaAttrs::default()), ("Ended\tz", "List", ended)].into_iter().enumerate()
|
||||
{
|
||||
let pf = ParaFormat { style: if style.is_empty() { ParaFormat::default().style } else { style.into() }, para, ..Default::default() };
|
||||
let y = 40.0 + 100.0 * i as f64;
|
||||
d.add_text_frame(SpreadRef::Doc(0), Rect::new(40.0, y, 300.0, y + 80.0), lid, text, pf).unwrap();
|
||||
}
|
||||
let back = import_idml(&export_idml(&d)).unwrap();
|
||||
let tabs = |prefix: &str| {
|
||||
let s = back.stories.values().find(|s| s.text.starts_with(prefix)).unwrap();
|
||||
back.styles.resolve_para(&s.paras[0]).0.tabs
|
||||
};
|
||||
assert_eq!(tabs("Local"), vec![stop(36.0, TabAlign::Left), stop(100.0, TabAlign::Right)]);
|
||||
assert_eq!(tabs("Styled"), vec![stop(24.0, TabAlign::Left)]);
|
||||
assert_eq!(tabs("Ended"), vec![], "a paragraph that ends the list has no list tab stop");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user