mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-28 20:37:10 -06:00
Keep an ordered list's start number in Markdown import (#3103)
Found while testing crates/formats/src/markdown.rs:160-161,463 and crates/formats/src/model.rs:111 (ListInfo) Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
74ab8d2819
commit
e8866ea58b
@@ -803,7 +803,7 @@ impl<'r> Builder<'r> {
|
||||
}
|
||||
}
|
||||
if let (Some((o, _)), Some(li)) = (list_level, li_at) {
|
||||
p.list = Some(ListInfo { ordered: o, level: lists.saturating_sub(1).min(8) as u8 });
|
||||
p.list = Some(ListInfo { ordered: o, level: lists.saturating_sub(1).min(8) as u8, start: 1 });
|
||||
self.para_cont = self.stack.get(li).is_some_and(|e| e.used);
|
||||
if let Some(e) = self.stack.get_mut(li) {
|
||||
e.used = true;
|
||||
|
||||
@@ -2377,7 +2377,7 @@ impl<'a> Parser<'a> {
|
||||
let level = self.lists.len().saturating_sub(1).min(8) as u8;
|
||||
self.para = Para::default();
|
||||
if let Some(&ordered) = self.lists.last() {
|
||||
self.para.list = Some(ListInfo { ordered, level });
|
||||
self.para.list = Some(ListInfo { ordered, level, start: 1 });
|
||||
}
|
||||
let label = self.opt_arg();
|
||||
if let Some(l) = label {
|
||||
@@ -2896,8 +2896,8 @@ Caf\'e na\"ive \c{c}a \v{s}\'{e} and {\bfseries grouped bold} after.
|
||||
assert_eq!(find("On Sample Documents").kind, Kind::Title);
|
||||
assert_eq!(find("Introduction").kind, Kind::Heading(1));
|
||||
assert_eq!(find("Lists").kind, Kind::Heading(2));
|
||||
assert_eq!(find("First").list, Some(ListInfo { ordered: false, level: 0 }));
|
||||
assert_eq!(find("Nested one").list, Some(ListInfo { ordered: true, level: 1 }));
|
||||
assert_eq!(find("First").list, Some(ListInfo { ordered: false, level: 0, start: 1 }));
|
||||
assert_eq!(find("Nested one").list, Some(ListInfo { ordered: true, level: 1, start: 1 }));
|
||||
assert!(ps.iter().any(|p| p.text().trim() == "b) Nested two" && p.list.is_some()), "{:?}", texts(ARTICLE));
|
||||
assert_eq!(find("Quoted text.").kind, Kind::Quote);
|
||||
assert_eq!(find("Centered.").align, Some(Align::Center));
|
||||
|
||||
@@ -460,7 +460,7 @@ fn parse_list(lines: &[String], mut i: usize, depth: usize, level: u8, out: &mut
|
||||
while item.len() > 1 && item.last().is_some_and(|x| x.is_empty()) {
|
||||
item.pop();
|
||||
}
|
||||
let li = ListInfo { ordered: first.ordered, level: level.min(8) };
|
||||
let li = ListInfo { ordered: first.ordered, level: level.min(8), start: first.start };
|
||||
let inner = if depth >= MAX_NEST {
|
||||
let mut p = Para::default();
|
||||
p.push_text(&item.join(" "), &Fmt::default());
|
||||
@@ -1355,7 +1355,10 @@ fn write_blocks(blocks: &[FBlock], out: &mut Vec<String>) {
|
||||
prev_top_ordered = li.ordered;
|
||||
}
|
||||
let n = counters.get_mut(lv).map(|c| {
|
||||
*c += 1;
|
||||
if *c == 0 {
|
||||
*c = li.start.saturating_sub(1);
|
||||
}
|
||||
*c = c.saturating_add(1);
|
||||
*c
|
||||
});
|
||||
for d in counters.iter_mut().skip(lv + 1) {
|
||||
@@ -1703,6 +1706,14 @@ mod tests {
|
||||
assert_eq!(back_table2.rows[1][1].text(), "CRITICAL_DISTINCTIVE_INFO");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordered_list_keeps_start_number() {
|
||||
let txt = |md: &str| String::from_utf8_lossy(&crate::txt::export(&import(md))).into_owned();
|
||||
assert_eq!(txt("5. a\n6. b"), "5.\ta\r\n6.\tb");
|
||||
assert_eq!(txt("3. a\n- b\n4. c"), "3.\ta\r\n•\tb\r\n4.\tc");
|
||||
assert_eq!(export(&import("5. a\n6. b")).trim_end(), "5. a\n6. b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lines_after_line_break_stay_in_paragraph() {
|
||||
use wordcraft_doc::{CharProps, Document, Paragraph, StoryRef, para_block};
|
||||
@@ -1720,10 +1731,10 @@ mod tests {
|
||||
fn lines_after_line_break_stay_in_list_item_and_quote() {
|
||||
let cases = ["a\n- x", "a\n# h", "a\n1. x", "a\n--", "a\n---", "a\n==", "a\n> q"];
|
||||
let shapes: [(Kind, Option<ListInfo>); 5] = [
|
||||
(Kind::Normal, Some(ListInfo { ordered: false, level: 0 })),
|
||||
(Kind::Normal, Some(ListInfo { ordered: true, level: 0 })),
|
||||
(Kind::Normal, Some(ListInfo { ordered: false, level: 1 })),
|
||||
(Kind::Normal, Some(ListInfo { ordered: true, level: 1 })),
|
||||
(Kind::Normal, Some(ListInfo { ordered: false, level: 0, start: 1 })),
|
||||
(Kind::Normal, Some(ListInfo { ordered: true, level: 0, start: 1 })),
|
||||
(Kind::Normal, Some(ListInfo { ordered: false, level: 1, start: 1 })),
|
||||
(Kind::Normal, Some(ListInfo { ordered: true, level: 1, start: 1 })),
|
||||
(Kind::Quote, None),
|
||||
];
|
||||
for s in cases {
|
||||
@@ -1731,7 +1742,7 @@ mod tests {
|
||||
let mut blocks = Vec::new();
|
||||
if let Some(li) = list.filter(|l| l.level > 0) {
|
||||
let mut parent = Para::new(Kind::Normal);
|
||||
parent.list = Some(ListInfo { ordered: li.ordered, level: 0 });
|
||||
parent.list = Some(ListInfo { ordered: li.ordered, level: 0, start: 1 });
|
||||
parent.push_text("top", &Fmt::default());
|
||||
blocks.push(FBlock::Para(parent));
|
||||
}
|
||||
|
||||
@@ -112,6 +112,8 @@ pub struct ListInfo {
|
||||
pub ordered: bool,
|
||||
/// 0-based nesting level (0..=8).
|
||||
pub level: u8,
|
||||
/// Number of the first item of an ordered list (1 unless the source says otherwise).
|
||||
pub start: u32,
|
||||
}
|
||||
|
||||
/// Paragraph spacing and indents (points) that a source states on the paragraph itself (RTF);
|
||||
@@ -587,13 +589,25 @@ impl<'a> Builder<'a> {
|
||||
*k = Some(li.ordered);
|
||||
}
|
||||
}
|
||||
let mut starts: [Option<u32>; 9] = [None; 9];
|
||||
for li in group {
|
||||
if let Some(s) = starts.get_mut(li.level.min(8) as usize)
|
||||
&& s.is_none()
|
||||
{
|
||||
*s = Some(li.start);
|
||||
}
|
||||
}
|
||||
// Levels nobody used follow the first level's kind.
|
||||
let first = group.first().map(|l| l.ordered).unwrap_or(false);
|
||||
let levels: Vec<Level> = (0..9)
|
||||
.map(|i| {
|
||||
let ordered = kinds.get(i).copied().flatten().unwrap_or(first);
|
||||
let src = if ordered { &num_levels } else { &bul_levels };
|
||||
src.get(i).cloned().unwrap_or_default()
|
||||
let mut level = src.get(i).cloned().unwrap_or_default();
|
||||
if ordered && let Some(s) = starts.get(i).copied().flatten() {
|
||||
level.start = s;
|
||||
}
|
||||
level
|
||||
})
|
||||
.collect();
|
||||
let numbering = &mut self.doc.numbering;
|
||||
@@ -851,7 +865,7 @@ fn props_kind(doc: &Document, props: &ParaProps, blank: bool) -> (Kind, Option<L
|
||||
};
|
||||
let list = rp.numbering.and_then(|n| {
|
||||
let lv = doc.numbering.level(n.num, n.level)?;
|
||||
Some(ListInfo { ordered: !matches!(lv.format, NumFormat::Bullet | NumFormat::None), level: n.level.min(8) })
|
||||
Some(ListInfo { ordered: !matches!(lv.format, NumFormat::Bullet | NumFormat::None), level: n.level.min(8), start: lv.start })
|
||||
});
|
||||
(kind, list)
|
||||
}
|
||||
@@ -1249,6 +1263,9 @@ impl ListCounter {
|
||||
}
|
||||
let n = match self.counts.get_mut(lv) {
|
||||
Some(c) => {
|
||||
if *c == 0 {
|
||||
*c = li.start.saturating_sub(1);
|
||||
}
|
||||
*c = c.saturating_add(1);
|
||||
*c
|
||||
}
|
||||
@@ -1361,12 +1378,12 @@ mod tests {
|
||||
#[test]
|
||||
fn list_labels() {
|
||||
let mut c = ListCounter::default();
|
||||
let o = |level| ListInfo { ordered: true, level };
|
||||
let o = |level| ListInfo { ordered: true, level, start: 1 };
|
||||
assert_eq!(c.label(o(0)), "1.");
|
||||
assert_eq!(c.label(o(1)), "a.");
|
||||
assert_eq!(c.label(o(1)), "b.");
|
||||
assert_eq!(c.label(o(0)), "2.");
|
||||
assert_eq!(c.label(ListInfo { ordered: false, level: 0 }), "•");
|
||||
assert_eq!(c.label(ListInfo { ordered: false, level: 0, start: 1 }), "•");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1611,7 +1611,7 @@ impl Body<'_> {
|
||||
{
|
||||
let style = self.lists.iter().rev().find_map(|l| l.0.clone());
|
||||
let ordered = style.and_then(|s| self.styles.lists.get(&s)).and_then(|v| v.get(lv.min(9)).copied()).unwrap_or(false);
|
||||
p.list = Some(ListInfo { ordered, level: lv.min(8) as u8 });
|
||||
p.list = Some(ListInfo { ordered, level: lv.min(8) as u8, start: 1 });
|
||||
self.para_cont = self.item_used.last().copied().unwrap_or(false);
|
||||
if let Some(u) = self.item_used.last_mut() {
|
||||
*u = true;
|
||||
|
||||
@@ -1556,7 +1556,7 @@ impl Reader {
|
||||
let list = self.overrides.get(&ls).and_then(|id| self.lists.get(id));
|
||||
let lv = st.ilvl.min(8);
|
||||
let ordered = list.and_then(|l| l.get(lv as usize).copied()).unwrap_or(false);
|
||||
p.list = Some(ListInfo { ordered, level: lv });
|
||||
p.list = Some(ListInfo { ordered, level: lv, start: 1 });
|
||||
}
|
||||
p
|
||||
}
|
||||
@@ -3264,7 +3264,7 @@ mod tests {
|
||||
fn list_item_with_a_formatting_change_keeps_its_list_on_reject() {
|
||||
use model::{FmtChange, ListInfo};
|
||||
// A numbered item Alice centred (it was left-aligned), and a second plain item.
|
||||
let li = Some(ListInfo { ordered: true, level: 0 });
|
||||
let li = Some(ListInfo { ordered: true, level: 0, start: 1 });
|
||||
let mark = RevMark { author: "Alice".into(), date: "2024-03-15T10:30:00Z".into(), id: None };
|
||||
let old = Para { list: li, align: Some(Align::Left), ..Default::default() };
|
||||
let mut item = Para { list: li, align: Some(Align::Center), fmt_change: Some(Box::new(FmtChange { mark, old })), ..Default::default() };
|
||||
@@ -3294,7 +3294,7 @@ mod tests {
|
||||
use model::{FmtChange, ListInfo};
|
||||
// Alice took the paragraph out of a numbered list: it is no list item now, but was one.
|
||||
let mark = RevMark { author: "Alice".into(), date: "2024-03-15T10:30:00Z".into(), id: None };
|
||||
let old = Para { list: Some(ListInfo { ordered: true, level: 0 }), ..Default::default() };
|
||||
let old = Para { list: Some(ListInfo { ordered: true, level: 0, start: 1 }), ..Default::default() };
|
||||
let mut item = Para { fmt_change: Some(Box::new(FmtChange { mark, old })), ..Default::default() };
|
||||
item.push_text("Item", &Fmt::default());
|
||||
let flow = Flow { blocks: vec![FBlock::Para(item)], meta: Meta::default() };
|
||||
@@ -3310,9 +3310,12 @@ mod tests {
|
||||
fn numbered_item_made_bulleted_gets_its_numbered_list_back_on_reject() {
|
||||
use model::{FmtChange, ListInfo};
|
||||
let mark = RevMark { author: "Alice".into(), date: "2024-03-15T10:30:00Z".into(), id: None };
|
||||
let old = Para { list: Some(ListInfo { ordered: true, level: 0 }), ..Default::default() };
|
||||
let mut item =
|
||||
Para { list: Some(ListInfo { ordered: false, level: 0 }), fmt_change: Some(Box::new(FmtChange { mark, old })), ..Default::default() };
|
||||
let old = Para { list: Some(ListInfo { ordered: true, level: 0, start: 1 }), ..Default::default() };
|
||||
let mut item = Para {
|
||||
list: Some(ListInfo { ordered: false, level: 0, start: 1 }),
|
||||
fmt_change: Some(Box::new(FmtChange { mark, old })),
|
||||
..Default::default()
|
||||
};
|
||||
item.push_text("Item", &Fmt::default());
|
||||
let flow = Flow { blocks: vec![FBlock::Para(item)], meta: Meta::default() };
|
||||
let doc = model::to_doc(&flow);
|
||||
|
||||
@@ -205,7 +205,7 @@ fn markdown_list_items_that_start_like_blocks_keep_their_text() {
|
||||
let bytes = export("md", &d).unwrap().unwrap();
|
||||
let blocks = flat(&import("md", &bytes).unwrap().unwrap());
|
||||
let [FBlock::Para(p)] = blocks.as_slice() else { panic!("{text}: {}", String::from_utf8_lossy(&bytes)) };
|
||||
assert_eq!((p.text().as_str(), p.kind, p.list), (text, Kind::Normal, Some(model::ListInfo { ordered: false, level: 0 })));
|
||||
assert_eq!((p.text().as_str(), p.kind, p.list), (text, Kind::Normal, Some(model::ListInfo { ordered: false, level: 0, start: 1 })));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user