Skip to content

Commit d77993d

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 d77993d

File tree

10 files changed

+488
-1
lines changed

10 files changed

+488
-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() 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)
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: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
---
2+
title: "jupyter dark mode - matplotlib"
3+
engine: jupyter
4+
format:
5+
html:
6+
theme:
7+
dark: slate
8+
light: united
9+
keep-md: true
10+
_quarto:
11+
tests:
12+
html:
13+
ensureHtmlElements:
14+
-
15+
- 'body.quarto-dark'
16+
- 'div.cell div.light-content'
17+
- 'div.cell div.dark-content'
18+
- []
19+
---
20+
21+
```{python}
22+
#| echo: false
23+
import yaml
24+
import tempfile
25+
import os
26+
27+
def apply_mpl_colors(bgcolor, fgcolor, primarycolor):
28+
fd, name = tempfile.mkstemp("mplstyle")
29+
os.close(fd)
30+
with open(name, "w") as out:
31+
out.write("axes.facecolor: \"%s\"\n" % bgcolor)
32+
out.write("axes.edgecolor: \"%s\"\n" % fgcolor)
33+
out.write("axes.labelcolor: \"%s\"\n" % fgcolor)
34+
out.write("axes.titlecolor: \"%s\"\n" % fgcolor)
35+
out.write("figure.facecolor: \"%s\"\n" % bgcolor)
36+
out.write("figure.edgecolor: \"%s\"\n" % fgcolor)
37+
out.write("text.color: \"%s\"\n" % fgcolor)
38+
out.write("xtick.color: \"%s\"\n" % fgcolor)
39+
out.write("ytick.color: \"%s\"\n" % fgcolor)
40+
# seems to require named color, is there a better way?
41+
out.write("axes.prop_cycle: cycler('color', ['%s'])" % primarycolor)
42+
plt.style.use(name)
43+
os.unlink(name)
44+
45+
def united_colors():
46+
apply_mpl_colors("#ffffff", "#333333", "red")
47+
48+
def slate_colors():
49+
apply_mpl_colors("#282B30", "#aaaaaa", "white")
50+
```
51+
52+
### No crossref or caption
53+
```{python}
54+
#| echo: false
55+
#| renderings: [light, dark]
56+
import numpy as np
57+
import matplotlib.pyplot as plt
58+
59+
# Parameters for the normal distribution
60+
mean = 0
61+
std_dev = 1
62+
63+
# Generate data
64+
x = np.linspace(mean - 4*std_dev, mean + 4*std_dev, 1000)
65+
y = (1/(std_dev * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x - mean) / std_dev)**2)
66+
67+
# Plotting
68+
united_colors()
69+
plt.figure(figsize=(8, 5))
70+
plt.plot(x, y, label='Normal Distribution')
71+
plt.title('Normal Distribution Curve')
72+
plt.xlabel('X-axis')
73+
plt.ylabel('Probability Density')
74+
plt.legend()
75+
plt.grid(True)
76+
plt.show()
77+
78+
slate_colors()
79+
plt.figure(figsize=(8, 5))
80+
plt.plot(x, y, label='Normal Distribution')
81+
plt.title('Normal Distribution Curve')
82+
plt.xlabel('X-axis')
83+
plt.ylabel('Probability Density')
84+
plt.legend()
85+
plt.grid(True)
86+
plt.show()
87+
```
88+
89+
### With crossref but no caption
90+
91+
::: {#fig-matplotlib-line}
92+
```{python}
93+
#| echo: false
94+
#| renderings: [light, dark]
95+
import matplotlib.pyplot as plt
96+
97+
united_colors()
98+
plt.title("Hello")
99+
plt.plot([1,2,3])
100+
plt.grid(True)
101+
plt.show(block=False)
102+
103+
slate_colors()
104+
plt.figure()
105+
plt.title("Hello")
106+
plt.plot([1,2,3])
107+
plt.grid(True)
108+
plt.show(block=False)
109+
```
110+
:::
111+
112+
### With caption but no crossref
113+
114+
::: {}
115+
```{python}
116+
#| echo: false
117+
#| renderings: [light, dark]
118+
119+
# author: "anthropic claude-3-5-sonnet-20240620"
120+
import numpy as np
121+
import matplotlib.pyplot as plt
122+
123+
# Generate data points
124+
x = np.linspace(0, 2 * np.pi, 100)
125+
y = np.sin(x)
126+
127+
united_colors()
128+
plt.figure(figsize=(10, 6))
129+
plt.plot(x, y)
130+
plt.title('Sine Wave')
131+
plt.xlabel('x')
132+
plt.ylabel('sin(x)')
133+
plt.grid(True)
134+
plt.axhline(y=0, color='k', linestyle='--')
135+
plt.axvline(x=0, color='k', linestyle='--')
136+
plt.show()
137+
138+
slate_colors()
139+
plt.figure(figsize=(10, 6))
140+
plt.plot(x, y)
141+
plt.title('Sine Wave')
142+
plt.xlabel('x')
143+
plt.ylabel('sin(x)')
144+
plt.grid(True)
145+
plt.axhline(y=0, color='k', linestyle='--')
146+
plt.axvline(x=0, color='k', linestyle='--')
147+
plt.show()
148+
```
149+
matplotlib sine wave
150+
151+
:::
152+
153+
### With crossref and caption
154+
155+
::: {#fig-matplotlib-cosine}
156+
```{python}
157+
#| echo: false
158+
#| renderings: [dark, light]
159+
import numpy as np
160+
import matplotlib.pyplot as plt
161+
162+
# Generate data points
163+
x = np.linspace(0, 2 * np.pi, 100)
164+
y = np.cos(x)
165+
166+
# Create the plot
167+
slate_colors()
168+
plt.figure(figsize=(10, 6))
169+
plt.plot(x, y)
170+
plt.title('Cosine Wave')
171+
plt.xlabel('x')
172+
plt.ylabel('cos(x)')
173+
plt.grid(True)
174+
plt.axhline(y=0, color='k', linestyle='--')
175+
plt.axvline(x=0, color='k', linestyle='--')
176+
plt.show()
177+
178+
united_colors()
179+
plt.figure(figsize=(10, 6))
180+
plt.plot(x, y)
181+
plt.title('Cosine Wave')
182+
plt.xlabel('x')
183+
plt.ylabel('cos(x)')
184+
plt.grid(True)
185+
plt.axhline(y=0, color='k', linestyle='--')
186+
plt.axvline(x=0, color='k', linestyle='--')
187+
plt.show()
188+
```
189+
190+
matplotlib cosine wave
191+
:::
192+
193+
Here's a [link](https://example.com).
194+
195+
196+
{{< lipsum 3 >}}

0 commit comments

Comments
 (0)