Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/format/html/format-html-title.ts
6450 views
1
/*
2
* format-html-title.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { existsSync } from "../../deno_ral/fs.ts";
8
import { dirname, isAbsolute, join } from "../../deno_ral/path.ts";
9
import { kDateFormat, kTocLocation } from "../../config/constants.ts";
10
import { Format, Metadata, PandocFlags } from "../../config/types.ts";
11
import { Document, Element } from "../../core/deno-dom.ts";
12
import { formatResourcePath } from "../../core/resources.ts";
13
import { sassLayer } from "../../core/sass.ts";
14
import { TempContext } from "../../core/temp-types.ts";
15
import { MarkdownPipeline } from "../../core/markdown-pipeline.ts";
16
import {
17
HtmlPostProcessResult,
18
PandocInputTraits,
19
RenderedFormat,
20
} from "../../command/render/types.ts";
21
import { InternalError } from "../../core/lib/error.ts";
22
23
export const kTitleBlockStyle = "title-block-style";
24
const kTitleBlockBanner = "title-block-banner";
25
const ktitleBlockColor = "title-block-banner-color";
26
const kTitleBlockCategories = "title-block-categories";
27
28
export interface DocumentTitleContext {
29
pipeline: MarkdownPipeline;
30
}
31
32
export function documentTitleScssLayer(format: Format) {
33
if (
34
format.metadata[kTitleBlockStyle] === false ||
35
format.metadata[kTitleBlockStyle] === "none" ||
36
format.metadata[kTitleBlockStyle] === "plain"
37
) {
38
return undefined;
39
} else if (format.metadata[kTitleBlockStyle] === "manuscript") {
40
// TODO: Tweak style for manuscript
41
// This code path is here so that we can add manuscript-specific styles
42
// For now it is just identical to non-manuscript
43
const titleBlockScss = formatResourcePath(
44
"html",
45
join("templates", "title-block.scss"),
46
);
47
return sassLayer(titleBlockScss);
48
} else {
49
const titleBlockScss = formatResourcePath(
50
"html",
51
join("templates", "title-block.scss"),
52
);
53
return sassLayer(titleBlockScss);
54
}
55
}
56
57
export function documentTitleMetadata(
58
format: Format,
59
) {
60
if (
61
format.metadata[kTitleBlockStyle] !== false &&
62
format.metadata[kTitleBlockStyle] !== "none" &&
63
format.metadata[kDateFormat] === undefined
64
) {
65
return {
66
[kDateFormat]: "long",
67
};
68
} else {
69
return undefined;
70
}
71
}
72
73
export function documentTitleIncludeInHeader(
74
input: string,
75
format: Format,
76
temp: TempContext,
77
) {
78
// Inject variables
79
const headingVars: string[] = [];
80
const containerVars: string[] = [];
81
82
const banner = format.metadata[kTitleBlockBanner] as string | boolean;
83
if (banner) {
84
// $title-banner-bg
85
// $title-banner-color
86
// $title-banner-image
87
const titleBlockColor = titleColor(format.metadata[ktitleBlockColor]);
88
if (titleBlockColor) {
89
const color = `color: ${titleBlockColor};`;
90
headingVars.push(color);
91
containerVars.push(color);
92
}
93
94
if (banner === true) {
95
// The default appearance, use navbar color
96
} else if (isBannerImage(input, banner)) {
97
// An image background
98
containerVars.push(`background-image: url(${banner});`);
99
containerVars.push(`background-size: cover;`);
100
} else {
101
containerVars.push(`background: ${banner};`);
102
}
103
}
104
105
if (headingVars.length || containerVars.length) {
106
const styles: string[] = ["<style>"];
107
if (headingVars.length) {
108
styles.push(`
109
.quarto-title-block .quarto-title-banner h1,
110
.quarto-title-block .quarto-title-banner h2,
111
.quarto-title-block .quarto-title-banner h3,
112
.quarto-title-block .quarto-title-banner h4,
113
.quarto-title-block .quarto-title-banner h5,
114
.quarto-title-block .quarto-title-banner h6
115
{
116
${headingVars.join("\n")}
117
}`);
118
}
119
if (containerVars.length) {
120
styles.push(`
121
.quarto-title-block .quarto-title-banner {
122
${containerVars.join("\n")}
123
}`);
124
}
125
126
styles.push("</style>");
127
const file = temp.createFile({ suffix: ".css" });
128
Deno.writeTextFileSync(file, styles.join("\n"));
129
return file;
130
}
131
}
132
133
export function documentTitlePartial(
134
format: Format,
135
) {
136
if (
137
format.metadata[kTitleBlockStyle] === false ||
138
format.metadata[kTitleBlockStyle] === "none"
139
) {
140
return {
141
partials: [],
142
templateParams: {},
143
};
144
} else {
145
const partials = [];
146
const templateParams: Metadata = {};
147
148
// Note whether we should be showing categories
149
templateParams[kTitleBlockCategories] =
150
format.metadata[kTitleBlockCategories] !== false ? "true" : "";
151
152
// Select the appropriate title block partial (banner vs no banner)
153
const banner = format.metadata[kTitleBlockBanner] as string | boolean;
154
const manuscriptTitle = format.metadata[kTitleBlockStyle] === "manuscript";
155
156
partials.push("_title-meta-author.html");
157
partials.push("title-metadata.html");
158
159
if (manuscriptTitle) {
160
partials.push("manuscript/title-block.html");
161
partials.push("manuscript/title-metadata.html");
162
} else if (banner) {
163
partials.push("banner/title-block.html");
164
} else {
165
partials.push("title-block.html");
166
}
167
168
// For banner partials, configure the options and pass them along in the metadata
169
if (banner || manuscriptTitle) {
170
// When the toc is on the left, be sure to add the special grid notation
171
const tocLeft = format.metadata[kTocLocation] === "left";
172
if (tocLeft) {
173
templateParams["banner-header-class"] = "toc-left";
174
}
175
}
176
177
return {
178
partials: partials.map((partial) => {
179
return formatResourcePath("html", join("templates", partial));
180
}),
181
templateParams,
182
};
183
}
184
}
185
186
export async function canonicalizeTitlePostprocessor(
187
doc: Document,
188
_options: {
189
inputMetadata: Metadata;
190
inputTraits: PandocInputTraits;
191
renderedFormats: RenderedFormat[];
192
quiet?: boolean;
193
},
194
): Promise<HtmlPostProcessResult> {
195
// https://github.com/quarto-dev/quarto-cli/issues/10567
196
// this fix cannot happen in `processDocumentTitle` because
197
// that's too late in the postprocessing order
198
const titleBlock = doc.querySelector("header.quarto-title-block");
199
200
const main = doc.querySelector("main");
201
// if no main element exists, this is likely a revealjs presentation
202
// which will generally have a title slide instead of a title block
203
// so we don't need to do anything
204
205
if (!titleBlock && main) {
206
const header = doc.createElement("header");
207
header.id = "title-block-header";
208
header.classList.add("quarto-title-block");
209
main.insertBefore(header, main.firstChild);
210
const h1s = Array.from(doc.querySelectorAll("h1"));
211
for (const h1n of h1s) {
212
const h1 = h1n as Element;
213
if (h1.classList.contains("quarto-secondary-nav-title")) {
214
continue;
215
}
216
217
// Now we need to check whether this is a plausible title element.
218
if (h1.parentElement?.tagName === "SECTION") {
219
// If the parent element is a section, then we need to check if there's
220
// any content before the section. If there is, then this is not a title
221
if (
222
h1.parentElement?.parentElement?.firstElementChild !==
223
h1.parentElement
224
) {
225
continue;
226
}
227
} else {
228
// If the parent element is not a section, then we need to check if there's
229
// any content before the h1. If there is, then this is not a title
230
if (h1.parentElement?.firstElementChild !== h1) {
231
continue;
232
}
233
}
234
235
const div = doc.createElement("div");
236
div.classList.add("quarto-title-banner");
237
h1.classList.add("title");
238
header.appendChild(h1);
239
break;
240
}
241
}
242
243
return {
244
resources: [],
245
supporting: [],
246
};
247
}
248
249
export function processDocumentTitle(
250
input: string,
251
format: Format,
252
_flags: PandocFlags,
253
doc: Document,
254
) {
255
const resources: string[] = [];
256
257
// when in banner mode, note on the main content region and
258
// add any image to resources
259
const banner = format.metadata[kTitleBlockBanner] as string | boolean;
260
const manuscriptTitle = format.metadata[kTitleBlockStyle] === "manuscript";
261
if (banner || manuscriptTitle) {
262
// Move the header above the content
263
const headerEl = doc.getElementById("title-block-header");
264
const contentEl = doc.getElementById("quarto-content");
265
if (contentEl && headerEl) {
266
headerEl.remove();
267
contentEl.parentElement?.insertBefore(headerEl, contentEl);
268
}
269
270
const mainEl = doc.querySelector("main.content");
271
mainEl?.classList.add("quarto-banner-title-block");
272
273
if (isBannerImage(input, banner)) {
274
resources.push(banner as string);
275
}
276
277
// Decorate the header
278
const quartoHeaderEl = doc.getElementById("quarto-header");
279
if (quartoHeaderEl) {
280
quartoHeaderEl.classList.add("quarto-banner");
281
}
282
}
283
284
return resources;
285
}
286
287
function isBannerImage(input: string, banner: unknown) {
288
if (typeof banner === "string") {
289
let path;
290
291
if (isAbsolute(banner)) {
292
path = banner;
293
} else {
294
path = join(dirname(input), banner);
295
}
296
return existsSync(path);
297
} else {
298
return false;
299
}
300
}
301
302
const titleColor = (block: unknown) => {
303
if (block === "body" || block === "body-bg") {
304
return undefined;
305
} else {
306
return block;
307
}
308
};
309
310
const _titleColorClass = (block: unknown) => {
311
if (block === "body") {
312
return "body";
313
} else if (block === "body-bg" || block === undefined) {
314
return "body-bg";
315
} else {
316
return "none";
317
}
318
};
319
320