Skip to content

Commit f036382

Browse files
diluculoclaude
andcommitted
Keep a numbered run together when a right-aligned marker rolls a digit
A numbered run with right-aligned markers (plos_game references) slides its left edge left by about one digit width when the number rolls 9->10, so the strict-same-left guard in Candidate.CanExtend vetoed the extension: reference 10 — the last item of the left column — escaped the run, kept its "10." marker, and its bold first line was misread as a heading while its wrapped continuation was orphaned. Grant an extra left-edge tolerance of one digit width per added digit when the marker grows and shifts left, so the run rolls through the digit boundary and reference 10 is emitted as a marker paragraph with its continuation absorbed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent fe47a95 commit f036382

2 files changed

Lines changed: 51 additions & 2 deletions

File tree

src/PdfStruct.Tests/NumberedParagraphFixtureTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,31 @@ public void ConstitutionEnumeration_RendersAsMarkerParagraphs()
6666
Assert.Contains("1.", markers);
6767
Assert.Contains("9.", markers);
6868
}
69+
70+
[Fact]
71+
public void RightAlignedMarkers_SurviveDigitRoll_AtColumnBottom()
72+
{
73+
// plos_game references use right-aligned markers, so "10." sits a digit
74+
// width left of "1."…"9.". The run extension must tolerate that slide;
75+
// otherwise reference 10 — the last item of the left column — escapes
76+
// the run and its bold first line is misread as a heading.
77+
var result = new PdfStructParser().Parse(FixturePath("plos_game_based_education.pdf"));
78+
79+
var markerParagraphs = result.Document.Kids
80+
.OfType<ParagraphElement>()
81+
.Where(p => p.Marker is not null)
82+
.ToList();
83+
84+
var tenth = markerParagraphs.FirstOrDefault(p => p.Marker == "10.");
85+
Assert.NotNull(tenth);
86+
Assert.StartsWith("Martos-Cabrera", tenth!.Text.Content);
87+
// The wrapped continuation across the column break is absorbed, not orphaned.
88+
Assert.Contains("PMID: 32143452", tenth.Text.Content);
89+
90+
// Reference 10 is not left as a heading.
91+
var numberedHeadings = result.Document.Kids
92+
.OfType<HeadingElement>()
93+
.Where(h => Regex.IsMatch(h.Text.Content, @"^\d+\.\s"));
94+
Assert.Empty(numberedHeadings);
95+
}
6996
}

src/PdfStruct/Analysis/ListDetector.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,28 @@ private static void AbsorbTerritories(
286286

287287
private static double SameLeftTolerance(double fontSize) => Math.Max(fontSize, 1.0) / 3.0;
288288

289+
/// <summary>
290+
/// Approximate width of one marker digit as a fraction of the font size.
291+
/// When a numbered run rolls a digit (9→10, 99→100) the marker is right-
292+
/// aligned on its terminator, so the left edge slides left by about one
293+
/// digit width — a legitimate shift that must not break the run's
294+
/// strict-left-alignment streak.
295+
/// </summary>
296+
private const double MarkerDigitWidthRatio = 0.6;
297+
298+
/// <summary>
299+
/// Extra left-edge tolerance (PDF points) granted when <paramref name="label"/>
300+
/// carries more printed digits than <paramref name="lastLabel"/> and the new
301+
/// line sits to the left of <paramref name="lastLine"/> — the expected slide
302+
/// of a right-aligned marker as it rolls to a wider number. Zero otherwise.
303+
/// </summary>
304+
private static double DigitGrowthLeftTolerance(TextLineBlock line, ListLabel label, TextLineBlock lastLine, ListLabel lastLabel)
305+
{
306+
var extraDigits = label.DigitText.Length - lastLabel.DigitText.Length;
307+
if (extraDigits <= 0 || line.Left >= lastLine.Left) return 0.0;
308+
return extraDigits * Math.Max(lastLine.FontSize, 1.0) * MarkerDigitWidthRatio;
309+
}
310+
289311
/// <summary>
290312
/// Maximum distance, in points, a continuation line may sit to the left of
291313
/// its item's start before it counts as a structural outdent that ends the
@@ -327,7 +349,7 @@ public bool CanExtend(TextLineBlock line, ListLabel label)
327349
if (last.Label.Terminator != label.Terminator) return false;
328350
if (label.Number != last.Label.Number + 1) return false;
329351

330-
var tol = SameLeftTolerance(last.Line.FontSize);
352+
var tol = SameLeftTolerance(last.Line.FontSize) + DigitGrowthLeftTolerance(line, label, last.Line, last.Label);
331353
var diff = Math.Abs(line.Left - last.Line.Left);
332354
var sameLeft = diff <= tol;
333355
var nearLeft = diff <= NearLeftToleranceMultiplier * tol;
@@ -341,7 +363,7 @@ public bool CanExtend(TextLineBlock line, ListLabel label)
341363
public void Append(int lineIndex, TextLineBlock line, ListLabel label, int labelLength)
342364
{
343365
var last = _items[^1];
344-
var tol = SameLeftTolerance(last.Line.FontSize);
366+
var tol = SameLeftTolerance(last.Line.FontSize) + DigitGrowthLeftTolerance(line, label, last.Line, last.Label);
345367
var sameLeft = Math.Abs(line.Left - last.Line.Left) <= tol;
346368
if (!sameLeft) _everyTransitionWasStrictSameLeft = false;
347369

0 commit comments

Comments
 (0)