use crate::theme::ResolvedTheme;
const VIEWER_HTML: &str = include_str!("../assets/viewer.html");
const GITHUB_CSS: &str = include_str!("../assets/github-markdown.css");
const PAGE_CSS: &str = include_str!("../assets/page.css");
const SYNTAX_CSS: &str = include_str!("../assets/syntax.css");
const ALERTS_CSS: &str = include_str!("../assets/alerts.css");
const THEME_OVERRIDES: &str = include_str!("../assets/theme-overrides.css");
pub struct PageOptions<'a> {
pub filename: &'a str,
pub file_stats: &'a str,
pub content_html: &'a str,
pub source_html: Option<&'a str>,
pub custom_css: Option<&'a str>,
pub font_css: Option<&'a str>,
pub show_header: bool,
pub reading_mode: bool,
pub raw_mode: bool,
pub theme: &'a ResolvedTheme,
pub theme_names: &'a [&'a str],
pub variant_explicit: bool,
pub static_mode: bool,
pub keybindings_json: &'a str,
pub current_path: Option<&'a str>,
}
pub fn render_page(opts: &PageOptions<'_>) -> String {
let PageOptions {
filename,
content_html,
custom_css,
font_css,
show_header,
reading_mode,
theme,
theme_names,
..
} = opts;
let custom_style = match custom_css {
Some(css) => format!("<style>{css}</style>"),
None => String::new(),
};
let active = theme.active_data();
let is_github = theme.is_github();
let syntax_css = SYNTAX_CSS;
let theme_vars_css = if is_github {
String::new()
} else {
active.css_vars.clone()
};
let theme_attr = if is_github {
String::new()
} else {
format!("data-birta-theme=\"{}\"", theme.name)
};
let theme_mode = if theme.has_toggle() {
"toggle"
} else {
match theme.active_variant {
crate::theme::Variant::Light => "fixed-light",
crate::theme::Variant::Dark => "fixed-dark",
}
};
let active_variant = theme.active_variant.as_str();
let variant_explicit = if opts.variant_explicit {
"true"
} else {
"false"
};
let theme_options: String = theme_names
.iter()
.map(|&name| {
let active = if name == theme.name { " active" } else { "" };
format!(
"<button class=\"theme-dropdown-item{active}\" data-theme=\"{name}\">{name}</button>"
)
})
.collect::<Vec<_>>()
.join("\n ");
let variants_json =
serde_json::to_string(&theme.variant_names()).unwrap_or_else(|_| "[]".to_string());
VIEWER_HTML
.replace("{{GITHUB_CSS}}", GITHUB_CSS)
.replace("{{THEME_OVERRIDES}}", THEME_OVERRIDES)
.replace("{{PAGE_CSS}}", PAGE_CSS)
.replace("{{SYNTAX_CSS}}", syntax_css)
.replace("{{ALERTS_CSS}}", ALERTS_CSS)
.replace("{{THEME_VARS_CSS}}", &theme_vars_css)
.replace("{{FONT_CSS}}", font_css.unwrap_or(""))
.replace("{{CUSTOM_CSS}}", &custom_style)
.replace(
"{{HEADER_CLASS}}",
if *show_header { "" } else { " header-hidden" },
)
.replace(
"{{BODY_CLASS}}",
if *reading_mode { " reading-mode" } else { "" },
)
.replace("{{THEME_MODE}}", theme_mode)
.replace("{{THEME_ATTR}}", &theme_attr)
.replace("{{ACTIVE_VARIANT}}", active_variant)
.replace("{{VARIANT_EXPLICIT}}", variant_explicit)
.replace("{{ACTIVE_THEME}}", &theme.name)
.replace("{{THEME_OPTIONS}}", &theme_options)
.replace("{{VARIANTS_JSON}}", &variants_json)
.replace("{{FILENAME}}", filename)
.replace("{{FILE_STATS}}", opts.file_stats)
.replace(
"{{STATIC_MODE}}",
if opts.static_mode { "true" } else { "false" },
)
.replace("{{KEYBINDINGS_JSON}}", opts.keybindings_json)
.replace(
"{{INITIAL_VIEW}}",
if opts.raw_mode { "raw" } else { "preview" },
)
.replace("{{CURRENT_PATH}}", opts.current_path.unwrap_or(""))
.replace("{{SOURCE_HTML}}", opts.source_html.unwrap_or(""))
.replace("{{CONTENT}}", content_html)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::theme::{ResolvedTheme, ThemeVariants, Variant, VariantData};
fn github_theme() -> ResolvedTheme {
ResolvedTheme {
name: "github".to_string(),
variants: ThemeVariants::Both {
light: Box::new(VariantData {
css_vars: String::new(),
syntax: None,
}),
dark: Box::new(VariantData {
css_vars: String::new(),
syntax: None,
}),
},
active_variant: Variant::Dark,
}
}
fn test_page(content: &str, custom_css: Option<&str>) -> String {
let theme = github_theme();
render_page(&PageOptions {
filename: "test.md",
file_stats: "1 lines (1 loc) ยท 5 B",
content_html: content,
source_html: None,
custom_css,
font_css: None,
show_header: true,
reading_mode: false,
raw_mode: false,
theme: &theme,
theme_names: &["github"],
variant_explicit: false,
static_mode: false,
keybindings_json: "{}",
current_path: None,
})
}
#[test]
fn render_page_contains_filename() {
let page = test_page("<p>hello</p>", None);
assert!(page.contains("test.md"));
}
#[test]
fn render_page_contains_file_stats() {
let page = test_page("<p>hello</p>", None);
assert!(page.contains("1 lines (1 loc) ยท 5 B"));
}
#[test]
fn render_page_contains_content() {
let page = test_page("<p>hello</p>", None);
assert!(page.contains("<p>hello</p>"));
}
#[test]
fn render_page_contains_markdown_body_class() {
let page = test_page("", None);
assert!(page.contains("markdown-body"));
}
#[test]
fn render_page_contains_github_css() {
let page = test_page("", None);
assert!(page.contains(".markdown-body"));
}
#[test]
fn render_page_includes_custom_css() {
let page = test_page("", Some("body { color: red; }"));
assert!(page.contains("body { color: red; }"));
}
fn render_with(
show_header: bool,
reading_mode: bool,
static_mode: bool,
font_css: Option<&str>,
theme: &ResolvedTheme,
theme_names: &[&str],
) -> String {
render_page(&PageOptions {
filename: "test.md",
file_stats: "1 lines (1 loc) ยท 7 B",
content_html: "<p>test</p>",
source_html: None,
custom_css: None,
font_css,
show_header,
reading_mode,
raw_mode: false,
theme,
theme_names,
variant_explicit: false,
static_mode,
keybindings_json: "{}",
current_path: None,
})
}
fn dark_only_theme() -> ResolvedTheme {
ResolvedTheme {
name: "dracula".to_string(),
variants: ThemeVariants::Single(Box::new(VariantData {
css_vars: ":root { --birta-fg: #f8f8f2; }".to_string(),
syntax: None,
})),
active_variant: Variant::Dark,
}
}
#[test]
fn render_page_hidden_header() {
let theme = github_theme();
let page = render_with(false, false, false, None, &theme, &["github"]);
assert!(
page.contains("class=\"header header-hidden\""),
"should add header-hidden class when show_header is false"
);
}
#[test]
fn render_page_visible_header_has_no_hidden_class() {
let theme = github_theme();
let page = render_with(true, false, false, None, &theme, &["github"]);
assert!(
page.contains("class=\"header\""),
"header should have no hidden class when show_header is true"
);
}
#[test]
fn render_page_reading_mode_class() {
let theme = github_theme();
let page = render_with(true, true, false, None, &theme, &["github"]);
assert!(
page.contains("<body class=\" reading-mode\">"),
"should add reading-mode to body class"
);
}
#[test]
fn render_page_no_reading_mode_class() {
let theme = github_theme();
let page = render_with(true, false, false, None, &theme, &["github"]);
assert!(
page.contains("<body class=\"\">"),
"body class should be empty when reading mode is disabled"
);
}
#[test]
fn render_page_static_mode_true() {
let theme = github_theme();
let page = render_with(true, false, true, None, &theme, &["github"]);
assert!(
page.contains("var STATIC_MODE = true;"),
"should set STATIC_MODE to true"
);
}
#[test]
fn render_page_static_mode_false() {
let theme = github_theme();
let page = render_with(true, false, false, None, &theme, &["github"]);
assert!(
page.contains("var STATIC_MODE = false;"),
"should set STATIC_MODE to false"
);
}
#[test]
fn render_page_theme_mode_toggle() {
let theme = github_theme();
let page = render_with(true, false, false, None, &theme, &["github"]);
assert!(
page.contains("var THEME_MODE = 'toggle';"),
"dual-variant theme should produce toggle mode"
);
}
#[test]
fn render_page_theme_mode_fixed_dark() {
let theme = dark_only_theme();
let page = render_with(true, false, false, None, &theme, &["dracula"]);
assert!(
page.contains("var THEME_MODE = 'fixed-dark';"),
"single dark-only theme should produce fixed-dark mode"
);
}
#[test]
fn render_page_theme_mode_fixed_light() {
let theme = ResolvedTheme {
name: "custom-light".to_string(),
variants: ThemeVariants::Single(Box::new(VariantData {
css_vars: String::new(),
syntax: None,
})),
active_variant: Variant::Light,
};
let page = render_with(true, false, false, None, &theme, &["custom-light"]);
assert!(
page.contains("var THEME_MODE = 'fixed-light';"),
"single light-only theme should produce fixed-light mode"
);
}
fn render_with_explicit(variant_explicit: bool, theme: &ResolvedTheme) -> String {
render_page(&PageOptions {
filename: "test.md",
file_stats: "1 lines (1 loc) ยท 7 B",
content_html: "<p>test</p>",
source_html: None,
custom_css: None,
font_css: None,
show_header: true,
reading_mode: false,
raw_mode: false,
theme,
theme_names: &["github"],
variant_explicit,
static_mode: false,
keybindings_json: "{}",
current_path: None,
})
}
#[test]
fn render_page_variant_explicit_true() {
let theme = github_theme();
let page = render_with_explicit(true, &theme);
assert!(
page.contains("var VARIANT_EXPLICIT = true;"),
"explicit --light/--dark should set VARIANT_EXPLICIT to true so the \
browser does not override it with the OS preference"
);
}
#[test]
fn render_page_variant_explicit_false() {
let theme = github_theme();
let page = render_with_explicit(false, &theme);
assert!(
page.contains("var VARIANT_EXPLICIT = false;"),
"without an explicit variant the browser should fall back to OS preference"
);
}
#[test]
fn render_page_font_css_injected() {
let theme = github_theme();
let css = ".markdown-body { font-family: Georgia, serif !important; }";
let page = render_with(true, false, false, Some(css), &theme, &["github"]);
assert!(
page.contains(css),
"font CSS should appear in rendered page"
);
}
#[test]
fn render_page_theme_dropdown_options() {
let theme = github_theme();
let page = render_with(true, false, false, None, &theme, &["github", "dracula"]);
assert!(
page.contains("data-theme=\"github\">github</button>"),
"active theme should appear in dropdown"
);
assert!(
page.contains("theme-dropdown-item active"),
"active theme should have active class"
);
assert!(
page.contains("data-theme=\"dracula\">dracula</button>"),
"other themes should appear in dropdown"
);
}
#[test]
fn render_page_custom_theme_sets_data_attr() {
let theme = dark_only_theme();
let page = render_with(true, false, false, None, &theme, &["dracula"]);
assert!(
page.contains("data-birta-theme=\"dracula\""),
"non-github theme should set data-birta-theme attribute"
);
}
#[test]
fn render_page_github_theme_no_data_attr() {
let theme = github_theme();
let page = render_with(true, false, false, None, &theme, &["github"]);
assert!(
!page.contains("data-birta-theme=\"github\""),
"github theme should not set data-birta-theme attribute on html element"
);
}
}