Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

244 changes: 27 additions & 217 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,66 +247,12 @@ fn resolve_build_dir(
}
}

fn get_output_filename(typ_file: &std::path::Path) -> Result<String> {
typ_file
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
.ok_or_else(|| RheoError::project_config(format!("invalid .typ filename: {:?}", typ_file)))
}

/// Per-plugin invariants shared across all files in a single-plugin compilation pass.
struct PerFileCtx<'a> {
plugin: &'a dyn FormatPlugin,
plugin_output_dir: &'a Path,
project: &'a ProjectConfig,
output_config: &'a OutputConfig,
spine: &'a TracedSpine,
plugin_section: &'a PluginSection,
resolved_inputs: &'a HashMap<&'static str, PathBuf>,
content_dir: &'a Path,
}

/// Compile one file with the given world, recording success/failure in `results`.
///
/// `get_output_filename` errors propagate; `plugin.compile()` errors are recorded
/// as failures rather than propagated (so other files in the batch still compile).
fn compile_one_file(
world: &mut RheoWorld,
typ_file: &Path,
pfc: &PerFileCtx<'_>,
results: &mut CompilationResults,
) -> Result<()> {
let filename = get_output_filename(typ_file)?;
let output_path = pfc
.plugin_output_dir
.join(&filename)
.with_extension(pfc.plugin.output_extension());
let options = RheoCompileOptions::new(&output_path, pfc.content_dir, world);
let ctx = PluginContext {
project: pfc.project,
output_config: pfc.output_config,
options,
spine: pfc.spine.clone(),
config: pfc.plugin_section.clone(),
inputs: pfc.resolved_inputs.clone(),
};
match pfc.plugin.compile(ctx) {
Ok(_) => results.record_success(pfc.plugin.name()),
Err(e) => {
error!(file = %typ_file.display(), error = %e, "{} compilation failed", pfc.plugin.name());
results.record_failure(pfc.plugin.name());
}
}
Ok(())
}

