use std::fmt::Write as _;
use std::path::PathBuf;
use std::time::Instant;
use anyhow::{Result, bail};
use log::info;
use typst::layout::PagedDocument;
use crate::compile::{
BoundIndex, ContentIndex, FontCache, LoadedImages, MluxWorld, Prescan, compile_document,
dump_document, extract_diagrams, load_images, markdown_to_typst, prescan, render_diagrams,
};
use crate::frame::{ContentMapping, TiledDocument, VisualLine, extract_visual_lines_with_map};
#[derive(Clone)]
pub struct BuildParams {
pub theme_spec: String,
pub detected_light: bool,
pub markdown: String,
pub base_dir: Option<PathBuf>,
pub file_path: Option<PathBuf>,
pub width_pt: f64,
pub sidebar_width_pt: f64,
pub tile_height_pt: f64,
pub ppi: f32,
pub fonts: &'static FontCache,
pub allow_remote_images: bool,
pub fast_png: bool,
}
struct CompiledContent {
theme_name: String,
world: MluxWorld,
document: PagedDocument,
content_index: ContentIndex,
}
fn compile_content(
params: &BuildParams,
prescan: &Prescan,
mut image_files: LoadedImages,
) -> Result<CompiledContent> {
info!(
"prescan: has_cjk={}, image_paths={}",
prescan.has_cjk,
prescan.image_paths.len(),
);
let theme_name = crate::theme::resolve_theme_name(
¶ms.theme_spec,
params.detected_light,
prescan.has_cjk,
);
info!(
"theme: spec={:?} → resolved={:?}",
params.theme_spec, theme_name
);
let theme_text = crate::theme::get(theme_name)
.ok_or_else(|| anyhow::anyhow!("unknown theme '{theme_name}'"))?;
let data_files = crate::theme::data_files(theme_name);
let diagrams = extract_diagrams(¶ms.markdown);
let mermaid_colors = crate::theme::mermaid_colors(theme_name);
for (key, svg) in render_diagrams(&diagrams, mermaid_colors) {
image_files.insert(key, svg);
}
let loaded_set = image_files.key_set();
let (content_text, content_index) = markdown_to_typst(¶ms.markdown, Some(&loaded_set));
let world = MluxWorld::new(
theme_text,
data_files,
&content_text,
params.width_pt,
params.fonts,
image_files,
);
let document = compile_document(&world)?;
Ok(CompiledContent {
theme_name: theme_name.to_string(),
world,
document,
content_index,
})
}
pub(crate) fn compile_and_dump(
params: &BuildParams,
prescan: &Prescan,
image_files: LoadedImages,
) -> Result<()> {
let compiled = compile_content(params, prescan, image_files)?;
let source_text = compiled.world.main_source().text();
eprintln!(
"=== Generated main.typ ({} lines) ===",
source_text.lines().count()
);
for (i, line) in source_text.lines().enumerate() {
eprintln!("{:>4} | {}", i + 1, line);
}
eprintln!();
dump_document(&compiled.document);
Ok(())
}
pub fn build_tiled_document(params: &BuildParams) -> Result<TiledDocument> {
let ps = prescan(¶ms.markdown);
let (images, errors) = load_images(
&ps.image_paths,
params.base_dir.as_deref(),
params.allow_remote_images,
);
for err in &errors {
log::warn!("{err}");
}
compile_and_tile(params, &ps, images)
}
pub(crate) fn compile_and_tile(
params: &BuildParams,
prescan: &Prescan,
image_files: LoadedImages,
) -> Result<TiledDocument> {
let start = Instant::now();
let CompiledContent {
theme_name,
world: content_world,
document,
content_index,
} = compile_content(params, prescan, image_files)?;
let bound_index = BoundIndex::new(
&content_index,
content_world.main_source(),
content_world.content_offset(),
¶ms.markdown,
);
let mut visual_lines = extract_visual_lines_with_map(&document, params.ppi, Some(&bound_index));
let mut deletion_gaps = Vec::new();
if let Some(ref fp) = params.file_path {
let diff_ranges = crate::diff::diff_against_head(fp);
if !diff_ranges.is_empty() {
crate::diff::apply_diff_to_visual_lines(
&mut visual_lines,
&diff_ranges,
¶ms.markdown,
);
deletion_gaps =
crate::diff::find_deletion_gaps(&visual_lines, &diff_ranges, ¶ms.markdown);
}
}
if document.pages.is_empty() {
bail!("[BUG] document has no pages");
}
let page_height_pt = document.pages[0].frame.size().y.to_pt();
let sidebar_source = generate_sidebar_typst(
&visual_lines,
params.sidebar_width_pt,
page_height_pt,
&theme_name,
&deletion_gaps,
);
let sidebar_world = MluxWorld::new_raw(&sidebar_source, params.fonts);
let sidebar_doc = compile_document(&sidebar_world)?;
let tiled_doc = TiledDocument::new(
&document,
&sidebar_doc,
visual_lines,
params.tile_height_pt,
params.ppi,
ContentMapping {
source: content_world.main_source().clone(),
content_index,
content_offset: content_world.content_offset(),
},
params.fast_png,
)?;
info!(
"build_tiled_document completed in {:.1}ms",
start.elapsed().as_secs_f64() * 1000.0
);
Ok(tiled_doc)
}
pub fn generate_sidebar_typst(
lines: &[VisualLine],
sidebar_width_pt: f64,
page_height_pt: f64,
theme_name: &str,
deletion_gaps: &[f64],
) -> String {
let (bg, fg) = crate::theme::sidebar_colors(theme_name);
let mut src = String::new();
writeln!(
src,
"#set page(width: {sidebar_width_pt}pt, height: {page_height_pt}pt, margin: 0pt, fill: rgb(\"{bg}\"))"
)
.unwrap();
writeln!(
src,
"#set text(font: \"Fira Mono\", size: 8pt, fill: rgb(\"{fg}\"))"
)
.unwrap();
for (i, line) in lines.iter().enumerate() {
let line_num = i + 1;
let dy = line.y_pt;
if let Some(status) = &line.diff_status {
let color = match status {
crate::diff::DiffStatus::Added => "#a6e3a1",
crate::diff::DiffStatus::Modified => "#f9e2af",
crate::diff::DiffStatus::Deleted => "#f38ba8",
};
let height = if i + 1 < lines.len() {
lines[i + 1].y_pt - dy
} else {
crate::diff::DEFAULT_LINE_HEIGHT_PT
};
writeln!(
src,
"#place(top + left, dy: {dy}pt - 6pt, dx: 2pt)[#rect(width: 2pt, height: {height:.1}pt, fill: rgb(\"{color}\"))]"
)
.unwrap();
}
writeln!(
src,
"#place(top + right, dy: {dy}pt - 6pt, dx: -4pt)[#text(size: 8pt)[{line_num}]]"
)
.unwrap();
}
for &gap_y in deletion_gaps {
let dy = gap_y - 6.0; writeln!(
src,
"#place(top + left, dy: {dy:.1}pt - 5pt, dx: 0pt)[#polygon(fill: rgb(\"#f38ba8\"), (0pt, 0pt), (8pt, 0pt), (4pt, 5pt))]"
)
.unwrap();
}
src
}