Skip to content

Commit 60dd710

Browse files
authored
fix(typst): title block no longer takes the full width (#14036)
* fix: Title misaligned in Typst output Fixes #14031 * test: update pattern * test: PDF utility for vertical/horizontal centering * test: horizontal centering checks * test: update regex and tolerance for inset * fix: adjust tolerance for centerX alignment test
1 parent 2ba0acb commit 60dd710

7 files changed

Lines changed: 154 additions & 48 deletions

File tree

src/resources/formats/typst/pandoc/quarto/typst-template.typ

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -61,54 +61,61 @@
6161
}
6262
}
6363

64-
place(top, float: true, scope: "parent", clearance: 4mm)[
65-
#if title != none {
66-
align(center, block(inset: 2em)[
67-
#set par(leading: heading-line-height) if heading-line-height != none
68-
#set text(font: heading-family) if heading-family != none
69-
#set text(weight: heading-weight)
70-
#set text(style: heading-style) if heading-style != "normal"
71-
#set text(fill: heading-color) if heading-color != black
64+
place(
65+
top,
66+
float: true,
67+
scope: "parent",
68+
clearance: 4mm,
69+
block(below: 1em, width: 100%)[
7270

73-
#text(size: title-size)[#title #if thanks != none {
74-
footnote(thanks, numbering: "*")
75-
counter(footnote).update(n => n - 1)
76-
}]
77-
#(if subtitle != none {
78-
parbreak()
79-
text(size: subtitle-size)[#subtitle]
80-
})
81-
])
82-
}
71+
#if title != none {
72+
align(center, block(inset: 2em)[
73+
#set par(leading: heading-line-height) if heading-line-height != none
74+
#set text(font: heading-family) if heading-family != none
75+
#set text(weight: heading-weight)
76+
#set text(style: heading-style) if heading-style != "normal"
77+
#set text(fill: heading-color) if heading-color != black
78+
79+
#text(size: title-size)[#title #if thanks != none {
80+
footnote(thanks, numbering: "*")
81+
counter(footnote).update(n => n - 1)
82+
}]
83+
#(if subtitle != none {
84+
parbreak()
85+
text(size: subtitle-size)[#subtitle]
86+
})
87+
])
88+
}
8389

84-
#if authors != none and authors != () {
85-
let count = authors.len()
86-
let ncols = calc.min(count, 3)
87-
grid(
88-
columns: (1fr,) * ncols,
89-
row-gutter: 1.5em,
90-
..authors.map(author =>
91-
align(center)[
92-
#author.name \
93-
#author.affiliation \
94-
#author.email
95-
]
90+
#if authors != none and authors != () {
91+
let count = authors.len()
92+
let ncols = calc.min(count, 3)
93+
grid(
94+
columns: (1fr,) * ncols,
95+
row-gutter: 1.5em,
96+
..authors.map(author =>
97+
align(center)[
98+
#author.name \
99+
#author.affiliation \
100+
#author.email
101+
]
102+
)
96103
)
97-
)
98-
}
104+
}
99105

100-
#if date != none {
101-
align(center)[#block(inset: 1em)[
102-
#date
103-
]]
104-
}
106+
#if date != none {
107+
align(center)[#block(inset: 1em)[
108+
#date
109+
]]
110+
}
105111

106-
#if abstract != none {
107-
block(inset: 2em)[
108-
#text(weight: "semibold")[#abstract-title] #h(1em) #abstract
109-
]
110-
}
111-
]
112+
#if abstract != none {
113+
block(inset: 2em)[
114+
#text(weight: "semibold")[#abstract-title] #h(1em) #abstract
115+
]
116+
}
117+
]
118+
)
112119