/// Bundle compilation: generate bundle entry, inject into world, compile once.
/// Used by plugins that use the bundle API (HTML, PDF non-merge).
/// Used by all plugins (HTML, PDF, EPUB) with the bundle API.
#[allow(clippy::too_many_arguments)]
fn compile_bundle(
fn compile_with_bundle(
plugin: &dyn FormatPlugin,
plugin_output_dir: &Path,
output: &Path,
project: &ProjectConfig,
output_config: &OutputConfig,
spine: &TracedSpine,
Expand Down Expand Up @@ -334,7 +280,7 @@ fn compile_bundle(
);
bundle_world.inject_bundle_entry(bundle_entry_source);

let options = RheoCompileOptions::new(plugin_output_dir, &project.root, &mut bundle_world);
let options = RheoCompileOptions::new(output, compilation_root, &mut bundle_world);

let ctx = PluginContext {
project,
Expand All @@ -357,115 +303,10 @@ fn compile_bundle(
Ok(())
}

/// Merged compilation: single output from all spine documents.
/// Used by PDF merge mode and EPUB.
#[allow(clippy::too_many_arguments)]
fn compile_merged(
plugin: &dyn FormatPlugin,
plugin_output_dir: &Path,
project: &ProjectConfig,
output_config: &OutputConfig,
spine: &TracedSpine,
plugin_section: &PluginSection,
resolved_inputs: HashMap<&'static str, PathBuf>,
results: &mut CompilationResults,
compilation_root: &Path,
) -> Result<()> {
let output_path = plugin_output_dir
.join(&project.name)
.with_extension(plugin.output_extension());

let plugin_library = plugin.typst_library().map(|s| s.to_string());
let mut bundle_world = RheoWorld::new(
compilation_root,
spine
.documents
.first()
.map(|d| d.path.as_path())
.unwrap_or(compilation_root),
plugin_library,
)?;

let bundle_entry_source = generate_bundle_entry(
spine,
compilation_root,
plugin.name(),
plugin.typst_library().unwrap_or_default(),
);
bundle_world.inject_bundle_entry(bundle_entry_source);

let options = RheoCompileOptions::new(&output_path, compilation_root, &mut bundle_world);

let ctx = PluginContext {
project,
output_config,
options,
spine: spine.clone(),
config: plugin_section.clone(),
inputs: resolved_inputs,
};

match plugin.compile(ctx) {
Ok(_) => {
results.record_success(plugin.name());
}
Err(e) => {
error!(error = %e, "{} generation failed", plugin.name());
results.record_failure(plugin.name());
}
}
Ok(())
}

/// Per-file compilation: loop through spine documents, compile each individually.
/// Used by PDF non-merge mode.
#[allow(clippy::too_many_arguments)]
fn compile_per_file(
plugin: &dyn FormatPlugin,
plugin_output_dir: &Path,
project: &ProjectConfig,
output_config: &OutputConfig,
spine: &TracedSpine,
plugin_section: &PluginSection,
resolved_inputs: &HashMap<&'static str, PathBuf>,
content_dir: &Path,
world: &mut Option<&mut RheoWorld>,
results: &mut CompilationResults,
) -> Result<()> {
let files: Vec<PathBuf> = spine.documents.iter().map(|d| d.path.clone()).collect();

let pfc = PerFileCtx {
plugin,
plugin_output_dir,
project,
output_config,
spine,
plugin_section,
resolved_inputs,
content_dir,
};

if let Some(ref mut existing_world) = *world {
for typ_file in &files {
existing_world.set_main(typ_file)?;
existing_world.reset();
compile_one_file(existing_world, typ_file, &pfc, results)?;
}
} else {
let plugin_library = plugin.typst_library().map(|s| s.to_string());
for typ_file in &files {
let mut fresh_world = RheoWorld::new(content_dir, typ_file, plugin_library.clone())?;
compile_one_file(&mut fresh_world, typ_file, &pfc, results)?;
}
}
Ok(())
}

fn perform_compilation(
project: &ProjectConfig,
output_config: &OutputConfig,
plugins: &[Box<dyn FormatPlugin>],
mut world: Option<&mut RheoWorld>,
) -> Result<()> {
if project.typ_files.is_empty() {
return Err(RheoError::project_config("no .typ files found in project"));
Expand Down Expand Up @@ -605,59 +446,29 @@ fn perform_compilation(
)?
};

// For non-single-file mode, use compilation_root; for single file mode, use file's parent
let content_dir = if project.mode == ProjectMode::SingleFile {
// For single file mode, use the file's parent directory
project.typ_files[0]
.parent()
.unwrap_or(&project.root)
.to_path_buf()
} else {
compilation_root.clone()
};

// Get full plugin section
let plugin_section = project.config.plugin_section(plugin.name());

// Dispatch to appropriate compilation path
if !spine.merge && plugin.uses_bundle_api() {
compile_bundle(
plugin.as_ref(),
&plugin_output_dir,
project,
output_config,
&spine,
&plugin_section,
resolved_inputs,
&mut results,
&compilation_root,
)?;
} else if spine.merge {
compile_merged(
plugin.as_ref(),
&plugin_output_dir,
project,
output_config,
&spine,
&plugin_section,
resolved_inputs,
&mut results,
&compilation_root,
)?;
// Determine output path based on merge mode
let output = if spine.merge {
plugin_output_dir
.join(&project.name)
.with_extension(plugin.output_extension())
} else {
compile_per_file(
plugin.as_ref(),
&plugin_output_dir,
project,
output_config,
&spine,
&plugin_section,
&resolved_inputs,
&content_dir,
&mut world,
&mut results,
)?;
}
plugin_output_dir.clone()
};

compile_with_bundle(
plugin.as_ref(),
&output,
project,
output_config,
&spine,
&plugin_section,
resolved_inputs,
&mut results,
&compilation_root,
)?;
}

let names: Vec<&str> = plugins.iter().map(|p| p.name()).collect();
Expand Down Expand Up @@ -846,7 +657,7 @@ fn run_watch(sub: &ArgMatches) -> Result<()> {
)?;

// Initial compilation (best-effort; watch continues on failure)
if let Err(e) = perform_compilation(&ctx.project, &ctx.output_config, &ctx.plugins, None) {
if let Err(e) = perform_compilation(&ctx.project, &ctx.output_config, &ctx.plugins) {
warn!(error = %e, "initial compilation failed");
}

Expand All @@ -873,8 +684,7 @@ fn run_watch(sub: &ArgMatches) -> Result<()> {
match event {
WatchEvent::FilesChanged => {
info!("files changed, recompiling");
if perform_compilation(&ctx.project, &ctx.output_config, &ctx.plugins, None).is_ok()
{
if perform_compilation(&ctx.project, &ctx.output_config, &ctx.plugins).is_ok() {
for handle in &open_handles {
if let OpenHandle::Server(server) = handle {
server.reload();
Expand All @@ -892,7 +702,7 @@ fn run_watch(sub: &ArgMatches) -> Result<()> {
) {
Ok(new_ctx) => {
ctx = new_ctx;
if perform_compilation(&ctx.project, &ctx.output_config, &ctx.plugins, None)
if perform_compilation(&ctx.project, &ctx.output_config, &ctx.plugins)
.is_ok()
{
for handle in &open_handles {
Expand Down Expand Up @@ -920,7 +730,7 @@ fn run_compile(sub: &ArgMatches) -> Result<()> {

let ctx = setup_compilation_context(&path, config.as_deref(), build_dir, enabled)?;

perform_compilation(&ctx.project, &ctx.output_config, &ctx.plugins, None)
perform_compilation(&ctx.project, &ctx.output_config, &ctx.plugins)
}

fn run_clean(sub: &ArgMatches) -> Result<()> {
Expand Down
1 change: 1 addition & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ typst-library = { workspace = true }
typst-kit = { workspace = true }
typst-syntax = { workspace = true }
typst-layout = { workspace = true }
typst-bundle = { workspace = true }
comemo = { workspace = true }

# Error handling and diagnostics
Expand Down
42 changes: 42 additions & 0 deletions crates/core/src/bundle_compile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use crate::Result;
use crate::RheoError;
use crate::RheoWorld;
use crate::diagnostics::print_diagnostics;
use typst::diag::Warned;

/// Compile and export a Typst bundle to file bytes.
///
/// This helper function consolidates the common bundle compilation and export
/// logic used by both HTML and PDF plugins. It compiles the bundle using the
/// world, prints diagnostics, and returns the exported file bytes.
///
/// # Arguments
/// * `world` - The RheoWorld for compilation context
///
/// # Returns
/// A vector of (filename, bytes) pairs representing the exported bundle files
///
/// # Errors
/// Returns `RheoError::project_config` if compilation or export fails
pub fn export_typst_bundle(world: &RheoWorld) -> Result<Vec<(String, Vec<u8>)>> {
let Warned { output, warnings } = typst::compile::<typst_bundle::Bundle>(world);
let _ = print_diagnostics(world, &[], &warnings);
let bundle = output.map_err(|errors| {
let _ = print_diagnostics(world, &errors[..], &[]);
let msgs: Vec<String> = errors.iter().map(|e| e.message.to_string()).collect();
RheoError::project_config(format!(
"bundle compilation had errors: {}",
msgs.join(", ")
))
})?;
let bundle_options = typst_bundle::BundleOptions {
pixel_per_pt: 144.0,
pdf: typst_pdf::PdfOptions::default(),
};
let fs = typst_bundle::export(&bundle, &bundle_options)
.map_err(|e| RheoError::project_config(format!("bundle export failed: {:?}", e)))?;
Ok(fs
.into_iter()
.map(|(p, b)| (p.get_without_slash().to_string(), b.to_vec()))
.collect())
}
4 changes: 4 additions & 0 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod bundle_compile;
pub mod compile;
pub mod config;
pub mod constants;
Expand Down Expand Up @@ -71,6 +72,9 @@ pub use reticulate::spine::generate_bundle_entry;
// PDF utilities
pub use pdf_utils::DocumentTitle;

// Bundle compilation helper
pub use bundle_compile::export_typst_bundle;

// Typst types (commonly used by plugins)
pub use typst_types::{
EcoString, HeadingElem, HtmlDocument, NativeElement, OutlineNode, StyleChain, eco_format,
Expand Down
Loading
Loading