From 95a8a5f814dc321b1173d70bd97e0044f0f847e8 Mon Sep 17 00:00:00 2001 From: agryphus Date: Fri, 15 May 2026 22:42:48 -0400 Subject: [PATCH] add: render_direct fn in lib --- src/lib.rs | 185 ++++++++++++++++++++++++++++---------------------- src/main.rs | 2 +- src/plugin.rs | 21 +++--- 3 files changed, 112 insertions(+), 96 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 34bc866..0396bcc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,11 +6,11 @@ use std::path::PathBuf; pub use plugin::{concat_plugin_sources, embedded_prepend_source, list_embedded_plugin_ids}; +use log::info; use typst::ecow::EcoString; use typst::syntax::Source; -use typst_as_lib::{typst_kit_options::TypstKitFontOptions, TypstAsLibError, TypstEngine}; +use typst_as_lib::{TypstAsLibError, TypstEngine, typst_kit_options::TypstKitFontOptions}; use typst_html::{HtmlAttr, HtmlDocument, HtmlElement, HtmlNode}; -use log::info; fn format_typst_compile_error( err: TypstAsLibError, @@ -31,13 +31,8 @@ fn format_typst_compile_error( if byte >= index_byte_start && byte < index_end { let rel = byte - index_byte_start; if let Some((line, col)) = index_only.lines().byte_to_line_column(rel) { - let _ = writeln!( - &mut out, - " index.typ:{}:{}: {}", - line + 1, - col + 1, - msg - ); + let _ = + writeln!(&mut out, " index.typ:{}:{}: {}", line + 1, col + 1, msg); } else { let _ = writeln!(&mut out, " {msg}"); } @@ -66,64 +61,30 @@ fn format_typst_compile_error( std::io::Error::new(std::io::ErrorKind::Other, report) } - -pub fn compile_article( - article_dir: &PathBuf, - prepend: &Option, - plugins: &[impl AsRef], +/// Internal compilation kernel. `index_byte_start` is the byte offset within +/// `full_source` where the user's source begins (plugins/prepend precede it). +fn compile_source( + full_source: &str, + index_byte_start: usize, + base_dir: &PathBuf, include_title: bool, -) -> Result<(), Box> { - info!("compiling {} ...", article_dir.display()); - - let template_file = article_dir.join("index.typ"); - let output = article_dir.join("index.html"); - let outline_file = article_dir.join("outline.html"); - - let plugin_block = concat_plugin_sources(plugins)?; - - let user_prepend = if let Some(prepend_file) = prepend { - fs::read_to_string(&prepend_file).map_err(|e| { - format!( - "could not read prepend file {}: {e}", - prepend_file.display() - ) - })? - } else { - fs::read_to_string(article_dir.join("prepend.typ")).unwrap_or_default() - }; - - let index_source = fs::read_to_string(&template_file).map_err(|e| { - format!( - "could not read template {}: {e}", - template_file.display() - ) - })?; - - let mut template: EcoString = EcoString::new(); - template.push_str(&plugin_block); - if !plugin_block.is_empty() && !user_prepend.is_empty() { - template.push('\n'); - } - template.push_str(&user_prepend); - let index_byte_start = template.len(); - template.push_str(&index_source); - - let full_source_str = template.to_string(); +) -> Result<(String, String), Box> { + let index_source = &full_source[index_byte_start..]; let engine = TypstEngine::builder() - .main_file(full_source_str.clone()) + .main_file(full_source.to_string()) .search_fonts_with( TypstKitFontOptions::default() .include_system_fonts(false) .include_embedded_fonts(true), ) - .with_file_system_resolver(article_dir) + .with_file_system_resolver(base_dir) .build(); let mut doc: HtmlDocument = engine .compile() .output - .map_err(|e| format_typst_compile_error(e, &full_source_str, index_byte_start, &index_source))?; + .map_err(|e| format_typst_compile_error(e, full_source, index_byte_start, index_source))?; let mut outline = EcoString::new(); let mut curr_level = 1u32; @@ -139,19 +100,12 @@ pub fn compile_article( &mut title_h2_pending, &mut first_outline_heading, ); - // `ul_depth` counts open `
    ` tags; it can diverge from `curr_level - 1` when the first - // outline heading uses lazy depth (skip title). Always drain by `ul_depth`, not `curr_level`. while ul_depth > 0 { ul_depth -= 1; outline.push_str(" ".repeat(ul_depth as usize).as_str()); outline.push_str("
\n"); } - fs::write(&outline_file, outline.as_bytes()).map_err(|e| { - format!( - "could not write outline {}: {e}", - outline_file.display() - ) - })?; + let outline_str = outline.to_string(); let mut body: Option = None; for child in &doc.root.children { @@ -165,16 +119,11 @@ pub fn compile_article( let body = body.ok_or("compiled HTML has no element")?; doc.root = body; - let mut html: String = typst_html::html(&doc).map_err(|e| format!("html generation failed: {e:?}"))?; + let mut html: String = + typst_html::html(&doc).map_err(|e| format!("html generation failed: {e:?}"))?; let lines = html .lines() - .map(|line| { - if line.len() >= 2 { - &line[2..] - } else { - "" - } - }) + .map(|line| if line.len() >= 2 { &line[2..] } else { "" }) .collect::>(); if lines.len() > 2 { @@ -183,12 +132,88 @@ pub fn compile_article( html = String::new(); } - fs::write(&output, html).map_err(|e| { - format!( - "could not write output {}: {e}", - output.display() - ) - })?; + Ok((html, outline_str)) +} + +/// Render Typst source directly from a string. Plugins are prepended +/// automatically. `base_dir` is used by the Typst engine for file-system +/// resolution (images, includes, etc.). Returns `(index_html, outline_html)`. +pub fn render_direct( + source: &str, + plugins: &[impl AsRef], + include_title: bool, + base_dir: &PathBuf, +) -> Result<(String, String), Box> { + let plugin_block = concat_plugin_sources(plugins)?; + + let mut full_source = EcoString::new(); + full_source.push_str(&plugin_block); + if !plugin_block.is_empty() { + full_source.push('\n'); + } + let index_byte_start = full_source.len(); + full_source.push_str(source); + + compile_source( + full_source.as_str(), + index_byte_start, + base_dir, + include_title, + ) +} + +/// Render Typst source read from `article_dir/index.typ`. Supports an +/// optional `prepend` file and writes the output to `index.html` and +/// `outline.html` in the same directory. +pub fn compile_article( + article_dir: &PathBuf, + prepend: &Option, + plugins: &[impl AsRef], + include_title: bool, +) -> Result<(), Box> { + info!("compiling {} ...", article_dir.display()); + + let output = article_dir.join("index.html"); + let outline_file = article_dir.join("outline.html"); + + let plugin_block = concat_plugin_sources(plugins)?; + + let user_prepend = if let Some(prepend_file) = prepend { + fs::read_to_string(prepend_file).map_err(|e| { + format!( + "could not read prepend file {}: {e}", + prepend_file.display() + ) + })? + } else { + fs::read_to_string(article_dir.join("prepend.typ")).unwrap_or_default() + }; + + let template_file = article_dir.join("index.typ"); + let index_source = fs::read_to_string(&template_file) + .map_err(|e| format!("could not read template {}: {e}", template_file.display()))?; + + let mut full_source: EcoString = EcoString::new(); + full_source.push_str(&plugin_block); + if !plugin_block.is_empty() && !user_prepend.is_empty() { + full_source.push('\n'); + } + full_source.push_str(&user_prepend); + let index_byte_start = full_source.len(); + full_source.push_str(&index_source); + + let (index_html, outline_html) = compile_source( + full_source.as_str(), + index_byte_start, + article_dir, + include_title, + )?; + + fs::write(&output, index_html) + .map_err(|e| format!("could not write output {}: {e}", output.display()))?; + + fs::write(&outline_file, outline_html.as_bytes()) + .map_err(|e| format!("could not write outline {}: {e}", outline_file.display()))?; Ok(()) } @@ -283,13 +308,7 @@ fn parse_outline( *curr_level = level; outline.push_str(" ".repeat(*ul_depth as usize).as_str()); - outline.push_str( - format!( - "
  • {}
  • \n", - slug, header_text - ) - .as_str(), - ); + outline.push_str(format!("
  • {}
  • \n", slug, header_text).as_str()); elem.attrs.push(HtmlAttr::intern("id").unwrap(), slug); return; diff --git a/src/main.rs b/src/main.rs index ba7c1a5..72e2250 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,8 @@ use std::env; use std::path::PathBuf; use clap::Parser; +use log::{error, info}; use typssg::{compile_all, compile_article}; -use log::{info, error}; #[derive(Parser)] struct Args { diff --git a/src/plugin.rs b/src/plugin.rs index 3f17a7d..8c88ae5 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::sync::OnceLock; -use include_dir::{include_dir, Dir}; +use include_dir::{Dir, include_dir}; static PREPENDS_DIR: Dir<'static> = include_dir!("prepends"); @@ -40,17 +40,14 @@ pub fn embedded_prepend_source(id: &str) -> Result { if id.is_empty() { return Err("empty plugin id".into()); } - prepends_table() - .get(&id) - .cloned() - .ok_or_else(|| { - let known = list_embedded_plugin_ids().join(", "); - if known.is_empty() { - format!("unknown plugin '{id}' (no embedded prepends in this build)") - } else { - format!("unknown plugin '{id}' (embedded: {known})") - } - }) + prepends_table().get(&id).cloned().ok_or_else(|| { + let known = list_embedded_plugin_ids().join(", "); + if known.is_empty() { + format!("unknown plugin '{id}' (no embedded prepends in this build)") + } else { + format!("unknown plugin '{id}' (embedded: {known})") + } + }) } pub fn concat_plugin_sources(plugin_ids: &[impl AsRef]) -> Result {