use crate::brand::{BrandSpec, LogoPosition, LogoSpec};
pub(crate) fn brand_css(spec: &BrandSpec) -> Option<String> {
if spec.palette.is_empty() && spec.fonts.is_empty() {
return None;
}
let mut vars = String::new();
if let Some(v) = spec.palette.get("background") {
push_var(&mut vars, "--r-background-color", v);
}
if let Some(v) = spec
.palette
.get("heading")
.or_else(|| spec.palette.get("primary"))
{
push_var(&mut vars, "--r-heading-color", v);
}
if let Some(v) = spec.palette.get("text") {
push_var(&mut vars, "--r-main-color", v);
}
if let Some(v) = spec.palette.get("link") {
push_var(&mut vars, "--r-link-color", v);
push_var(&mut vars, "--r-link-color-hover", v);
}
if let Some(v) = spec.palette.get("accent") {
push_var(&mut vars, "--r-selection-background-color", v);
}
if let Some(v) = spec.fonts.get("body") {
push_var(&mut vars, "--r-main-font", v);
}
if let Some(v) = spec.fonts.get("heading") {
push_var(&mut vars, "--r-heading-font", v);
}
if let Some(v) = spec.fonts.get("code") {
push_var(&mut vars, "--r-code-font", v);
}
for (key, value) in &spec.palette {
push_var(&mut vars, &format!("--brand-{key}"), value);
}
Some(format!(".reveal {{\n{vars}}}\n"))
}
fn push_var(out: &mut String, name: &str, value: &str) {
out.push_str(" ");
out.push_str(name);
out.push_str(": ");
out.push_str(&escape_css_value(value));
out.push_str(";\n");
}
fn escape_css_value(value: &str) -> String {
value
.chars()
.filter(|c| !matches!(c, '{' | '}' | ';' | '<' | '>' | '"' | '\n' | '\r'))
.collect()
}
pub(crate) fn logo_html(spec: &LogoSpec, bytes: &[u8], mime: &str) -> String {
let b64 = base64_encode(bytes);
let (top, right, bottom, left) = corner_css(spec.position);
let width = spec
.width
.as_deref()
.map(|w| format!("width: {};", escape_css_value(w)))
.unwrap_or_default();
format!(
"<img src=\"data:{mime};base64,{b64}\" style=\"position: fixed; {top}{right}{bottom}{left}{width} z-index: 2; pointer-events: none;\">\n"
)
}
fn corner_css(position: LogoPosition) -> (&'static str, &'static str, &'static str, &'static str) {
match position {
LogoPosition::TopRight => ("top: 20px; ", "right: 20px; ", "", ""),
LogoPosition::TopLeft => ("top: 20px; ", "", "", "left: 20px; "),
LogoPosition::BottomRight => ("", "right: 20px; ", "bottom: 20px; ", ""),
LogoPosition::BottomLeft => ("", "", "bottom: 20px; ", "left: 20px; "),
}
}
const BASE64_ALPHABET: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
fn base64_encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(BASE64_ALPHABET[(n >> 18 & 0x3F) as usize] as char);
out.push(BASE64_ALPHABET[(n >> 12 & 0x3F) as usize] as char);
out.push(if chunk.len() > 1 {
BASE64_ALPHABET[(n >> 6 & 0x3F) as usize] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
BASE64_ALPHABET[(n & 0x3F) as usize] as char
} else {
'='
});
}
out
}
#[cfg(test)]
#[path = "reveal_brand_tests.rs"]
mod tests;