orbit_md/parser/mod.rs
1//! Pull-based Markdown-to-HTML conversion via `pulldown-cmark`.
2
3use pulldown_cmark::{Options, Parser, html};
4
5use crate::components::{ComponentRegistry, expand_components};
6use crate::error::PageError;
7use crate::models::{CompiledPage, UncompiledPage};
8
9/// Shared pulldown-cmark options for page bodies.
10fn markdown_options() -> Options {
11 Options::ENABLE_STRIKETHROUGH
12 | Options::ENABLE_TABLES
13 | Options::ENABLE_FOOTNOTES
14 | Options::ENABLE_TASKLISTS
15}
16
17/// Converts Markdown source text into an HTML fragment.
18pub fn markdown_to_html(body: &str) -> String {
19 let parser = Parser::new_ext(body, markdown_options());
20 let mut content_html = String::with_capacity(body.len().saturating_mul(2));
21 html::push_html(&mut content_html, parser);
22 content_html
23}
24
25/// Converts an [`UncompiledPage`] into a [`CompiledPage`] HTML fragment.
26///
27/// JSX-style component tags are expanded before Markdown parsing.
28///
29/// # Examples
30///
31/// ```
32/// use orbit_md::components::ComponentRegistry;
33/// use orbit_md::config::Config;
34/// use orbit_md::models::{FrontMatter, SourcePage, UncompiledPage};
35/// use orbit_md::parser::compile_markdown;
36/// use std::path::PathBuf;
37///
38/// let registry = ComponentRegistry::from_config(&Config::default()).unwrap();
39/// let page = UncompiledPage::new(SourcePage {
40/// source_path: PathBuf::from("content/post.md"),
41/// relative_path: PathBuf::from("post.md"),
42/// front_matter: FrontMatter::default(),
43/// body: "# Hello".to_owned(),
44/// });
45///
46/// let compiled = compile_markdown(page, ®istry).unwrap();
47/// assert!(compiled.content_html.contains("<h1>"));
48/// ```
49pub fn compile_markdown(
50 page: UncompiledPage,
51 registry: &ComponentRegistry,
52) -> Result<CompiledPage, PageError> {
53 let source = page.into_inner();
54 let path = source.source_path.clone();
55
56 // NOTE: component expansion first — slots compile MD to HTML inside the registry.
57 let expanded = expand_components(&source.body, registry, &path)?;
58
59 // NOTE: Parser borrows `expanded` — no extra clone beyond this stage.
60 let parser = Parser::new_ext(&expanded, markdown_options());
61 let mut content_html = String::with_capacity(expanded.len().saturating_mul(2));
62 html::push_html(&mut content_html, parser);
63
64 Ok(CompiledPage::new(source, content_html))
65}
66
67/// Parallel map-reduce over all uncompiled pages.
68///
69/// Failures are returned as the first error encountered; callers may extend
70/// this to collect all errors if needed.
71pub fn compile_all(
72 pages: Vec<UncompiledPage>,
73 registry: &ComponentRegistry,
74) -> Result<Vec<CompiledPage>, PageError> {
75 use rayon::prelude::*;
76
77 pages
78 .into_par_iter()
79 .map(|page| compile_markdown(page, registry))
80 .collect()
81}