use std::fs;
use std::path::{Component, Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use crate::typst::io::write_if_changed;
use crate::typst::paths::CALEPIN_DIR;
const RUNTIME_DIR: &str = "runtime";
const GENERATED_SYNTAX_THEME_FILE: &str = "00_syntax-theme.typ";
const FACADE_FILE: &str = "calepin.typ";
const RUNTIME_FACADE_SOURCE: &str = r#"// Generated by Calepin. Do not edit.
#import "runtime/core/state.typ" as state
#import "runtime/core/target.typ" as target
#import "runtime/notebook/render.typ" as render
#import "runtime/notebook/options.typ" as options
#import "runtime/notebook/chunk.typ" as chunks
#import "runtime/elements/mod.typ" as elementmod
#let pages = state.pages
#let setup = options.setup
#let chunk = chunks.chunk
#let inline = chunks.inline
#let results = chunks.results
#let chunk_from_raw_plain = chunks.chunk_from_raw_plain
#let code-block = render.code-block
#let elements = elementmod
#let _mode = target._mode
#let _is-html = target._is-html
#let _is-paged = target._is-paged
#let _is-query = target._is-query
#let _is-render = target._is-render
#let _call-defaults = state._call-defaults
#let _disable-raw-chunk-transforms = state._disable-raw-chunk-transforms
#let _resolve-options = options._resolve-options
#let _html-themed-raw-block = render._html-themed-raw-block
#let _without-raw-chunk-transforms = chunks._without-raw-chunk-transforms
#let _fenced-chunks-runs = chunks._fenced-chunks-runs
"#;
struct RuntimeFile {
path: &'static str,
source: &'static str,
}
include!(concat!(env!("OUT_DIR"), "/typst_runtime_assets.rs"));
fn runtime_source_files() -> &'static [RuntimeFile] {
RUNTIME_FILES
}
fn runtime_facade_source() -> &'static str {
RUNTIME_FACADE_SOURCE
}
fn runtime_asset_path(path: &str) -> Result<&Path> {
let path = Path::new(path);
let is_valid = path.is_relative()
&& path.extension().and_then(|extension| extension.to_str()) == Some("typ")
&& path
.components()
.all(|component| matches!(component, Component::Normal(_)));
if !is_valid {
return Err(anyhow!(
"invalid embedded runtime asset path `{}`",
path.display()
));
}
Ok(path)
}
#[cfg(test)]
pub fn write_runtime(root: &Path) -> Result<PathBuf> {
write_runtime_with_syntax_theme_in_dir(
root,
Path::new(CALEPIN_DIR),
&crate::html::HtmlSyntaxTheme::builtin(),
)
}
pub(crate) fn write_runtime_with_syntax_theme(
root: &Path,
syntax_theme: &crate::html::HtmlSyntaxTheme,
) -> Result<PathBuf> {
write_runtime_with_syntax_theme_in_dir(root, Path::new(CALEPIN_DIR), syntax_theme)
}
pub(crate) fn write_runtime_with_syntax_theme_in_dir(
root: &Path,
asset_dir: &Path,
syntax_theme: &crate::html::HtmlSyntaxTheme,
) -> Result<PathBuf> {
let calepin_dir = root.join(asset_dir);
let generated_runtime_dir = calepin_dir.join(RUNTIME_DIR);
fs::create_dir_all(&generated_runtime_dir)
.with_context(|| format!("failed to create {}", generated_runtime_dir.display()))?;
write_if_changed(
&generated_runtime_dir.join(GENERATED_SYNTAX_THEME_FILE),
syntax_theme.typst_runtime_source(),
)?;
for source_file in runtime_source_files() {
let destination = generated_runtime_dir.join(runtime_asset_path(source_file.path)?);
write_if_changed(&destination, source_file.source)?;
}
let facade = calepin_dir.join(FACADE_FILE);
write_if_changed(&facade, runtime_facade_source())?;
Ok(facade)
}
#[cfg(test)]
mod embedded_runtime_tests {
use super::*;
#[test]
fn runtime_source_files_are_embedded_relative_typ_files() {
let files = runtime_source_files();
assert!(!files.is_empty(), "expected embedded Typst runtime files");
for file in files {
let path = Path::new(file.path);
assert!(
path.is_relative(),
"runtime asset path should be relative: {}",
file.path
);
assert_eq!(
path.extension().and_then(|extension| extension.to_str()),
Some("typ")
);
assert!(
!file.source.is_empty(),
"runtime asset should have embedded source: {}",
file.path
);
}
}
#[test]
fn runtime_asset_path_rejects_non_normal_paths() {
for path in ["../escape.typ", "core/../escape.typ", "/absolute.typ"] {
assert!(
runtime_asset_path(path).is_err(),
"expected runtime asset path to be rejected: {path}"
);
}
}
#[test]
fn runtime_facade_imports_embedded_runtime_files() {
let embedded_paths = runtime_source_files()
.iter()
.map(|file| file.path)
.collect::<std::collections::HashSet<_>>();
for target in runtime_facade_imports(runtime_facade_source()) {
let Some(runtime_path) = target.strip_prefix("runtime/") else {
panic!("facade import should target runtime directory: {target}");
};
assert!(
embedded_paths.contains(runtime_path),
"facade import target is not embedded: {target}"
);
}
}
fn runtime_facade_imports(source: &str) -> impl Iterator<Item = &str> {
source.lines().filter_map(|line| {
line.trim_start()
.strip_prefix("#import \"")
.and_then(|line| line.split('"').next())
})
}
}
#[cfg(test)]
#[path = "runtime_tests.rs"]
mod runtime_tests;