diff --git a/crates/engine/src/cmd/slide.rs b/crates/engine/src/cmd/slide.rs index 3d8ad344..105678ca 100644 --- a/crates/engine/src/cmd/slide.rs +++ b/crates/engine/src/cmd/slide.rs @@ -108,7 +108,12 @@ fn new(s: &mut Session, p: &Value) -> Result { if let Some(b) = str_param(p, "body") && let Some(sh) = slide.shapes.iter_mut().find(|x| x.ph_type().is_some_and(|k| !k.is_title())) { - sh.text = Some(TextBody::from_text(b)); + let mut body = TextBody::from_text(b); + // Leading tabs set the outline level, as in text.set. + for para in &mut body.paragraphs { + super::text::apply_leading_tabs(para); + } + sh.text = Some(body); } let at = usize_param(p, "at").unwrap_or(if doc.slides.is_empty() { 0 } else { sel.slide + 1 }).min(doc.slides.len()); let id = slide.id; @@ -584,3 +589,24 @@ fn from_outline(s: &mut Session, p: &Value) -> Result { Ok(json!({"slides": n})) }) } + +#[cfg(test)] +mod tests { + use serde_json::json; + + use crate::Session; + + #[test] + fn new_slide_body_leading_tabs_become_levels() { + let mut s = Session::with_new(); + let text = "Level one\n\tLevel two\n\t\tLevel three\nLevel one again"; + s.execute("slide.new", &json!({"layout": "titleAndContent", "title": "Indent", "body": text})).expect("new slide"); + let st = s.doc().expect("doc"); + let slide = st.doc.slides.get(st.selection.slide).expect("slide"); + let body = slide.shapes.iter().find(|x| x.ph_type().is_some_and(|k| !k.is_title())).expect("body"); + let paras = &body.text.as_ref().expect("text").paragraphs; + let got: Vec<(u8, String)> = paras.iter().map(|p| (p.level, p.text())).collect(); + let want = [(0, "Level one"), (1, "Level two"), (2, "Level three"), (0, "Level one again")]; + assert_eq!(got, want.iter().map(|(l, t)| (*l, t.to_string())).collect::>()); + } +} diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index 1b2cb2d2..545ce54c 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -687,6 +687,20 @@ fn select_para(s: &mut Session, p: &Value) -> Result { ok() } +/// Turn leading tabs of a paragraph into its outline level (capped at 8) and strip them. +/// Returns whether any tab was found. +pub(crate) fn apply_leading_tabs(para: &mut deckcraft_model::text::Paragraph) -> bool { + let tabs = para.runs.first().map(|r| r.text.chars().take_while(|c| *c == '\t').count()).unwrap_or(0); + if tabs == 0 { + return false; + } + if let Some(r) = para.runs.first_mut() { + r.text = r.text.trim_start_matches('\t').to_string(); + } + para.level = tabs.min(8) as u8; + true +} + fn set(s: &mut Session, p: &Value) -> Result { let text = str_param(p, "text").ok_or_else(|| bad("text.set", "missing `text`"))?.to_string(); let id = match id_param(p, "id") { @@ -706,13 +720,9 @@ fn set(s: &mut Session, p: &Value) -> Result { let mut nb = TextBody::from_text(&text); for (i, para) in nb.paragraphs.iter_mut().enumerate() { // Leading tabs set the outline level. - let tabs = para.runs.first().map(|r| r.text.chars().take_while(|c| *c == '\t').count()).unwrap_or(0); - if tabs > 0 - && let Some(r) = para.runs.first_mut() + if !apply_leading_tabs(para) + && let Some((lv, pp)) = levels.get(i) { - r.text = r.text.trim_start_matches('\t').to_string(); - para.level = tabs.min(8) as u8; - } else if let Some((lv, pp)) = levels.get(i) { para.level = *lv; para.props = pp.clone(); }