Skip to content

Commit 06dd45a

Browse files
authored
Merge pull request #11841 from quarto-dev/bugfix/issue-11835
Detect `#` headings with AST
2 parents fc6cd8d + e4cfc51 commit 06dd45a

File tree

8 files changed

+89
-6
lines changed

8 files changed

+89
-6
lines changed

news/changelog-1.7.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ All changes included in 1.7:
2828
## `typst` Format
2929

3030
- ([#11578](https://github.com/quarto-dev/quarto-cli/issues/11578)): Typst column layout widths use fractional `fr` units instead of percent `%` units for unitless and default widths in order to fill the enclosing block and not spill outside it.
31+
- ([#11835](https://github.com/quarto-dev/quarto-cli/issues/11835)): Take markdown structure into account when detecting minimum heading level.
32+
33+
## `pdf` Format
34+
35+
- ([#11835](https://github.com/quarto-dev/quarto-cli/issues/11835)): Take markdown structure into account when detecting minimum heading level.
3136

3237
## Lua Filters and extensions
3338

src/core/lib/break-quarto-md.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* Copyright (C) 2021-2022 Posit Software, PBC
88
*/
99

10-
import { lineOffsets, lines } from "./text.ts";
10+
import { lineOffsets } from "./text.ts";
1111
import { Range, rangedLines, RangedSubstring } from "./ranged-text.ts";
1212
import {
1313
asMappedString,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* level-one-headings.ts
3+
*
4+
* Copyright (C) 2025 Posit Software, PBC
5+
*/
6+
7+
import { join } from "../../../deno_ral/path.ts";
8+
import { execProcess } from "../../process.ts";
9+
import { pandocBinaryPath, resourcePath } from "../../resources.ts";
10+
11+
export async function hasLevelOneHeadings(markdown: string): Promise<boolean> {
12+
// this is O(n * m) where n is the number of blocks and m is the number of matches
13+
// we could do better but won't until we profile and show it's a problem
14+
15+
const path = pandocBinaryPath();
16+
const filterPath = resourcePath(
17+
join("filters", "quarto-internals", "leveloneanalysis.lua"),
18+
);
19+
const result = await execProcess({
20+
cmd: [path, "-f", "markdown", "-t", "markdown", "-L", filterPath],
21+
stdout: "piped",
22+
}, markdown);
23+
return result.stdout?.trim() === "true";
24+
}

src/format/pdf/format-pdf.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { kTemplatePartials } from "../../command/render/template.ts";
5353
import { copyTo } from "../../core/copy.ts";
5454
import { kCodeAnnotations } from "../html/format-html-shared.ts";
5555
import { safeModeFromFile } from "../../deno_ral/fs.ts";
56+
import { hasLevelOneHeadings as hasL1Headings } from "../../core/lib/markdown-analysis/level-one-headings.ts";
5657

5758
export function pdfFormat(): Format {
5859
return mergeConfigs(
@@ -138,7 +139,7 @@ function createPdfFormat(
138139
metadata: {
139140
["block-headings"]: true,
140141
},
141-
formatExtras: (
142+
formatExtras: async (
142143
_input: string,
143144
markdown: string,
144145
flags: PandocFlags,
@@ -254,7 +255,7 @@ function createPdfFormat(
254255
};
255256

256257
// Don't shift the headings if we see any H1s (we can't shift up any longer)
257-
const hasLevelOneHeadings = !!markdown.match(/\n^#\s.*$/gm);
258+
const hasLevelOneHeadings = await hasL1Headings(markdown);
258259

259260
// pdfs with no other heading level oriented options get their heading level shifted by -1
260261
if (

src/format/typst/format-typst.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
} from "../../config/types.ts";
3030
import { formatResourcePath } from "../../core/resources.ts";
3131
import { createFormat } from "../formats-shared.ts";
32+
import { hasLevelOneHeadings as hasL1Headings } from "../../core/lib/markdown-analysis/level-one-headings.ts";
3233

3334
export function typstFormat(): Format {
3435
return createFormat("Typst", "pdf", {
@@ -44,14 +45,14 @@ export function typstFormat(): Format {
4445
[kCiteproc]: false,
4546
},
4647
resolveFormat: typstResolveFormat,
47-
formatExtras: (
48+
formatExtras: async (
4849
_input: string,
4950
markdown: string,
5051
flags: PandocFlags,
5152
format: Format,
5253
_libDir: string,
5354
_services: RenderServices,
54-
): FormatExtras => {
55+
): Promise<FormatExtras> => {
5556
const pandoc: FormatPandoc = {};
5657
const metadata: Metadata = {};
5758

@@ -68,7 +69,7 @@ export function typstFormat(): Format {
6869

6970
// unless otherwise specified, pdfs with only level 2 or greater headings get their
7071
// heading level shifted by -1.
71-
const hasLevelOneHeadings = !!markdown.match(/\n^#\s.*$/gm);
72+
const hasLevelOneHeadings = await hasL1Headings(markdown);
7273
if (
7374
!hasLevelOneHeadings &&
7475
flags?.[kShiftHeadingLevelBy] === undefined &&
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
local found = false
2+
function Header(el)
3+
found = found or el.level == 1
4+
return nil
5+
end
6+
7+
function Pandoc(doc)
8+
if found then
9+
doc.blocks = pandoc.Blocks({
10+
pandoc.Str("true")
11+
})
12+
else
13+
doc.blocks = pandoc.Blocks({
14+
pandoc.Str("false")
15+
})
16+
end
17+
return doc
18+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
format: latex
3+
number-sections: true
4+
_quarto:
5+
tests:
6+
latex:
7+
ensureFileRegexMatches:
8+
- []
9+
- ["subsection{Section}"]
10+
---
11+
12+
## Section
13+
14+
```r
15+
# this is a comment that shouldn't break quarto
16+
print("Hello, world")
17+
```
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
format: typst
3+
number-sections: true
4+
_quarto:
5+
tests:
6+
typst:
7+
ensurePdfRegexMatches:
8+
- ["1 Section"]
9+
- ["0.1 Section"]
10+
---
11+
12+
## Section
13+
14+
```r
15+
# this is a comment that shouldn't break quarto
16+
print("Hello, world")
17+
```

0 commit comments

Comments
 (0)