mod serializer;
use crate::error::FormatError;
use crate::format::Format;
use lex_core::lex::ast::Document;
use std::fs;
pub use serializer::HtmlOptions;
pub fn get_default_css() -> &'static str {
include_str!("../../../css/baseline.css")
}
pub struct HtmlFormat {
theme: HtmlTheme,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HtmlTheme {
FancySerif,
#[default]
Modern,
}
impl Default for HtmlFormat {
fn default() -> Self {
Self::new(HtmlTheme::Modern)
}
}
impl HtmlFormat {
pub fn new(theme: HtmlTheme) -> Self {
Self { theme }
}
pub fn with_fancy_serif() -> Self {
Self::new(HtmlTheme::FancySerif)
}
pub fn with_modern() -> Self {
Self::new(HtmlTheme::Modern)
}
}
impl Format for HtmlFormat {
fn name(&self) -> &str {
"html"
}
fn description(&self) -> &str {
"HTML5 format with embedded CSS"
}
fn file_extensions(&self) -> &[&str] {
&["html", "htm"]
}
fn supports_parsing(&self) -> bool {
false }
fn supports_serialization(&self) -> bool {
true
}
fn parse(&self, _source: &str) -> Result<Document, FormatError> {
Err(FormatError::NotSupported(
"HTML import not yet implemented".to_string(),
))
}
fn serialize(&self, doc: &Document) -> Result<String, FormatError> {
serializer::serialize_to_html(doc, self.theme)
}
fn serialize_with_options(
&self,
doc: &Document,
options: &std::collections::HashMap<String, String>,
) -> Result<crate::format::SerializedDocument, FormatError> {
let mut theme = self.theme;
if let Some(theme_str) = options.get("theme") {
theme = match theme_str.as_str() {
"fancy-serif" => HtmlTheme::FancySerif,
"modern" | "default" => HtmlTheme::Modern,
_ => {
HtmlTheme::Modern
}
};
}
let mut html_options = HtmlOptions::new(theme);
if let Some(css_content) = options.get("custom_css") {
html_options = html_options.with_custom_css(css_content.clone());
} else if let Some(css_path) = options.get("css-path").or_else(|| options.get("css_path")) {
let css = fs::read_to_string(css_path).map_err(|err| {
FormatError::SerializationError(format!(
"Failed to read CSS at '{css_path}': {err}"
))
})?;
html_options = html_options.with_custom_css(css);
}
serializer::serialize_to_html_with_options(doc, html_options)
.map(crate::format::SerializedDocument::Text)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::SerializedDocument;
use lex_core::lex::ast::Document;
use std::collections::HashMap;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_get_default_css_returns_baseline() {
let css = get_default_css();
assert!(css.contains(".lex-document"));
assert!(css.contains(".lex-paragraph"));
assert!(css.contains(".lex-session"));
assert!(css.len() > 1000);
}
#[test]
fn test_get_default_css_is_same_as_embedded() {
let css = get_default_css();
assert!(css.contains("--lex-"));
}
#[test]
fn test_css_path_option_loads_file() {
let mut temp = NamedTempFile::new().expect("failed to create temp file");
writeln!(temp, ".from-path {{ color: blue; }}").expect("failed to write temp css");
let doc = Document::new();
let format = HtmlFormat::default();
let mut options = HashMap::new();
options.insert(
"css-path".to_string(),
temp.path().to_string_lossy().to_string(),
);
let html = format
.serialize_with_options(&doc, &options)
.expect("html export should succeed");
let SerializedDocument::Text(content) = html else {
panic!("expected text html output");
};
assert!(content.contains(".from-path { color: blue; }"));
}
#[test]
fn test_css_path_option_errors_on_missing_file() {
let doc = Document::new();
let format = HtmlFormat::default();
let mut options = HashMap::new();
options.insert("css-path".to_string(), "/no/such/file.css".to_string());
let err = match format.serialize_with_options(&doc, &options) {
Ok(_) => panic!("expected css-path lookup to fail"),
Err(err) => err,
};
match err {
FormatError::SerializationError(msg) => {
assert!(msg.contains("/no/such/file.css"));
}
other => panic!("expected serialization error, got {other:?}"),
}
}
}