From 6d387f92065890001de5dd608f02e31413a25979 Mon Sep 17 00:00:00 2001 From: messense <messense@icloud.com> Date: Thu, 6 Mar 2025 21:45:19 +0800 Subject: [PATCH] Allow passing typst template file as bytes --- python/typst/__init__.pyi | 44 +++++++++--------- src/compiler.rs | 14 +++--- src/lib.rs | 20 ++++++--- src/query.rs | 4 -- src/world.rs | 95 ++++++++++++++++++--------------------- 5 files changed, 86 insertions(+), 91 deletions(-) diff --git a/python/typst/__init__.pyi b/python/typst/__init__.pyi index 8e23fd9..4c8af5a 100644 --- a/python/typst/__init__.pyi +++ b/python/typst/__init__.pyi @@ -1,22 +1,22 @@ import pathlib from typing import List, Optional, TypeVar, overload, Dict, Union, Literal -PathLike = TypeVar("PathLike", str, pathlib.Path) +Input = TypeVar("Input", str, pathlib.Path, bytes) OutputFormat = Literal["pdf", "svg", "png", "html"] class Compiler: def __init__( self, - input: PathLike, - root: Optional[PathLike] = None, - font_paths: List[PathLike] = [], + input: Input, + root: Optional[Input] = None, + font_paths: List[Input] = [], ignore_system_fonts: bool = False, sys_inputs: Dict[str, str] = {}, pdf_standards: Optional[Union[Literal["1.7", "a-2b", "a-3b"], List[Literal["1.7", "a-2b", "a-3b"]]]] = [] ) -> None: """Initialize a Typst compiler. Args: - input (PathLike): Project's main .typ file. + input: .typ file bytes or path to project's main .typ file. root (Optional[PathLike], optional): Root path for the Typst project. font_paths (List[PathLike]): Folders with fonts. ignore_system_fonts (bool): Ignore system fonts. @@ -25,7 +25,7 @@ class Compiler: def compile( self, - output: Optional[PathLike] = None, + output: Optional[Input] = None, format: Optional[OutputFormat] = None, ppi: Optional[float] = None, ) -> Optional[Union[bytes, List[bytes]]]: @@ -59,10 +59,10 @@ class Compiler: @overload def compile( - input: PathLike, - output: PathLike, - root: Optional[PathLike] = None, - font_paths: List[PathLike] = [], + input: Input, + output: Input, + root: Optional[Input] = None, + font_paths: List[Input] = [], ignore_system_fonts: bool = False, format: Optional[OutputFormat] = None, ppi: Optional[float] = None, @@ -71,10 +71,10 @@ def compile( ) -> None: ... @overload def compile( - input: PathLike, + input: Input, output: None = None, - root: Optional[PathLike] = None, - font_paths: List[PathLike] = [], + root: Optional[Input] = None, + font_paths: List[Input] = [], ignore_system_fonts: bool = False, format: Optional[OutputFormat] = None, ppi: Optional[float] = None, @@ -82,10 +82,10 @@ def compile( pdf_standards: Optional[Union[Literal["1.7", "a-2b", "a-3b"], List[Literal["1.7", "a-2b", "a-3b"]]]] = [] ) -> bytes: ... def compile( - input: PathLike, - output: Optional[PathLike] = None, - root: Optional[PathLike] = None, - font_paths: List[PathLike] = [], + input: Input, + output: Optional[Input] = None, + root: Optional[Input] = None, + font_paths: List[Input] = [], ignore_system_fonts: bool = False, format: Optional[OutputFormat] = None, ppi: Optional[float] = None, @@ -94,7 +94,7 @@ def compile( ) -> Optional[Union[bytes, List[bytes]]]: """Compile a Typst project. Args: - input (PathLike): Project's main .typ file. + input: .typ file bytes or path to project's main .typ file. output (Optional[PathLike], optional): Path to save the compiled file. Allowed extensions are `.pdf`, `.svg` and `.png` root (Optional[PathLike], optional): Root path for the Typst project. @@ -109,19 +109,19 @@ def compile( """ def query( - input: PathLike, + input: Input, selector: str, field: Optional[str] = None, one: bool = False, format: Optional[Literal["json", "yaml"]] = None, - root: Optional[PathLike] = None, - font_paths: List[PathLike] = [], + root: Optional[Input] = None, + font_paths: List[Input] = [], ignore_system_fonts: bool = False, sys_inputs: Dict[str, str] = {}, ) -> str: """Query a Typst document. Args: - input (PathLike): Project's main .typ file. + input: .typ file bytes or path to project's main .typ file. selector (str): Typst selector like `<label>`. field (Optional[str], optional): Field to query. one (bool, optional): Query only one element. diff --git a/src/compiler.rs b/src/compiler.rs index a8ee35c..fd360d2 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -7,7 +7,7 @@ use typst::foundations::Datetime; use typst::html::HtmlDocument; use typst::layout::PagedDocument; use typst::syntax::{FileId, Source, Span}; -use typst::{World, WorldExt}; +use typst::WorldExt; use crate::world::SystemWorld; @@ -21,10 +21,6 @@ impl SystemWorld { ppi: Option<f32>, pdf_standards: &[typst_pdf::PdfStandard], ) -> StrResult<Vec<Vec<u8>>> { - // Reset everything and ensure that the main file is present. - self.reset(); - self.source(self.main()).map_err(|err| err.to_string())?; - let Warned { output, warnings } = typst::compile(self); match output { @@ -40,7 +36,10 @@ impl SystemWorld { "png" => Ok(export_image(&document, ImageExportFormat::Png, ppi)?), "svg" => Ok(export_image(&document, ImageExportFormat::Svg, ppi)?), "html" => { - let Warned { output, warnings } = typst::compile::<HtmlDocument>(self); + let Warned { + output, + warnings: _, + } = typst::compile::<HtmlDocument>(self); Ok(vec![export_html(&output.unwrap(), self)?]) } fmt => Err(eco_format!("unknown format: {fmt}")), @@ -70,11 +69,10 @@ fn export_pdf( world: &SystemWorld, standards: typst_pdf::PdfStandards, ) -> StrResult<Vec<u8>> { - let ident = world.input().to_string_lossy(); let buffer = typst_pdf::pdf( document, &typst_pdf::PdfOptions { - ident: typst::foundations::Smart::Custom(&ident), + ident: typst::foundations::Smart::Auto, timestamp: now().map(typst_pdf::Timestamp::new_utc), standards, ..Default::default() diff --git a/src/lib.rs b/src/lib.rs index 1004d9c..ad5aabc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,12 @@ mod output_template { } } +#[derive(FromPyObject)] +pub enum Input { + Path(PathBuf), + Bytes(Vec<u8>), +} + /// A typst compiler #[pyclass(module = "typst._typst")] pub struct Compiler { @@ -104,17 +110,19 @@ impl Compiler { sys_inputs = HashMap::new() ))] fn new( - input: PathBuf, + input: Input, root: Option<PathBuf>, font_paths: Vec<PathBuf>, ignore_system_fonts: bool, sys_inputs: HashMap<String, String>, ) -> PyResult<Self> { - let input = input.canonicalize()?; let root = if let Some(root) = root { root.canonicalize()? - } else if let Some(dir) = input.parent() { - dir.into() + } else if let Input::Path(path) = &input { + path.canonicalize()? + .parent() + .map(Into::into) + .unwrap_or_else(|| PathBuf::new()) } else { PathBuf::new() }; @@ -227,7 +235,7 @@ impl Compiler { #[allow(clippy::too_many_arguments)] fn compile( py: Python<'_>, - input: PathBuf, + input: Input, output: Option<PathBuf>, root: Option<PathBuf>, font_paths: Vec<PathBuf>, @@ -260,7 +268,7 @@ fn compile( #[allow(clippy::too_many_arguments)] fn py_query( py: Python<'_>, - input: PathBuf, + input: Input, selector: &str, field: Option<&str>, one: bool, diff --git a/src/query.rs b/src/query.rs index 23852b2..fff5444 100644 --- a/src/query.rs +++ b/src/query.rs @@ -35,10 +35,6 @@ pub enum SerializationFormat { /// Execute a query command. pub fn query(world: &mut SystemWorld, command: &QueryCommand) -> StrResult<String> { - // Reset everything and ensure that the main file is present. - world.reset(); - world.source(world.main()).map_err(|err| err.to_string())?; - let Warned { output, warnings } = typst::compile(world); match output { diff --git a/src/world.rs b/src/world.rs index d755939..dd0b5ec 100644 --- a/src/world.rs +++ b/src/world.rs @@ -3,7 +3,6 @@ use std::path::{Path, PathBuf}; use std::sync::{Mutex, OnceLock}; use chrono::{DateTime, Datelike, Local}; -use ecow::eco_format; use typst::diag::{FileError, FileResult, StrResult}; use typst::foundations::{Bytes, Datetime, Dict}; use typst::syntax::{FileId, Source, VirtualPath}; @@ -17,14 +16,12 @@ use typst_kit::{ use std::collections::HashMap; -use crate::download::SlientDownload; +use crate::{download::SlientDownload, Input}; /// A world that provides access to the operating system. pub struct SystemWorld { /// The working directory. workdir: Option<PathBuf>, - /// The canonical path to the input file. - input: PathBuf, /// The root relative to which absolute paths are resolved. root: PathBuf, /// The input path. @@ -86,8 +83,8 @@ impl World for SystemWorld { } impl SystemWorld { - pub fn builder(root: PathBuf, main: PathBuf) -> SystemWorldBuilder { - SystemWorldBuilder::new(root, main) + pub fn builder(root: PathBuf, input: Input) -> SystemWorldBuilder { + SystemWorldBuilder::new(root, input) } /// Access the canonical slot for the given file id. @@ -99,11 +96,6 @@ impl SystemWorld { f(map.entry(id).or_insert_with(|| FileSlot::new(id))) } - /// The id of the main source file. - pub fn main(&self) -> FileId { - self.main - } - /// The root relative to which absolute paths are resolved. pub fn root(&self) -> &Path { &self.root @@ -114,19 +106,6 @@ impl SystemWorld { self.workdir.as_deref().unwrap_or(Path::new(".")) } - /// Reset the compilation state in preparation of a new compilation. - pub fn reset(&mut self) { - for slot in self.slots.lock().unwrap().values_mut() { - slot.reset(); - } - self.now.take(); - } - - /// Return the canonical path to the input file. - pub fn input(&self) -> &PathBuf { - &self.input - } - /// Lookup a source file by id. #[track_caller] pub fn lookup(&self, id: FileId) -> Source { @@ -137,17 +116,17 @@ impl SystemWorld { pub struct SystemWorldBuilder { root: PathBuf, - main: PathBuf, + input: Input, font_paths: Vec<PathBuf>, ignore_system_fonts: bool, inputs: Dict, } impl SystemWorldBuilder { - pub fn new(root: PathBuf, main: PathBuf) -> Self { + pub fn new(root: PathBuf, input: Input) -> Self { Self { root, - main, + input, font_paths: Vec::new(), ignore_system_fonts: false, inputs: Dict::default(), @@ -174,18 +153,36 @@ impl SystemWorldBuilder { .include_system_fonts(!self.ignore_system_fonts) .search_with(&self.font_paths); - let input = self.main.canonicalize().map_err(|_| { - eco_format!("input file not found (searched at {})", self.main.display()) - })?; - // Resolve the virtual path of the main file within the project root. - let main_path = VirtualPath::within_root(&self.main, &self.root) - .ok_or("input file must be contained in project root")?; + let mut slots = HashMap::new(); + let main = match self.input { + Input::Path(path) => { + // Resolve the virtual path of the main file within the project root. + let path = path + .canonicalize() + .map_err(|err| format!("Failed to canonicalize path: {}", err))?; + FileId::new( + None, + VirtualPath::within_root(&path, &self.root) + .ok_or("input file must be contained in project root")?, + ) + } + Input::Bytes(bytes) => { + // Fake file ID + let file_id = FileId::new_fake(VirtualPath::new("<bytes>")); + let mut file_slot = FileSlot::new(file_id); + file_slot + .source + .init(Source::new(file_id, decode_utf8(&bytes)?.to_string())); + file_slot.file.init(Bytes::new(bytes)); + slots.insert(file_id, file_slot); + file_id + } + }; let world = SystemWorld { workdir: std::env::current_dir().ok(), - input, root: self.root, - main: FileId::new(None, main_path), + main, library: LazyHash::new( LibraryBuilder::default() .with_features(vec![typst::Feature::Html].into_iter().collect()) @@ -194,7 +191,7 @@ impl SystemWorldBuilder { ), book: LazyHash::new(fonts.book), fonts: fonts.fonts, - slots: Mutex::default(), + slots: Mutex::new(slots), package_storage: PackageStorage::new(None, None, crate::download::downloader()), now: OnceLock::new(), }; @@ -224,17 +221,14 @@ impl FileSlot { } } - /// Marks the file as not yet accessed in preparation of the next - /// compilation. - fn reset(&mut self) { - self.source.reset(); - self.file.reset(); - } - - fn source(&mut self, root: &Path, package_storage: &PackageStorage) -> FileResult<Source> { + fn source( + &mut self, + project_root: &Path, + package_storage: &PackageStorage, + ) -> FileResult<Source> { let id = self.id; self.source.get_or_init( - || system_path(root, id, package_storage), + || system_path(project_root, id, package_storage), |data, prev| { let text = decode_utf8(&data)?; if let Some(mut prev) = prev { @@ -247,10 +241,10 @@ impl FileSlot { ) } - fn file(&mut self, root: &Path, package_storage: &PackageStorage) -> FileResult<Bytes> { + fn file(&mut self, project_root: &Path, package_storage: &PackageStorage) -> FileResult<Bytes> { let id = self.id; self.file.get_or_init( - || system_path(root, id, package_storage), + || system_path(project_root, id, package_storage), |data, _| Ok(Bytes::new(data)), ) } @@ -292,10 +286,9 @@ impl<T: Clone> SlotCell<T> { } } - /// Marks the cell as not yet accessed in preparation of the next - /// compilation. - fn reset(&mut self) { - self.accessed = false; + fn init(&mut self, data: T) { + self.data = Some(Ok(data)); + self.accessed = true; } /// Gets the contents of the cell or initialize them.