frame_host/syntax_theme.rs
1//! The built-in default syntax theme (IRIDIUM-A2 C5, amended 2026-07-21):
2//! a highlighter must visibly highlight out of the box, so frame-host
3//! ships a default capture-name → `#rrggbb` palette rather than leaving
4//! the wire's optional `syntaxTheme` member perpetually absent.
5//!
6//! The house no-assumed-defaults law forbids inventing numbers, so this
7//! palette is not new: it is copied verbatim from the proven `SYNTAX_THEME`
8//! constant in `examples/code-view-console/src/fake-feed.ts`, the console's
9//! own scripted demo theme. [`crate::doc_binding::DocBinding::boot`] uses
10//! this default whenever `[document].syntax_theme` is absent from
11//! `frame.toml`; a declared table REPLACES it wholesale (see
12//! [`crate::config::DocumentSection::syntax_theme`]).
13
14use std::collections::BTreeMap;
15
16/// The built-in default palette as `(capture name, "#rrggbb")` pairs,
17/// copied from `examples/code-view-console/src/fake-feed.ts`'s
18/// `SYNTAX_THEME`. Kept as a flat array (rather than a `BTreeMap` literal,
19/// which `const`/`static` cannot express) so the single source of truth is
20/// visible at a glance and easy to diff against the TypeScript original.
21const DEFAULT_PALETTE: [(&str, &str); 4] = [
22 ("keyword", "#ff7b72"),
23 ("function", "#d2a8ff"),
24 ("comment", "#8b949e"),
25 ("type", "#ffa657"),
26];
27
28/// Builds the built-in default syntax theme: capture-name → `#rrggbb`.
29///
30/// This is the frame-host binding seat's fallback (IRIDIUM-A2 C5,
31/// 2026-07-21 amendment), used whenever `[document].syntax_theme` is not
32/// declared in `frame.toml`. The palette values are fixed at compile time
33/// and always well-formed `#rrggbb` hex, so this function cannot fail.
34#[must_use]
35pub fn default_syntax_theme() -> BTreeMap<String, String> {
36 DEFAULT_PALETTE
37 .iter()
38 .map(|(capture, color)| ((*capture).to_owned(), (*color).to_owned()))
39 .collect()
40}
41
42#[cfg(test)]
43mod tests {
44 use super::default_syntax_theme;
45
46 /// Every built-in palette value is well-formed `#rrggbb` hex — the same
47 /// shape `[document].syntax_theme` config values are validated against
48 /// (`config::is_hex_color`) and the wire codec re-validates
49 /// (`frame_editor_wire::snapshot`).
50 #[test]
51 fn default_palette_values_are_hex_colors() {
52 let theme = default_syntax_theme();
53 assert_eq!(theme.len(), 4);
54 for (capture, color) in &theme {
55 assert!(!capture.is_empty(), "capture name must be non-empty");
56 assert_eq!(color.len(), 7, "{capture}: {color} must be #rrggbb");
57 assert!(
58 color.starts_with('#'),
59 "{capture}: {color} must start with #"
60 );
61 assert!(
62 color[1..].bytes().all(|byte| byte.is_ascii_hexdigit()),
63 "{capture}: {color} must be hex after #"
64 );
65 }
66 }
67
68 /// Pinned against the exact TypeScript source
69 /// (`examples/code-view-console/src/fake-feed.ts`'s `SYNTAX_THEME`) so a
70 /// future edit to either side fails loudly instead of silently drifting.
71 #[test]
72 fn default_palette_matches_the_fake_feed_source_of_truth() {
73 let theme = default_syntax_theme();
74 assert_eq!(theme.get("keyword").map(String::as_str), Some("#ff7b72"));
75 assert_eq!(theme.get("function").map(String::as_str), Some("#d2a8ff"));
76 assert_eq!(theme.get("comment").map(String::as_str), Some("#8b949e"));
77 assert_eq!(theme.get("type").map(String::as_str), Some("#ffa657"));
78 }
79}