Skip to content

Commit 0596f3f

Browse files
cell renderings, light and dark
a filter to detect e.g. renderings: [light, dark] retain any cell content before the first cell-output-display then any div.cell-output-display are given the rendering names formats choose what to do with these in the filter currently HTML uses light and dark and classes them .light-content and .dark-content and the other formats just use light rendering applies body.quarto-light or body.quarto-dark based on default theme for a decent NoJS experience
1 parent 40e447a commit 0596f3f

File tree

11 files changed

+604
-1
lines changed

11 files changed

+604
-1
lines changed

news/changelog-1.7.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ All changes included in 1.7:
4343

4444
## `html` format
4545

46+
- ([#12277](https://github.com/quarto-dev/quarto-cli/pull/12277)): Provide light and dark plot and table renderings with `renderings: [light,dark]`
4647
- ([#11860](https://github.com/quarto-dev/quarto-cli/issues/11860)): ES6 modules that import other local JS modules in documents with `embed-resources: true` are now correctly embedded.
4748

4849
## `pdf` format

src/format/html/format-html-bootstrap.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,18 @@ function bootstrapHtmlFinalizer(format: Format, flags: PandocFlags) {
10601060
}
10611061
}
10621062

1063+
// start body with light or dark class for proper display when JS is disabled
1064+
let initialLightDarkClass = "quarto-light";
1065+
// some logic duplicated from resolveThemeLayer
1066+
const theme = format.metadata.theme;
1067+
if (theme && !Array.isArray(theme) && typeof theme === "object") {
1068+
const keys = Object.keys(theme);
1069+
if(keys.length > 1 && keys[0] === "dark") {
1070+
initialLightDarkClass = "quarto-dark";
1071+
}
1072+
}
1073+
doc.body.classList.add(initialLightDarkClass);
1074+
10631075
// If there is no margin content and no toc in the right margin
10641076
// then lower the z-order so everything else can get on top
10651077
// of the sidebar

src/resources/filters/main.lua

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import("./quarto-init/knitr-fixup.lua")
6161
import("./quarto-post/render-asciidoc.lua")
6262
import("./quarto-post/book.lua")
6363
import("./quarto-post/cites.lua")
64+
import("./quarto-post/cell-renderings.lua")
6465
import("./quarto-post/delink.lua")
6566
import("./quarto-post/docx.lua")
6667
import("./quarto-post/fig-cleanup.lua")
@@ -393,6 +394,10 @@ local quarto_post_filters = {
393394
},
394395
traverser = 'jog',
395396
},
397+
{ name = "post-choose-cell_renderings",
398+
filter = choose_cell_renderings(),
399+
flags = { "has_renderings" },
400+
},
396401
{ name = "post-landscape-div",
397402
filter = landscape_div(),
398403
flags = { "has_landscape" },

src/resources/filters/normalize/flags.lua

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ function compute_flags()
109109
flags.needs_output_unrolling = true
110110
end
111111
end
112+
113+
-- cell-renderings.lua
114+
if node.attributes["renderings"] then
115+
flags.has_renderings = true
116+
end
112117
end
113118
end,
114119
CodeBlock = function(node)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
function choose_cell_renderings()
2+
function jsonDecodeArray(json)
3+
if json:sub(1, 1) == '[' then
4+
return quarto.json.decode(json)
5+
elseif json:sub(1, 1) == '{' then
6+
quarto.log.warning('expected array or scalar', json)
7+
else
8+
return {json}
9+
end
10+
end
11+
12+
return {
13+
Div = function(div)
14+
-- Only process cell div with renderings attr
15+
if not div.classes:includes("cell") or not div.attributes["renderings"] then
16+
return nil
17+
end
18+
local renderingsJson = div.attributes['renderings']
19+
local renderings = jsonDecodeArray(renderingsJson)
20+
if not type(renderings) == "table" or #renderings == 0 then
21+
quarto.log.warning("renderings expected array of rendering names, got", renderings)
22+
return nil
23+
end
24+
local cods = {}
25+
local firstCODIndex = nil
26+
for i, cellOutput in ipairs(div.content) do
27+
if cellOutput.classes:includes("cell-output-display") then
28+
if not firstCODIndex then
29+
firstCODIndex = i
30+
end
31+
table.insert(cods, cellOutput)
32+
end
33+
end
34+
35+
if #cods ~= #renderings then
36+
quarto.log.warning("need", #renderings, "cell-output-display for renderings", table.concat(renderings, ",") .. ";", "got", #cods)
37+
return nil
38+
end
39+
40+
local outputs = {}
41+
for i, r in ipairs(renderings) do
42+
outputs[r] = cods[i]
43+
end
44+
local lightDiv = outputs['light']
45+
local darkDiv = outputs['dark']
46+
local blocks = pandoc.Blocks({table.unpack(div.content, 1, firstCODIndex - 1)})
47+
if quarto.format.isHtmlOutput() and lightDiv and darkDiv then
48+
blocks:insert(pandoc.Div(lightDiv.content, pandoc.Attr("", {'light-content'}, {})))
49+
blocks:insert(pandoc.Div(darkDiv.content, pandoc.Attr("", {'dark-content'}, {})))
50+
else
51+
blocks:insert(lightDiv or darkDiv)
52+
end
53+
div.content = blocks
54+
return div
55+
end
56+
}
57+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
body.quarto-light .dark-content {
2+
display: none;
3+
}
4+
5+
body.quarto-dark .light-content {
6+
display: none;
7+
}

src/resources/formats/html/bootstrap/dist/scss/bootstrap.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
@import "mixins/banner";
22
@include bsBanner("");
33

4-
54
// scss-docs-start import-stack
65
// Configuration
76
@import "variables-dark";
@@ -14,6 +13,7 @@
1413
@import "type";
1514
@import "images";
1615
@import "containers";
16+
@import "light-dark";
1717
@import "grid";
1818
@import "tables";
1919
@import "forms";

src/resources/schema/cell-attributes.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
schema: string
1111
description: "Classes to apply to cell container"
1212

13+
- name: renderings
14+
schema:
15+
arrayOf: string
16+
description: "Array of rendering names"
17+
1318
- name: tags
1419
tags:
1520
engine: jupyter
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
title: "knitr dark mode - thematic"
3+
format:
4+
html:
5+
theme:
6+
light: united
7+
dark: slate
8+
keep-md: true
9+
execute:
10+
echo: false
11+
warning: false
12+
_quarto:
13+
tests:
14+
html:
15+
ensureHtmlElements:
16+
-
17+
- 'body.quarto-light'
18+
- 'div.cell div.light-content'
19+
- 'div.cell div.dark-content'
20+
- 'div.cell div.cell-code pre.code-with-copy'
21+
- []
22+
---
23+
24+
```{r}
25+
#| echo: false
26+
#| warning: false
27+
library(ggplot2)
28+
29+
ggplot_theme <- function(bgcolor, fgcolor) {
30+
theme_minimal(base_size = 11) %+%
31+
theme(
32+
panel.border = element_blank(),
33+
panel.grid.major.y = element_blank(),
34+
panel.grid.minor.y = element_blank(),
35+
panel.grid.major.x = element_blank(),
36+
panel.grid.minor.x = element_blank(),
37+
text = element_text(colour = fgcolor),
38+
axis.text = element_text(colour = fgcolor),
39+
rect = element_rect(colour = bgcolor, fill = bgcolor),
40+
plot.background = element_rect(fill = bgcolor, colour = NA),
41+
axis.line = element_line(colour = fgcolor),
42+
axis.ticks = element_line(colour = fgcolor)
43+
)
44+
}
45+
46+
united_theme <- ggplot_theme("#ffffff", "#333333")
47+
slate_theme <- ggplot_theme("#282B30", "#aaaaaa")
48+
49+
colour_scale <- scale_colour_manual(values = c("darkorange", "purple", "cyan4"))
50+
```
51+
52+
### no crossref, no caption
53+
54+
```{r}
55+
#| renderings: [light, dark]
56+
theme_set(united_theme)
57+
ggplot(mtcars, aes(mpg, wt)) +
58+
geom_point(aes(colour = factor(cyl))) + colour_scale
59+
theme_set(slate_theme)
60+
ggplot(mtcars, aes(mpg, wt)) +
61+
geom_point(aes(colour = factor(cyl))) + colour_scale
62+
```
63+
64+
### with crossref but no caption
65+
66+
::: {#fig-thematic-ggplot}
67+
```{r}
68+
#| echo: true
69+
#| renderings:
70+
#| - dark
71+
#| - light
72+
theme_set(slate_theme)
73+
ggplot(mtcars, aes(mpg, disp)) +
74+
geom_point(aes(colour = factor(cyl))) + colour_scale
75+
theme_set(united_theme)
76+
ggplot(mtcars, aes(mpg, disp)) +
77+
geom_point(aes(colour = factor(cyl))) + colour_scale
78+
```
79+
:::
80+
81+
### with caption but no crossref
82+
83+
<div>
84+
85+
```{r}
86+
#| renderings: [dark]
87+
theme_set(slate_theme)
88+
ggplot(mtcars, aes(mpg, disp)) +
89+
geom_point(aes(colour = factor(cyl))) + colour_scale
90+
```
91+
92+
thematic - base r graphics
93+
94+
</div>
95+
96+
## patchwork
97+
98+
### with crossref and caption
99+
100+
::: {#fig-thematic-patchwork}
101+
```{r}
102+
#| renderings: [light, dark]
103+
theme_set(united_theme)
104+
ggplot(mtcars, aes(mpg, hp)) +
105+
geom_point(aes(colour = factor(cyl))) + colour_scale
106+
theme_set(slate_theme)
107+
ggplot(mtcars, aes(mpg, hp)) +
108+
geom_point(aes(colour = factor(cyl))) + colour_scale
109+
```
110+
111+
mtcars - mpg vs hp
112+
:::
113+
114+
Here's a [link](https://example.com).
115+
116+
{{< lipsum 3 >}}

0 commit comments

Comments
 (0)