Commit 0e8d2cb26d
Verified · cmc
Layout: unified · split
Cargo.lock +1 −1
| @@ -675,7 +675,7 @@ dependencies = [ | ||
| 675 | 675 | |
| 676 | 676 | [[package]] |
| 677 | 677 | name = "orgo" |
| 678 | version = "0.21.0" | |
| 678 | version = "0.22.0" | |
| 679 | 679 | dependencies = [ |
| 680 | 680 | "anyhow", |
| 681 | 681 | "blake3", |
Cargo.toml +1 −1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | [package] |
| 2 | 2 | name = "orgo" |
| 3 | version = "0.21.0" | |
| 3 | version = "0.22.0" | |
| 4 | 4 | edition = "2021" |
| 5 | 5 | description = "Org-mode static site generator that renders the org element tree straight to HTML" |
| 6 | 6 | license = "0BSD" |
docs/guide/02-configuration.org +31 −7
| @@ -30,6 +30,7 @@ expose_page_list = false | ||
| 30 | 30 | |
| 31 | 31 | [highlight] |
| 32 | 32 | theme = "InspiredGitHub" |
| 33 | theme_dark = "" | |
| 33 | 34 | syntaxes_dir = "syntaxes" |
| 34 | 35 | |
| 35 | 36 | [build] |
| @@ -92,13 +93,14 @@ and redefine the handful you care about: | ||
| 92 | 93 | } |
| 93 | 94 | #+END_SRC |
| 94 | 95 | |
| 95 | Code /blocks/ are the one part a theme cannot make follow =prefers-color-scheme=: | |
| 96 | =syntax.css= is generated from a single syntect theme, so a theme's dark mode keeps its | |
| 97 | block surface light to stay readable against the default =InspiredGitHub=. Pair a dark | |
| 98 | =highlight.theme= with overrides of =--orgo-code-bg=, =--orgo-code-fg= and | |
| 99 | =--orgo-code-rule=. Those three colour blocks only — inline =~code~= follows the page's | |
| 100 | own scheme, so it stays legible whichever highlight theme you use. The documentation site | |
| 101 | does exactly this; its =style.css= is those three lines and nothing else. | |
| 96 | Code /blocks/ are the one part a built-in theme leaves light in dark mode: =syntax.css= | |
| 97 | is coloured by =highlight.theme=, and that default is a light theme. Two pieces make a | |
| 98 | block follow =prefers-color-scheme= — =highlight.theme_dark= for the tokens, and | |
| 99 | =--orgo-code-bg=, =--orgo-code-fg= and =--orgo-code-rule= for the surface under them. | |
| 100 | Set both, or neither and blocks stay light in both schemes. Those three properties | |
| 101 | colour blocks only — inline =~code~= follows the page's own scheme, so it stays legible | |
| 102 | whichever highlight theme you use. The documentation site keeps a dark surface in both | |
| 103 | schemes instead; its =style.css= is those three lines and nothing else. | |
| 102 | 104 | |
| 103 | 105 | When you outgrow a theme, drop =theme= from the config and write =templates/base.html= |
| 104 | 106 | against your own CSS. Nothing else changes. |
| @@ -223,12 +225,34 @@ adding a post a one-page rebuild. | ||
| 223 | 225 | | Key | Default | Meaning | |
| 224 | 226 | |-----+---------+---------| |
| 225 | 227 | | =theme= | ="InspiredGitHub"= | A syntect theme name. | |
| 228 | | =theme_dark= | =""= | A second theme for readers in dark mode. | | |
| 226 | 229 | | =syntaxes_dir= | ="syntaxes"= | Extra =.sublime-syntax= files. | |
| 227 | 230 | |
| 228 | 231 | Any theme syntect ships: =InspiredGitHub=, =Solarized (dark)=, =Solarized (light)=, |
| 229 | 232 | =base16-ocean.dark=, =base16-ocean.light=, =base16-eighties.dark=, =base16-mocha.dark=. |
| 230 | 233 | An unknown name is an error listing the valid ones. |
| 231 | 234 | |
| 235 | =theme_dark= is off by default, and one theme colours every reader. Name a second one and | |
| 236 | =syntax.css= carries both, each behind the =prefers-color-scheme= query it belongs to, so | |
| 237 | a reader's system picks the colours — one stylesheet, no JavaScript, nothing extra for a | |
| 238 | layout to link: | |
| 239 | ||
| 240 | #+BEGIN_SRC toml | |
| 241 | [highlight] | |
| 242 | theme = "InspiredGitHub" | |
| 243 | theme_dark = "base16-ocean.dark" | |
| 244 | #+END_SRC | |
| 245 | ||
| 246 | Only the token colours change with the scheme. The surface a block sits on is the page's, | |
| 247 | so give your dark mode a dark =pre= background — a dark theme's colours are chosen for | |
| 248 | one — with =--orgo-code-bg= under a built-in theme, or your own CSS. | |
| 249 | ||
| 250 | The two themes are separated rather than stacked because they name different scopes: a | |
| 251 | light theme's =.source.python .keyword= outranks a dark theme's =.keyword=, so appending | |
| 252 | one to the other would leave light colours on some tokens. The cost of the separation is | |
| 253 | that a browser too old to know =prefers-color-scheme= matches neither query and shows | |
| 254 | code unhighlighted. Leaving =theme_dark= empty keeps the unconditional rules of before. | |
| 255 | ||
| 232 | 256 | Highlighting emits *CSS classes*, never inline styles, so themes live in a stylesheet. |
| 233 | 257 | Each build writes =syntax.css= into the output and every page links it. |
| 234 | 258 | |
docs/style.css +5 −4
| @@ -4,10 +4,11 @@ | ||
| 4 | 4 | * image or font in a source directory reaches the output. It is linked after theme.css, |
| 5 | 5 | * which is what lets these definitions win. |
| 6 | 6 | * |
| 7 | * Only the code surface is changed. syntax.css is generated from one syntect theme and | |
| 8 | * cannot follow prefers-color-scheme, so a site pairing a dark `highlight.theme` with a | |
| 9 | * theme has to say what colour code sits on — here, dark in both schemes, matching | |
| 10 | * base16-ocean.dark. Everything else is the theme's. */ | |
| 7 | * Only the code surface is changed. syntax.css colours tokens, never the surface under | |
| 8 | * them, so a site pairing a dark `highlight.theme` with a theme has to say what colour | |
| 9 | * code sits on — here, dark in both schemes, matching base16-ocean.dark. A site wanting | |
| 10 | * blocks to follow prefers-color-scheme sets `highlight.theme_dark` and gives these | |
| 11 | * three a dark query too. Everything else is the theme's. */ | |
| 11 | 12 | |
| 12 | 13 | :root { |
| 13 | 14 | --orgo-code-bg: #2b303b; |
src/config.rs +9
| @@ -419,6 +419,11 @@ pub struct Highlight { | ||
| 419 | 419 | /// `base16-ocean.light`. Highlighting emits CSS classes, and this theme is what the |
| 420 | 420 | /// generated `syntax.css` colours them with. |
| 421 | 421 | pub theme: String, |
| 422 | /// A second syntect theme for readers whose system asks for dark mode. `syntax.css` | |
| 423 | /// then carries both, each behind its own `prefers-color-scheme` query, so one | |
| 424 | /// stylesheet serves both schemes — see [`crate::render::syntax_stylesheet`]. Empty, | |
| 425 | /// the default, means `theme` colours every reader whatever their scheme. | |
| 426 | pub theme_dark: String, | |
| 422 | 427 | } |
| 423 | 428 | |
| 424 | 429 | impl Default for Highlight { |
| @@ -426,6 +431,7 @@ impl Default for Highlight { | ||
| 426 | 431 | Highlight { |
| 427 | 432 | syntaxes_dir: Utf8PathBuf::from("syntaxes"), |
| 428 | 433 | theme: "InspiredGitHub".to_string(), |
| 434 | theme_dark: String::new(), | |
| 429 | 435 | } |
| 430 | 436 | } |
| 431 | 437 | } |
| @@ -617,6 +623,9 @@ expose_page_list = false | ||
| 617 | 623 | # A syntect theme name: InspiredGitHub, Solarized (dark), base16-ocean.dark, |
| 618 | 624 | # base16-eighties.dark, base16-mocha.dark, base16-ocean.light. |
| 619 | 625 | theme = "InspiredGitHub" |
| 626 | # A second theme for readers in dark mode. Set it and syntax.css carries both themes, | |
| 627 | # each behind its own prefers-color-scheme query. Empty means `theme` colours everyone. | |
| 628 | theme_dark = "" | |
| 620 | 629 | # Extra .sublime-syntax files for languages neither syntect nor orgo bundles. |
| 621 | 630 | syntaxes_dir = "syntaxes" |
| 622 | 631 | |
src/main.rs +1 −7
| @@ -294,13 +294,7 @@ fn build_file(input: &Utf8Path, output: &Utf8Path) -> Result<()> { | ||
| 294 | 294 | let config = Config::load(dir)?; |
| 295 | 295 | config.validate()?; |
| 296 | 296 | let templater = Templater::load(Some(&dir.join(&config.templates.dir)), &config.site.base_url)?; |
| 297 | let css_text = render::syntax_css(&config.highlight.theme).ok_or_else(|| { | |
| 298 | anyhow::anyhow!( | |
| 299 | "unknown highlight.theme {:?}. Available: {}", | |
| 300 | config.highlight.theme, | |
| 301 | render::available_themes().join(", ") | |
| 302 | ) | |
| 303 | })?; | |
| 297 | let css_text = render::syntax_stylesheet(&config.highlight)?; | |
| 304 | 298 | |
| 305 | 299 | let site = SiteContext { |
| 306 | 300 | title: config.site.title.clone(), |
src/render.rs +38
| @@ -119,6 +119,44 @@ pub fn available_themes() -> Vec<&'static str> { | ||
| 119 | 119 | theme_set().themes.keys().map(String::as_str).collect() |
| 120 | 120 | } |
| 121 | 121 | |
| 122 | /// The whole stylesheet a build writes: `highlight.theme`'s rules, and — when | |
| 123 | /// `highlight.theme_dark` is set — a second theme's, each behind the | |
| 124 | /// `prefers-color-scheme` query it belongs to. One file, both schemes, no JavaScript and | |
| 125 | /// nothing extra for a layout to link. | |
| 126 | /// | |
| 127 | /// The two themes are *separated* rather than stacked, because stacking does not work: | |
| 128 | /// syntect writes a rule per scope its theme names, and the themes name different ones. | |
| 129 | /// A light theme's language-specific selector (`.source.python .keyword`) outranks a | |
| 130 | /// dark theme's plain `.keyword`, so a dark theme appended after a light one would leave | |
| 131 | /// light colours on some tokens — the half-themed look this setting exists to avoid. | |
| 132 | /// The cost is that a browser too old to know `prefers-color-scheme` matches neither | |
| 133 | /// query and renders code unhighlighted; a site that sets one theme is untouched by this | |
| 134 | /// and keeps its unconditional rules. | |
| 135 | /// | |
| 136 | /// Only token colours change with the scheme. A code block's *surface* is the page's, | |
| 137 | /// set by whatever stylesheet the layout links, so a dark theme needs a dark background | |
| 138 | /// there to sit on. | |
| 139 | pub fn syntax_stylesheet(cfg: &crate::config::Highlight) -> anyhow::Result<String> { | |
| 140 | let light = theme_css_or_error(&cfg.theme, "highlight.theme")?; | |
| 141 | if cfg.theme_dark.is_empty() { | |
| 142 | return Ok(light); | |
| 143 | } | |
| 144 | let dark = theme_css_or_error(&cfg.theme_dark, "highlight.theme_dark")?; | |
| 145 | Ok(format!( | |
| 146 | "@media (prefers-color-scheme: light) {{\n{light}}}\n\n\ | |
| 147 | @media (prefers-color-scheme: dark) {{\n{dark}}}\n" | |
| 148 | )) | |
| 149 | } | |
| 150 | ||
| 151 | fn theme_css_or_error(theme: &str, key: &str) -> anyhow::Result<String> { | |
| 152 | syntax_css(theme).ok_or_else(|| { | |
| 153 | anyhow::anyhow!( | |
| 154 | "unknown {key} {theme:?}. Available: {}", | |
| 155 | available_themes().join(", ") | |
| 156 | ) | |
| 157 | }) | |
| 158 | } | |
| 159 | ||
| 122 | 160 | /// The v1 highlighter: syntect tokenizing to CSS-class spans (spec §3.2, §4.2). A block |
| 123 | 161 | /// whose language syntect does not know falls back to escaped `<pre><code>`. |
| 124 | 162 | pub struct SyntectHighlighter { |
src/site.rs +1 −7
| @@ -975,13 +975,7 @@ pub fn build_site(src: &Utf8Path, out: &Utf8Path, opts: &BuildOptions) -> Result | ||
| 975 | 975 | |
| 976 | 976 | let templater = Templater::load(Some(&src.join(&cfg.templates.dir)), &cfg.site.base_url)?; |
| 977 | 977 | check_page_templates(&templater, &preps)?; |
| 978 | let syntax_css = render::syntax_css(&cfg.highlight.theme).ok_or_else(|| { | |
| 979 | anyhow::anyhow!( | |
| 980 | "unknown highlight.theme {:?}. Available: {}", | |
| 981 | cfg.highlight.theme, | |
| 982 | render::available_themes().join(", ") | |
| 983 | ) | |
| 984 | })?; | |
| 978 | let syntax_css = render::syntax_stylesheet(&cfg.highlight)?; | |
| 985 | 979 | |
| 986 | 980 | // The global hash classes (spec §4.1): a change in any invalidates the site. The |
| 987 | 981 | // config hash is combined with a site-structure hash covering the global chrome each |
tests/config.rs +59
| @@ -381,6 +381,65 @@ fn an_unknown_highlight_theme_is_rejected_with_the_available_ones() { | ||
| 381 | 381 | ); |
| 382 | 382 | } |
| 383 | 383 | |
| 384 | /// Highlighting emits classes, so a dark reading of a page is a second set of colours | |
| 385 | /// for them — not a second stylesheet the layout has to know to link. Each theme is | |
| 386 | /// behind its own query: the two name different scopes, and a light theme's | |
| 387 | /// language-specific selectors would outrank a dark theme's plain ones if both applied. | |
| 388 | #[test] | |
| 389 | fn a_dark_highlight_theme_is_written_into_the_same_stylesheet() { | |
| 390 | let root = tmpdir("theme-dark"); | |
| 391 | let src = root.join("src"); | |
| 392 | std::fs::create_dir_all(&src).unwrap(); | |
| 393 | write_site(&src); | |
| 394 | std::fs::write( | |
| 395 | src.join("orgo.toml"), | |
| 396 | "[highlight]\ntheme = \"InspiredGitHub\"\ntheme_dark = \"base16-ocean.dark\"\n", | |
| 397 | ) | |
| 398 | .unwrap(); | |
| 399 | let out = root.join("out"); | |
| 400 | build(&src, &out); | |
| 401 | ||
| 402 | let css = std::fs::read_to_string(out.join("syntax.css")).expect("syntax.css"); | |
| 403 | let light = css | |
| 404 | .find("@media (prefers-color-scheme: light)") | |
| 405 | .expect("the light theme is behind a scheme query"); | |
| 406 | let dark = css | |
| 407 | .find("@media (prefers-color-scheme: dark)") | |
| 408 | .expect("the dark theme is behind a scheme query"); | |
| 409 | assert!(light < dark, "light first, dark second: {css:.200}"); | |
| 410 | assert!( | |
| 411 | css[..dark].contains(".comment") && css[dark..].contains("Base16 Ocean Dark"), | |
| 412 | "each theme's rules sit inside its own query" | |
| 413 | ); | |
| 414 | assert!( | |
| 415 | !css[dark..].contains("@media (prefers-color-scheme: light)"), | |
| 416 | "the queries are siblings, not nested" | |
| 417 | ); | |
| 418 | } | |
| 419 | ||
| 420 | /// The same mystery as an unknown `theme`, and it must name the key that is wrong — | |
| 421 | /// the two differ only in which scheme they colour. | |
| 422 | #[test] | |
| 423 | fn an_unknown_dark_highlight_theme_is_rejected_by_name() { | |
| 424 | let root = tmpdir("theme-dark-bad"); | |
| 425 | let src = root.join("src"); | |
| 426 | std::fs::create_dir_all(&src).unwrap(); | |
| 427 | write_site(&src); | |
| 428 | std::fs::write( | |
| 429 | src.join("orgo.toml"), | |
| 430 | "[highlight]\ntheme_dark = \"nope\"\n", | |
| 431 | ) | |
| 432 | .unwrap(); | |
| 433 | ||
| 434 | let err = build_site(&src, &root.join("out"), &BuildOptions::default()) | |
| 435 | .expect_err("unknown dark theme must fail"); | |
| 436 | let message = format!("{err:#}"); | |
| 437 | assert!( | |
| 438 | message.contains("highlight.theme_dark") && message.contains("nope"), | |
| 439 | "names the key and the bad theme: {message}" | |
| 440 | ); | |
| 441 | } | |
| 442 | ||
| 384 | 443 | // --------------------------------------------------------------------------- |
| 385 | 444 | // Discovery |
| 386 | 445 | // --------------------------------------------------------------------------- |
themes/blog.css +4 −3
| @@ -19,9 +19,10 @@ | ||
| 19 | 19 | --orgo-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
| 20 | 20 | --orgo-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; |
| 21 | 21 | /* These three colour code *blocks* only; inline code follows the page. A block keeps |
| 22 | a light surface in both colour schemes because syntax.css is generated from one | |
| 23 | syntect theme (`highlight.theme`, InspiredGitHub by default) and cannot follow | |
| 24 | prefers-color-scheme. Pair a dark highlight theme with overrides of these three. */ | |
| 22 | a light surface in both colour schemes, because syntax.css is coloured by | |
| 23 | `highlight.theme` and that default (InspiredGitHub) is light. To let blocks follow | |
| 24 | prefers-color-scheme, set `highlight.theme_dark` to a dark theme and override these | |
| 25 | three in a dark query of your own. */ | |
| 25 | 26 | --orgo-todo: #b02a37; |
| 26 | 27 | --orgo-done: #2c7a4b; |
| 27 | 28 | --orgo-code-bg: #f5f2ec; |
themes/docs.css +4 −3
| @@ -18,9 +18,10 @@ | ||
| 18 | 18 | --orgo-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
| 19 | 19 | --orgo-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; |
| 20 | 20 | /* These three colour code *blocks* only; inline code follows the page. A block keeps |
| 21 | a light surface in both colour schemes because syntax.css is generated from one | |
| 22 | syntect theme (`highlight.theme`, InspiredGitHub by default) and cannot follow | |
| 23 | prefers-color-scheme. Pair a dark highlight theme with overrides of these three. */ | |
| 21 | a light surface in both colour schemes, because syntax.css is coloured by | |
| 22 | `highlight.theme` and that default (InspiredGitHub) is light. To let blocks follow | |
| 23 | prefers-color-scheme, set `highlight.theme_dark` to a dark theme and override these | |
| 24 | three in a dark query of your own. */ | |
| 24 | 25 | --orgo-todo: #b02a37; |
| 25 | 26 | --orgo-done: #2c7a4b; |
| 26 | 27 | --orgo-code-bg: #f6f8fa; |
themes/plain.css +4 −3
| @@ -18,9 +18,10 @@ | ||
| 18 | 18 | --orgo-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
| 19 | 19 | --orgo-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; |
| 20 | 20 | /* These three colour code *blocks* only; inline code follows the page. A block keeps |
| 21 | a light surface in both colour schemes because syntax.css is generated from one | |
| 22 | syntect theme (`highlight.theme`, InspiredGitHub by default) and cannot follow | |
| 23 | prefers-color-scheme. Pair a dark highlight theme with overrides of these three. */ | |
| 21 | a light surface in both colour schemes, because syntax.css is coloured by | |
| 22 | `highlight.theme` and that default (InspiredGitHub) is light. To let blocks follow | |
| 23 | prefers-color-scheme, set `highlight.theme_dark` to a dark theme and override these | |
| 24 | three in a dark query of your own. */ | |
| 24 | 25 | --orgo-todo: #b02a37; |
| 25 | 26 | --orgo-done: #2c7a4b; |
| 26 | 27 | --orgo-code-bg: #f6f8fa; |
themes/wiki.css +4 −3
| @@ -21,9 +21,10 @@ | ||
| 21 | 21 | --orgo-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
| 22 | 22 | --orgo-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; |
| 23 | 23 | /* These three colour code *blocks* only; inline code follows the page. A block keeps |
| 24 | a light surface in both colour schemes because syntax.css is generated from one | |
| 25 | syntect theme (`highlight.theme`, InspiredGitHub by default) and cannot follow | |
| 26 | prefers-color-scheme. Pair a dark highlight theme with overrides of these three. */ | |
| 24 | a light surface in both colour schemes, because syntax.css is coloured by | |
| 25 | `highlight.theme` and that default (InspiredGitHub) is light. To let blocks follow | |
| 26 | prefers-color-scheme, set `highlight.theme_dark` to a dark theme and override these | |
| 27 | three in a dark query of your own. */ | |
| 27 | 28 | --orgo-todo: #b02a37; |
| 28 | 29 | --orgo-done: #2c7a4b; |
| 29 | 30 | --orgo-code-bg: #f6f8fa; |