Skip to main content

orbit_md/
lib.rs

1//! Orbit — high-speed parallel static site generator.
2//!
3//! Markdown pages with JSX-style components, compiled to static HTML.
4//!
5//! Architecture:
6//! - [`discovery`] — parallel filesystem crawl + front matter extraction
7//! - [`components`] — JSX-style Markdown component expansion
8//! - [`parser`] — `pulldown-cmark` HTML fragment generation
9//! - [`template`] — pre-compiled Handlebars layouts
10//! - [`writer`] — parallel flush to `.orbit/`
11
12#![deny(unsafe_code)]
13
14pub mod cli;
15pub mod components;
16pub mod config;
17pub mod dev;
18pub mod discovery;
19pub mod error;
20pub mod models;
21pub mod parser;
22pub mod scaffold;
23pub mod template;
24pub mod writer;
25
26use std::path::Path;
27use std::time::Instant;
28
29use rayon::prelude::*;
30
31use crate::components::ComponentRegistry;
32use crate::config::Config;
33use crate::discovery::discover_pages;
34use crate::error::{OrbitError, PageError};
35use crate::models::RenderedPage;
36use crate::parser::{compile_all, compile_markdown};
37use crate::template::{TemplateEngine, render_all};
38use crate::writer::{clean_output_dir, write_all};
39
40/// Build statistics returned after a successful compilation.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct BuildReport {
43    /// Number of pages written to disk.
44    pub pages_written: usize,
45    /// Wall-clock duration of the full pipeline.
46    pub elapsed_ms: u128,
47}
48
49/// Runs the full discover → compile → render → write pipeline.
50///
51/// # Errors
52///
53/// Returns the first per-page or configuration error encountered.
54pub fn build(config: &Config) -> Result<BuildReport, OrbitError> {
55    let started = Instant::now();
56
57    let components = ComponentRegistry::from_config(config)?;
58    let engine = TemplateEngine::from_config(config)?;
59    clean_output_dir(&config.output_dir).map_err(OrbitError::from)?;
60
61    let uncompiled = discover_pages(&config.source_dir).map_err(OrbitError::from)?;
62    let compiled = compile_all(uncompiled, &components).map_err(OrbitError::from)?;
63    let rendered = render_all(&engine, compiled, &config.output_dir).map_err(OrbitError::from)?;
64
65    let count = rendered.len();
66    write_all(&rendered).map_err(OrbitError::from)?;
67
68    Ok(BuildReport {
69        pages_written: count,
70        elapsed_ms: started.elapsed().as_millis(),
71    })
72}
73
74/// Map-reduce pipeline exposing each stage for testing and benchmarking.
75///
76/// Accepts pre-discovered pages and returns rendered output without writing.
77pub fn compile_pipeline(
78    config: &Config,
79    content_root: &Path,
80) -> Result<Vec<RenderedPage>, PageError> {
81    let components = ComponentRegistry::from_config(config)
82        .map_err(|err| PageError::new(content_root, err.to_string()))?;
83    let engine = TemplateEngine::from_config(config)
84        .map_err(|err| PageError::new(content_root, err.to_string()))?;
85
86    let uncompiled = discover_pages(content_root)?;
87    let compiled = compile_all(uncompiled, &components)?;
88    render_all(&engine, compiled, &config.output_dir)
89}
90
91/// Parallel markdown-only compilation used in throughput benchmarks.
92pub fn compile_markdown_batch(
93    pages: Vec<crate::models::UncompiledPage>,
94    registry: &ComponentRegistry,
95) -> Result<usize, PageError> {
96    let count = pages
97        .into_par_iter()
98        .map(|page| compile_markdown(page, registry))
99        .collect::<Result<Vec<_>, _>>()?
100        .len();
101    Ok(count)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::models::{FrontMatter, SourcePage, UncompiledPage};
108    use std::path::PathBuf;
109
110    #[test]
111    fn compile_markdown_batch_parallel() {
112        let registry = ComponentRegistry::from_config(&Config::default()).unwrap();
113        let pages: Vec<_> = (0..32)
114            .map(|i| {
115                UncompiledPage::new(SourcePage {
116                    source_path: PathBuf::from(format!("content/{i}.md")),
117                    relative_path: PathBuf::from(format!("{i}.md")),
118                    front_matter: FrontMatter::default(),
119                    body: format!("# Post {i}\n\nParagraph."),
120                })
121            })
122            .collect();
123
124        let count = compile_markdown_batch(pages, &registry).unwrap();
125        assert_eq!(count, 32);
126    }
127}