113120
if toc {
114121
let title = if toc_title == none {

tests/docs/smoke-all/typst/columns/basic-two-column.qmd

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,20 @@ _quarto:
1212
# Page columns set via set page()
1313
- 'columns: 2,'
1414
# Title block wrapped with place(scope: "parent") for column spanning
15-
- 'place\(top, float: true, scope: "parent", clearance: 4mm\)'
15+
- 'place\(\s+top,\s+float: true,\s+scope: "parent",\s+clearance: 4mm,\s+block\(below: 1em, width: 100%\)'
1616
- # Patterns that must NOT be found
1717
# Should NOT have columns() function call wrapping doc at end of article()
1818
- 'columns\(cols, doc\)\n\}'
19+
ensurePdfTextPositions:
20+
- - subject:
21+
text: "Basic Two Column Layout"
22+
edge: centerX
23+
relation: leftAligned
24+
object:
25+
role: Page
26+
page: 1
27+
edge: centerX
28+
tolerance: 5
1929
---
2030

2131
## Introduction

tests/docs/smoke-all/typst/columns/two-column-landscape.qmd

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,33 @@ _quarto:
2222
# Page columns set via set page()
2323
- 'columns: 2,'
2424
# Title block wrapped with place(scope: "parent") for column spanning
25-
- 'place\(top, float: true, scope: "parent", clearance: 4mm\)'
25+
- 'place\(\s+top,\s+float: true,\s+scope: "parent",\s+clearance: 4mm,\s+block\(below: 1em, width: 100%\)'
2626
# Landscape page start
2727
- '#set page\(flipped: true\)'
2828
# Landscape page end
2929
- '#set page\(flipped: false\)'
3030
- # Patterns that must NOT be found
3131
# Should NOT have columns() function call wrapping doc at end of article()
3232
- 'columns\(cols, doc\)\n\}'
33+
ensurePdfTextPositions:
34+
- - subject:
35+
text: "Two Column with Landscape Section"
36+
edge: centerX
37+
relation: leftAligned
38+
object:
39+
role: Page
40+
page: 1
41+
edge: centerX
42+
tolerance: 22
43+
- subject:
44+
text: "Testing Column Layout with Page Orientation Changes"
45+
edge: centerX
46+
relation: leftAligned
47+
object:
48+
role: Page
49+
page: 1
50+
edge: centerX
51+
tolerance: 22
3352
---
3453

3554
## Introduction

tests/docs/smoke-all/typst/columns/two-column-title-block.qmd

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ _quarto:
3030
# Page columns set via set page()
3131
- 'columns: 2,'
3232
# Title block wrapped with place(scope: "parent") for column spanning
33-
- 'place\(top, float: true, scope: "parent", clearance: 4mm\)'
33+
- 'place\(\s+top,\s+float: true,\s+scope: "parent",\s+clearance: 4mm,\s+block\(below: 1em, width: 100%\)'
3434
# Title passed to article()
3535
- 'title: \[Two Column with Full Title Block\]'
3636
# Subtitle passed to article()
@@ -44,6 +44,25 @@ _quarto:
4444
- # Patterns that must NOT be found
4545
# Should NOT have columns() function call wrapping doc at end of article()
4646
- 'columns\(cols, doc\)\n\}'
47+
ensurePdfTextPositions:
48+
- - subject:
49+
text: "Two Column with Full Title Block"
50+
edge: centerX
51+
relation: leftAligned
52+
object:
53+
role: Page
54+
page: 1
55+
edge: centerX
56+
tolerance: 22
57+
- subject:
58+
text: "Testing Column-Spanning Title Elements"
59+
edge: centerX
60+
relation: leftAligned
61+
object:
62+
role: Page
63+
page: 1
64+
edge: centerX
65+
tolerance: 22
4766
---
4867

4968
## Introduction

tests/docs/smoke-all/typst/columns/two-column-toc.qmd

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ _quarto:
1616
# Page columns set via set page()
1717
- 'columns: 2,'
1818
# Title block wrapped with place(scope: "parent") for column spanning
19-
- 'place\(top, float: true, scope: "parent", clearance: 4mm\)'
19+
- 'place\(\s+top,\s+float: true,\s+scope: "parent",\s+clearance: 4mm,\s+block\(below: 1em, width: 100%\)'
2020
# TOC enabled in article() call
2121
- 'toc: true'
2222
# TOC title passed
@@ -28,6 +28,16 @@ _quarto:
2828
- # Patterns that must NOT be found
2929
# Should NOT have columns() function call wrapping doc at end of article()
3030
- 'columns\(cols, doc\)\n\}'
31+
ensurePdfTextPositions:
32+
- - subject:
33+
text: "Two Column with Table of Contents"
34+
edge: centerX
35+
relation: leftAligned
36+
object:
37+
role: Page
38+
page: 1
39+
edge: centerX
40+
tolerance: 22
3141
---
3242

3343
## Introduction

tests/smoke/verify/pdf-text-position.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ testQuartoCmd("render", [fixtureQmd, "--to", "typst"], [], {
5353
await runDistanceConstraintTests();
5454
await runDistanceConstraintErrorTests();
5555
await runPageRoleWithEdgeTests();
56+
await runCenterEdgeTests();
5657

5758
// Cleanup
5859
if (safeExistsSync(fixturePdf)) {
@@ -575,3 +576,39 @@ async function runPageRoleWithEdgeTests() {
575576
]);
576577
await headerNearPageTop.verify([]);
577578
}
579+
580+
/**
581+
* Test centerX and centerY edge functionality
582+
*/
583+
async function runCenterEdgeTests() {
584+
// Test: centerX - title's horizontal centre should be near page's horizontal centre (inset tolerance for minor misalignment)
585+
const centerXPageTest = ensurePdfTextPositions(fixturePdf, [
586+
{
587+
subject: { text: "FIXTURE_TITLE_TEXT", edge: "centerX" },
588+
relation: "leftAligned",
589+
object: { role: "Page", page: 1, edge: "centerX" },
590+
tolerance: 35,
591+
},
592+
]);
593+
await centerXPageTest.verify([]);
594+
595+
// Test: centerY directional - header decoration's centerY should be above title's centerY
596+
const centerYDirectionalTest = ensurePdfTextPositions(fixturePdf, [
597+
{
598+
subject: { text: "FIXTURE_HEADER_TEXT", role: "Decoration", edge: "centerY" },
599+
relation: "above",
600+
object: { text: "FIXTURE_TITLE_TEXT", edge: "centerY" },
601+
},
602+
]);
603+
await centerYDirectionalTest.verify([]);
604+
605+
// Test: centerX directional - a left-aligned heading's centerX should be leftOf page centerX
606+
const centerXDirectionalTest = ensurePdfTextPositions(fixturePdf, [
607+
{
608+
subject: { text: "FIXTURE_H1_TEXT", edge: "centerX" },
609+
relation: "leftOf",
610+
object: { role: "Page", page: 1, edge: "centerX" },
611+
},
612+
]);
613+
await centerXDirectionalTest.verify([]);
614+
}

tests/verify-pdf-text-position.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { ExecuteOutput, Verify } from "./test.ts";
4040
// ============================================================================
4141

4242
// Edge schema for precise bbox edge selection
43-
export const EdgeSchema = z.enum(["left", "right", "top", "bottom"]);
43+
export const EdgeSchema = z.enum(["left", "right", "top", "bottom", "centerX", "centerY"]);
4444
export type Edge = z.infer<typeof EdgeSchema>;
4545

4646
// Relation schemas
@@ -215,6 +215,10 @@ function getEdgeValue(bbox: BBox, edge: Edge): number {
215215
return bbox.y;
216216
case "bottom":
217217
return bbox.y + bbox.height;
218+
case "centerX":
219+
return bbox.x + bbox.width / 2;
220+
case "centerY":
221+
return bbox.y + bbox.height / 2;
218222
}
219223
}
220224

0 commit comments

Comments
 (0)