use chrono::{DateTime, Datelike, FixedOffset, Local, Utc};
use parking_lot::Mutex;
use rustler::{Atom, Binary, Decoder, Encoder, Env, NewBinary, NifStruct, ResourceArc, Term};
use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::panic::{RefUnwindSafe, UnwindSafe};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use std::sync::OnceLock;
use std::{fs, mem};
use typst::diag::{FileError, FileResult, Severity, SourceDiagnostic};
use typst::ecow::EcoVec;
use typst::foundations::{Bytes, Datetime, Dict, Duration, Smart, Str, Value};
use typst::layout::PageRanges;
use typst::syntax::{DiagSpan, FileId, RootedPath, Source, VirtualPath, VirtualRoot};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::{Feature, Features, Library, LibraryExt, World, WorldExt};
use typst_bundle::{export as export_bundle, Bundle, BundleOptions};
use typst_html::{HtmlDocument, HtmlOptions};
use typst_kit::downloader::SystemDownloader;
use typst_kit::files::FsRoot;
use typst_kit::fonts::{embedded, scan, system, FontStore};
use typst_kit::packages::SystemPackages;
use typst_layout::PagedDocument;
use typst_pdf::{PdfOptions, PdfStandard, PdfStandards};
use typst_svg::SvgOptions;
use typst_timing::{timed, TimingScope};
static MARKUP_ID: LazyLock<FileId> = LazyLock::new(|| {
FileId::unique(RootedPath::new(
VirtualRoot::Project,
VirtualPath::new("MARKUP.typ").unwrap(),
))
});
/// Stack size for Typst compilation threads (64 MB).
///
/// `typst::compile()` recurses deeply for nested content. On BEAM dirty-
/// scheduler threads the default stack is ~2 MB (Linux) / ~8 MB (macOS),
/// which overflows at ~27 / ~31 nesting levels. A stack overflow triggers
/// SIGSEGV — not a Rust panic — and kills the entire BEAM VM.
///
/// Spawning compilation on a dedicated thread with a large stack pushes the
/// overflow threshold to ~800+ levels, far beyond any realistic document.
const COMPILE_STACK_SIZE: usize = 64 * 1024 * 1024;
/// Compiler features enabled for every compilation: HTML export and the
/// multi-file `bundle` export target.
fn enabled_features() -> Features {
Features::from_iter([Feature::Html, Feature::Bundle])
}
rustler::atoms! {
ok,
pdf_1_7,
pdf_a_2b,
pdf_a_3b,
error,
warning
}
#[derive(NifStruct)]
#[module = "AshTypst.Context.Options"]
pub struct ContextOptionsNif {
pub root: String,
pub font_paths: Vec<String>,
pub ignore_system_fonts: bool,
}
#[derive(NifStruct)]
#[module = "AshTypst.PDFOptions"]
pub struct PdfOptionsNif {
pub pages: Option<String>,
pub pdf_standards: Vec<PdfStandardNif>,
pub document_id: Option<String>,
}
#[derive(NifStruct)]
#[module = "AshTypst.BundleOptions"]
pub struct BundleOptionsNif {
pub pretty: bool,
pub render_bleed: bool,
}
#[derive(NifStruct)]
#[module = "AshTypst.BundleResult"]
pub struct BundleResultNif<'a> {
pub files: HashMap<String, Binary<'a>>,
pub warnings: Vec<DiagnosticNif>,
}
#[derive(NifStruct)]
#[module = "AshTypst.FontOptions"]
pub struct FontOptionsNif {
pub font_paths: Vec<String>,
pub ignore_system_fonts: bool,
}
#[derive(NifStruct)]
#[module = "AshTypst.CompileResult"]
pub struct CompileResultNif {
pub page_count: usize,
pub warnings: Vec<DiagnosticNif>,
}
#[derive(NifStruct)]
#[module = "AshTypst.CompileError"]
pub struct CompileErrorNif {
pub diagnostics: Vec<DiagnosticNif>,
}
#[derive(NifStruct)]
#[module = "AshTypst.Diagnostic"]
pub struct DiagnosticNif {
pub severity: SeverityNif,
pub message: String,
pub span: Option<SpanNif>,
pub trace: Vec<TraceItemNif>,
pub hints: Vec<String>,
}
#[derive(NifStruct)]
#[module = "AshTypst.Span"]
pub struct SpanNif {
pub start: usize,
pub end: usize,
pub line: Option<usize>,
pub column: Option<usize>,
}
#[derive(NifStruct)]
#[module = "AshTypst.TraceItem"]
pub struct TraceItemNif {
pub span: Option<SpanNif>,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeverityNif {
Error,
Warning,
}
impl Decoder<'_> for SeverityNif {
fn decode(term: Term) -> Result<Self, rustler::Error> {
let atom: Atom = term.decode()?;
if atom == error() {
Ok(SeverityNif::Error)
} else if atom == warning() {
Ok(SeverityNif::Warning)
} else {
Err(rustler::Error::BadArg)
}
}
}
impl Encoder for SeverityNif {
fn encode<'a>(&self, env: Env<'a>) -> Term<'a> {
match self {
SeverityNif::Error => error().encode(env),
SeverityNif::Warning => warning().encode(env),
}
}
}
impl From<Severity> for SeverityNif {
fn from(severity: Severity) -> Self {
match severity {
Severity::Error => SeverityNif::Error,
Severity::Warning => SeverityNif::Warning,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PdfStandardNif {
Pdf17,
PdfA2b,
PdfA3b,
}
impl Decoder<'_> for PdfStandardNif {
fn decode(term: Term) -> Result<Self, rustler::Error> {
let atom: Atom = term.decode()?;
if atom == pdf_1_7() {
Ok(PdfStandardNif::Pdf17)
} else if atom == pdf_a_2b() {
Ok(PdfStandardNif::PdfA2b)
} else if atom == pdf_a_3b() {
Ok(PdfStandardNif::PdfA3b)
} else {
Err(rustler::Error::BadArg)
}
}
}
impl Encoder for PdfStandardNif {
fn encode<'a>(&self, env: Env<'a>) -> Term<'a> {
match self {
PdfStandardNif::Pdf17 => pdf_1_7().encode(env),
PdfStandardNif::PdfA2b => pdf_a_2b().encode(env),
PdfStandardNif::PdfA3b => pdf_a_3b().encode(env),
}
}
}
impl From<PdfStandardNif> for PdfStandard {
fn from(standard: PdfStandardNif) -> Self {
match standard {
PdfStandardNif::Pdf17 => PdfStandard::V_1_7,
PdfStandardNif::PdfA2b => PdfStandard::A_2b,
PdfStandardNif::PdfA3b => PdfStandard::A_3b,
}
}
}
impl PdfOptionsNif {
fn to_pdf_options(&self) -> Result<PdfOptions, String> {
let mut opts = PdfOptions::default();
if let Some(ref document_id) = self.document_id {
opts.ident = Smart::Custom(document_id.clone());
}
if !self.pdf_standards.is_empty() {
let standards: Vec<PdfStandard> =
self.pdf_standards.iter().map(|&s| s.into()).collect();
opts.standards = PdfStandards::new(&standards)
.map_err(|e| format!("Invalid PDF standards: {}", e.message()))?;
}
Ok(opts)
}
}
pub struct SystemWorld {
root: PathBuf,
main: FileId,
markup: String,
library: LazyHash<Library>,
fonts: FontStore,
slots: Mutex<HashMap<FileId, FileSlot>>,
packages: SystemPackages,
now: Now,
virtual_files: HashMap<String, Vec<u8>>,
inputs: HashMap<String, String>,
}
impl SystemWorld {
pub fn new(root: PathBuf, font_paths: Vec<PathBuf>, ignore_system_fonts: bool) -> Self {
let filtered_paths: Vec<PathBuf> = font_paths
.into_iter()
.filter(|p| p.exists() && p.is_dir())
.collect();
let include_system_fonts = !ignore_system_fonts;
let mut fonts = FontStore::new();
for path in &filtered_paths {
fonts.extend(scan(path));
}
if include_system_fonts {
fonts.extend(system());
}
fonts.extend(embedded());
let user_agent = concat!("typst/", env!("CARGO_PKG_VERSION"));
Self {
root,
main: *MARKUP_ID,
markup: String::new(),
library: LazyHash::new(Library::builder().with_features(enabled_features()).build()),
fonts,
slots: Mutex::new(HashMap::new()),
packages: SystemPackages::new(SystemDownloader::new(user_agent)),
now: Now::System(OnceLock::new()),
virtual_files: HashMap::new(),
inputs: HashMap::new(),
}
}
pub fn reset(&mut self) {
for slot in self.slots.get_mut().values_mut() {
slot.reset();
}
if let Now::System(time_lock) = &mut self.now {
time_lock.take();
}
}
fn rebuild_library(&mut self) {
let mut dict = Dict::new();
for (key, value) in &self.inputs {
dict.insert(
Str::from(key.as_str()),
Value::Str(Str::from(value.as_str())),
);
}
self.library = LazyHash::new(
Library::builder()
.with_inputs(dict)
.with_features(enabled_features())
.build(),
);
}
}
impl World for SystemWorld {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
self.fonts.book()
}
fn main(&self) -> FileId {
self.main
}
fn source(&self, id: FileId) -> FileResult<Source> {
if id == *MARKUP_ID {
return Ok(Source::new(id, self.markup.clone()));
}
if let Some(content) = self.virtual_files.get(id.vpath().get_without_slash()) {
let text = decode_utf8(content)?;
return Ok(Source::new(id, text.into()));
}
self.slot(id, |slot| slot.source(&self.root, &self.packages))
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
if let Some(content) = self.virtual_files.get(id.vpath().get_without_slash()) {
return Ok(Bytes::new(content.clone()));
}
self.slot(id, |slot| slot.file(&self.root, &self.packages))
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts.font(index)
}
fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
let now = match &self.now {
Now::Fixed(time) => time,
Now::System(time) => time.get_or_init(Utc::now),
};
let with_offset = match offset {
None => now.with_timezone(&Local).fixed_offset(),
Some(duration) => {
let seconds = i32::try_from(duration.seconds() as i64).ok()?;
now.with_timezone(&FixedOffset::east_opt(seconds)?)
}
};
Datetime::from_ymd(
with_offset.year(),
with_offset.month().try_into().ok()?,
with_offset.day().try_into().ok()?,
)
}
}
impl SystemWorld {
fn slot<F, T>(&self, id: FileId, f: F) -> T
where
F: FnOnce(&mut FileSlot) -> T,
{
let mut map = self.slots.lock();
f(map.entry(id).or_insert_with(|| FileSlot::new(id)))
}
}
struct FileSlot {
id: FileId,
source: SlotCell<Source>,
file: SlotCell<Bytes>,
}
impl FileSlot {
fn new(id: FileId) -> Self {
Self {
id,
file: SlotCell::new(),
source: SlotCell::new(),
}
}
fn reset(&mut self) {
self.source.reset();
self.file.reset();
}
fn source(&mut self, project_root: &Path, packages: &SystemPackages) -> FileResult<Source> {
self.source.get_or_init(
|| read(self.id, project_root, packages),
|data, prev| {
let name = if prev.is_some() {
"reparsing file"
} else {
"parsing file"
};
let _scope = TimingScope::new(name);
let text = decode_utf8(&data)?;
if let Some(mut prev) = prev {
prev.replace(text);
Ok(prev)
} else {
Ok(Source::new(self.id, text.into()))
}
},
)
}
fn file(&mut self, project_root: &Path, packages: &SystemPackages) -> FileResult<Bytes> {
self.file.get_or_init(
|| read(self.id, project_root, packages),
|data, _| Ok(Bytes::new(data)),
)
}
}
struct SlotCell<T> {
data: Option<FileResult<T>>,
fingerprint: u128,
accessed: bool,
}
impl<T: Clone> SlotCell<T> {
fn new() -> Self {
Self {
data: None,
fingerprint: 0,
accessed: false,
}
}
fn reset(&mut self) {
self.accessed = false;
}
fn get_or_init(
&mut self,
load: impl FnOnce() -> FileResult<Vec<u8>>,
f: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
) -> FileResult<T> {
if mem::replace(&mut self.accessed, true) {
if let Some(data) = &self.data {
return data.clone();
}
}
let result = timed!("loading file", load());
let fingerprint = timed!("hashing file", typst::utils::hash128(&result));
if mem::replace(&mut self.fingerprint, fingerprint) == fingerprint {
if let Some(data) = &self.data {
return data.clone();
}
}
let prev = self.data.take().and_then(Result::ok);
let value = result.and_then(|data| f(data, prev));
self.data = Some(value.clone());
value
}
}
fn system_path(project_root: &Path, id: FileId, packages: &SystemPackages) -> FileResult<PathBuf> {
let root = match id.root() {
VirtualRoot::Package(spec) => packages.obtain(spec)?,
VirtualRoot::Project => FsRoot::new(project_root.to_path_buf()),
};
root.resolve(id.vpath())
}
fn read(id: FileId, project_root: &Path, packages: &SystemPackages) -> FileResult<Vec<u8>> {
read_from_disk(&system_path(project_root, id, packages)?)
}
fn read_from_disk(path: &Path) -> FileResult<Vec<u8>> {
let f = |e| FileError::from_io(e, path);
if fs::metadata(path).map_err(f)?.is_dir() {
Err(FileError::IsDirectory)
} else {
fs::read(path).map_err(f)
}
}
fn decode_utf8(buf: &[u8]) -> FileResult<&str> {
Ok(std::str::from_utf8(
buf.strip_prefix(b"\xef\xbb\xbf").unwrap_or(buf),
)?)
}
enum Now {
#[allow(dead_code)]
Fixed(DateTime<Utc>),
System(OnceLock<DateTime<Utc>>),
}
pub struct TypstContext {
world: Mutex<SystemWorld>,
document: Mutex<Option<PagedDocument>>,
}
impl UnwindSafe for TypstContext {}
impl RefUnwindSafe for TypstContext {}
#[rustler::resource_impl]
impl rustler::Resource for TypstContext {}
fn span_to_nif(span: impl Into<DiagSpan>, world: &SystemWorld) -> Option<SpanNif> {
let span = span.into();
let range = world.range(span)?;
let (line, column) = span
.id()
.and_then(|id| world.source(id).ok())
.and_then(|source| source.lines().byte_to_line_column(range.start))
.map(|(line, column)| (Some(line + 1), Some(column + 1)))
.unwrap_or((None, None));
Some(SpanNif {
start: range.start,
end: range.end,
line,
column,
})
}
fn diagnostic_to_nif(d: &SourceDiagnostic, world: &SystemWorld) -> DiagnosticNif {
DiagnosticNif {
severity: d.severity.into(),
message: d.message.to_string(),
span: span_to_nif(d.span, world),
trace: d
.trace
.iter()
.map(|item| TraceItemNif {
span: span_to_nif(item.span, world),
message: item.v.to_string(),
})
.collect(),
hints: d.hints.iter().map(|h| h.v.to_string()).collect(),
}
}
fn diagnostics_to_vec(
diagnostics: EcoVec<SourceDiagnostic>,
world: &SystemWorld,
) -> Vec<DiagnosticNif> {
diagnostics
.iter()
.map(|d| diagnostic_to_nif(d, world))
.collect()
}
fn simple_error(message: &str) -> CompileErrorNif {
CompileErrorNif {
diagnostics: vec![DiagnosticNif {
severity: SeverityNif::Error,
message: message.to_string(),
span: None,
trace: vec![],
hints: vec![],
}],
}
}
/// Parse "1-3,5,7-9" into PageRanges (1-indexed inclusive ranges using NonZeroUsize).
fn parse_page_ranges(pages: &str, total: usize) -> Result<PageRanges, String> {
use std::ops::RangeInclusive;
let mut ranges: Vec<RangeInclusive<Option<NonZeroUsize>>> = Vec::new();
for part in pages.split(',') {
let part = part.trim();
if part.contains('-') {
let mut iter = part.splitn(2, '-');
let start: usize = iter
.next()
.unwrap()
.trim()
.parse()
.map_err(|_| format!("Invalid page number in range: {}", part))?;
let end: usize = iter
.next()
.unwrap()
.trim()
.parse()
.map_err(|_| format!("Invalid page number in range: {}", part))?;
if start < 1 || end < 1 || start > total || end > total || start > end {
return Err(format!("Page range out of bounds: {}", part));
}
ranges.push(NonZeroUsize::new(start)..=NonZeroUsize::new(end));
} else {
let page: usize = part
.parse()
.map_err(|_| format!("Invalid page number: {}", part))?;
if page < 1 || page > total {
return Err(format!("Page number out of bounds: {}", page));
}
let nz = NonZeroUsize::new(page);
ranges.push(nz..=nz);
}
}
Ok(PageRanges::new(ranges))
}
#[rustler::nif(schedule = "DirtyIo")]
fn context_new(opts: ContextOptionsNif) -> ResourceArc<TypstContext> {
let root = PathBuf::from(&opts.root);
let font_paths: Vec<PathBuf> = opts.font_paths.iter().map(PathBuf::from).collect();
let world = SystemWorld::new(root, font_paths, opts.ignore_system_fonts);
ResourceArc::new(TypstContext {
world: Mutex::new(world),
document: Mutex::new(None),
})
}
#[rustler::nif]
fn context_set_markup(ctx: ResourceArc<TypstContext>, markup: String) -> Atom {
let mut world = ctx.world.lock();
world.markup = markup;
world.reset();
*ctx.document.lock() = None;
ok()
}
#[rustler::nif(schedule = "DirtyCpu")]
fn context_compile(ctx: ResourceArc<TypstContext>) -> Result<CompileResultNif, CompileErrorNif> {
std::thread::Builder::new()
.name("typst-compile".into())
.stack_size(COMPILE_STACK_SIZE)
.spawn(move || {
let mut world_guard = ctx.world.lock();
world_guard.reset();
let result = typst::compile::<PagedDocument>(&*world_guard);
match result.output {
Ok(document) => {
let page_count = document.pages().len();
let warnings = diagnostics_to_vec(result.warnings, &world_guard);
*ctx.document.lock() = Some(document);
Ok(CompileResultNif {
page_count,
warnings,
})
}
Err(errors) => {
let diagnostics = diagnostics_to_vec(errors, &world_guard);
*ctx.document.lock() = None;
Err(CompileErrorNif { diagnostics })
}
}
})
.map_err(|e| simple_error(&format!("Failed to spawn compile thread: {e}")))?
.join()
.unwrap_or_else(|_| Err(simple_error("Compilation panicked")))
}
#[rustler::nif(schedule = "DirtyCpu")]
fn context_render_svg(
ctx: ResourceArc<TypstContext>,
page: usize,
pretty: bool,
render_bleed: bool,
) -> Result<String, CompileErrorNif> {
let doc_guard = ctx.document.lock();
let document = doc_guard
.as_ref()
.ok_or_else(|| simple_error("No compiled document. Call compile() first."))?;
if page >= document.pages().len() {
return Err(simple_error(&format!(
"Page index {} out of bounds (document has {} pages)",
page,
document.pages().len()
)));
}
let opts = SvgOptions {
pretty,
render_bleed,
};
Ok(typst_svg::svg(&document.pages()[page], &opts))
}
#[rustler::nif(schedule = "DirtyCpu")]
fn context_export_pdf<'a>(
env: Env<'a>,
ctx: ResourceArc<TypstContext>,
opts: PdfOptionsNif,
) -> Result<Binary<'a>, CompileErrorNif> {
let doc_guard = ctx.document.lock();
let document = doc_guard
.as_ref()
.ok_or_else(|| simple_error("No compiled document. Call compile() first."))?;
let mut pdf_opts = opts.to_pdf_options().map_err(|e| simple_error(&e))?;
if let Some(ref pages_str) = opts.pages {
pdf_opts.page_ranges = Some(
parse_page_ranges(pages_str, document.pages().len()).map_err(|e| simple_error(&e))?,
);
}
let result = typst_pdf::pdf(document, &pdf_opts);
drop(doc_guard);
let pdf_bytes = result.map_err(|e| {
let world = ctx.world.lock();
CompileErrorNif {
diagnostics: diagnostics_to_vec(e, &world),
}
})?;
let mut binary = NewBinary::new(env, pdf_bytes.len());
binary.as_mut_slice().copy_from_slice(&pdf_bytes);
Ok(binary.into())
}
#[rustler::nif]
fn context_font_families(ctx: ResourceArc<TypstContext>) -> Vec<String> {
let world = ctx.world.lock();
world
.fonts
.book()
.families()
.map(|(name, _)| name.to_string())
.collect()
}
#[rustler::nif]
fn context_set_virtual_file(ctx: ResourceArc<TypstContext>, path: String, content: String) -> Atom {
let mut world = ctx.world.lock();
world.virtual_files.insert(path, content.into_bytes());
*ctx.document.lock() = None;
ok()
}
#[rustler::nif]
fn context_set_virtual_file_binary<'a>(
ctx: ResourceArc<TypstContext>,
path: String,
content: Binary<'a>,
) -> Atom {
let mut world = ctx.world.lock();
world
.virtual_files
.insert(path, content.as_slice().to_vec());
*ctx.document.lock() = None;
ok()
}
#[rustler::nif]
fn context_append_virtual_file(
ctx: ResourceArc<TypstContext>,
path: String,
chunk: String,
) -> Atom {
let mut world = ctx.world.lock();
world
.virtual_files
.entry(path)
.or_default()
.extend_from_slice(chunk.as_bytes());
ok()
}
#[rustler::nif]
fn context_clear_virtual_file(ctx: ResourceArc<TypstContext>, path: String) -> Atom {
let mut world = ctx.world.lock();
world.virtual_files.remove(&path);
*ctx.document.lock() = None;
ok()
}
#[rustler::nif]
fn context_set_input(ctx: ResourceArc<TypstContext>, key: String, value: String) -> Atom {
let mut world = ctx.world.lock();
world.inputs.insert(key, value);
world.rebuild_library();
ok()
}
#[rustler::nif]
fn context_set_inputs(ctx: ResourceArc<TypstContext>, inputs: HashMap<String, String>) -> Atom {
let mut world = ctx.world.lock();
world.inputs = inputs;
world.rebuild_library();
ok()
}
#[rustler::nif(schedule = "DirtyCpu")]
fn context_export_html(
ctx: ResourceArc<TypstContext>,
pretty: bool,
) -> Result<String, CompileErrorNif> {
std::thread::Builder::new()
.name("typst-html".into())
.stack_size(COMPILE_STACK_SIZE)
.spawn(move || {
let mut world_guard = ctx.world.lock();
world_guard.reset();
let result = typst::compile::<HtmlDocument>(&*world_guard);
let opts = HtmlOptions { pretty };
match result.output {
Ok(html_doc) => match typst_html::html(&html_doc, &opts) {
Ok(html_string) => Ok(html_string),
Err(errors) => Err(CompileErrorNif {
diagnostics: diagnostics_to_vec(errors, &world_guard),
}),
},
Err(errors) => Err(CompileErrorNif {
diagnostics: diagnostics_to_vec(errors, &world_guard),
}),
}
})
.map_err(|e| simple_error(&format!("Failed to spawn HTML compile thread: {e}")))?
.join()
.unwrap_or_else(|_| Err(simple_error("HTML compilation panicked")))
}
#[rustler::nif(schedule = "DirtyCpu")]
fn context_export_bundle<'a>(
env: Env<'a>,
ctx: ResourceArc<TypstContext>,
opts: BundleOptionsNif,
) -> Result<BundleResultNif<'a>, CompileErrorNif> {
let (files, warnings) = std::thread::Builder::new()
.name("typst-bundle".into())
.stack_size(COMPILE_STACK_SIZE)
.spawn(move || {
let mut world_guard = ctx.world.lock();
world_guard.reset();
let result = typst::compile::<Bundle>(&*world_guard);
let bundle = match result.output {
Ok(bundle) => bundle,
Err(errors) => {
return Err(CompileErrorNif {
diagnostics: diagnostics_to_vec(errors, &world_guard),
})
}
};
let bundle_opts = BundleOptions {
html: HtmlOptions {
pretty: opts.pretty,
},
svg: SvgOptions {
pretty: opts.pretty,
render_bleed: opts.render_bleed,
},
..Default::default()
};
match export_bundle(&bundle, &bundle_opts) {
Ok(vfs) => {
let files = vfs
.into_iter()
.map(|(path, bytes)| (path.get_without_slash().to_string(), bytes.to_vec()))
.collect::<Vec<(String, Vec<u8>)>>();
let warnings = diagnostics_to_vec(result.warnings, &world_guard);
Ok((files, warnings))
}
Err(errors) => Err(CompileErrorNif {
diagnostics: diagnostics_to_vec(errors, &world_guard),
}),
}
})
.map_err(|e| simple_error(&format!("Failed to spawn bundle compile thread: {e}")))?
.join()
.unwrap_or_else(|_| Err(simple_error("Bundle compilation panicked")))?;
let mut map = HashMap::with_capacity(files.len());
for (path, bytes) in files {
let mut binary = NewBinary::new(env, bytes.len());
binary.as_mut_slice().copy_from_slice(&bytes);
map.insert(path, binary.into());
}
Ok(BundleResultNif {
files: map,
warnings,
})
}
#[rustler::nif(schedule = "DirtyIo")]
fn font_families(opts: FontOptionsNif) -> Vec<String> {
let include_system_fonts = !opts.ignore_system_fonts;
let font_paths_vec: Vec<PathBuf> = opts
.font_paths
.iter()
.map(PathBuf::from)
.filter(|p| p.exists() && p.is_dir())
.collect();
let mut fonts = FontStore::new();
for path in &font_paths_vec {
fonts.extend(scan(path));
}
if include_system_fonts {
fonts.extend(system());
}
fonts.extend(embedded());
fonts
.book()
.families()
.map(|(name, _info)| name.to_string())
.collect()
}
rustler::init!("Elixir.AshTypst.NIF");