Skip to main content

markdown/html/
style.rs

1//! The stylesheet [`Html`]'s markup is set with: the document's theme as CSS.
2
3use crate::{QuoteKind, Typography, html::Html, render};
4use gpui::Hsla;
5use theme::{HighlightKind, Metrics, Theme};
6use ui::color::to_hex;
7
8const ALERTS: [QuoteKind; 5] = [
9    QuoteKind::Note,
10    QuoteKind::Tip,
11    QuoteKind::Important,
12    QuoteKind::Warning,
13    QuoteKind::Caution,
14];
15
16impl Html {
17    /// CSS for the markup [`Html::render`] writes, inside an element of class
18    /// `md`. Colors are custom properties on `.md`, `light`'s by default and
19    /// `dark`'s under `prefers-color-scheme: dark`; sizes are in `px` from
20    /// `typography` at the theme's base text size; fonts are `light`'s.
21    pub fn stylesheet(light: &Theme, dark: &Theme, typography: &Typography) -> String {
22        let mut css = String::new();
23        css.push_str(".md {\n");
24        colors(&mut css, light);
25        css.push_str("}\n@media (prefers-color-scheme: dark) {\n  .md {\n");
26        colors(&mut css, dark);
27        css.push_str("  }\n}\n");
28
29        let body = format!("\"{}\", system-ui, sans-serif", light.font_body);
30        let mono = format!("\"{}\", ui-monospace, monospace", light.font_mono);
31        rule(
32            &mut css,
33            ".md",
34            &[
35                ("color", "var(--md-text)".into()),
36                ("font-family", body),
37                ("font-size", px(typography.body.size())),
38                ("line-height", typography.body.leading.to_string()),
39                ("font-weight", weight(typography.body)),
40            ],
41        );
42        for level in 1..=6 {
43            let metrics = typography.heading(level);
44            rule(
45                &mut css,
46                &format!(".md h{level}"),
47                &[
48                    ("font-size", px(metrics.size())),
49                    ("line-height", metrics.leading.to_string()),
50                    ("font-weight", weight(metrics)),
51                ],
52            );
53        }
54        rule(&mut css, ".md a", &[("color", "var(--md-accent)".into())]);
55        rule(
56            &mut css,
57            ".md code, .md pre",
58            &[
59                ("font-family", mono),
60                ("font-size", px(typography.code.size())),
61                ("line-height", typography.code.leading.to_string()),
62                ("color", "var(--md-code-text)".into()),
63            ],
64        );
65        rule(
66            &mut css,
67            ".md :not(pre) > code",
68            &[
69                ("background", "var(--md-code-wash)".into()),
70                ("border-radius", "4px".into()),
71                ("padding", "0 0.25em".into()),
72            ],
73        );
74        rule(
75            &mut css,
76            ".md pre",
77            &[
78                ("background", "var(--md-code-wash)".into()),
79                ("border-radius", "8px".into()),
80                ("padding", "12px 16px".into()),
81                ("overflow-x", "auto".into()),
82            ],
83        );
84        rule(
85            &mut css,
86            ".md blockquote",
87            &[
88                ("margin-left", "0".into()),
89                ("padding-left", "1em".into()),
90                ("border-left", "3px solid var(--md-border)".into()),
91                ("color", "var(--md-text-muted)".into()),
92            ],
93        );
94        for alert in ALERTS {
95            let name = alert.label().to_lowercase();
96            rule(
97                &mut css,
98                &format!(".md .md-alert-{name}"),
99                &[("border-left-color", format!("var(--md-alert-{name})"))],
100            );
101            rule(
102                &mut css,
103                &format!(".md .md-alert-{name} > .md-alert-title"),
104                &[
105                    ("color", format!("var(--md-alert-{name})")),
106                    ("font-weight", "600".into()),
107                ],
108            );
109        }
110        rule(
111            &mut css,
112            ".md hr",
113            &[
114                ("border", "none".into()),
115                ("border-top", "1px solid var(--md-border)".into()),
116            ],
117        );
118        rule(
119            &mut css,
120            ".md table",
121            &[("border-collapse", "collapse".into())],
122        );
123        rule(
124            &mut css,
125            ".md th, .md td",
126            &[
127                ("border", "1px solid var(--md-border)".into()),
128                ("padding", "4px 10px".into()),
129            ],
130        );
131        rule(
132            &mut css,
133            ".md .md-tasks",
134            &[("list-style", "none".into()), ("padding-left", "0".into())],
135        );
136        rule(
137            &mut css,
138            ".md .md-indent",
139            &[("margin-left", "1.5em".into())],
140        );
141        rule(
142            &mut css,
143            ".md .md-image img",
144            &[("max-width", "100%".into())],
145        );
146        rule(
147            &mut css,
148            ".md figcaption",
149            &[
150                ("color", "var(--md-text-muted)".into()),
151                ("font-size", px(typography.caption.size())),
152                ("line-height", typography.caption.leading.to_string()),
153            ],
154        );
155        rule(
156            &mut css,
157            ".md .md-bookmark",
158            &[
159                ("display", "block".into()),
160                ("padding", "12px 16px".into()),
161                ("border", "1px solid var(--md-border)".into()),
162                ("border-radius", "8px".into()),
163                ("font-size", px(typography.card.size())),
164            ],
165        );
166        for kind in HighlightKind::ALL {
167            let name = kind.name();
168            rule(
169                &mut css,
170                &format!(".md .hl-{name}"),
171                &[("color", format!("var(--md-hl-{name})"))],
172            );
173        }
174        css
175    }
176}
177
178fn colors(css: &mut String, theme: &Theme) {
179    let mut var = |name: &str, color: Hsla| {
180        css.push_str(&format!("  --md-{name}: {};\n", to_hex(color, true)));
181    };
182    var("text", theme.text);
183    var("text-muted", theme.text_muted);
184    var("border", theme.border);
185    var("accent", theme.accent);
186    var("code-text", theme.code_text);
187    var("code-wash", theme.code_wash);
188    for alert in ALERTS {
189        var(
190            &format!("alert-{}", alert.label().to_lowercase()),
191            render::alert_color(alert, theme),
192        );
193    }
194    for kind in HighlightKind::ALL {
195        var(&format!("hl-{}", kind.name()), theme.syntax.color(kind));
196    }
197}
198
199fn rule(css: &mut String, selector: &str, declarations: &[(&str, String)]) {
200    css.push_str(selector);
201    css.push_str(" {\n");
202    for (property, value) in declarations {
203        css.push_str(&format!("  {property}: {value};\n"));
204    }
205    css.push_str("}\n");
206}
207
208fn px(size: f32) -> String {
209    format!("{}px", (size * 100.0).round() / 100.0)
210}
211
212fn weight(metrics: Metrics) -> String {
213    format!("{}", metrics.weight.0)
214}