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-shared.ts
6450 views
1
/*
2
* format-html-shared.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
import { dirname, join, relative } from "../../deno_ral/path.ts";
7
import { outputVariable, sassLayer, sassVariable } from "../../core/sass.ts";
8
import {
9
kCapLoc,
10
kCapTop,
11
kCitationLocation,
12
kCodeOverflow,
13
kCopyButtonTooltip,
14
kFigCapLoc,
15
kLinkExternalIcon,
16
kReferenceLocation,
17
kSectionTitleFootnotes,
18
kSectionTitleReferences,
19
kTblCapLoc,
20
} from "../../config/constants.ts";
21
import {
22
Format,
23
FormatDependency,
24
FormatLanguage,
25
PandocFlags,
26
} from "../../config/types.ts";
27
28
import { formatResourcePath } from "../../core/resources.ts";
29
import { Document, Element } from "../../core/deno-dom.ts";
30
import { normalizePath } from "../../core/path.ts";
31
32
// features that are enabled by default for 'html'. setting
33
// all of these to false will yield the minimal html output
34
// that quarto can produce (there is still some CSS we generate
35
// to provide figure layout, etc.). you can also set the
36
// 'minimal' option to do this in one shot
37
export const kTabsets = "tabsets";
38
export const kCodeCopy = "code-copy";
39
export const kAnchorSections = "anchor-sections";
40
export const kCitationsHover = "citations-hover";
41
export const kFootnotesHover = "footnotes-hover";
42
export const kXrefsHover = "crossrefs-hover";
43
export const kSmoothScroll = "smooth-scroll";
44
45
// Code Annotation
46
export const kCodeAnnotations = "code-annotations";
47
48
// turn off optional html features as well as all themes
49
export const kMinimal = "minimal";
50
51
export const kPageLayout = "page-layout";
52
export const kPageLayoutArticle = "article";
53
export const kPageLayoutCustom = "custom";
54
export const kPageLayoutFull = "full";
55
export const kComments = "comments";
56
export const kHypothesis = "hypothesis";
57
export const kUtterances = "utterances";
58
export const kGiscus = "giscus";
59
export const kAxe = "axe";
60
61
export const kGiscusRepoId = "repo-id";
62
export const kGiscusCategoryId = "category-id";
63
64
export const kDraft = "draft";
65
66
export const kAppendixStyle = "appendix-style";
67
export const kAppendixCiteAs = "appendix-cite-as";
68
export const kLicense = "license";
69
export const kCopyright = "copyright";
70
71
export const kCitation = "citation";
72
73
export const kDocumentCss = "document-css";
74
export const kBootstrapDependencyName = "bootstrap";
75
76
export const clipboardDependency = () => {
77
const dependency: FormatDependency = { name: "clipboard" };
78
dependency.scripts = [];
79
dependency.scripts.push({
80
name: "clipboard.min.js",
81
path: formatResourcePath("html", join("clipboard", "clipboard.min.js")),
82
});
83
return dependency;
84
};
85
86
export const bootstrapFunctions = () => {
87
return Deno.readTextFileSync(
88
join(bootstrapResourceDir(), "_functions.scss"),
89
);
90
};
91
92
export const bootstrapMixins = () => {
93
return Deno.readTextFileSync(
94
join(bootstrapResourceDir(), "_mixins.scss"),
95
);
96
};
97
98
export const bootstrapVariables = () => {
99
return Deno.readTextFileSync(
100
join(bootstrapResourceDir(), "_variables.scss"),
101
);
102
};
103
104
export const bootstrapRules = () => {
105
return Deno.readTextFileSync(
106
join(bootstrapResourceDir(), "bootstrap.scss"),
107
);
108
};
109
110
export const bslibComponentMixins = () => {
111
const bootstrapDistDir = formatResourcePath(
112
"html",
113
"bslib",
114
);
115
const mixinsDir = join(bootstrapDistDir, "components", "scss", "mixins");
116
return Deno.readTextFileSync(join(mixinsDir, "_mixins.scss"));
117
};
118
119
export const bslibComponentRules = () => {
120
const bslibDistDir = formatResourcePath(
121
"html",
122
join("bslib"),
123
);
124
125
const bslibDirs = [
126
join(bslibDistDir, "bslib-scss"),
127
join(bslibDistDir, "components", "scss"),
128
];
129
130
const scss = [];
131
for (const bsLibDir of bslibDirs) {
132
for (
133
const walk of Deno.readDirSync(bsLibDir)
134
) {
135
if (walk.isFile) {
136
const contents = Deno.readTextFileSync(join(bsLibDir, walk.name));
137
scss.push(contents);
138
}
139
}
140
}
141
142
return scss.join("\n");
143
};
144
145
export const htmlToolsRules = () => {
146
const htmlToolsDir = formatResourcePath(
147
"html",
148
join("htmltools"),
149
);
150
const fillCss = Deno.readTextFileSync(join(htmlToolsDir, "fill.css"));
151
return fillCss;
152
};
153
154
export const bootstrapResourceDir = () => {
155
return formatResourcePath(
156
"html",
157
join("bootstrap", "dist", "scss"),
158
);
159
};
160
161
export const bslibResourceDir = () => {
162
return formatResourcePath(
163
"html",
164
join("bslib", "bslib-scss"),
165
);
166
};
167
168
export const sassUtilFunctions = (name: string) => {
169
const bootstrapDistDir = formatResourcePath(
170
"html",
171
join("bootstrap", "dist"),
172
);
173
174
const path = join(bootstrapDistDir, "sass-utils", name);
175
return Deno.readTextFileSync(path);
176
};
177
178
export const quartoRules = () =>
179
Deno.readTextFileSync(formatResourcePath(
180
"html",
181
"_quarto-rules.scss",
182
));
183
184
export const quartoCopyCodeDefaults = () =>
185
Deno.readTextFileSync(formatResourcePath(
186
"html",
187
"_quarto-variables-copy-code.scss",
188
));
189
190
export const quartoCopyCodeRules = () =>
191
Deno.readTextFileSync(formatResourcePath(
192
"html",
193
"_quarto-rules-copy-code.scss",
194
));
195
196
export const quartoLinkExternalRules = () =>
197
Deno.readTextFileSync(formatResourcePath(
198
"html",
199
"_quarto-rules-link-external.scss",
200
));
201
202
export const quartoCodeFilenameRules = () =>
203
Deno.readTextFileSync(formatResourcePath(
204
"html",
205
"_quarto-rules-code-filename.scss",
206
));
207
208
export const quartoTabbyRules = () =>
209
Deno.readTextFileSync(formatResourcePath(
210
"html",
211
"_quarto-rules-tabby.scss",
212
));
213
214
export const quartoFigResponsiveRules = () => {
215
return [
216
".img-fluid {",
217
" max-width: 100%;",
218
" height: auto;",
219
"}",
220
].join("\n");
221
};
222
223
export const quartoGlobalCssVariableRules = () => {
224
return `
225
$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !default;
226
/*! quarto-variables-start */
227
:root {
228
--quarto-font-monospace: #{inspect($font-family-monospace)};
229
}
230
/*! quarto-variables-end */
231
`;
232
};
233
export const quartoBootstrapCustomizationLayer = () => {
234
const path = formatResourcePath(
235
"html",
236
join("bootstrap", "_bootstrap-customize.scss"),
237
);
238
return sassLayer(path);
239
};
240
241
export const quartoBootstrapRules = () =>
242
Deno.readTextFileSync(formatResourcePath(
243
"html",
244
join("bootstrap", "_bootstrap-rules.scss"),
245
));
246
247
export const quartoBootstrapMixins = () =>
248
Deno.readTextFileSync(formatResourcePath(
249
"html",
250
join("bootstrap", "_bootstrap-mixins.scss"),
251
));
252
253
export const quartoBootstrapFunctions = () =>
254
Deno.readTextFileSync(formatResourcePath(
255
"html",
256
join("bootstrap", "_bootstrap-functions.scss"),
257
));
258
259
export const quartoBaseLayer = (
260
format: Format,
261
codeCopy = false,
262
tabby = false,
263
figResponsive = false,
264
codeFilename = false,
265
) => {
266
const rules: string[] = [quartoRules()];
267
const defaults: string[] = [quartoDefaults(format), quartoVariables()];
268
if (codeCopy) {
269
rules.push(quartoCopyCodeRules());
270
defaults.push(quartoCopyCodeDefaults());
271
}
272
if (tabby) {
273
rules.push(quartoTabbyRules());
274
}
275
if (figResponsive) {
276
rules.push(quartoFigResponsiveRules());
277
}
278
if (codeFilename) {
279
rules.push(quartoCodeFilenameRules());
280
}
281
if (format.render[kLinkExternalIcon]) {
282
rules.push(quartoLinkExternalRules());
283
}
284
285
return {
286
uses: quartoUses(),
287
defaults: defaults.join("\n"),
288
functions: quartoFunctions(),
289
mixins: "",
290
rules: rules.join("\n"),
291
};
292
};
293
294
export const quartoVariables = () =>
295
Deno.readTextFileSync(formatResourcePath(
296
"html",
297
"_quarto-variables.scss",
298
));
299
300
export const quartoUses = () =>
301
Deno.readTextFileSync(formatResourcePath(
302
"html",
303
"_quarto-uses.scss",
304
));
305
306
export const quartoFunctions = () =>
307
Deno.readTextFileSync(formatResourcePath(
308
"html",
309
"_quarto-functions.scss",
310
));
311
312
export const quartoDefaults = (format: Format) => {
313
const defaults: string[] = [];
314
defaults.push(
315
outputVariable(
316
sassVariable(
317
"code-copy-selector",
318
format.metadata[kCodeCopy] === undefined ||
319
format.metadata[kCodeCopy] === "hover"
320
// ? '"div.sourceCode:hover > "'
321
? '"div.code-copy-outer-scaffold:hover > "'
322
: '""',
323
),
324
),
325
);
326
defaults.push(
327
outputVariable(
328
sassVariable(
329
"code-white-space",
330
format.render[kCodeOverflow] === "wrap" ? "pre-wrap" : "pre",
331
),
332
),
333
);
334
defaults.push(
335
outputVariable(
336
sassVariable(
337
kTblCapLoc,
338
format.metadata[kTblCapLoc] ||
339
format.metadata[kCapLoc] || kCapTop,
340
),
341
),
342
);
343
return defaults.join("\n");
344
};
345
346
export function insertFootnotesTitle(
347
doc: Document,
348
footnotesEl: Element,
349
language: FormatLanguage,
350
level = 2,
351
classes: string[] = [],
352
) {
353
prependHeading(
354
doc,
355
footnotesEl,
356
language[kSectionTitleFootnotes],
357
level,
358
classes,
359
);
360
}
361
362
export function insertReferencesTitle(
363
doc: Document,
364
refsEl: Element,
365
language: FormatLanguage,
366
level = 2,
367
classes: string[] = [],
368
) {
369
prependHeading(
370
doc,
371
refsEl,
372
language[kSectionTitleReferences],
373
level,
374
classes,
375
);
376
}
377
378
export function insertTitle(
379
doc: Document,
380
el: Element,
381
title: string,
382
level = 2,
383
headingClasses: string[] = [],
384
) {
385
prependHeading(doc, el, title, level, headingClasses);
386
}
387
388
function prependHeading(
389
doc: Document,
390
el: Element,
391
title: string | undefined,
392
level: number,
393
classes: string[],
394
) {
395
const heading = doc.createElement("h" + level);
396
if (typeof title == "string" && title !== "none") {
397
heading.innerHTML = title;
398
}
399
if (classes) {
400
classes.forEach((clz) => {
401
heading.classList.add(clz);
402
});
403
}
404
405
el.insertBefore(heading, el.firstChild);
406
const hr = el.querySelector("hr");
407
if (hr) {
408
hr.remove();
409
}
410
}
411
412
export function removeFootnoteBacklinks(footnotesEl: Element) {
413
const backlinks = footnotesEl.querySelectorAll(".footnote-back");
414
for (const backlink of backlinks) {
415
(backlink as Element).remove();
416
}
417
}
418
419
export function setMainColumn(doc: Document, column: string) {
420
const selectors = [
421
"main.content",
422
".page-navigation",
423
".quarto-title-banner .quarto-title",
424
".quarto-title-block .quarto-title-meta-author",
425
".quarto-title-block .quarto-title-meta",
426
"div[class^='quarto-about-']",
427
"div[class*=' quarto-about-']",
428
];
429
selectors.forEach((selector) => {
430
const el = doc.querySelector(selector);
431
if (el) {
432
// Clear existing column
433
for (const clz of el.classList) {
434
if (clz.startsWith("column-")) {
435
el.classList.remove(clz);
436
}
437
}
438
439
// Set the new column
440
el.classList.add(column);
441
}
442
});
443
}
444
445
export function hasMarginRefs(format: Format, flags: PandocFlags) {
446
// If margin footnotes are enabled move them
447
return format.pandoc[kReferenceLocation] === "margin" ||
448
flags[kReferenceLocation] === "margin";
449
}
450
451
export function hasMarginCites(format: Format) {
452
// If margin cites are enabled, move them
453
return format.metadata[kCitationLocation] === "margin";
454
}
455
456
export function hasMarginFigCaps(format: Format) {
457
return format.metadata[kFigCapLoc] === "margin";
458
}
459
460
export function computeUrl(
461
input: string,
462
baseUrl: string,
463
offset: string,
464
outputFileName: string,
465
) {
466
const rootDir = normalizePath(join(dirname(input), offset));
467
if (outputFileName === "index.html") {
468
return `${baseUrl}/${relative(rootDir, dirname(input))}`;
469
} else {
470
return `${baseUrl}/${
471
relative(rootDir, join(dirname(input), outputFileName))
472
}`;
473
}
474
}
475
476
export function createCodeCopyButton(doc: Document, format: Format) {
477
const copyButton = doc.createElement("button");
478
const title = format.language[kCopyButtonTooltip]!;
479
copyButton.setAttribute("title", title);
480
copyButton.classList
481
.add("code-copy-button");
482
const copyIcon = doc.createElement("i");
483
copyIcon.classList.add("bi");
484
copyButton.appendChild(copyIcon);
485
return copyButton;
486
}
487
488
export function createCodeBlock(
489
doc: Document,
490
htmlContents: string,
491
language?: string,
492
) {
493
const preEl = doc.createElement("PRE");
494
preEl.classList.add("sourceCode");
495
preEl.classList.add("code-with-copy");
496
497
const codeEl = doc.createElement("CODE");
498
codeEl.classList.add("sourceCode");
499
if (language) {
500
codeEl.classList.add(language);
501
}
502
codeEl.innerHTML = htmlContents;
503
preEl.appendChild(codeEl);
504
return preEl;
505
}
506
507
export function writeMetaTag(name: string, content: string, doc: Document) {
508
// Meta tag
509
const m = doc.createElement("META");
510
if (name.startsWith("og:")) {
511
m.setAttribute("property", name);
512
} else {
513
m.setAttribute("name", name);
514
}
515
m.setAttribute("content", content);
516
517
// New Line
518
const nl = doc.createTextNode("\n");
519
520
// Insert the nodes
521
doc.querySelector("head")?.appendChild(m);
522
doc.querySelector("head")?.appendChild(nl);
523
}
524
525
export function writeLinkTag(rel: string, href: string, doc: Document) {
526
// Meta tag
527
const l = doc.createElement("LINK");
528
l.setAttribute("rel", rel);
529
l.setAttribute("href", href);
530
531
// New Line
532
const nl = doc.createTextNode("\n");
533
534
// Insert the nodes
535
doc.querySelector("head")?.appendChild(l);
536
doc.querySelector("head")?.appendChild(nl);
537
}
538
539
export function formatPageLayout(format: Format) {
540
return format.metadata[kPageLayout] as string || kPageLayoutArticle;
541
}
542
543
export function formatHasFullLayout(format: Format) {
544
return format.metadata[kPageLayout] === kPageLayoutFull;
545
}
546
547
export function formatHasArticleLayout(format: Format) {
548
return format.metadata[kPageLayout] === undefined ||
549
format.metadata[kPageLayout] === kPageLayoutArticle ||
550
format.metadata[kPageLayout] === kPageLayoutFull;
551
}
552
553