From 3f9430e176dc915b9ea56bb7cdc8da1a0e0a808b Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Mon, 30 Mar 2026 14:57:58 +0200 Subject: [PATCH 1/7] =?UTF-8?q?Deletes=20unified=5Fcompile.rs=20=E2=80=94?= =?UTF-8?q?=20removes=20pure=20indirection=20layer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/core/src/lib.rs | 8 ---- crates/core/src/unified_compile.rs | 67 ------------------------------ 2 files changed, 75 deletions(-) delete mode 100644 crates/core/src/unified_compile.rs diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 1157590..4852f60 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -17,7 +17,6 @@ pub mod project; pub mod results; pub mod reticulate; pub mod typst_types; -pub mod unified_compile; pub mod validation; pub mod watch; pub mod world; @@ -56,13 +55,6 @@ pub use html_compile::{ // PDF compilation functions pub use pdf_compile::{compile_pdf_to_document, compile_pdf_with_world, document_to_pdf_bytes}; -// Unified compilation API (consistent naming pattern) -pub use unified_compile::{ - HtmlDocument as HtmlDoc, HtmlString, PagedDocument as PdfDoc, PdfBytes, - compile_to_html_document, compile_to_html_document_with_world, compile_to_html_string, - compile_to_pdf_bytes, compile_to_pdf_document, compile_to_pdf_document_with_world, -}; - // World (Typst compilation context) pub use world::RheoWorld; diff --git a/crates/core/src/unified_compile.rs b/crates/core/src/unified_compile.rs deleted file mode 100644 index 0ef59ef..0000000 --- a/crates/core/src/unified_compile.rs +++ /dev/null @@ -1,67 +0,0 @@ -/// Unified compilation interface for rheo plugins. -/// -/// This module provides consistently-named compilation functions at the -/// rheo_core top level, replacing the scattered html_compile and pdf_compile -/// submodules. -use crate::Result; -use std::path::Path; - -// Re-export output types for convenience -pub use typst_html::HtmlDocument; -pub use typst_layout::PagedDocument; - -// Output type aliases for clarity -pub type HtmlString = String; -pub type PdfBytes = Vec; - -// ============================================================================ -// HTML compilation functions -// ============================================================================ - -/// Compile a Typst file to an HTML document. -/// -/// Creates a new RheoWorld for the given input and compiles to HtmlDocument. -pub fn compile_to_html_document( - path: &Path, - root: &Path, - format_name: &str, - plugin_library: Option, -) -> Result { - crate::html_compile::compile_html_to_document(path, root, format_name, plugin_library) -} - -/// Compile using an existing RheoWorld to an HTML document. -pub fn compile_to_html_document_with_world(world: &crate::RheoWorld) -> Result { - crate::html_compile::compile_html_with_world(world) -} - -/// Export an HtmlDocument to an HTML string. -pub fn compile_to_html_string(document: &HtmlDocument) -> Result { - crate::html_compile::compile_document_to_string(document) -} - -// ============================================================================ -// PDF compilation functions -// ============================================================================ - -/// Compile a Typst file to a PDF document. -/// -/// Creates a new RheoWorld for the given input and compiles to PagedDocument. -pub fn compile_to_pdf_document( - path: &Path, - root: &Path, - format_name: Option<&str>, - plugin_library: Option, -) -> Result { - crate::pdf_compile::compile_pdf_to_document(path, root, format_name, plugin_library) -} - -/// Compile using an existing RheoWorld to a PDF document. -pub fn compile_to_pdf_document_with_world(world: &crate::RheoWorld) -> Result { - crate::pdf_compile::compile_pdf_with_world(world) -} - -/// Export a PagedDocument to PDF bytes. -pub fn compile_to_pdf_bytes(document: &PagedDocument) -> Result { - crate::pdf_compile::document_to_pdf_bytes(document) -} From a489a29f440c4d41d34e44eb7587d774c069b215 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Mon, 30 Mar 2026 15:00:36 +0200 Subject: [PATCH 2/7] Converts export_typst_bundle and compile_*_with_world to RheoWorld methods --- crates/core/src/bundle_compile.rs | 42 -------------------------- crates/core/src/html_compile.rs | 17 ----------- crates/core/src/lib.rs | 7 +---- crates/core/src/pdf_compile.rs | 24 --------------- crates/core/src/world.rs | 50 ++++++++++++++++++++++++++++++- crates/html/src/lib.rs | 4 +-- crates/pdf/src/lib.rs | 6 ++-- 7 files changed, 55 insertions(+), 95 deletions(-) delete mode 100644 crates/core/src/bundle_compile.rs diff --git a/crates/core/src/bundle_compile.rs b/crates/core/src/bundle_compile.rs deleted file mode 100644 index 9cad9ca..0000000 --- a/crates/core/src/bundle_compile.rs +++ /dev/null @@ -1,42 +0,0 @@ -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)>> { - let Warned { output, warnings } = typst::compile::(world); - let _ = print_diagnostics(world, &[], &warnings); - let bundle = output.map_err(|errors| { - let _ = print_diagnostics(world, &errors[..], &[]); - let msgs: Vec = 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()) -} diff --git a/crates/core/src/html_compile.rs b/crates/core/src/html_compile.rs index a60e631..f2a4b34 100644 --- a/crates/core/src/html_compile.rs +++ b/crates/core/src/html_compile.rs @@ -35,23 +35,6 @@ pub fn compile_html_to_document_with_polyfill( unwrap_compilation_result(Some(&world), result, Some(html_filter)) } -/// Compile using an existing RheoWorld to an HTML document. -/// -/// This function uses a pre-configured RheoWorld (with main file already set) -/// and compiles it to an HtmlDocument. Useful for per-file compilation where -/// the world is shared across multiple files. -pub fn compile_html_with_world(world: &RheoWorld) -> Result { - info!("compiling to HTML"); - let result = typst::compile::(world); - - let html_filter = |w: &SourceDiagnostic| { - !w.message - .contains("html export is under active development and incomplete") - }; - - unwrap_compilation_result(Some(world), result, Some(html_filter)) -} - pub fn compile_document_to_string(document: &HtmlDocument) -> Result { typst_html::html(document).map_err(|e| handle_export_errors(e, ExportErrorType::Html)) } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 4852f60..ded8c9f 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,4 +1,3 @@ -pub mod bundle_compile; pub mod compile; pub mod config; pub mod constants; @@ -49,11 +48,10 @@ pub use reticulate::{SpineDocument, TracedSpine}; // HTML compilation functions pub use html_compile::{ compile_document_to_string, compile_html_to_document, compile_html_to_document_with_polyfill, - compile_html_with_world, }; // PDF compilation functions -pub use pdf_compile::{compile_pdf_to_document, compile_pdf_with_world, document_to_pdf_bytes}; +pub use pdf_compile::{compile_pdf_to_document, document_to_pdf_bytes}; // World (Typst compilation context) pub use world::RheoWorld; @@ -64,9 +62,6 @@ 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, diff --git a/crates/core/src/pdf_compile.rs b/crates/core/src/pdf_compile.rs index 7b8edec..84d87f5 100644 --- a/crates/core/src/pdf_compile.rs +++ b/crates/core/src/pdf_compile.rs @@ -42,30 +42,6 @@ pub fn compile_pdf_to_document( unwrap_compilation_result(Some(&world), result, None:: bool>) } -/// Compile using an existing RheoWorld to a PDF document. -/// -/// This function uses a pre-configured RheoWorld (with main file already set) -/// and compiles it to a PagedDocument. Useful for per-file compilation where -/// the world is shared across multiple files. -/// -/// # Arguments -/// * `world` - A configured RheoWorld with the main file set -/// -/// # Returns -/// A PagedDocument ready for PDF export -/// -/// # Example -/// ```ignore -/// let document = compile_pdf_with_world(&world)?; -/// let pdf_bytes = document_to_pdf_bytes(&document)?; -/// std::fs::write("output.pdf", &pdf_bytes)?; -/// ``` -pub fn compile_pdf_with_world(world: &RheoWorld) -> Result { - info!("compiling to PDF"); - let result = typst::compile::(world); - unwrap_compilation_result(Some(world), result, None:: bool>) -} - /// Export a PagedDocument to PDF bytes. /// /// Converts a compiled PagedDocument into its PDF representation as bytes. diff --git a/crates/core/src/world.rs b/crates/core/src/world.rs index 6c483bc..40cb2b7 100644 --- a/crates/core/src/world.rs +++ b/crates/core/src/world.rs @@ -7,7 +7,9 @@ use chrono::{Datelike, Local}; use codespan_reporting::files::{Error as CodespanError, Files}; use parking_lot::Mutex; use tracing::warn; -use typst::diag::{FileError, FileResult}; +use typst::diag::{FileError, FileResult, Warned}; +use typst_html::HtmlDocument; +use typst_layout::PagedDocument; use typst::foundations::{Bytes, Datetime}; use typst::syntax::{FileId, Lines, RootedPath, Source, VirtualPath, VirtualRoot}; use typst::text::{Font, FontBook}; @@ -224,6 +226,52 @@ impl RheoWorld { pub fn root(&self) -> &Path { &self.root } + + /// Compile and export a Typst bundle to file bytes. + pub fn export_bundle(&self) -> crate::Result)>> { + use crate::diagnostics::print_diagnostics; + let Warned { output, warnings } = typst::compile::(self); + let _ = print_diagnostics(self, &[], &warnings); + let bundle = output.map_err(|errors| { + let _ = print_diagnostics(self, &errors[..], &[]); + let msgs: Vec = errors.iter().map(|e| e.message.to_string()).collect(); + crate::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| crate::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()) + } + + /// Compile to an HTML document using this world. + pub fn compile_html(&self) -> crate::Result { + use crate::diagnostics::unwrap_compilation_result; + use typst::diag::SourceDiagnostic; + tracing::info!("compiling to HTML"); + let result = typst::compile::(self); + let html_filter = |w: &SourceDiagnostic| { + !w.message + .contains("html export is under active development and incomplete") + }; + unwrap_compilation_result(Some(self), result, Some(html_filter)) + } + + /// Compile to a PDF document using this world. + pub fn compile_pdf(&self) -> crate::Result { + use crate::diagnostics::unwrap_compilation_result; + tracing::info!("compiling to PDF"); + let result = typst::compile::(self); + unwrap_compilation_result(Some(self), result, None:: bool>) + } } impl World for RheoWorld { diff --git a/crates/html/src/lib.rs b/crates/html/src/lib.rs index b4a2189..fb4aacc 100644 --- a/crates/html/src/lib.rs +++ b/crates/html/src/lib.rs @@ -8,7 +8,7 @@ pub const DEFAULT_STYLESHEET: &str = include_str!("templates/style.css"); use rheo_core::{ FormatPlugin, OpenHandle, PluginContext, PluginSection, Result, RheoCompileOptions, RheoError, - ServerHandle, export_typst_bundle, + ServerHandle, }; use std::path::Path; use tracing::{debug, info, warn}; @@ -120,7 +120,7 @@ fn compile_html_bundle(options: RheoCompileOptions, config: &PluginSection) -> R info!("compiling HTML bundle"); // Compile and export the bundle using the core helper - let fs = export_typst_bundle(options.world)?; + let fs = options.world.export_bundle()?; debug!(file_count = fs.len(), "exported HTML bundle"); diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 295ae9d..546e3d7 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -1,4 +1,4 @@ -use rheo_core::{FormatPlugin, PluginContext, Result, RheoError, RheoWorld, export_typst_bundle}; +use rheo_core::{FormatPlugin, PluginContext, Result, RheoError, RheoWorld}; use std::path::Path; use tracing::{debug, info}; @@ -47,7 +47,7 @@ fn compile_pdf_merged_bundle(world: &RheoWorld, output_path: &Path) -> Result<() info!("compiling merged PDF bundle"); // Compile and export the bundle using the core helper - let fs = export_typst_bundle(world)?; + let fs = world.export_bundle()?; debug!(file_count = fs.len(), "exported PDF bundle"); @@ -73,7 +73,7 @@ fn compile_pdf_per_file_bundle(world: &RheoWorld, output_dir: &Path) -> Result<( info!("compiling per-file PDF bundle"); // Compile and export the bundle using the core helper - let fs = export_typst_bundle(world)?; + let fs = world.export_bundle()?; debug!(file_count = fs.len(), "exported PDF bundle"); From ed527ca6d50d2ad2c01dbaace38b6988fe4fd351 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Mon, 30 Mar 2026 15:02:08 +0200 Subject: [PATCH 3/7] Merges html_compile.rs and pdf_compile.rs into compile.rs --- crates/core/src/compile.rs | 133 +++++++++++++------------------- crates/core/src/html_compile.rs | 40 ---------- crates/core/src/lib.rs | 10 +-- crates/core/src/pdf_compile.rs | 66 ---------------- 4 files changed, 55 insertions(+), 194 deletions(-) delete mode 100644 crates/core/src/html_compile.rs delete mode 100644 crates/core/src/pdf_compile.rs diff --git a/crates/core/src/compile.rs b/crates/core/src/compile.rs index 41b7334..46489df 100644 --- a/crates/core/src/compile.rs +++ b/crates/core/src/compile.rs @@ -1,5 +1,11 @@ +use crate::diagnostics::{ExportErrorType, handle_export_errors, unwrap_compilation_result}; use crate::world::RheoWorld; -use std::path::PathBuf; +use crate::Result; +use std::path::{Path, PathBuf}; +use tracing::info; +use typst::diag::SourceDiagnostic; +use typst_html::HtmlDocument; +use typst_layout::PagedDocument; /// Common compilation options used across all output formats. /// @@ -29,11 +35,6 @@ pub struct RheoCompileOptions<'a> { impl<'a> RheoCompileOptions<'a> { /// Create compilation options. - /// - /// # Arguments - /// * `output` - The output file path - /// * `root` - Root directory for resolving imports - /// * `world` - The RheoWorld with bundle entry pre-configured pub fn new( output: impl Into, root: impl Into, @@ -47,83 +48,53 @@ impl<'a> RheoCompileOptions<'a> { } } -#[cfg(test)] -mod tests { - use crate::pdf_utils; - - #[test] - fn test_filename_to_title() { - assert_eq!( - pdf_utils::DocumentTitle::to_readable_name("severance-ep-1"), - "Severance Ep 1" - ); - assert_eq!( - pdf_utils::DocumentTitle::to_readable_name("my_document"), - "My Document" - ); - assert_eq!( - pdf_utils::DocumentTitle::to_readable_name("chapter-01"), - "Chapter 01" - ); - assert_eq!( - pdf_utils::DocumentTitle::to_readable_name("hello_world"), - "Hello World" - ); - assert_eq!( - pdf_utils::DocumentTitle::to_readable_name("single"), - "Single" - ); - } - - #[test] - fn test_extract_document_title_from_metadata() { - let source = r#"#set document(title: [My Great Title]) - -= Chapter 1 -Content here."#; - - let title = pdf_utils::DocumentTitle::from_source(source, "fallback").extract(); - assert_eq!(title, "My Great Title"); - } - - #[test] - fn test_extract_document_title_fallback() { - let source = r#"= Chapter 1 -Content here."#; - - let title = pdf_utils::DocumentTitle::from_source(source, "my-chapter").extract(); - assert_eq!(title, "My Chapter"); - } - - #[test] - fn test_extract_document_title_with_markup() { - let source = r#"#set document(title: [Good news about hell - #emph[Severance]])"#; - - let title = pdf_utils::DocumentTitle::from_source(source, "fallback").extract(); - // Should strip #emph and underscores - // Note: complex nested bracket handling is limited by regex - assert!(title.contains("Good news")); - assert!(title.contains("Severance")); - } - - #[test] - fn test_extract_document_title_empty() { - let source = r#"#set document(title: []) +pub fn compile_html_to_document( + input: &Path, + root: &Path, + _format_name: &str, + plugin_library: Option, +) -> Result { + compile_html_to_document_with_polyfill(input, root, plugin_library, false) +} -Content"#; +/// Compile to HTML document with optional EPUB polyfill mode. +pub fn compile_html_to_document_with_polyfill( + input: &Path, + root: &Path, + plugin_library: Option, + epub_polyfill_mode: bool, +) -> Result { + let mut world = RheoWorld::new(root, input, plugin_library)?; + world.epub_polyfill_mode = epub_polyfill_mode; + info!(input = %input.display(), "compiling to HTML"); + let result = typst::compile::(&world); + + let html_filter = |w: &SourceDiagnostic| { + !w.message + .contains("html export is under active development and incomplete") + }; + + unwrap_compilation_result(Some(&world), result, Some(html_filter)) +} - let title = pdf_utils::DocumentTitle::from_source(source, "default-name").extract(); - // Empty title should fall back to filename - assert_eq!(title, "Default Name"); - } +pub fn compile_document_to_string(document: &HtmlDocument) -> Result { + typst_html::html(document).map_err(|e| handle_export_errors(e, ExportErrorType::Html)) +} - #[test] - fn test_extract_document_title_complex() { - let source = r#"#set document(title: [Half Loop - _Severance_ [s1/e2]], author: [Test])"#; +pub fn compile_pdf_to_document( + input: &Path, + root: &Path, + _format_name: Option<&str>, + plugin_library: Option, +) -> Result { + let world = RheoWorld::new(root, input, plugin_library)?; + info!(input = %input.display(), "compiling to PDF"); + let result = typst::compile::(&world); + unwrap_compilation_result(Some(&world), result, None:: bool>) +} - let title = pdf_utils::DocumentTitle::from_source(source, "fallback").extract(); - // Should extract title and strip markup - assert!(title.contains("Half Loop")); - assert!(title.contains("Severance")); - } +pub fn document_to_pdf_bytes(document: &PagedDocument) -> Result> { + use typst_pdf::PdfOptions; + typst_pdf::pdf(document, &PdfOptions::default()) + .map_err(|e| handle_export_errors(e, ExportErrorType::Pdf)) } diff --git a/crates/core/src/html_compile.rs b/crates/core/src/html_compile.rs deleted file mode 100644 index f2a4b34..0000000 --- a/crates/core/src/html_compile.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::Result; -use crate::diagnostics::{ExportErrorType, handle_export_errors, unwrap_compilation_result}; -use crate::world::RheoWorld; -use std::path::Path; -use tracing::info; -use typst::diag::SourceDiagnostic; -use typst_html::HtmlDocument; - -pub fn compile_html_to_document( - input: &Path, - root: &Path, - _format_name: &str, - plugin_library: Option, -) -> Result { - compile_html_to_document_with_polyfill(input, root, plugin_library, false) -} - -/// Compile to HTML document with optional EPUB polyfill mode. -pub fn compile_html_to_document_with_polyfill( - input: &Path, - root: &Path, - plugin_library: Option, - epub_polyfill_mode: bool, -) -> Result { - let mut world = RheoWorld::new(root, input, plugin_library)?; - world.epub_polyfill_mode = epub_polyfill_mode; - info!(input = %input.display(), "compiling to HTML"); - let result = typst::compile::(&world); - - let html_filter = |w: &SourceDiagnostic| { - !w.message - .contains("html export is under active development and incomplete") - }; - - unwrap_compilation_result(Some(&world), result, Some(html_filter)) -} - -pub fn compile_document_to_string(document: &HtmlDocument) -> Result { - typst_html::html(document).map_err(|e| handle_export_errors(e, ExportErrorType::Html)) -} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index ded8c9f..7a9077e 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -3,13 +3,11 @@ pub mod config; pub mod constants; pub mod diagnostics; pub mod error; -pub mod html_compile; pub mod init_templates; pub mod logging; pub mod manifest_version; pub mod output; pub mod path_utils; -pub mod pdf_compile; pub mod pdf_utils; pub mod plugins; pub mod project; @@ -45,14 +43,12 @@ pub use plugins::{FormatPlugin, OpenHandle, PluginContext, PluginInput, ServerHa // Re-export TracedSpine for use in bundle compilation pub use reticulate::{SpineDocument, TracedSpine}; -// HTML compilation functions -pub use html_compile::{ +// HTML and PDF compilation functions +pub use compile::{ compile_document_to_string, compile_html_to_document, compile_html_to_document_with_polyfill, + compile_pdf_to_document, document_to_pdf_bytes, }; -// PDF compilation functions -pub use pdf_compile::{compile_pdf_to_document, document_to_pdf_bytes}; - // World (Typst compilation context) pub use world::RheoWorld; diff --git a/crates/core/src/pdf_compile.rs b/crates/core/src/pdf_compile.rs deleted file mode 100644 index 84d87f5..0000000 --- a/crates/core/src/pdf_compile.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::Result; -/// PDF compilation wrappers for rheo plugins. -/// -/// These functions encapsulate Typst PDF compilation, allowing plugin crates -/// to compile PDFs without directly importing typst crates. -use crate::diagnostics::{ExportErrorType, handle_export_errors, unwrap_compilation_result}; -use crate::world::RheoWorld; -use std::path::Path; -use tracing::info; -use typst_layout::PagedDocument; - -/// Compile a Typst source file to a PDF document. -/// -/// This function creates a new RheoWorld for the given input and compiles -/// it to a PagedDocument. The result can be exported to PDF bytes using -/// `document_to_pdf_bytes`. -/// -/// # Arguments -/// * `input` - Path to the .typ file to compile -/// * `root` - Project root directory for resolving imports -/// * `format_name` - Output format name for link transformations (e.g., "pdf", None) -/// * `plugin_library` - Optional plugin-contributed Typst library code to inject -/// -/// # Returns -/// A PagedDocument ready for PDF export -/// -/// # Example -/// ```ignore -/// let document = compile_pdf_to_document(&input_path, &project_root, Some("pdf"), None)?; -/// let pdf_bytes = document_to_pdf_bytes(&document)?; -/// std::fs::write("output.pdf", &pdf_bytes)?; -/// ``` -pub fn compile_pdf_to_document( - input: &Path, - root: &Path, - _format_name: Option<&str>, - plugin_library: Option, -) -> Result { - let world = RheoWorld::new(root, input, plugin_library)?; - info!(input = %input.display(), "compiling to PDF"); - let result = typst::compile::(&world); - unwrap_compilation_result(Some(&world), result, None:: bool>) -} - -/// Export a PagedDocument to PDF bytes. -/// -/// Converts a compiled PagedDocument into its PDF representation as bytes. -/// The resulting bytes can be written directly to a file. -/// -/// # Arguments -/// * `document` - The compiled PagedDocument to export -/// -/// # Returns -/// PDF file content as a byte vector -/// -/// # Example -/// ```ignore -/// let document = compile_pdf_to_document(&input_path, &root, Some("pdf"))?; -/// let pdf_bytes = document_to_pdf_bytes(&document)?; -/// std::fs::write("output.pdf", &pdf_bytes)?; -/// ``` -pub fn document_to_pdf_bytes(document: &PagedDocument) -> Result> { - use typst_pdf::PdfOptions; - typst_pdf::pdf(document, &PdfOptions::default()) - .map_err(|e| handle_export_errors(e, ExportErrorType::Pdf)) -} From 13cd9f1aa936d33db9964e285d0e86c5673a76d6 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Mon, 30 Mar 2026 15:03:39 +0200 Subject: [PATCH 4/7] Replaces lazy_static! with std::sync::LazyLock in constants.rs --- Cargo.lock | 1 - Cargo.toml | 1 - crates/core/Cargo.toml | 1 - crates/core/src/constants.rs | 27 ++++++++++++--------------- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 473ce10..9620918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2900,7 +2900,6 @@ dependencies = [ "ecow", "glob", "globset", - "lazy_static", "notify", "opener", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index 1e9b646..5dd7d3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,6 @@ itertools = "0.14.0" # Other dependencies ecow = "0.2" regex = "1.10" -lazy_static = "1.4" serde-xml-rs = "0.8.2" # Development override: tracks typst git main. diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index dffcb14..6689f37 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -46,7 +46,6 @@ atty = { workspace = true } # Concurrency and utilities parking_lot = { workspace = true } -lazy_static = { workspace = true } regex = { workspace = true } notify = { workspace = true } ecow = { workspace = true } diff --git a/crates/core/src/constants.rs b/crates/core/src/constants.rs index 7d61baa..558b9fe 100644 --- a/crates/core/src/constants.rs +++ b/crates/core/src/constants.rs @@ -1,6 +1,6 @@ /// File extension constants and shared regex patterns used throughout rheo -use lazy_static::lazy_static; use regex::Regex; +use std::sync::LazyLock; // File extensions pub const TYP_EXT: &str = ".typ"; @@ -11,19 +11,16 @@ pub const XHTML_EXT: &str = ".xhtml"; pub const EPUB_EXT: &str = ".epub"; // Regex patterns -lazy_static! { - /// Pattern for Typst #link() syntax: #link("url")(body) or #link("url", body) - pub static ref TYPST_LINK_PATTERN: Regex = - Regex::new(r#"#link\("([^"]+)"\)(\[[^\]]+\]|,\s*[^)]+)"#) - .expect("invalid TYPST_LINK_PATTERN"); - /// Pattern for HTML href attributes: href="url" - pub static ref HTML_HREF_PATTERN: Regex = - Regex::new(r#"href="([^"]*)""#) - .expect("invalid HTML_HREF_PATTERN"); +/// Pattern for Typst #link() syntax: #link("url")(body) or #link("url", body) +pub static TYPST_LINK_PATTERN: LazyLock = + LazyLock::new(|| Regex::new(r#"#link\("([^"]+)"\)(\[[^\]]+\]|,\s*[^)]+)"#) + .expect("invalid TYPST_LINK_PATTERN")); - /// Pattern for Typst label references: #label[text] - pub static ref TYPST_LABEL_PATTERN: Regex = - Regex::new(r"#\w+\[([^\]]+)\]") - .expect("invalid TYPST_LABEL_PATTERN"); -} +/// Pattern for HTML href attributes: href="url" +pub static HTML_HREF_PATTERN: LazyLock = + LazyLock::new(|| Regex::new(r#"href="([^"]*)""#).expect("invalid HTML_HREF_PATTERN")); + +/// Pattern for Typst label references: #label[text] +pub static TYPST_LABEL_PATTERN: LazyLock = + LazyLock::new(|| Regex::new(r"#\w+\[([^\]]+)\]").expect("invalid TYPST_LABEL_PATTERN")); From 57e6ea7cc0685ed8742ac7ffe5f07264083d6d19 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Mon, 30 Mar 2026 15:03:59 +0200 Subject: [PATCH 5/7] Deletes orphaned reticulate sub-files (types, parser, transformer, serializer, validator) --- crates/core/src/reticulate/parser.rs | 216 ------------------ crates/core/src/reticulate/serializer.rs | 163 -------------- crates/core/src/reticulate/transformer.rs | 257 ---------------------- crates/core/src/reticulate/types.rs | 34 --- crates/core/src/reticulate/validator.rs | 160 -------------- 5 files changed, 830 deletions(-) delete mode 100644 crates/core/src/reticulate/parser.rs delete mode 100644 crates/core/src/reticulate/serializer.rs delete mode 100644 crates/core/src/reticulate/transformer.rs delete mode 100644 crates/core/src/reticulate/types.rs delete mode 100644 crates/core/src/reticulate/validator.rs diff --git a/crates/core/src/reticulate/parser.rs b/crates/core/src/reticulate/parser.rs deleted file mode 100644 index 50fef3a..0000000 --- a/crates/core/src/reticulate/parser.rs +++ /dev/null @@ -1,216 +0,0 @@ -use crate::reticulate::types::LinkInfo; -use typst::syntax::{Source, SyntaxKind, SyntaxNode}; - -/// The identifier in the Typst AST for links. -const LINK_IDENT_ID: &str = "link"; - -/// Extract all links from Typst source by parsing and traversing AST -pub fn extract_links(source: &Source) -> Vec { - let root = typst::syntax::parse(source.text()); - let mut links = Vec::new(); - extract_links_from_node(&root, &root, &mut links); - links -} - -fn extract_links_from_node(node: &SyntaxNode, root: &SyntaxNode, links: &mut Vec) { - // Check if this node itself is a function call - if node.kind() == SyntaxKind::FuncCall - && let Some(link_info) = parse_link_call(node, root) - { - links.push(link_info); - } - - // Recursively traverse children - for child in node.children() { - extract_links_from_node(child, root, links); - } -} - -fn parse_link_call(node: &SyntaxNode, root: &SyntaxNode) -> Option { - // Parse #link("url")[body] or #link("url", body) - // Extract: - // 1. Function name (must be "link") - // 2. URL argument (first string argument) - // 3. Body text (from content block or second argument) - // 4. Byte range by calculating AST node position - - let ident = node.children().find(|n| n.kind() == SyntaxKind::Ident)?; - if ident.text() != LINK_IDENT_ID { - return None; - } - - let args = node.children().find(|n| n.kind() == SyntaxKind::Args)?; - - // Extract URL (first string argument) - let url = extract_first_string_arg(args)?; - - // Extract body text - let body = extract_link_body(node)?; - - // Calculate byte range directly from AST node position - let offset = calculate_node_offset(root, node)?; - let byte_range = offset..(offset + node.len()); - - // Get span for error reporting - let span = node.span(); - - Some(LinkInfo { - url, - body, - span, - byte_range, - }) -} - -fn extract_first_string_arg(args: &SyntaxNode) -> Option { - for child in args.children() { - if child.kind() == SyntaxKind::Str { - // Remove quotes - let text = child.text(); - return Some(text.trim_matches('"').to_string()); - } - } - None -} - -fn extract_link_body(func_call: &SyntaxNode) -> Option { - // The ContentBlock is inside the Args node as the second argument - let args = func_call - .children() - .find(|n| n.kind() == SyntaxKind::Args)?; - - // Find ContentBlock inside Args - let content_block = args - .children() - .find(|n| n.kind() == SyntaxKind::ContentBlock)?; - - // Extract text from inside the ContentBlock - // The structure is: ContentBlock -> Markup -> Text - extract_text_from_node(content_block) -} - -fn extract_text_from_node(node: &SyntaxNode) -> Option { - // If this is a Text node, return its content - if node.kind() == SyntaxKind::Text { - return Some(node.text().to_string()); - } - - // If this is a Space node, return a space - if node.kind() == SyntaxKind::Space { - return Some(" ".to_string()); - } - - // Otherwise, collect text from ALL children (not just the first) - let mut texts = Vec::new(); - for child in node.children() { - if let Some(text) = extract_text_from_node(child) { - texts.push(text); - } - } - - if texts.is_empty() { - None - } else { - Some(texts.join("")) - } -} - -/// Calculate the byte offset of a target node within the root AST -fn calculate_node_offset(root: &SyntaxNode, target: &SyntaxNode) -> Option { - calculate_node_offset_impl(root, target, 0) -} - -fn calculate_node_offset_impl( - current: &SyntaxNode, - target: &SyntaxNode, - offset: usize, -) -> Option { - // Check if this is the target node (pointer equality) - if std::ptr::eq(current as *const _, target as *const _) { - return Some(offset); - } - - // Recursively search children, tracking offset - let mut child_offset = offset; - for child in current.children() { - if let Some(found_offset) = calculate_node_offset_impl(child, target, child_offset) { - return Some(found_offset); - } - child_offset += child.len(); - } - - None -} - -#[cfg(test)] -mod tests { - use super::*; - use typst::syntax::Source; - - #[test] - fn test_extract_link_with_content_block() { - let source = Source::detached(r#"#link("./file.typ")[text]"#); - let links = extract_links(&source); - - assert_eq!(links.len(), 1); - assert_eq!(links[0].url, "./file.typ"); - assert_eq!(links[0].body, "text"); - } - - #[test] - fn test_extract_multiple_links() { - let source = Source::detached( - r#" - Some text #link("./file1.typ")[first] and more - #link("./file2.typ")[second] content. - "#, - ); - let links = extract_links(&source); - - assert_eq!(links.len(), 2); - assert_eq!(links[0].url, "./file1.typ"); - assert_eq!(links[0].body, "first"); - assert_eq!(links[1].url, "./file2.typ"); - assert_eq!(links[1].body, "second"); - } - - #[test] - fn test_no_links() { - let source = Source::detached("Just plain text with no links"); - let links = extract_links(&source); - - assert_eq!(links.len(), 0); - } - - #[test] - fn test_external_urls() { - let source = Source::detached(r#"#link("https://example.com")[external]"#); - let links = extract_links(&source); - - assert_eq!(links.len(), 1); - assert_eq!(links[0].url, "https://example.com"); - assert_eq!(links[0].body, "external"); - } - - #[test] - fn test_extract_link_with_nested_markup() { - let source = Source::detached(r#"#link("./url")[text #super[2]]"#); - let links = extract_links(&source); - - assert_eq!(links.len(), 1); - assert_eq!(links[0].url, "./url"); - assert_eq!(links[0].body, "text 2"); // All text concatenated - // Byte range should cover the entire link (exact start may vary by 1 due to Source::detached) - assert!(links[0].byte_range.len() >= 29); - } - - #[test] - fn test_extract_link_with_multiple_markup() { - let source = Source::detached(r#"#link("url")[#strong[bold] and #emph[italic]]"#); - let links = extract_links(&source); - - assert_eq!(links.len(), 1); - assert_eq!(links[0].url, "url"); - assert_eq!(links[0].body, "bold and italic"); - } -} diff --git a/crates/core/src/reticulate/serializer.rs b/crates/core/src/reticulate/serializer.rs deleted file mode 100644 index 4a42aac..0000000 --- a/crates/core/src/reticulate/serializer.rs +++ /dev/null @@ -1,163 +0,0 @@ -use super::types::LinkTransform; -use std::ops::Range; -use typst::syntax::{Source, SyntaxKind, SyntaxNode}; - -/// Apply link transformations to source code -/// -/// Applies transformations by replacing text at specified byte ranges. -/// Transformations that overlap with code blocks are filtered out. -/// Replacements are applied back-to-front to preserve byte offsets. -/// -/// # Arguments -/// * `source` - Original source text -/// * `transformations` - List of (byte_range, transform) tuples -/// * `code_block_ranges` - Byte ranges of code blocks to protect -pub fn apply_transformations( - source: &str, - transformations: &[(Range, LinkTransform)], - code_block_ranges: &[Range], -) -> String { - // Filter out transformations that overlap with code blocks - let mut active_transforms: Vec<_> = transformations - .iter() - .filter(|(range, _)| !overlaps_with_any(range, code_block_ranges)) - .collect(); - - // Sort by byte range (back-to-front for stable offsets) - active_transforms.sort_by_key(|(range, _)| std::cmp::Reverse(range.start)); - - // Build result string by applying transformations - let mut result = source.to_string(); - - for (range, transform) in active_transforms { - // Get the original link text - let original = &source[range.clone()]; - - // Compute replacement text based on transformation - let replacement = match transform { - LinkTransform::Remove { body } => { - // Just the body text in brackets: [body] - format!("[{}]", body) - } - LinkTransform::ReplaceUrl { new_url } => { - // Replace URL but keep the rest of the syntax - // Original: #link("old.typ")[body] - // New: #link("new.typ")[body] - replace_url_in_link(original, new_url, false) - } - LinkTransform::ReplaceUrlWithLabel { new_label } => { - // Replace URL but keep the rest of the syntax - // Original: #link("old.typ")[body] - // New: #link(