fix(make-pdf): no blank first page when content precedes the first H1

Two paths put invisible content ahead of the first H1 and cost users a
blank page 1 (#1904):

- A visually-empty preamble (leading <style> block, HTML comment)
  became its own .chapter. That section took the `.chapter:first-of-type
  { break-before: auto }` exception, so the first real chapter inherited
  `break-before: page` and started on page 2. Non-rendering preambles
  now fold into the first real chapter (markup preserved, no page
  break); real text preambles keep their own chapter.
- Leading YAML frontmatter rendered as a literal paragraph of body text
  on its own first page (marked has no frontmatter awareness). It is
  now stripped before parsing; a `---` thematic break elsewhere is
  untouched.

Contributed by @jbetala7 (PR #1913).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-14 20:20:51 -07:00
co-authored by Claude Fable 5
parent 7b71a62e40
commit 29a717211a
2 changed files with 82 additions and 4 deletions
+42
View File
@@ -277,6 +277,48 @@ describe("render (end-to-end)", () => {
expect(result.printCss).toContain("Hiragino Kaku Gothic");
expect(result.printCss).toContain("Noto Sans CJK");
});
// ─── blank-first-page regression (#1904) ────────────────────────
// Any content before the first <h1> used to become a standalone preamble
// .chapter. That section takes the `:first-of-type { break-before: auto }`
// exception, so the first real chapter inherits `break-before: page` and
// starts on page 2 — leaving a blank/near-blank page 1.
test("a leading <style> preamble does not get its own chapter (no blank page 1)", () => {
const result = render({ markdown: `<style>h1{color:#000}</style>\n\n# Hello\n\nBody.\n` });
const chapters = result.html.match(/class="chapter"/g) ?? [];
// One chapter, not two — the invisible <style> no longer forces a page break.
expect(chapters.length).toBe(1);
// ...and the style is preserved (folded into the first chapter, not dropped).
expect(result.html).toContain("<style>h1{color:#000}</style>");
expect(result.html).toMatch(/<section class="chapter"><style>[\s\S]*?<\/style>[\s\S]*?<h1/);
});
test("leading YAML frontmatter is stripped, not rendered as body text", () => {
const result = render({
markdown: `---\ntitle: My Doc\ntype: memo\ntags: [a, b]\n---\n\n# Hello\n\nBody.\n`,
});
// Frontmatter keys must not leak into the rendered body.
expect(result.html).not.toContain("type: memo");
expect(result.html).not.toContain("tags: [a, b]");
// No preamble chapter -> single chapter, first H1 on page 1.
const chapters = result.html.match(/class="chapter"/g) ?? [];
expect(chapters.length).toBe(1);
expect(result.meta.title).toBe("Hello");
});
test("a real text preamble still gets its own chapter (unchanged behavior)", () => {
const result = render({ markdown: `Intro paragraph.\n\n# Hello\n\nBody.\n` });
const chapters = result.html.match(/class="chapter"/g) ?? [];
expect(chapters.length).toBe(2);
});
test("a `---` thematic break that is not frontmatter is left intact", () => {
// Opening with visible text means the later `---` is a real <hr>, not frontmatter.
const result = render({ markdown: `# Hello\n\nBefore.\n\n---\n\nAfter.\n` });
expect(result.html).toContain("<hr>");
expect(result.html).toContain("After.");
});
});
// ─── print-css ──────────────────────────────────────────────