Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/package/src/common/update-html-dependencies.ts
6450 views
1
/*
2
* bootstrap.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
import { copySync, ensureDir, ensureDirSync, existsSync, walkSync } from "../../../src/deno_ral/fs.ts";
7
import { info } from "../../../src/deno_ral/log.ts";
8
import { dirname, basename, extname, join } from "../../../src/deno_ral/path.ts";
9
import { lines } from "../../../src/core/text.ts";
10
import * as ld from "../../../src/core/lodash.ts";
11
12
import { runCmd } from "../util/cmd.ts";
13
import { applyGitPatches, Repo, withRepo } from "../util/git.ts";
14
15
import { download } from "../util/utils.ts";
16
import { Configuration } from "./config.ts";
17
import { visitLines } from "../../../src/core/file.ts";
18
import { copyTo } from "../../../src/core/copy.ts";
19
import { kSourceMappingRegexes } from "../../../src/config/constants.ts";
20
import { unzip } from "../../../src/core/zip.ts";
21
22
export async function updateHtmlDependencies(config: Configuration) {
23
info("Updating Bootstrap with version info:");
24
25
// Read the version information from the environment
26
const workingDir = Deno.makeTempDirSync();
27
28
const bsCommit = Deno.env.get("BOOTSTRAP");
29
if (!bsCommit) {
30
throw new Error(`BOOTSTRAP is not defined`);
31
}
32
const bsIconVersion = Deno.env.get("BOOTSTRAP_FONT");
33
if (!bsIconVersion) {
34
throw new Error(`BOOTSTRAP_FONT is not defined`);
35
}
36
const htmlToolsVersion = Deno.env.get("HTMLTOOLS");
37
if (!htmlToolsVersion) {
38
throw new Error("HTMLTOOLS is not defined");
39
}
40
41
info(`Boostrap: ${bsCommit}`);
42
info(`Boostrap Icon: ${bsIconVersion}`);
43
info(`Html Tools: ${htmlToolsVersion}`);
44
45
// the bootstrap and dist/themes dir
46
const formatDir = join(
47
config.directoryInfo.src,
48
"resources",
49
"formats",
50
"html"
51
);
52
53
const bsDir = join(formatDir, "bootstrap");
54
55
const bsThemesDir = join(bsDir, "themes");
56
57
const bsDistDir = join(bsDir, "dist");
58
59
const htmlToolsDir = join(formatDir, "htmltools");
60
const bslibDir = join(formatDir, "bslib");
61
62
// For applying git patch to what we retreive
63
const patchesDir = join(config.directoryInfo.pkg, "src", "common", "patches");
64
65
function resolvePatches(patches: string[]) {
66
return patches.map((patch) => {
67
return join(patchesDir, patch);
68
});
69
}
70
71
// Anchor
72
const anchorJs = join(formatDir, "anchor", "anchor.min.js");
73
await updateUnpkgDependency(
74
"ANCHOR_JS",
75
"anchor-js",
76
"anchor.min.js",
77
anchorJs
78
);
79
cleanSourceMap(anchorJs);
80
81
// Poppper
82
const popperJs = join(formatDir, "popper", "popper.min.js");
83
await updateUnpkgDependency(
84
"POPPER_JS",
85
"@popperjs/core",
86
"dist/umd/popper.min.js",
87
popperJs
88
);
89
cleanSourceMap(popperJs);
90
91
// Clipboard
92
const clipboardJs = join(formatDir, "clipboard", "clipboard.min.js");
93
await updateGithubSourceCodeDependency(
94
"clipboardjs",
95
"zenorocha/clipboard.js",
96
"CLIPBOARD_JS",
97
workingDir,
98
(dir: string, version: string) => {
99
// Copy the js file
100
Deno.copyFileSync(
101
join(dir, `clipboard.js-${version}`, "dist", "clipboard.min.js"),
102
clipboardJs
103
);
104
return Promise.resolve();
105
}
106
);
107
cleanSourceMap(clipboardJs);
108
109
// Day.js locales
110
// https://github.com/iamkun/dayjs/tree/dev/src/locale
111
const dayJsDir = join(
112
config.directoryInfo.src,
113
"resources",
114
"library",
115
"dayjs"
116
);
117
await updateGithubSourceCodeDependency(
118
"dayjs",
119
"iamkun/dayjs",
120
"DAY_JS",
121
workingDir,
122
async (dir: string, version: string) => {
123
const sourceDir = join(dir, `dayjs-${version}`, "src", "locale");
124
const targetDir = join(dayJsDir, "locale");
125
ensureDirSync(targetDir);
126
127
const files = Deno.readDirSync(sourceDir);
128
for (const file of files) {
129
const targetFile = join(targetDir, file.name);
130
// Move the file
131
Deno.copyFileSync(join(sourceDir, file.name), targetFile);
132
133
// Fixup the file to remove these lines
134
const ignore = [
135
"import dayjs from 'dayjs'",
136
"dayjs.locale(locale, null, true)",
137
];
138
info("Visiting lines of " + targetFile);
139
const output: string[] = [];
140
await visitLines(targetFile, (line: string | null, _count: number) => {
141
if (line !== null) {
142
if (!ignore.includes(line)) {
143
output.push(line);
144
}
145
}
146
return true;
147
});
148
149
Deno.writeTextFileSync(targetFile, output.join("\n"));
150
}
151
}
152
);
153
154
// Tippy
155
const tippyUmdJs = join(formatDir, "tippy", "tippy.umd.min.js");
156
await updateUnpkgDependency(
157
"TIPPY_JS",
158
"tippy.js",
159
"dist/tippy.umd.min.js",
160
tippyUmdJs
161
);
162
cleanSourceMap(tippyUmdJs);
163
164
// List.js
165
const listJs = join(
166
config.directoryInfo.src,
167
"resources",
168
"projects",
169
"website",
170
"listing",
171
"list.min.js"
172
);
173
await updateGithubSourceCodeDependency(
174
"listjs",
175
"javve/list.js",
176
"LIST_JS",
177
workingDir,
178
(dir: string, version: string) => {
179
ensureDirSync(dirname(listJs));
180
// Copy the js file
181
Deno.copyFileSync(
182
join(dir, `list.js-${version}`, "dist", "list.min.js"),
183
listJs
184
);
185
186
// Omit regular expression escaping
187
// (Fixes https://github.com/quarto-dev/quarto-cli/issues/8435)
188
const contents = Deno.readTextFileSync(listJs);
189
const removeContent = /(\(e=t\.utils\.toString\(e\)\.toLowerCase\(\)\))\.replace\(.*\)(,r=e)/g;
190
const cleaned = contents.replace(removeContent, "$1$2");
191
Deno.writeTextFileSync(listJs, cleaned);
192
193
return Promise.resolve();
194
}
195
);
196
197
// Zenscroll
198
const zenscrollJs = join(formatDir, "zenscroll", "zenscroll-min.js");
199
await updateGithubSourceCodeDependency(
200
"zenscroll",
201
"zengabor/zenscroll",
202
"ZENSCROLL_JS",
203
workingDir,
204
(dir: string, version: string) => {
205
ensureDirSync(dirname(zenscrollJs));
206
// Copy the js file
207
Deno.copyFileSync(
208
join(dir, `zenscroll-${version}`, "zenscroll-min.js"),
209
zenscrollJs
210
);
211
return Promise.resolve();
212
}
213
);
214
215
// Tippy
216
const tippyCss = join(formatDir, "tippy", "tippy.css");
217
await updateUnpkgDependency(
218
"TIPPY_JS",
219
"tippy.js",
220
"dist/tippy.css",
221
tippyCss
222
);
223
cleanSourceMap(tippyCss);
224
225
// Glightbox
226
const glightboxDir = join(formatDir, "glightbox");
227
const glightBoxVersion = Deno.env.get("GLIGHTBOX_JS");;
228
229
info("Updating glightbox");
230
const fileName = `glightbox-master.zip`;
231
const distUrl = `https://github.com/biati-digital/glightbox/releases/download/${glightBoxVersion}/${fileName}`;
232
const zipFile = join(workingDir, fileName);
233
234
// Download and unzip the release
235
const glightboxWorking = join(workingDir, "glightbox-master");
236
ensureDirSync(glightboxWorking);
237
238
info(`Downloading ${distUrl}`);
239
await download(distUrl, zipFile);
240
await unzip(zipFile, glightboxWorking);
241
242
// Remove extraneous files
243
[
244
{
245
from: join("dist", "js", "glightbox.min.js"),
246
to: "glightbox.min.js",
247
},
248
{
249
from: join("dist", "css", "glightbox.min.css"),
250
to: "glightbox.min.css",
251
},
252
].forEach((depends) => {
253
// Copy the js file
254
Deno.copyFileSync(
255
join(glightboxWorking, depends.from),
256
join(glightboxDir, depends.to)
257
);
258
});
259
info("");
260
261
// Fuse
262
const fuseJs = join(
263
config.directoryInfo.src,
264
"resources",
265
"projects",
266
"website",
267
"search",
268
"fuse.min.js"
269
);
270
await updateGithubSourceCodeDependency(
271
"fusejs",
272
"krisk/Fuse",
273
"FUSE_JS",
274
workingDir,
275
(dir: string, version: string) => {
276
// Copy the js file
277
ensureDirSync(dirname(fuseJs));
278
Deno.copyFileSync(
279
join(dir, `Fuse-${version}`, "dist", "fuse.min.js"),
280
fuseJs
281
);
282
return Promise.resolve();
283
}
284
);
285
cleanSourceMap(fuseJs);
286
287
// reveal.js
288
const revealJs = join(
289
config.directoryInfo.src,
290
"resources",
291
"formats",
292
"revealjs",
293
"reveal"
294
);
295
296
await updateGithubSourceCodeDependency(
297
"reveal.js",
298
"hakimel/reveal.js",
299
"REVEAL_JS",
300
workingDir,
301
(dir: string, version: string) => {
302
// Copy the desired resource files
303
info("Copying reveal.js resources' directory");
304
if (existsSync(revealJs)) {
305
Deno.removeSync(revealJs, { recursive: true });
306
}
307
ensureDirSync(revealJs);
308
309
info("Copying css/");
310
const cssDir = join(revealJs, "css");
311
copyTo(join(dir, `reveal.js-${version}`, "css"), cssDir, { overwrite: true, preserveTimestamps: true });
312
info("Port native scss themes to quarto theme");
313
const sourceThemes = join(cssDir, "theme", "source");
314
const portedThemes = join(dirname(revealJs), "themes");
315
for (const fileEntry of Deno.readDirSync(sourceThemes)) {
316
if (fileEntry.isFile && extname(fileEntry.name) === ".scss") {
317
// Ignore specific files that are aliased to custom quarto theme
318
if (["white.scss", "black.scss", "white-contrast.scss", "black-contrast.scss"].includes(fileEntry.name)) {
319
info(`-> ignore ${fileEntry.name} - do not port to quarto.`);
320
continue;
321
}
322
info(`-> porting ${fileEntry.name} to quarto theme.`);
323
copyTo(join(sourceThemes, fileEntry.name), join(portedThemes, fileEntry.name), { overwrite: true, preserveTimestamps: true });
324
portRevealTheme(join(portedThemes, fileEntry.name));
325
}
326
}
327
// copy settings.scss and patch to help check correct addition of theme
328
const templateDir = join(cssDir, "theme", "template");
329
const templateDirNew = join(portedThemes, "template");
330
ensureDirSync(templateDirNew);
331
copyTo(join(templateDir, "settings.scss"), join(templateDirNew, "settings.scss"), { overwrite: true, preserveTimestamps: true });
332
portRevealTheme(join(templateDirNew, "settings.scss"));
333
info("Copying dist/");
334
const dist = join(revealJs, "dist");
335
copyTo(join(dir, `reveal.js-${version}`, "dist"), dist, { overwrite: true, preserveTimestamps: true });
336
// remove unneeded CSS files
337
const theme = join(dist, "theme");
338
for (const fileEntry of Deno.readDirSync(theme)) {
339
if (fileEntry.isFile && extname(fileEntry.name) === ".css") {
340
info(`-> Removing unneeded ${fileEntry.name}.`);
341
Deno.removeSync(join(theme, fileEntry.name));
342
}
343
}
344
info("Copying plugin/");
345
copyTo(
346
join(dir, `reveal.js-${version}`, "plugin"),
347
join(revealJs, "plugin"),
348
{ overwrite: true, preserveTimestamps: true }
349
);
350
return Promise.resolve();
351
},
352
true,
353
false,
354
resolvePatches([
355
// patche for each themes
356
...["beige", "blood", "dracula", "league", "moon", "night", "serif", "simple", "sky", "solarized"].map(
357
(theme) => `revealjs-theme-0001-${theme}.patch`
358
),
359
// global patches
360
"revealjs-theme-0002-input-panel-bg.patch",
361
"revealjs-theme-0003-code-block-fixup.patch"
362
])
363
);
364
365
// revealjs-chalkboard
366
const revealJsChalkboard = join(
367
config.directoryInfo.src,
368
"resources",
369
"formats",
370
"revealjs",
371
"plugins",
372
"chalkboard"
373
);
374
await updateGithubSourceCodeDependency(
375
"reveal.js-chalkboard",
376
"rajgoel/reveal.js-plugins",
377
"REVEAL_JS_CHALKBOARD",
378
workingDir,
379
(dir: string, version: string) => {
380
ensureDirSync(dirname(revealJsChalkboard));
381
copyTo(
382
join(dir, `reveal.js-plugins-${version}`, "chalkboard"),
383
revealJsChalkboard,
384
{ overwrite: true, preserveTimestamps: true }
385
);
386
return Promise.resolve();
387
},
388
true, // true if commit, false otherwise
389
false, // no v prefix,
390
);
391
392
// revealjs-menu
393
const revealJsMenu = join(
394
config.directoryInfo.src,
395
"resources",
396
"formats",
397
"revealjs",
398
"plugins",
399
"menu"
400
);
401
await updateGithubSourceCodeDependency(
402
"reveal.js-menu",
403
"denehyg/reveal.js-menu",
404
"REVEAL_JS_MENU",
405
workingDir,
406
(dir: string, version: string) => {
407
// Copy the js file (modify to disable loadResource)
408
ensureDirSync(revealJsMenu);
409
const menuJs = Deno.readTextFileSync(
410
join(dir, `reveal.js-menu-${version}`, "menu.js")
411
).replace(
412
/function P\(e,t,n\).*?function M/,
413
"function P(e,t,n){n.call()}function M"
414
);
415
Deno.writeTextFileSync(join(revealJsMenu, "menu.js"), menuJs);
416
417
// copy the css file
418
Deno.copyFileSync(
419
join(dir, `reveal.js-menu-${version}`, "menu.css"),
420
join(revealJsMenu, "menu.css")
421
);
422
423
// copy font-awesome to chalkboard
424
copyTo(
425
join(dir, `reveal.js-menu-${version}`, "font-awesome"),
426
join(revealJsChalkboard, "font-awesome"),
427
{ overwrite: true, preserveTimestamps: true }
428
);
429
return Promise.resolve();
430
},
431
false, // not a commit
432
false // no v prefix
433
);
434
435
// reveal-pdfexport
436
const revealJsPdfExport = join(
437
config.directoryInfo.src,
438
"resources",
439
"formats",
440
"revealjs",
441
"plugins",
442
"pdfexport"
443
);
444
445
await updateGithubSourceCodeDependency(
446
"reveal-pdfexport",
447
"McShelby/reveal-pdfexport",
448
"REVEAL_JS_PDFEXPORT",
449
workingDir,
450
(dir: string, version: string) => {
451
ensureDirSync(revealJsPdfExport);
452
copyTo(
453
join(dir, `reveal-pdfexport-${version}`, "pdfexport.js"),
454
join(revealJsPdfExport, "pdfexport.js"),
455
{ overwrite: true, preserveTimestamps: true }
456
);
457
return Promise.resolve();
458
},
459
false, // not a commit
460
false, // no v prefix,
461
resolvePatches([
462
"revealjs-plugin-0001-pdfexport-to-export-toggle-fun.patch",
463
"revealjs-plugin-0001-pdfexport-view-mode.patch"
464
])
465
);
466
467
// Github CSS (used for GFM HTML preview)
468
const ghCSS = join(
469
config.directoryInfo.src,
470
"resources",
471
"formats",
472
"gfm",
473
"github-markdown-css"
474
);
475
await updateGithubSourceCodeDependency(
476
"github-markdown-css",
477
"sindresorhus/github-markdown-css",
478
"GITHUB_MARKDOWN_CSS",
479
workingDir,
480
(dir: string, version: string) => {
481
ensureDirSync(ghCSS);
482
const files = [
483
"github-markdown-dark.css",
484
"github-markdown-light.css",
485
"github-markdown.css",
486
];
487
files.forEach((file) => {
488
// Copy the js file
489
Deno.copyFileSync(
490
join(dir, `github-markdown-css-${version}`, file),
491
join(ghCSS, file)
492
);
493
});
494
return Promise.resolve();
495
}
496
);
497
498
// Autocomplete
499
const autocompleteJs = join(
500
config.directoryInfo.src,
501
"resources",
502
"projects",
503
"website",
504
"search",
505
"autocomplete.umd.js"
506
);
507
await updateUnpkgDependency(
508
"AUTOCOMPLETE_JS",
509
"@algolia/autocomplete-js",
510
"dist/umd/index.production.js",
511
autocompleteJs
512
);
513
cleanSourceMap(autocompleteJs);
514
515
// Autocomplete preset
516
const autocompletePresetJs = join(
517
config.directoryInfo.src,
518
"resources",
519
"projects",
520
"website",
521
"search",
522
"autocomplete-preset-algolia.umd.js"
523
);
524
await updateUnpkgDependency(
525
"AUTOCOMPLETE_JS",
526
"@algolia/autocomplete-preset-algolia",
527
"dist/umd/index.production.js",
528
autocompletePresetJs
529
);
530
cleanSourceMap(autocompletePresetJs);
531
532
// Update PDF JS
533
await updatePdfJs(config, workingDir);
534
535
// Cookie-Consent
536
await updateCookieConsent(config, "4.0.0", workingDir);
537
538
// Sticky table headers
539
await updateStickyThead(config, workingDir);
540
541
// Datatables and PDF Make
542
await updateDatatables(config, workingDir);
543
544
// Clean existing directories
545
[bsThemesDir, bsDistDir].forEach((dir) => {
546
if (existsSync(dir)) {
547
Deno.removeSync(dir, { recursive: true });
548
}
549
ensureDirSync(dir);
550
});
551
552
const workingSubDir = (name: string) => {
553
const dir = join(workingDir, name);
554
ensureDirSync(dir);
555
return dir;
556
};
557
558
// Update bootstrap
559
await updateBootstrapFromBslib(
560
bsCommit,
561
workingSubDir("bsdist"),
562
bsDistDir,
563
bsThemesDir,
564
bslibDir
565
);
566
567
// Update Html Tools
568
await updateHtmlTools(
569
htmlToolsVersion,
570
workingSubDir("htmltools"),
571
htmlToolsDir
572
)
573
574
// Update Bootstrap icons
575
await updateBoostrapIcons(bsIconVersion, workingSubDir("bsicons"), bsDistDir);
576
577
// Update Pandoc themes
578
await updatePandocHighlighting(config);
579
580
//
581
582
// Clean up the temp dir
583
try {
584
Deno.removeSync(workingDir, { recursive: true });
585
} catch (_err) {
586
info(`Folder not deleted - Remove manually: ${workingDir}`);
587
}
588
info("\n** Done- please commit any files that have been updated. **\n");
589
}
590
591
async function updatePdfJs(config: Configuration, working: string) {
592
const version = Deno.env.get("PDF_JS");
593
594
info("Updating pdf.js...");
595
const fileName = `pdfjs-${version}-legacy-dist.zip`;
596
const distUrl = `https://github.com/mozilla/pdf.js/releases/download/v${version}/${fileName}`;
597
const zipFile = join(working, fileName);
598
599
// Download and unzip the release
600
const pdfjsDir = join(working, "pdfjs");
601
ensureDirSync(pdfjsDir);
602
603
info(`Downloading ${distUrl}`);
604
await download(distUrl, zipFile);
605
await unzip(zipFile, pdfjsDir);
606
607
// Remove extraneous files
608
const removeFiles = ["web/compressed.tracemonkey-pldi-09.pdf"];
609
removeFiles.forEach((file) => Deno.removeSync(join(pdfjsDir, file)));
610
611
const from = pdfjsDir;
612
const to = join(
613
config.directoryInfo.src,
614
"resources",
615
"formats",
616
"pdf",
617
"pdfjs"
618
);
619
copySync(from, to, { overwrite: true });
620
info("Done\n");
621
}
622
623
async function updateCookieConsent(
624
config: Configuration,
625
version: string,
626
working: string
627
) {
628
const fileName = "cookie-consent.js";
629
const url = `https://www.cookieconsent.com/releases/${version}/${fileName}`;
630
const tempPath = join(working, fileName);
631
632
info(`Downloading ${url}`);
633
await download(url, tempPath);
634
635
const targetDir = join(
636
config.directoryInfo.src,
637
"resources",
638
"projects",
639
"website",
640
"cookie-consent"
641
);
642
await ensureDir(targetDir);
643
644
await Deno.copyFile(tempPath, join(targetDir, fileName));
645
info("Done\n");
646
}
647
648
async function updateDatatables(
649
config: Configuration,
650
working: string
651
) {
652
// css:
653
// script: https://cdn.datatables.net/v/bs5/jszip-3.10.1/dt-1.13.8/b-2.4.2/b-html5-2.4.2/b-print-2.4.2/kt-2.11.0/r-2.5.0/datatables.min.js
654
655
// pdfmake
656
// https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.min.js
657
// https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js
658
const datatablesConfig = Deno.env.get("DATATABLES_CONFIG");;
659
const pdfMakeVersion = Deno.env.get("PDF_MAKE");;
660
const dtFiles = ["datatables.min.css", "datatables.min.js"];
661
const targetDir = join(
662
config.directoryInfo.src,
663
"resources",
664
"formats",
665
"dashboard",
666
"js",
667
"dt"
668
);
669
await ensureDir(targetDir);
670
671
for (const file of dtFiles) {
672
const url = `https://cdn.datatables.net/v/${datatablesConfig}/${file}`;
673
const tempPath = join(working, file);
674
info(`Downloading ${url}`);
675
await download(url, tempPath);
676
await Deno.copyFile(tempPath, join(targetDir, file));
677
}
678
679
const pdfMakeFiles = ["pdfmake.min.js", "vfs_fonts.js"];
680
for (const file of pdfMakeFiles) {
681
const url = `https://cdnjs.cloudflare.com/ajax/libs/pdfmake/${pdfMakeVersion}/${file}`;
682
const tempPath = join(working, file);
683
info(`Downloading ${url}`);
684
await download(url, tempPath);
685
await Deno.copyFile(tempPath, join(targetDir, file));
686
}
687
688
info("Done\n");
689
}
690
691
async function updateStickyThead(
692
config: Configuration,
693
working: string
694
) {
695
const fileName = "stickythead.js";
696
const url = `https://raw.githubusercontent.com/rohanpujaris/stickythead/master/dist/${fileName}`;
697
const tempPath = join(working, fileName);
698
699
info(`Downloading ${url}`);
700
await download(url, tempPath);
701
702
const targetDir = join(
703
config.directoryInfo.src,
704
"resources",
705
"formats",
706
"dashboard",
707
"js"
708
);
709
await ensureDir(targetDir);
710
711
await Deno.copyFile(tempPath, join(targetDir, fileName));
712
info("Done\n");
713
}
714
715
async function updateHtmlTools(
716
version: string,
717
working: string,
718
distDir: string
719
) {
720
// https://github.com/rstudio/htmltools/archive/refs/tags/v0.5.6.zip
721
info("Updating Html Tools...");
722
const dirName = `htmltools-${version}`;
723
const fileName = `v${version}.zip`;
724
const distUrl = `https://github.com/rstudio/htmltools/archive/refs/tags/${fileName}`;
725
const zipFile = join(working, fileName);
726
727
// Download and unzip the release
728
info(`Downloading ${distUrl}`);
729
await download(distUrl, zipFile);
730
await unzip(zipFile, working);
731
732
// Copy the fill css file
733
ensureDirSync(distDir);
734
Deno.copyFileSync(
735
join(working, dirName, "inst", "fill", "fill.css"),
736
join(distDir, "fill.css")
737
);
738
739
info("Done\n");
740
}
741
742
async function updateBootstrapFromBslib(
743
commit: string,
744
working: string,
745
distDir: string,
746
themesDir: string,
747
bsLibDir: string
748
) {
749
info("Updating Bootstrap Scss Files...");
750
await withRepo(
751
working,
752
"https://github.com/rstudio/bslib.git",
753
async (repo: Repo) => {
754
// Checkout the appropriate version
755
await repo.checkout(commit);
756
757
// Build the required JS files
758
info("Copying Components");
759
760
// Get the components
761
const componentsFrom = join(repo.dir, "inst", "components", "dist");
762
const componentsTo = join(distDir, "components");
763
const components = ["accordion", "card", "grid", "sidebar", "valuebox"];
764
for (const component of components) {
765
info(` - ${component}`);
766
const componentDir = join(componentsTo, component);
767
ensureDirSync(componentDir);
768
769
const files = [
770
`${component}.min.js`,
771
`${component}.css`
772
];
773
774
for (const file of files) {
775
const fromPath = join(componentsFrom, component, file);
776
if (existsSync(fromPath)) {
777
const toPath = join(componentsTo, component, file);
778
ensureDirSync(dirname(toPath));
779
Deno.copyFileSync(fromPath, toPath);
780
781
// Clean the source path
782
cleanSourceMap(toPath);
783
}
784
}
785
}
786
787
// Copy the scss files
788
info("Copying scss files");
789
const from = join(repo.dir, "inst", "lib", "bs5", "scss");
790
const to = join(distDir, "scss");
791
info(`Copying ${from} to ${to}`);
792
copySync(from, to);
793
794
// Fix up the Boostrap rules files
795
info(
796
"Rewriting bootstrap.scss to exclude functions, mixins, and variables."
797
);
798
const bootstrapFilter = [
799
'@import "functions";',
800
'@import "variables";',
801
'@import "mixins";',
802
];
803
const bootstrapScssFile = join(to, "bootstrap.scss");
804
const bootstrapScssContents = lines(
805
Deno.readTextFileSync(bootstrapScssFile)
806
)
807
.filter((line: string) => {
808
return !bootstrapFilter.includes(line);
809
})
810
.join("\n");
811
Deno.writeTextFileSync(bootstrapScssFile, bootstrapScssContents);
812
info("done.");
813
info("");
814
815
// Rewrite the use of css `var()` style values to base SCSS values
816
info("Rewriting _variables.scss file.");
817
const bootstrapVariablesFile = join(to, "_variables.scss");
818
const varContents = lines(Deno.readTextFileSync(bootstrapVariablesFile));
819
const outLines: string[] = [];
820
for (let line of varContents) {
821
line = line.replaceAll(
822
"var(--#{$prefix}font-sans-serif)",
823
"$$font-family-sans-serif"
824
);
825
line = line.replaceAll(
826
"var(--#{$prefix}font-monospace)",
827
"$$font-family-monospace"
828
);
829
line = line.replaceAll(
830
"var(--#{$prefix}success-rgb)",
831
"$$success"
832
);
833
line = line.replaceAll(
834
"var(--#{$prefix}danger-rgb)",
835
"$$danger"
836
);
837
line = line.replaceAll(
838
"var(--#{$prefix}body-color-rgb)",
839
"$$body-color"
840
);
841
line = line.replaceAll(
842
"var(--#{$prefix}body-bg-rgb)",
843
"$$body-bg"
844
);
845
line = line.replaceAll(
846
"var(--#{$prefix}emphasis-color-rgb)",
847
"$$body-emphasis-color"
848
);
849
line = line.replaceAll(
850
/RGBA?\(var\(--#\{\$prefix\}emphasis-color-rgb,(.*?)\).*?\)/gm,
851
"$$body-emphasis-color"
852
);
853
line = line.replaceAll(
854
"var(--#{$prefix}secondary-color)",
855
"$$body-secondary-color"
856
);
857
line = line.replaceAll(
858
"var(--#{$prefix}secondary-bg)",
859
"$$body-secondary-bg"
860
);
861
line = line.replaceAll(
862
"var(--#{$prefix}tertiary-bg)",
863
"$$body-tertiary-bg"
864
);
865
line = line.replaceAll(
866
"var(--#{$prefix}tertiary-color)",
867
"$body-tertiary-color"
868
);
869
line = line.replaceAll(
870
"var(--#{$prefix}emphasis-bg)",
871
"$$body-emphasis-bg"
872
);
873
line = line.replaceAll(
874
"var(--#{$prefix}emphasis-color)",
875
"$$body-emphasis-color"
876
);
877
line = line.replaceAll(
878
"$emphasis-color-rgb",
879
"$$body-emphasis-color"
880
);
881
882
line = line.replaceAll(/var\(--#\{\$prefix\}(.*?)\)/gm, "$$$1");
883
outLines.push(line);
884
}
885
Deno.writeTextFileSync(bootstrapVariablesFile, outLines.join("\n"));
886
info("done.");
887
info("");
888
889
// Copy utils
890
info("Copying scss files");
891
const utilsFrom = join(repo.dir, "inst", "sass-utils");
892
const utilsTo = join(distDir, "sass-utils");
893
info(`Copying ${utilsFrom} to ${utilsTo}`);
894
copySync(utilsFrom, utilsTo);
895
896
// Copy bslib
897
info("Copying BSLIB scss files");
898
const bslibScssFrom = join(repo.dir, "inst", "bslib-scss");
899
const bslibScssTo = join(bsLibDir, "bslib-scss");
900
info(`Copying ${bslibScssFrom} to ${bslibScssTo}`);
901
Deno.removeSync(bslibScssTo, { recursive: true});
902
copySync(bslibScssFrom, bslibScssTo);
903
904
// Copy componennts
905
info("Copying BSLIB component scss files");
906
const componentFrom = join(repo.dir, "inst", "components", "scss");
907
const componentTo = join(bsLibDir, "components", "scss");
908
info(`Copying ${componentFrom} to ${componentTo}`);
909
copySync(componentFrom, componentTo, {overwrite: true});
910
911
info("Copying BSLIB dist files");
912
const componentDistFrom = join(repo.dir, "inst", "components", "dist");
913
const componentDistTo = join(bsLibDir, "components", "dist");
914
info(`Copying ${componentDistFrom} to ${componentDistTo}`);
915
ensureDirSync(componentDistTo);
916
copySync(componentDistFrom, componentDistTo, {overwrite: true});
917
// Clean map references
918
for (const entry of walkSync(componentDistTo)) {
919
if (entry.isFile) {
920
cleanSourceMap(entry.path);
921
}
922
}
923
924
// Grab the js file that we need
925
info("Copying dist files");
926
[
927
{
928
from: "bootstrap.bundle.min.js",
929
to: "bootstrap.min.js",
930
},
931
{
932
from: "bootstrap.bundle.min.js.map",
933
to: "bootstrap.min.js.map",
934
},
935
].forEach((file) => {
936
const from = join(
937
repo.dir,
938
"inst",
939
"lib",
940
"bs5",
941
"dist",
942
"js",
943
file.from
944
);
945
const to = join(distDir, file.to);
946
info(`Copying ${from} to ${to}`);
947
Deno.copyFileSync(from, to);
948
});
949
950
// Merge the bootswatch themes
951
info("Merging themes:");
952
const exclude = ["4"];
953
const distPath = join(repo.dir, "inst", "lib", "bsw5", "dist");
954
for (const dirEntry of Deno.readDirSync(distPath)) {
955
if (dirEntry.isDirectory && !exclude.includes(dirEntry.name)) {
956
// this is a theme directory
957
const theme = dirEntry.name;
958
const themeDir = join(distPath, theme);
959
960
info(`${theme}`);
961
const layer = mergedSassLayer(
962
join(themeDir, "_functions.scss"),
963
join(themeDir, "_variables.scss"),
964
join(themeDir, "_mixins.scss"),
965
join(themeDir, "_bootswatch.scss")
966
);
967
968
const patchedScss = patchTheme(theme, layer, bootswatchThemePatches);
969
970
const themeOut = join(themesDir, `${theme}.scss`);
971
Deno.writeTextFileSync(themeOut, patchedScss);
972
}
973
}
974
975
976
info("Done\n");
977
}
978
);
979
}
980
981
async function updateBoostrapIcons(
982
version: string,
983
working: string,
984
distDir: string
985
) {
986
info("Updating Bootstrap Icons...");
987
const dirName = `bootstrap-icons-${version}`;
988
const fileName = `${dirName}.zip`;
989
const distUrl = `https://github.com/twbs/icons/releases/download/v${version}/${fileName}`;
990
const zipFile = join(working, fileName);
991
992
// Download and unzip the release
993
info(`Downloading ${distUrl}`);
994
await download(distUrl, zipFile);
995
await unzip(zipFile, working);
996
997
// Copy the woff file
998
Deno.copyFileSync(
999
join(working, dirName, "fonts", "bootstrap-icons.woff"),
1000
join(distDir, "bootstrap-icons.woff")
1001
);
1002
1003
// Copy the css file, then fix it up
1004
const cssPath = join(distDir, "bootstrap-icons.css");
1005
Deno.copyFileSync(
1006
join(working, dirName, "bootstrap-icons.css"),
1007
cssPath
1008
);
1009
fixupFontCss(cssPath);
1010
1011
info("Done\n");
1012
}
1013
1014
async function updatePandocHighlighting(config: Configuration) {
1015
info("Updating Pandoc Highlighting Themes...");
1016
1017
const highlightDir = join(
1018
config.directoryInfo.src,
1019
"resources",
1020
"pandoc",
1021
"highlight-styles"
1022
);
1023
const pandoc =
1024
Deno.env.get("QUARTO_PANDOC") ||
1025
join(config.directoryInfo.bin, "tools", "pandoc");
1026
1027
// List the styles
1028
const result = await runCmd(pandoc, ["--list-highlight-styles"]);
1029
if (result.status.success) {
1030
const highlightStyles = result.stdout;
1031
if (highlightStyles) {
1032
// Got through the list of styles and extract each style to our resources
1033
const styles = lines(highlightStyles);
1034
info(`Updating ${styles.length} styles...`);
1035
for (const style of styles) {
1036
if (style) {
1037
info(`-> ${style}...`);
1038
const themeResult = await runCmd(pandoc, [
1039
"--print-highlight-style",
1040
style,
1041
]);
1042
1043
if (themeResult.status.success) {
1044
const themeData = themeResult.stdout;
1045
await Deno.writeTextFile(
1046
join(highlightDir, `${style}.theme`),
1047
themeData
1048
);
1049
}
1050
}
1051
}
1052
}
1053
}
1054
}
1055
1056
async function updateUnpkgDependency(
1057
versionEnvVar: string,
1058
pkg: string,
1059
filename: string,
1060
target: string
1061
) {
1062
const version = Deno.env.get(versionEnvVar);
1063
if (version) {
1064
info(`Updating ${pkg}...`);
1065
const url = `https://unpkg.com/${pkg}@${version}/${filename}`;
1066
1067
info(`Downloading ${url} to ${target}`);
1068
ensureDirSync(dirname(target));
1069
await download(url, target);
1070
info("done\n");
1071
} else {
1072
throw new Error(`${versionEnvVar} is not defined`);
1073
}
1074
}
1075
1076
/*
1077
async function updateJsDelivrDependency(
1078
versionEnvVar: string,
1079
pkg: string,
1080
filename: string,
1081
target: string,
1082
) {
1083
const version = Deno.env.get(versionEnvVar);
1084
if (version) {
1085
info(`Updating ${pkg}...`);
1086
const url = `https://cdn.jsdelivr.net/npm/${pkg}@${version}/${filename}`;
1087
1088
info(`Downloading ${url} to ${target}`);
1089
ensureDirSync(dirname(target));
1090
await download(url, target);
1091
info("done\n");
1092
} else {
1093
throw new Error(`${versionEnvVar} is not defined`);
1094
}
1095
}
1096
*/
1097
1098
async function updateGithubSourceCodeDependency(
1099
name: string,
1100
repo: string,
1101
versionEnvVar: string,
1102
working: string,
1103
onDownload: (dir: string, version: string) => Promise<void>,
1104
commit = false, // set to true when commit is used instead of a tag
1105
vPrefix = true, // set to false if github tags don't use a v prefix
1106
patches?: string[]
1107
) {
1108
info(`Updating ${name}...`);
1109
const version = Deno.env.get(versionEnvVar)?.trim();
1110
if (version) {
1111
const fileName = `${name}.zip`;
1112
const distUrl = join(
1113
`https://github.com/${repo}/archive`,
1114
commit
1115
? `${version}.zip`
1116
: `refs/tags/${vPrefix ? "v" : ""}${version}.zip`
1117
);
1118
const zipFile = join(working, fileName);
1119
1120
// Download and unzip the release
1121
info(`Downloading ${distUrl}`);
1122
await download(distUrl, zipFile);
1123
await unzip(zipFile, working);
1124
1125
await onDownload(working, version);
1126
if (patches) {
1127
await applyGitPatches(patches);
1128
}
1129
} else {
1130
throw new Error(`${versionEnvVar} is not defined`);
1131
}
1132
1133
info("Done\n");
1134
}
1135
1136
function fixupFontCss(path: string) {
1137
let css = Deno.readTextFileSync(path);
1138
1139
// Clear the woff2 reference
1140
const woff2Regex =
1141
/url\("\.\/fonts\/bootstrap-icons\.woff2.*format\("woff2"\),/;
1142
css = css.replace(woff2Regex, "");
1143
1144
// Update the font reference to point to the local font
1145
const woffPathRegex = /url\("\.(\/fonts)\/bootstrap-icons\.woff\?/;
1146
css = css.replace(woffPathRegex, (substring: string) => {
1147
return substring.replace("/fonts", "");
1148
});
1149
1150
Deno.writeTextFileSync(path, css);
1151
}
1152
1153
// Cleans the source map declaration at the end of a JS file
1154
function cleanSourceMap(path: string) {
1155
if (existsSync(path)) {
1156
const source = Deno.readTextFileSync(path);
1157
Deno.writeTextFileSync(
1158
path,
1159
source
1160
.replaceAll(kSourceMappingRegexes[0], "")
1161
.replaceAll(kSourceMappingRegexes[1], "")
1162
);
1163
}
1164
}
1165
1166
function mergedSassLayer(
1167
funcPath: string,
1168
defaultsPath: string,
1169
mixinsPath: string,
1170
rulesPath: string
1171
) {
1172
const merged: string[] = [];
1173
[
1174
{
1175
name: "functions",
1176
path: funcPath,
1177
},
1178
{
1179
name: "defaults",
1180
path: defaultsPath,
1181
},
1182
{
1183
name: "mixins",
1184
path: mixinsPath,
1185
},
1186
{
1187
name: "rules",
1188
path: rulesPath,
1189
},
1190
].forEach((part) => {
1191
const contents = existsSync(part.path)
1192
? Deno.readTextFileSync(part.path)
1193
: undefined;
1194
if (contents) {
1195
merged.push(`/*-- scss:${part.name} --*/`);
1196
1197
const inputLines = contents.split("\n");
1198
const outputLines: string[] = [];
1199
1200
// This filters out any leading comments
1201
// in the theme file (which we think could be confusing
1202
// to users who are using these files as exemplars)
1203
let emit = false;
1204
inputLines.forEach((line) => {
1205
if (!line.startsWith("//")) {
1206
emit = true;
1207
}
1208
1209
if (emit) {
1210
outputLines.push(line);
1211
}
1212
});
1213
1214
merged.push(outputLines.join("\n"));
1215
merged.push("\n");
1216
}
1217
});
1218
return merged.join("\n");
1219
}
1220
1221
function patchTheme(themeName: string, themeContents: string, themePatches: Record<string, ThemePatch[]> ) {
1222
const patches = themePatches[themeName];
1223
if (patches) {
1224
let patchedTheme = themeContents;
1225
patches.forEach((patch) => {
1226
if (patchedTheme.includes(patch.from)) {
1227
patchedTheme = patchedTheme.replace(patch.from, patch.to);
1228
} else {
1229
throw Error(
1230
`Unable to patch template ${themeName} because the target ${patch.from} cannot be found`
1231
);
1232
}
1233
});
1234
return patchedTheme;
1235
} else {
1236
return themeContents;
1237
}
1238
}
1239
1240
interface ThemePatch {
1241
from: string;
1242
to: string;
1243
}
1244
1245
const bootswatchThemePatches: Record<string, ThemePatch[]> = {
1246
litera: [
1247
{
1248
from: ".navbar {\n font-size: $font-size-sm;",
1249
to: ".navbar {\n font-size: $font-size-sm;\n border: 1px solid rgba(0, 0, 0, .1);",
1250
},
1251
],
1252
lumen: [
1253
{
1254
from: ".navbar {\n @include shadow();",
1255
to: ".navbar {\n @include shadow();\n border-color: shade-color($navbar-bg, 10%);",
1256
},
1257
{
1258
from: "$nav-link-color: var(--#{$prefix}link-color) !default;",
1259
to: "$nav-link-color: $primary !default;"
1260
}
1261
],
1262
simplex: [
1263
{
1264
from: ".navbar {\n border-style: solid;\n border-width: 1px;",
1265
to: ".navbar {\n border-width: 1px;\n border-style: solid;\n border-color: shade-color($navbar-bg, 13%);",
1266
},
1267
],
1268
solar: [
1269
{
1270
from: "$body-color: $gray-600 !default;",
1271
to: "$body-color: $gray-500 !default;",
1272
},
1273
],
1274
};
1275
1276
function portRevealTheme(themeFile: string) {
1277
const themeFileContent = Deno.readTextFileSync(themeFile);
1278
const themeName = basename(themeFile, extname(themeFile));
1279
const patchedScss = patchTheme(themeName, themeFileContent, revealjsThemePatches)
1280
Deno.writeTextFileSync(themeFile, patchedScss);
1281
}
1282
1283
// This maps the reveal.js theme variables to the Quarto theme variables
1284
// Revealjs variables can be seen in _settings.scss and this mapping needs to be used
1285
// to port the revealjs theme to a quarto theme so that users' layers can correctly override
1286
// framework defaults. Quarto layer insure the mapping of those SASS variable.
1287
// - Framework revealjs main theme file uses the revealjs variables
1288
// - each revealjs ported theme should use the quarto variables
1289
// - _quarto.scss maps the quarto variables to the revealjs variables,
1290
// using generic $presentation-* variants for the $revealjs-* specific
1291
const sassVarsMap = {
1292
// Background of the presentation
1293
backgroundColor: "body-bg",
1294
// Primary/body text
1295
mainFont: "font-family-sans-serif",
1296
mainFontSize: "presentation-font-size-root",
1297
mainColor: "body-color",
1298
// Vertical spacing between blocks of text
1299
blockMargin: "presentation-block-margin",
1300
// Headings
1301
// headingMargin is set directly in quarto.scss
1302
headingFont: "presentation-heading-font",
1303
headingColor: "presentation-heading-color",
1304
headingLineHeight: "presentation-heading-line-height",
1305
headingLetterSpacing: "presentation-heading-letter-spacing",
1306
headingTextTransform: "presentation-heading-text-transform",
1307
headingTextShadow: "presentation-heading-text-shadow",
1308
heading1TextShadow: "presentation-h1-text-shadow",
1309
headingFontWeight: "presentation-heading-font-weight",
1310
1311
heading1Size: "presentation-h1-font-size",
1312
heading2Size: "presentation-h2-font-size",
1313
heading3Size: "presentation-h3-font-size",
1314
heading4Size: "presentation-h4-font-size",
1315
1316
codeFont: "font-family-monospace",
1317
codeBackground: "code-bg",
1318
inlineCodeColor: "code-color", // from dracula.scss
1319
1320
// Links and actions
1321
linkColor: "link-color",
1322
linkColorHover: "link-color-hover",
1323
1324
// Text selection
1325
selectionBackgroundColor: "selection-bg",
1326
selectionColor: "selection-color",
1327
1328
// Lists
1329
listBulletColor: "presentation-list-bullet-color", // from dracula.scss
1330
};
1331
1332
let revealjsThemePatches: Record<string, ThemePatch[]> = {}; // Initialize the variable
1333
1334
const createRevealjsThemePatches = (keys: string[]): ThemePatch[] => {
1335
const filteredVarsMap: Record<string, string> = keys.reduce((acc, key) => {
1336
if (sassVarsMap[key]) {
1337
acc[key] = sassVarsMap[key];
1338
} else {
1339
throw Error(`Variable ${key} not found in the sassVarsMap`);
1340
}
1341
return acc;
1342
}, {});
1343
1344
return Object.entries(filteredVarsMap).map(([key, value]) => ({
1345
from: key,
1346
to: value,
1347
}));
1348
}
1349
1350
revealjsThemePatches["beige"] = createRevealjsThemePatches(["mainColor", "headingColor", "headingTextShadow", "backgroundColor", "linkColor","linkColorHover", "selectionBackgroundColor", "heading1TextShadow"])
1351
revealjsThemePatches["black-contrast"] = createRevealjsThemePatches(["backgroundColor", "mainColor", "headingColor", "mainFontSize", "mainFont", "headingFont", "headingTextShadow", "headingLetterSpacing", "headingTextTransform", "headingFontWeight", "linkColor", "linkColorHover", "selectionBackgroundColor", "heading1Size", "heading2Size", "heading3Size", "heading4Size"])
1352
revealjsThemePatches["black"] = createRevealjsThemePatches(["backgroundColor", "mainColor", "headingColor", "mainFontSize", "mainFont", "headingFont", "headingTextShadow", "headingLetterSpacing", "headingTextTransform", "headingFontWeight", "linkColor", "linkColorHover", "selectionBackgroundColor", "heading1Size", "heading2Size", "heading3Size", "heading4Size"])
1353
revealjsThemePatches["blood"] = createRevealjsThemePatches(["codeBackground", "backgroundColor", "mainFont", "mainColor", "headingFont", "headingTextShadow", "heading1TextShadow", "linkColor", "linkColorHover", "selectionBackgroundColor", "selectionColor"])
1354
revealjsThemePatches["dracula"] = createRevealjsThemePatches(["mainColor", "headingColor", "headingTextShadow", "headingTextTransform", "backgroundColor", "linkColor","linkColorHover", "selectionBackgroundColor", "inlineCodeColor", "listBulletColor", "mainFont", "codeFont"])
1355
revealjsThemePatches["league"] = createRevealjsThemePatches(["headingTextShadow", "heading1TextShadow"])
1356
revealjsThemePatches["moon"] = createRevealjsThemePatches(["mainColor", "headingColor", "headingTextShadow", "backgroundColor", "linkColor", "linkColorHover", "selectionBackgroundColor"])
1357
revealjsThemePatches["night"] = createRevealjsThemePatches(["backgroundColor", "mainFont", "linkColor", "linkColorHover", "headingFont", "headingTextShadow", "headingLetterSpacing", "headingTextTransform", "selectionBackgroundColor"])
1358
revealjsThemePatches["serif"] = createRevealjsThemePatches(["mainFont", "mainColor", "headingFont", "headingColor", "headingTextShadow", "headingTextTransform", "backgroundColor", "linkColor", "linkColorHover", "selectionBackgroundColor"])
1359
revealjsThemePatches["simple"] = createRevealjsThemePatches(["mainFont", "mainColor", "headingFont", "headingColor", "headingTextShadow", "headingTextTransform", "backgroundColor", "linkColor", "linkColorHover", "selectionBackgroundColor"])
1360
revealjsThemePatches["sky"] = createRevealjsThemePatches(["mainFont", "mainColor", "headingFont", "headingColor", "headingLetterSpacing", "headingTextShadow", "backgroundColor", "linkColor", "linkColorHover", "selectionBackgroundColor"])
1361
revealjsThemePatches["solarized"] = createRevealjsThemePatches(["mainColor", "headingColor", "headingTextShadow", "backgroundColor", "linkColor", "linkColorHover", "selectionBackgroundColor"])
1362
revealjsThemePatches["white-contrast"] = createRevealjsThemePatches(["backgroundColor", "mainColor", "headingColor", "mainFontSize", "mainFont", "headingFont", "headingTextShadow", "headingLetterSpacing", "headingTextTransform", "headingFontWeight", "linkColor", "linkColorHover", "selectionBackgroundColor", "heading1Size", "heading2Size", "heading3Size", "heading4Size"])
1363
revealjsThemePatches["white"] = createRevealjsThemePatches(["backgroundColor", "mainColor", "headingColor", "mainFontSize", "mainFont", "headingFont", "headingTextShadow", "headingLetterSpacing", "headingTextTransform", "headingFontWeight", "linkColor", "linkColorHover", "selectionBackgroundColor", "heading1Size", "heading2Size", "heading3Size", "heading4Size"])
1364
revealjsThemePatches["settings"] = createRevealjsThemePatches(["backgroundColor", "mainFont", "mainFontSize", "mainColor", "blockMargin", "headingFont", "headingColor", "headingLineHeight", "headingLetterSpacing", "headingTextTransform", "headingTextShadow", "headingFontWeight", "heading1TextShadow", "heading1Size", "heading2Size", "heading3Size", "heading4Size", "codeFont", "linkColor", "linkColorHover", "selectionBackgroundColor", "selectionColor"])
1365
1366