fix(verify-layout): find an orphaned entry header by where the text starts (#508)

find_orphans() compared each line's left edge with the document margin:
an un-indented last line followed by an indented first line was an entry
header orphaned from its bullets. A list marker moves a line's left edge
without moving its text, so the rule misread two templates:

  * one that merges each bullet's marker into its first line (#481): a
    bullet wrapping across the break was reported as an orphaned header,
    and a real orphaned header followed by such bullets was not reported;
  * the stock CV, when its outer marker extracts as a line of its own: a
    [Job Title] header left at the foot of a page, bullets overleaf, was
    not reported (its text at x70.4 counts as indented), and a one-line
    list item whose outer marker is set 1.3pt below it - so the marker
    sorts last on the page - was reported as a split bullet although the
    whole item is on that page.

Following discussion #481, the rule now reads text x:

  * parse_pdf records where a line's text starts after leading
    LIST_MARKERS words (Line.text_left, defaulted). The set is the agreed
    Unicode bullets without the dashes (`• ‣ ▪ ● ·`): a bullet's second
    line may open with an em dash, and the stock Honors item's second line
    opens with an en dash. Other glyphs are text, so a line that merges
    the stock outer marker (`○` on some setups, an emoji on others) keeps
    the marker's x, as the left-edge rule read it;
  * find_orphans compares the text x of a page's last printed line with
    that of the next page's first printed line, and reports an orphaned
    header when the latter is more than INDENT_PT further in. Bbox lines
    whose tops are within SAME_PRINTED_LINE_PT (3pt) are one printed line:
    pdftotext's rounded-yMin buckets split a line where a bold word sits
    0.2pt off, or where a marker sits 1.3pt below its text. A line whose
    text starts more than SET_IN_LIMIT_PT (60pt) past the margin is set
    right or centred (a running header) and is skipped as the opener;
  * a page ending on a marker with no text beside it keeps the lone-marker
    report, with its condition and message unchanged.

Stock cv/main_example.tex, \vspace* of 0-600pt in 8pt steps before four
entries, 304 lualatex builds (and the same 304 with xelatex): this adds
15 reports, each a [Job Title] header at the foot of a page with its
bullets overleaf; the other 289 builds give byte-identical output. The
reported.

A two-line outer item whose marker sorts last and that wraps across the
break is no longer reported as a split bullet: it is read like the #481
wrapped bullet, which is expected clean.

Tests: TestOrphansReadTextX (13; 7 fixes and the wrapped two-line item
fail on master's logic, 5 pin behaviour master already had) and
TestTextStart (4). On Python 3.10-3.14 `python -W error::SyntaxWarning
-m unittest discover -s tests -t .` passes 520 tests (master 503);
lint_skills.py, check_framework_version.py and security_guards.py OK.
This commit is contained in:
Yang authored and GitHub committed 2026-10-01 21:53:33 +02:00
1 parent a56b8d0414
commit e355eafe27
3 files changed
+267 -14

No files matched your search

+19
View File
@@ -15,6 +15,25 @@ per-file diff commands.
### Fixed
- **`verify_layout.py` finds an orphaned entry header by where the text starts**
(`tools/verify_layout.py`, `tests/test_verify_layout.py`) - the orphan rule compared line
left edges against the document margin, and a list marker moves a line's left edge without
moving its text. A template that merges each bullet's marker into its first line (#481) got
a bullet wrapping across the page break reported as an orphaned header, and a real orphaned
header followed by such bullets not reported at all. On the stock CV, when the outer marker
extracts as a line of its own, a `[Job Title]` header left at the foot of a page with its
bullets overleaf was not reported (its text at x70.4 counted as indented), and a list item
whose outer marker is set 1.3pt below its line was reported as a split bullet. The rule now
compares where the text of the page's last printed line starts - after any leading bullet
glyph from `• ‣ ▪ ● ·` (ASCII, dashes and other glyphs are text) - with where the text of
the next page's first printed line starts, and reports an orphaned header when the latter
is set further in. Bbox lines whose tops are within 3pt count as one printed line, and a
line whose text starts more than 60pt past the margin (a running header, a right-set date)
is not where the next page resumes. A lone marker with no text beside it keeps the
split-bullet report. On 304 builds of the stock CV (`\vspace*` 0-600pt before four
entries) this adds 15 reports, each a `[Job Title]` header at the foot of a page with its
bullets overleaf, and the other 289 builds give the same output as before.
- **One posting on a portal without a numeric id no longer gets a second key when its
title is re-listed with different casing** (#501, `tools/job_key.py`,
`tests/test_job_key.py`) - the key hashed a slugified title alongside the URL whenever