Skip to main content

sevenmark_html/render/
document.rs

1//! Document-level rendering
2
3use maud::{Markup, html};
4use sevenmark_utils::Utf16OffsetConverter;
5
6use super::{brace, element, markdown};
7use crate::classes;
8use crate::config::RenderConfig;
9use crate::context::RenderContext;
10use crate::section::{Section, SectionTree, build_section_tree};
11use sevenmark_parser::ast::Element;
12
13/// Render a document to semantic HTML
14///
15/// # Arguments
16/// * `ast` - The parsed AST elements
17/// * `config` - Render configuration
18pub fn render_document(ast: &[Element], config: &RenderConfig) -> String {
19    let tree = build_section_tree(ast);
20    let mut ctx = RenderContext::new(config);
21    let content = render_section_tree(&tree, config, &mut ctx);
22
23    let markup = html! {
24        (content)
25        @if !ctx.footnotes.is_empty() {
26            (brace::footnote::render_list(&ctx))
27        }
28    };
29
30    markup.into_string()
31}
32
33/// Render a document to semantic HTML with span data attributes
34///
35/// Each element will have `data-start` and `data-end` attributes with UTF-16 offsets.
36/// This is useful for editor synchronization (e.g., highlighting preview elements based on cursor position).
37///
38/// # Arguments
39/// * `ast` - The parsed AST elements
40/// * `config` - Render configuration (include_spans should be true)
41/// * `input` - Original input text for UTF-16 offset calculation
42pub fn render_document_with_spans(ast: &[Element], config: &RenderConfig, input: &str) -> String {
43    let tree = build_section_tree(ast);
44    let converter = Utf16OffsetConverter::new(input);
45    let mut ctx = RenderContext::with_converter(config, &converter);
46    let content = render_section_tree(&tree, config, &mut ctx);
47
48    let markup = html! {
49        (content)
50        @if !ctx.footnotes.is_empty() {
51            (brace::footnote::render_list(&ctx))
52        }
53    };
54
55    markup.into_string()
56}
57
58/// Render a section tree
59fn render_section_tree(
60    tree: &SectionTree<'_>,
61    config: &RenderConfig,
62    ctx: &mut RenderContext,
63) -> Markup {
64    html! {
65        @for el in &tree.preamble {
66            (element::render_element(el, ctx))
67        }
68        @for section in &tree.sections {
69            (render_section(section, config, ctx))
70        }
71    }
72}
73
74/// Render a single section with nested structure
75fn render_section(section: &Section<'_>, config: &RenderConfig, ctx: &mut RenderContext) -> Markup {
76    let header_markup = markdown::header::render_with_path(
77        section.header_level,
78        section.header_section_index,
79        section.header_children,
80        &section.section_path,
81        config,
82        ctx,
83    );
84
85    let section_content = html! {
86        div class=(classes::SECTION_CONTENT) {
87            @for el in &section.content {
88                (element::render_element(el, ctx))
89            }
90            @for child in &section.children {
91                (render_section(child, config, ctx))
92            }
93        }
94    };
95
96    if section.header_is_folded {
97        let class = format!("{} {}", classes::SECTION, classes::SECTION_FOLDED);
98        html! {
99            details class=(class) data-section=(section.header_section_index) {
100                summary { (header_markup) }
101                (section_content)
102            }
103        }
104    } else {
105        html! {
106            details class=(classes::SECTION) data-section=(section.header_section_index) open {
107                summary { (header_markup) }
108                (section_content)
109            }
110        }
111    }
112}