Lists: an empty paragraph gets no bullet or number

A list paragraph with no characters before its return draws no label,
and numbering carries on past it: in a story's own list, in named lists
(also across stories) and in cross-reference paragraph numbers.
This commit is contained in:
aiongg
2026-10-10 07:01:05 +00:00
parent 2a2d20e0c2
commit 107f658f0e
4 changed files with 34 additions and 8 deletions
+5 -2
View File
@@ -492,9 +492,10 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
story
.paras
.iter()
.map(|p| {
.zip(&para_ranges)
.map(|(p, r)| {
let (pp, _) = doc.styles.resolve_para(p);
if pp.list_type != designcraft_doc::ListType::Numbers || pp.list_name.is_empty() {
if pp.list_type != designcraft_doc::ListType::Numbers || pp.list_name.is_empty() || r.is_empty() {
return None;
}
let c = counters.entry(pp.list_name.clone()).or_insert_with(|| doc.list_start(story.id, &pp.list_name));
@@ -701,6 +702,8 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
// List labels take the default super/subscript settings.
let label_env = shape::TypeEnv { adv: Default::default(), ..env };
match pp.list_type {
// An empty paragraph has no bullet or number, and the numbering carries on past it.
designcraft_doc::ListType::Numbers | designcraft_doc::ListType::Bullets if prange.is_empty() => {}
designcraft_doc::ListType::Numbers if !pp.list_name.is_empty() => {
// A named list: carries on past other paragraphs (and from earlier stories).
let n = named_numbers.get(pi).copied().flatten().unwrap_or(1);
+18
View File
@@ -2262,3 +2262,21 @@ fn numbering_from_the_largest_start_number_does_not_overflow() {
let max = format!("{}.", u32::MAX);
assert_eq!(list_labels(&cs, LABEL_CHARS), [max.clone(), max]);
}
#[test]
fn empty_paragraphs_get_no_bullet_or_number() {
let bullets = ParaAttrs { list_type: Some(designcraft_doc::ListType::Bullets), ..Default::default() };
let (d, sid, _) = doc_with("One\n\nTwo\n", Rect::new(0.0, 0.0, 300.0, 300.0), bullets);
let cs = compose_story(&d, sid, &ComposeOptions::default());
assert_eq!(list_labels(&cs, LABEL_CHARS), ["\u{2022}", "", "\u{2022}", ""]);
// An empty paragraph doesn't use up a number, in a story's own list or a named one.
for name in ["", "Steps"] {
let attrs = ParaAttrs { list_name: Some(name.into()), ..numbered(designcraft_doc::NumberStyle::Arabic, "^#.^t") };
let (mut d, sid, _) = doc_with("One\n\nTwo\n", Rect::new(0.0, 0.0, 300.0, 300.0), attrs);
if !name.is_empty() {
d.settings.lists.push(designcraft_doc::NumberedList { name: name.into(), continue_across_stories: false });
}
let cs = compose_story(&d, sid, &ComposeOptions::default());
assert_eq!(list_labels(&cs, LABEL_CHARS), ["1.", "", "2.", ""], "list `{name}`");
}
}
+2 -2
View File
@@ -235,9 +235,9 @@ impl crate::Document {
before.sort_by_key(|st| key(st));
let mut n = 0u32;
for st in before {
for p in &st.paras {
for (p, r) in st.paras.iter().zip(st.para_ranges()) {
let (pp, _) = self.styles.resolve_para(p);
if pp.list_type == crate::ListType::Numbers && pp.list_name == name {
if pp.list_type == crate::ListType::Numbers && pp.list_name == name && !r.is_empty() {
n = pp.start_at.map_or(n.saturating_add(1), |s| s.max(1));
}
}
+9 -4
View File
@@ -347,12 +347,17 @@ impl Document {
let (sid, pos) = self.find_anchor(id)?;
let st = self.story(sid)?;
let pi = st.para_at(pos);
let r = st.para_ranges()[pi].clone();
let ranges = st.para_ranges();
let r = ranges[pi].clone();
let (pp, _) = self.styles.resolve_para(&st.paras[pi]);
let para_num = match pp.list_type {
crate::ListType::Numbers => {
// Count the run of numbered paragraphs before this one.
let n = (0..=pi).rev().take_while(|&k| self.styles.resolve_para(&st.paras[k]).0.list_type == crate::ListType::Numbers).count();
crate::ListType::Numbers if !r.is_empty() => {
// Count the run of numbered paragraphs before this one (empty ones have no number).
let n = (0..=pi)
.rev()
.take_while(|&k| self.styles.resolve_para(&st.paras[k]).0.list_type == crate::ListType::Numbers)
.filter(|&k| ranges.get(k).is_some_and(|r| !r.is_empty()))
.count();
pp.number_label(u32::try_from(n).unwrap_or(u32::MAX)).trim_end().to_string()
}
_ => String::new(),