[II5] A paragraph with its own numbering start begins a new run at that number

Layout added the paragraph's start to the running count, so start k set
paragraph by paragraph read 1, 3, 5. A change of start_at between
neighbouring numbered paragraphs now restarts the count, as PowerPoint's
Start at does; the same start on every paragraph still counts on.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Zach Berry
2026-10-29 18:02:10 -05:00
co-authored by Claude Sonnet 5.5
parent f893f6af7d
commit afe4c324a7
3 changed files with 68 additions and 21 deletions
+1 -1
View File
@@ -179,7 +179,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Numbering",
["Home", "Paragraph"],
None,
"{on?: bool, scheme?: arabicPeriod|arabicParenR|romanUcPeriod|romanLcPeriod|alphaUcPeriod|alphaLcParenR|alphaLcPeriod, start?: n, color?, size?: % of text}",
"{on?: bool, scheme?: arabicPeriod|arabicParenR|romanUcPeriod|romanLcPeriod|alphaUcPeriod|alphaLcParenR|alphaLcPeriod, start?: n (a numbered paragraph whose start differs from that of the last numbered paragraph at its level begins a new run of numbers at n, as PowerPoint's Start at does), color?, size?: % of text}",
has_text_or_shapes,
numbering
),
+42 -20
View File
@@ -132,38 +132,60 @@ impl BoxGeom {
}
}
/// Number shown for a paragraph at list level `lvl`, advancing the per-level counters.
fn next_number(numbering: &mut [u32], lvl: usize, auto_num: bool, empty: bool) -> Option<u32> {
if auto_num && !empty {
if let Some(n) = numbering.get_mut(lvl) {
*n += 1;
}
for deeper in numbering.iter_mut().skip(lvl + 1) {
*deeper = 0;
}
numbering.get(lvl).copied()
} else if auto_num {
// Preview the next number without consuming it: empty paragraphs do not advance
// numbering in the saved document or affect later populated paragraphs.
numbering.get(lvl).map(|n| n.saturating_add(1))
} else {
if !empty {
for n in numbering.iter_mut().skip(lvl) {
/// Number shown for a paragraph at list level `lvl`, advancing the per-level counters. `start_at`
/// is the paragraph's numbering start (`None`: not numbered); `run_start` holds the start each
/// level's current run of numbers began with, and a paragraph that changes it starts a new run
/// (PowerPoint's "Start at").
fn next_number(numbering: &mut [u32], run_start: &mut [u32], lvl: usize, start_at: Option<u32>, empty: bool) -> Option<u32> {
match start_at {
Some(start) if !empty => {
if let (Some(rs), Some(n)) = (run_start.get_mut(lvl), numbering.get_mut(lvl))
&& *rs != start
{
*rs = start;
*n = 0;
}
if let Some(n) = numbering.get_mut(lvl) {
*n += 1;
}
for deeper in numbering.iter_mut().skip(lvl + 1) {
*deeper = 0;
}
numbering.get(lvl).copied()
}
// Preview the next number without consuming it: empty paragraphs do not advance
// numbering in the saved document or affect later populated paragraphs. A paragraph
// with its own start would begin a new run, so it previews that run's first number.
Some(start) => {
if run_start.get(lvl).is_some_and(|rs| *rs != start) {
Some(1)
} else {
numbering.get(lvl).map(|n| n.saturating_add(1))
}
}
None => {
if !empty {
for n in numbering.iter_mut().skip(lvl) {
*n = 0;
}
}
None
}
None
}
}
fn shape_paragraphs(ctx: &Ctx, shape: &Shape, body: &TextBody, opts: &Opts, scale: f64, upright: Upright) -> Vec<ParaShaped> {
let mut numbering: Vec<u32> = vec![0; 9];
let mut run_start: Vec<u32> = vec![0; 9];
let mut shaped_paras: Vec<ParaShaped> = Vec::with_capacity(body.paragraphs.len());
for (pi, para) in body.paragraphs.iter().enumerate() {
let lvl = para.level.min(8) as usize;
let pp = resolve::para(ctx, shape, para);
let auto_num = matches!(pp.bullet, Some(Bullet::AutoNum { .. }));
let number = next_number(&mut numbering, lvl, auto_num, para.is_empty());
let start_at = match &pp.bullet {
Some(Bullet::AutoNum { start_at, .. }) => Some(*start_at),
_ => None,
};
let number = next_number(&mut numbering, &mut run_start, lvl, start_at, para.is_empty());
shaped_paras.push(shape_para(ctx, shape, body, pi, scale, opts, number, upright));
}
shaped_paras
+25
View File
@@ -1845,3 +1845,28 @@ fn an_empty_paragraph_keeps_its_paragraph_mark_height() {
let h = l.lines[1].bottom - l.lines[1].top;
assert!((h - h10).abs() < 0.5, "a 10pt empty paragraph should give {h10}, got {h}");
}
#[test]
fn a_paragraph_with_its_own_start_at_restarts_the_count_there() {
// PowerPoint's "Start at" on a paragraph begins a new run of numbers at that value, so setting
// start k on paragraph k reads 1, 2, 3, not 1, 3, 5.
// The bullets' glyph ids next to those of the expected labels, as pairs (drawn, expected).
let check = |starts: &[u32], expect: &[u32]| {
let (p, mut sh) = setup("a\nb\nc\nd", 600.0, 300.0);
for (para, st) in sh.text.as_mut().unwrap().paragraphs.iter_mut().zip(starts) {
para.props.bullet = Some(Bullet::AutoNum { scheme: "arabicPeriod".into(), start_at: *st });
}
let l = lay(&p, &sh, 600.0, 300.0);
let bullets: Vec<_> = l.runs.iter().filter(|r| r.cells.is_empty()).collect();
assert_eq!(bullets.len(), expect.len());
for (b, n) in bullets.iter().zip(expect) {
let want = measure_str(&b.face, b.size, &format!("{n}.")).1.iter().map(|g| g.0).collect::<Vec<_>>();
assert_eq!(b.glyphs.iter().map(|g| g.0).collect::<Vec<_>>(), want, "expected {n}. for starts {starts:?}");
}
};
check(&[1, 2, 3, 4], &[1, 2, 3, 4]);
// The same start on every paragraph still counts on from it (a whole box numbered at once).
check(&[3, 3, 3, 3], &[3, 4, 5, 6]);
// A later paragraph with a new start begins a fresh run.
check(&[1, 1, 7, 7], &[1, 2, 7, 8]);
}