Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/format/dashboard/format-dashboard.ts
6451 views
1
/*
2
* format-dashboard.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import {
8
HtmlPostProcessResult,
9
RenderServices,
10
} from "../../command/render/types.ts";
11
import {
12
kEcho,
13
kFilterParams,
14
kIncludeAfterBody,
15
kIpynbShellInteractivity,
16
kLogo,
17
kPlotlyConnected,
18
kTemplate,
19
kTheme,
20
kWarning,
21
} from "../../config/constants.ts";
22
import {
23
DependencyHtmlFile,
24
Format,
25
FormatExtras,
26
kDependencies,
27
kHtmlPostprocessors,
28
kSassBundles,
29
Metadata,
30
} from "../../config/types.ts";
31
import { LogoLightDarkSpecifier } from "../../resources/types/zod/schema-types.ts";
32
import { PandocFlags } from "../../config/types.ts";
33
import { mergeConfigs } from "../../core/config.ts";
34
import { Document, Element } from "../../core/deno-dom.ts";
35
import { InternalError } from "../../core/lib/error.ts";
36
import { formatResourcePath } from "../../core/resources.ts";
37
import { kLogoAlt, ProjectContext } from "../../project/types.ts";
38
import { registerWriterFormatHandler } from "../format-handlers.ts";
39
import { kPageLayout, kPageLayoutCustom } from "../html/format-html-shared.ts";
40
import { htmlFormat } from "../html/format-html.ts";
41
import { kDTTableSentinel } from "./format-dashboard-shared.ts";
42
43
import { join } from "../../deno_ral/path.ts";
44
import {
45
DashboardMeta,
46
dashboardMeta,
47
dashboardScssLayer,
48
kDashboard,
49
kDontMutateTags,
50
} from "./format-dashboard-shared.ts";
51
import { processCards } from "./format-dashboard-card.ts";
52
import { processValueBoxes } from "./format-dashboard-valuebox.ts";
53
import {
54
applyFillItemClasses,
55
processColumns,
56
processRows,
57
} from "./format-dashboard-layout.ts";
58
import { processSidebars } from "./format-dashboard-sidebar.ts";
59
import { kTemplatePartials } from "../../command/render/template.ts";
60
import { processPages } from "./format-dashboard-page.ts";
61
import { processNavButtons } from "./format-dashboard-navbutton.ts";
62
import { processNavigation } from "./format-dashboard-website.ts";
63
import { projectIsWebsite } from "../../project/project-shared.ts";
64
import { processShinyComponents } from "./format-dashboard-shiny.ts";
65
import { processToolbars } from "./format-dashboard-toolbar.ts";
66
import { processDatatables } from "./format-dashboard-tables.ts";
67
import { assert } from "testing/asserts";
68
import { brandBootstrapSassBundles } from "../../core/sass/brand.ts";
69
import { logoAddLeadingSlashes, resolveLogo } from "../../core/brand/brand.ts";
70
71
const kDashboardClz = "quarto-dashboard";
72
73
export function dashboardFormat() {
74
// use ~ the golden ratio
75
const baseHtmlFormat = htmlFormat(8, 5);
76
const dashboardFormat = mergeConfigs(
77
baseHtmlFormat,
78
{
79
execute: {
80
[kEcho]: false,
81
[kWarning]: false,
82
[kIpynbShellInteractivity]: "all",
83
[kPlotlyConnected]: false,
84
},
85
metadata: {
86
[kPageLayout]: kPageLayoutCustom,
87
},
88
},
89
);
90
91
if (baseHtmlFormat.formatExtras) {
92
const dashboardExtras = async (
93
input: string,
94
markdown: string,
95
flags: PandocFlags,
96
format: Format,
97
libDir: string,
98
services: RenderServices,
99
offset?: string,
100
project?: ProjectContext,
101
quiet?: boolean,
102
) => {
103
assert(project);
104
if (baseHtmlFormat.formatExtras) {
105
// Read the dashboard metadata
106
const dashboard = await dashboardMeta(format);
107
108
const isWebsiteProject = projectIsWebsite(project);
109
110
// Forward the theme along (from either the html format
111
// or from the dashboard format)
112
// TODO: There must be a beter way to do this
113
if (isWebsiteProject) {
114
const formats: Record<string, Metadata> = format.metadata
115
.format as Record<string, Metadata>;
116
const htmlFormat = formats["html"];
117
const dashboardFormat = formats["dashboard"];
118
if (dashboardFormat && dashboardFormat[kTheme]) {
119
format.metadata[kTheme] = dashboardFormat[kTheme];
120
} else if (htmlFormat && htmlFormat[kTheme]) {
121
format.metadata[kTheme] = htmlFormat[kTheme];
122
}
123
}
124
125
const brand = format.render.brand;
126
let logoSpec = format.metadata[kLogo] as LogoLightDarkSpecifier;
127
if (typeof logoSpec === "string" && format.metadata[kLogoAlt]) {
128
logoSpec = {
129
path: logoSpec,
130
alt: format.metadata[kLogoAlt] as string,
131
};
132
}
133
let logo = resolveLogo(brand, logoSpec, [
134
"small",
135
"medium",
136
"large",
137
]);
138
logo = logoAddLeadingSlashes(logo, brand, input);
139
140
format.metadata[kLogo] = logo;
141
const extras: FormatExtras = await baseHtmlFormat.formatExtras(
142
input,
143
markdown,
144
flags,
145
format,
146
libDir,
147
services,
148
offset,
149
project,
150
quiet,
151
);
152
153
extras.html = extras.html || {};
154
extras.html[kHtmlPostprocessors] = extras.html[kHtmlPostprocessors] ||
155
[];
156
extras.html[kHtmlPostprocessors].push(
157
dashboardHtmlPostProcessor(dashboard),
158
);
159
160
extras.metadata = extras.metadata || {};
161
extras.metadata[kTemplatePartials] = [
162
"title-block.html",
163
"_nav-container.html",
164
].map(
165
(file) => {
166
return formatResourcePath(
167
"dashboard",
168
file,
169
);
170
},
171
);
172
173
extras.pandoc = extras.pandoc || {};
174
extras.pandoc[kTemplate] = formatResourcePath(
175
"dashboard",
176
"template.html",
177
);
178
179
extras[kFilterParams] = extras[kFilterParams] || {};
180
extras[kFilterParams][kDashboard] = {
181
orientation: dashboard.orientation,
182
scrolling: dashboard.scrolling,
183
};
184
185
extras.html[kSassBundles] = extras.html[kSassBundles] || [];
186
if (!isWebsiteProject) {
187
// If this is a website project, it will inject the scss for dashboards
188
extras.html[kSassBundles].unshift(dashboardScssLayer());
189
}
190
191
// add _brand.yml sass bundle
192
extras.html[kSassBundles].push(
193
...await brandBootstrapSassBundles(input, project, "bootstrap"),
194
);
195
196
const scripts: DependencyHtmlFile[] = [];
197
const stylesheets: DependencyHtmlFile[] = [];
198
199
// Add the js script which we can use in dashboard to make client side
200
// adjustments
201
scripts.push({
202
name: "quarto-dashboard.js",
203
path: formatResourcePath("dashboard", "quarto-dashboard.js"),
204
});
205
206
// Add the sticky headers script
207
scripts.push({
208
name: "stickythead.js",
209
path: formatResourcePath("dashboard", join("js", "stickythead.js")),
210
});
211
212
// Add the DT scripts and CSS
213
// Note that the `tables` processing may remove this if no connected / matching DT tables
214
// are detected
215
scripts.push({
216
name: "datatables.min.js",
217
path: formatResourcePath(
218
"dashboard",
219
join("js", "dt", "datatables.min.js"),
220
),
221
attribs: {
222
[kDTTableSentinel]: "true",
223
},
224
});
225
stylesheets.push({
226
name: "datatables.min.css",
227
path: formatResourcePath(
228
"dashboard",
229
join("js", "dt", "datatables.min.css"),
230
),
231
attribs: {
232
[kDTTableSentinel]: "true",
233
},
234
});
235
scripts.push({
236
name: "pdfmake.min.js",
237
path: formatResourcePath(
238
"dashboard",
239
join("js", "dt", "pdfmake.min.js"),
240
),
241
attribs: {
242
[kDTTableSentinel]: "true",
243
},
244
});
245
scripts.push({
246
name: "vfs_fonts.js",
247
path: formatResourcePath(
248
"dashboard",
249
join("js", "dt", "vfs_fonts.js"),
250
),
251
attribs: {
252
[kDTTableSentinel]: "true",
253
},
254
});
255
256
const componentDir = join(
257
"bslib",
258
"components",
259
"dist",
260
);
261
262
[{
263
name: "web-components",
264
module: true,
265
}, { name: "components", module: false }].forEach(
266
(dependency) => {
267
const attribs: Record<string, string> = {};
268
if (dependency.module) {
269
attribs["type"] = "module";
270
}
271
272
scripts.push({
273
name: `${dependency.name}.js`,
274
path: formatResourcePath(
275
"html",
276
join(componentDir, `${dependency.name}.js`),
277
),
278
attribs,
279
});
280
},
281
);
282
283
extras.html[kDependencies] = extras.html[kDependencies] || [];
284
extras.html[kDependencies].push({
285
name: "quarto-dashboard",
286
scripts,
287
stylesheets,
288
});
289
290
extras[kIncludeAfterBody] = extras[kIncludeAfterBody] || [];
291
292
return extras;
293
} else {
294
throw new InternalError(
295
"Dashboard superclass must provide a format extras",
296
);
297
}
298
};
299
300
if (dashboardExtras) {
301
dashboardFormat.formatExtras = dashboardExtras;
302
}
303
}
304
305
return dashboardFormat;
306
}
307
308
registerWriterFormatHandler((format) => {
309
switch (format) {
310
case "dashboard":
311
return {
312
format: dashboardFormat(),
313
pandocTo: "html",
314
};
315
}
316
});
317
318
function dashboardHtmlPostProcessor(
319
dashboardMeta: DashboardMeta,
320
) {
321
return (doc: Document): Promise<HtmlPostProcessResult> => {
322
const result: HtmlPostProcessResult = {
323
resources: [],
324
supporting: [],
325
};
326
327
// Mark the body as a quarto dashboard
328
doc.body.classList.add(kDashboardClz);
329
330
// Note the orientation as fill if needed
331
if (!dashboardMeta.scrolling) {
332
doc.body.classList.add("dashboard-fill");
333
}
334
335
// Mark the page container with layout instructions
336
const containerEl = doc.querySelector("div.page-layout-custom");
337
if (containerEl) {
338
const containerClz = [
339
"quarto-dashboard-content",
340
"bslib-gap-spacing",
341
"html-fill-container",
342
];
343
344
// The scrolling behavior
345
if (!dashboardMeta.scrolling) {
346
containerClz.push("bslib-page-fill"); // only apply this if we aren't scrolling
347
} else {
348
containerClz.push("dashboard-scrolling"); // only apply this if we are scrolling
349
}
350
351
containerClz.forEach(
352
(clz) => {
353
containerEl.classList.add(clz);
354
},
355
);
356
}
357
358
// Mark the children with layout instructions
359
const children = containerEl?.children;
360
if (children) {
361
for (const childEl of children) {
362
// All the children of the dashboard container at the root level become
363
// fill children
364
if (
365
!childEl.classList.contains("quarto-title-block") &&
366
!kDontMutateTags.includes(childEl.tagName.toUpperCase())
367
) {
368
childEl.classList.add("bslib-grid-item");
369
applyFillItemClasses(childEl);
370
}
371
}
372
}
373
374
// Helper for forwarding supporting and resources
375
const addResults = (
376
res: {
377
resources: string[];
378
supporting: string[];
379
} | undefined,
380
) => {
381
if (res) {
382
result.resources.push(...res.resources);
383
result.supporting.push(...res.supporting);
384
}
385
};
386
387
// Process Data Tables
388
addResults(processDatatables(doc));
389
390
// Process navigation
391
processNavigation(doc);
392
393
// Process pages that may be present in the document
394
processPages(doc, dashboardMeta);
395
396
// Process Navbar buttons
397
processNavButtons(doc, dashboardMeta);
398
399
// Adjust the appearance of row elements
400
processRows(doc);
401
402
// Adjust the appearance of column element
403
processColumns(doc);
404
405
// Process card
406
processCards(doc, dashboardMeta);
407
408
// Process valueboxes
409
processValueBoxes(doc);
410
411
// Process sidedars
412
processSidebars(doc);
413
414
// Process toolbars
415
processToolbars(doc);
416
417
// Process tables
418
processTables(doc);
419
420
// Process Shiny Specific Components
421
processShinyComponents(doc);
422
423
// Process fill images to include proper fill behavior
424
const imgFillSelectors = [
425
"div.cell-output-display > div.quarto-figure > .quarto-float img",
426
"div.cell-output-display > img",
427
];
428
imgFillSelectors.forEach((selector) => {
429
const fillImgNodes = doc.body.querySelectorAll(selector);
430
for (const fillImgNode of fillImgNodes) {
431
const fillImgEl = fillImgNode as Element;
432
fillImgEl.classList.add("quarto-dashboard-img-contain");
433
fillImgEl.removeAttribute("height");
434
fillImgEl.removeAttribute("width");
435
}
436
});
437
438
return Promise.resolve(result);
439
};
440
}
441
442
function processTables(doc: Document) {
443
doc.querySelectorAll(".itables table").forEach((tableEl) => {
444
(tableEl as Element).setAttribute("style", "width:100%;");
445
});
446
}
447
448