diff --git a/crates/formats/src/markdown.rs b/crates/formats/src/markdown.rs index 824917434..ca688eafb 100644 --- a/crates/formats/src/markdown.rs +++ b/crates/formats/src/markdown.rs @@ -1221,6 +1221,7 @@ fn write_blocks(blocks: &[FBlock], out: &mut Vec) { let mut i = 0; let mut counters = [0u32; 9]; let mut prev_list = false; + let mut prev_top_ordered = false; while let Some(b) = blocks.get(i) { match b { FBlock::Table(t) => { @@ -1251,9 +1252,12 @@ fn write_blocks(blocks: &[FBlock], out: &mut Vec) { match p.list { Some(li) => { let lv = li.level.min(8) as usize; - if !prev_list { + if !prev_list || (lv == 0 && prev_top_ordered != li.ordered) { counters = [0; 9]; } + if lv == 0 { + prev_top_ordered = li.ordered; + } let n = counters.get_mut(lv).map(|c| { *c += 1; *c diff --git a/crates/formats/src/model.rs b/crates/formats/src/model.rs index 5edbeb0aa..c6c0f35b3 100644 --- a/crates/formats/src/model.rs +++ b/crates/formats/src/model.rs @@ -383,11 +383,19 @@ impl Builder<'_> { while let Some(b) = blocks.get(i) { match b { FBlock::Para(p) if p.list.is_some() => { - // A run of consecutive list paragraphs is one list. + // A run of consecutive list paragraphs is one list, until a top-level item of + // the other kind (bulleted or numbered) starts a new one. let mut j = i; - let mut group = Vec::new(); + let mut group: Vec = Vec::new(); + let mut top: Option = None; while let Some(FBlock::Para(q)) = blocks.get(j) { let Some(li) = q.list else { break }; + if li.level == 0 { + if top.is_some_and(|o| o != li.ordered) { + break; + } + top = Some(li.ordered); + } group.push(li); j += 1; } diff --git a/crates/formats/src/odt.rs b/crates/formats/src/odt.rs index 151b8c2ed..483e347e7 100644 --- a/crates/formats/src/odt.rs +++ b/crates/formats/src/odt.rs @@ -283,6 +283,10 @@ impl Writer { while let Some(FBlock::Para(q)) = blocks.get(i) { let Some(li) = q.list else { break }; let l = li.level.min(8) as usize; + // A top-level item of the other kind starts a new list. + if l == 0 && seen[0] && kinds[0] != li.ordered { + break; + } if let (Some(s), Some(k)) = (seen.get_mut(l), kinds.get_mut(l)) && !*s { diff --git a/crates/formats/src/rtf.rs b/crates/formats/src/rtf.rs index 5aeaa6e08..8713617dd 100644 --- a/crates/formats/src/rtf.rs +++ b/crates/formats/src/rtf.rs @@ -238,6 +238,7 @@ impl Writer { fn blocks(&mut self, blocks: &[FBlock], intbl: bool, last_end: &str, out: &mut String, depth: usize) { let mut counter = model::ListCounter::default(); let mut cur_list: Option = None; + let mut top_ordered = false; let n = blocks.len(); for (i, b) in blocks.iter().enumerate() { let end = if i + 1 == n { last_end } else { "\\par" }; @@ -245,6 +246,13 @@ impl Writer { FBlock::Para(p) => { let ls = match p.list { Some(li) => { + // A top-level item of the other kind starts a new list. + if li.level == 0 && cur_list.is_some() && top_ordered != li.ordered { + cur_list = None; + } + if li.level == 0 { + top_ordered = li.ordered; + } let id = match cur_list { Some(id) => id, None => { @@ -255,6 +263,9 @@ impl Writer { let FBlock::Para(q) = q else { break }; let Some(ql) = q.list else { break }; let l = ql.level.min(8) as usize; + if l == 0 && seen[0] && kinds[0] != ql.ordered { + break; + } if let (Some(s), Some(k)) = (seen.get_mut(l), kinds.get_mut(l)) && !*s { diff --git a/crates/formats/src/tests.rs b/crates/formats/src/tests.rs index 3ef61c2f3..7def809b2 100644 --- a/crates/formats/src/tests.rs +++ b/crates/formats/src/tests.rs @@ -163,6 +163,37 @@ fn check_round_trip(ext: &str, rich: bool) { assert_eq!(table.rows[1].iter().map(|c| c.text()).collect::>(), vec!["alpha", "42"], "{ext}"); } +/// Two bullets directly followed by two numbered items. +fn bullets_then_numbers() -> Document { + let mut d = Document::new(); + let bul = d.numbering.add_list(wordcraft_doc::ListKind::Bullet); + let num = d.numbering.add_list(wordcraft_doc::ListKind::Numbered); + let mut blocks = Vec::new(); + for (t, nm) in [("dot one", bul), ("dot two", bul), ("num one", num), ("num two", num)] { + let mut q = Paragraph::with_text(t, CharProps::default()).styled("ListParagraph"); + q.props.numbering = Some(wordcraft_doc::props::NumRef { num: nm, level: 0 }); + blocks.push(para_block(q)); + } + d.body = blocks; + d +} + +#[test] +fn numbered_list_after_bullets_keeps_its_kind() { + let d = bullets_then_numbers(); + for ext in ["odt", "rtf"] { + let bytes = export(ext, &d).expect("handled").expect("export"); + let back = import(ext, &bytes).expect("handled").expect("import"); + let b = flat(&back); + let kind = |t: &str| find_para(&b, t).and_then(|p| p.list).map(|l| l.ordered); + assert_eq!(kind("dot two"), Some(false), "{ext}"); + assert_eq!(kind("num one"), Some(true), "{ext}"); + assert_eq!(kind("num two"), Some(true), "{ext}"); + } + let md = String::from_utf8(export("md", &d).expect("handled").expect("export")).unwrap(); + assert!(md.contains("1. num one") && md.contains("2. num two"), "{md}"); +} + #[test] fn markdown_round_trip() { check_round_trip("md", true);