mod dom_build;
mod dom_helpers;
use crate::common::nested_to_flat::tree_to_events;
use crate::common::splice::SentinelBuffer;
use crate::error::FormatError;
use crate::formats::html::HtmlTheme;
use crate::ir::nodes::DocNode;
use lex_core::lex::ast::Document;
use dom_build::build_html_dom_with_splice;
use dom_helpers::{serialize_dom, wrap_in_document};
#[derive(Debug, Clone, Default)]
pub struct HtmlOptions {
pub theme: HtmlTheme,
pub custom_css: Option<String>,
}
impl HtmlOptions {
pub fn new(theme: HtmlTheme) -> Self {
Self {
theme,
custom_css: None,
}
}
pub fn with_custom_css(mut self, css: String) -> Self {
self.custom_css = Some(css);
self
}
}
pub fn serialize_to_html(doc: &Document, theme: HtmlTheme) -> Result<String, FormatError> {
serialize_to_html_with_options(doc, HtmlOptions::new(theme))
}
pub fn serialize_to_html_with_registry(
doc: &Document,
options: HtmlOptions,
registry: &lex_extension_host::Registry,
) -> Result<HtmlExportOutcome, FormatError> {
let ir_doc = crate::to_ir_with_registry(doc, registry);
let plan = crate::render_dispatch::dispatch_render(&ir_doc, registry, "html");
let body_plan = &plan.nodes[plan.doc_scope_count..];
let html = serialize_to_html_with_splice_from_ir(ir_doc, options, Some(body_plan))?;
let mut diagnostics = plan
.nodes
.iter()
.filter_map(|n| n.diagnostic.clone())
.collect::<Vec<_>>();
diagnostics.extend(plan.root_diagnostics);
Ok(HtmlExportOutcome { html, diagnostics })
}
fn serialize_to_html_with_splice(
doc: &Document,
options: HtmlOptions,
splice_plan: Option<&[crate::render_dispatch::RenderedNode]>,
) -> Result<String, FormatError> {
let ir_doc = crate::to_ir(doc);
serialize_to_html_with_splice_from_ir(ir_doc, options, splice_plan)
}
fn serialize_to_html_with_splice_from_ir(
ir_doc: crate::ir::nodes::Document,
options: HtmlOptions,
splice_plan: Option<&[crate::render_dispatch::RenderedNode]>,
) -> Result<String, FormatError> {
let title_text = ir_doc
.title
.as_ref()
.map(|t| crate::ir::to_wire::inlines_to_text(t));
let subtitle_text = ir_doc
.subtitle
.as_ref()
.map(|s| crate::ir::to_wire::inlines_to_text(s));
let head_title = match (&title_text, &subtitle_text) {
(Some(t), Some(s)) => format!("{t}: {s}"),
(Some(t), None) => t.clone(),
(None, _) => "Lex Document".to_string(),
};
let events = tree_to_events(&DocNode::Document(ir_doc));
let mut sentinels = SentinelBuffer::new();
let (dom, has_math) = build_html_dom_with_splice(&events, splice_plan, &mut sentinels)?;
let html_string = serialize_dom(&dom)?;
let html_string = sentinels.replace(&html_string);
wrap_in_document(
&html_string,
&head_title,
title_text.as_deref(),
subtitle_text.as_deref(),
has_math,
&options,
)
}
#[derive(Debug, Clone, PartialEq)]
pub struct HtmlExportOutcome {
pub html: String,
pub diagnostics: Vec<String>,
}
pub fn serialize_to_html_with_options(
doc: &Document,
options: HtmlOptions,
) -> Result<String, FormatError> {
serialize_to_html_with_splice(doc, options, None)
}
#[cfg(test)]
mod tests {
use super::*;
use lex_core::lex::transforms::standard::STRING_TO_AST;
#[test]
fn test_simple_paragraph() {
let lex_src = "This is a simple paragraph.\n";
let lex_doc = STRING_TO_AST.run(lex_src.to_string()).unwrap();
let html = serialize_to_html(&lex_doc, HtmlTheme::Modern).unwrap();
assert!(html.contains("<!DOCTYPE html>"));
assert!(html.contains("<p class=\"lex-paragraph\">"));
assert!(html.contains("This is a simple paragraph."));
}
#[test]
fn test_heading() {
let lex_src = "1. Introduction\n\n Content here.\n";
let lex_doc = STRING_TO_AST.run(lex_src.to_string()).unwrap();
let html = serialize_to_html(&lex_doc, HtmlTheme::Modern).unwrap();
assert!(html.contains("<section class=\"lex-session lex-session-2\">"));
assert!(html.contains("<h2>"));
assert!(html.contains("Introduction"));
}
#[test]
fn test_css_embedded() {
let lex_src = "Test document.\n";
let lex_doc = STRING_TO_AST.run(lex_src.to_string()).unwrap();
let html = serialize_to_html(&lex_doc, HtmlTheme::Modern).unwrap();
assert!(html.contains("<style>"));
assert!(html.contains(".lex-document"));
assert!(html.contains("Helvetica")); }
#[test]
fn test_fancy_serif_theme() {
let lex_src = "Test document.\n";
let lex_doc = STRING_TO_AST.run(lex_src.to_string()).unwrap();
let html = serialize_to_html(&lex_doc, HtmlTheme::FancySerif).unwrap();
assert!(html.contains("Cormorant")); }
#[test]
fn test_custom_css_appended() {
let lex_src = "Test document.\n";
let lex_doc = STRING_TO_AST.run(lex_src.to_string()).unwrap();
let custom_css = ".my-custom-class { color: red; }";
let options = HtmlOptions::new(HtmlTheme::Modern).with_custom_css(custom_css.to_string());
let html = serialize_to_html_with_options(&lex_doc, options).unwrap();
assert!(html.contains(".my-custom-class { color: red; }"));
assert!(html.contains(".lex-document"));
}
#[test]
fn test_html_options_default() {
let options = HtmlOptions::default();
assert_eq!(options.theme, HtmlTheme::Modern);
assert!(options.custom_css.is_none());
}
}