mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-30 13:27:27 -06:00
Lists: a numbered list after a bulleted list keeps its numbering in ODT, RTF and Markdown (#83) (#87)
* Lists: a numbered list after a bulleted list keeps its numbering in ODT, RTF and Markdown (#83) * Import: a top-level list item of the other kind starts a new list The writers now split a bulleted list from a following numbered one, but the shared importer merged consecutive list paragraphs back into one list, so ODT and RTF still read the numbered items as bullets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
8907b589ac
commit
c29e70ee8a
@@ -1221,6 +1221,7 @@ fn write_blocks(blocks: &[FBlock], out: &mut Vec<String>) {
|
||||
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<String>) {
|
||||
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
|
||||
|
||||
@@ -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<ListInfo> = Vec::new();
|
||||
let mut top: Option<bool> = 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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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<usize> = 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
|
||||
{
|
||||
|
||||
@@ -163,6 +163,37 @@ fn check_round_trip(ext: &str, rich: bool) {
|
||||
assert_eq!(table.rows[1].iter().map(|c| c.text()).collect::<Vec<_>>(), 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);
|
||||
|
||||
Reference in New Issue
Block a user