add: support for typst 0.15.0
This commit is contained in:
parent
8cfe4f8ff3
commit
943bd513f1
5 changed files with 855 additions and 1279 deletions
1881
Cargo.lock
generated
1881
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
11
Cargo.toml
11
Cargo.toml
|
|
@ -9,10 +9,9 @@ edition = "2021"
|
||||||
include_dir = "0.7.4"
|
include_dir = "0.7.4"
|
||||||
clap = { version = "4.6.1", features = ["derive"] }
|
clap = { version = "4.6.1", features = ["derive"] }
|
||||||
env_logger = "0.11.10"
|
env_logger = "0.11.10"
|
||||||
log = "0.4.29"
|
log = "0.4.32"
|
||||||
typst = "0.14.2"
|
typst = "0.15.0"
|
||||||
typst-as-lib = { version = "0.15.4", features = ["typst-html", "typst-kit-fonts", "typst-kit-embed-fonts"] }
|
typst-html = "0.15.0"
|
||||||
typst-html = "0.14.2"
|
typst-assets = { version = "0.15.0", features = ["fonts"] }
|
||||||
typst-pdf = "0.14.2"
|
time = "0.3"
|
||||||
typst-svg = "0.14.2"
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ rustPlatform.buildRustPackage {
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
rust-analyzer
|
rust-analyzer
|
||||||
|
cargo-edit
|
||||||
];
|
];
|
||||||
buildInputs = [];
|
buildInputs = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "stable"
|
channel = "nightly"
|
||||||
|
|
||||||
|
|
|
||||||
179
src/lib.rs
179
src/lib.rs
|
|
@ -7,43 +7,114 @@ use std::path::{Path, PathBuf};
|
||||||
pub use plugin::{concat_plugin_sources, embedded_prepend_source, list_embedded_plugin_ids};
|
pub use plugin::{concat_plugin_sources, embedded_prepend_source, list_embedded_plugin_ids};
|
||||||
|
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
|
use typst::diag::{SourceDiagnostic, Warned};
|
||||||
use typst::ecow::EcoString;
|
use typst::ecow::EcoString;
|
||||||
use typst::syntax::Source;
|
use typst::foundations::{Datetime, Duration};
|
||||||
use typst_as_lib::{typst_kit_options::TypstKitFontOptions, TypstAsLibError, TypstEngine};
|
use typst::syntax::{DiagSpanKind, FileId, RootedPath, Source, VirtualPath, VirtualRoot};
|
||||||
use typst_html::{HtmlAttr, HtmlDocument, HtmlElement, HtmlNode};
|
use typst::text::{Font, FontBook};
|
||||||
|
use typst::utils::LazyHash;
|
||||||
|
use typst::Library;
|
||||||
|
use typst::{LibraryExt, World};
|
||||||
|
use typst_html::{HtmlAttr, HtmlDocument, HtmlElement, HtmlNode, HtmlOptions};
|
||||||
|
|
||||||
|
struct TypssgWorld {
|
||||||
|
library: LazyHash<Library>,
|
||||||
|
book: LazyHash<FontBook>,
|
||||||
|
main_source_id: FileId,
|
||||||
|
main_source: Source,
|
||||||
|
root: PathBuf,
|
||||||
|
fonts: Vec<Font>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl World for TypssgWorld {
|
||||||
|
fn library(&self) -> &LazyHash<Library> {
|
||||||
|
&self.library
|
||||||
|
}
|
||||||
|
|
||||||
|
fn book(&self) -> &LazyHash<FontBook> {
|
||||||
|
&self.book
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main(&self) -> FileId {
|
||||||
|
self.main_source_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source(&self, id: FileId) -> typst::diag::FileResult<Source> {
|
||||||
|
if id == self.main_source_id {
|
||||||
|
Ok(self.main_source.clone())
|
||||||
|
} else {
|
||||||
|
let path = id.vpath().realize(&self.root).ok();
|
||||||
|
if let Some(path) = path {
|
||||||
|
let text = std::fs::read_to_string(&path)
|
||||||
|
.map_err(|e| typst::diag::FileError::from_io(e, &path))?;
|
||||||
|
Ok(Source::new(id, text))
|
||||||
|
} else {
|
||||||
|
Err(typst::diag::FileError::NotFound(self.root.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file(&self, id: FileId) -> typst::diag::FileResult<typst::foundations::Bytes> {
|
||||||
|
if id == self.main_source_id {
|
||||||
|
Ok(typst::foundations::Bytes::from_string(
|
||||||
|
self.main_source.text().to_owned(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
let path = id.vpath().realize(&self.root).ok();
|
||||||
|
if let Some(path) = path {
|
||||||
|
let data =
|
||||||
|
std::fs::read(&path).map_err(|e| typst::diag::FileError::from_io(e, &path))?;
|
||||||
|
Ok(typst::foundations::Bytes::new(data))
|
||||||
|
} else {
|
||||||
|
Err(typst::diag::FileError::NotFound(self.root.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font(&self, index: usize) -> Option<Font> {
|
||||||
|
self.fonts.get(index).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
|
||||||
|
let mut now = time::OffsetDateTime::now_utc();
|
||||||
|
if let Some(offset) = offset {
|
||||||
|
let td: time::Duration = offset.into();
|
||||||
|
now = now.checked_add(td)?;
|
||||||
|
}
|
||||||
|
Datetime::from_ymd(now.year(), now.month() as u8, now.day())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn article_main_vpath(article_dir: &Path, root_dir: &Path) -> String {
|
fn article_main_vpath(article_dir: &Path, root_dir: &Path) -> String {
|
||||||
let article = fs::canonicalize(article_dir).unwrap_or_else(|_| article_dir.to_path_buf());
|
let article = fs::canonicalize(article_dir).unwrap_or_else(|_| article_dir.to_path_buf());
|
||||||
let root = fs::canonicalize(root_dir).unwrap_or_else(|_| root_dir.to_path_buf());
|
let root = fs::canonicalize(root_dir).unwrap_or_else(|_| root_dir.to_path_buf());
|
||||||
let rel = article
|
let rel = article.strip_prefix(&root).unwrap_or(article.as_path());
|
||||||
.strip_prefix(&root)
|
rel.join("index.typ").to_string_lossy().replace('\\', "/")
|
||||||
.unwrap_or(article.as_path());
|
|
||||||
rel.join("index.typ")
|
|
||||||
.to_string_lossy()
|
|
||||||
.replace('\\', "/")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_typst_compile_error(
|
fn format_typst_compile_error(
|
||||||
err: TypstAsLibError,
|
diagnostics: Vec<SourceDiagnostic>,
|
||||||
full_source: &str,
|
full_source: &str,
|
||||||
index_byte_start: usize,
|
index_byte_start: usize,
|
||||||
index_source: &str,
|
index_source: &str,
|
||||||
) -> std::io::Error {
|
) -> std::io::Error {
|
||||||
let report = match err {
|
|
||||||
TypstAsLibError::TypstSource(diagnostics) if !diagnostics.is_empty() => {
|
|
||||||
let combined = Source::detached(full_source);
|
let combined = Source::detached(full_source);
|
||||||
let index_only = Source::detached(index_source);
|
let index_only = Source::detached(index_source);
|
||||||
let index_end = index_byte_start.saturating_add(index_source.len());
|
let index_end = index_byte_start.saturating_add(index_source.len());
|
||||||
let mut out = String::from("Typst compile failed:\n");
|
let mut out = String::from("Typst compile failed:\n");
|
||||||
for d in diagnostics.iter() {
|
for d in &diagnostics {
|
||||||
let msg = d.message.as_str();
|
let msg = d.message.as_str();
|
||||||
if let Some(range) = combined.range(d.span) {
|
let range = match d.span.get() {
|
||||||
|
DiagSpanKind::Number { num, sub_range, .. } => combined.range(num, sub_range),
|
||||||
|
DiagSpanKind::Range { range, .. } => Some(range),
|
||||||
|
DiagSpanKind::Detached => None,
|
||||||
|
};
|
||||||
|
if let Some(range) = range {
|
||||||
let byte = range.start;
|
let byte = range.start;
|
||||||
if byte >= index_byte_start && byte < index_end {
|
if byte >= index_byte_start && byte < index_end {
|
||||||
let rel = byte - index_byte_start;
|
let rel = byte - index_byte_start;
|
||||||
if let Some((line, col)) = index_only.lines().byte_to_line_column(rel) {
|
if let Some((line, col)) = index_only.lines().byte_to_line_column(rel) {
|
||||||
let _ =
|
let _ = writeln!(&mut out, " index.typ:{}:{}: {}", line + 1, col + 1, msg);
|
||||||
writeln!(&mut out, " index.typ:{}:{}: {}", line + 1, col + 1, msg);
|
|
||||||
} else {
|
} else {
|
||||||
let _ = writeln!(&mut out, " {msg}");
|
let _ = writeln!(&mut out, " {msg}");
|
||||||
}
|
}
|
||||||
|
|
@ -61,15 +132,11 @@ fn format_typst_compile_error(
|
||||||
} else {
|
} else {
|
||||||
let _ = writeln!(&mut out, " {msg}");
|
let _ = writeln!(&mut out, " {msg}");
|
||||||
}
|
}
|
||||||
for hint in d.hints.iter() {
|
for hint in &d.hints {
|
||||||
let _ = writeln!(&mut out, " hint: {}", hint.as_str());
|
let _ = writeln!(&mut out, " hint: {}", hint.v.as_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
std::io::Error::other(out)
|
||||||
}
|
|
||||||
other => format!("typst compile failed: {other}"),
|
|
||||||
};
|
|
||||||
std::io::Error::new(std::io::ErrorKind::Other, report)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Internal compilation kernel. `index_byte_start` is the byte offset within
|
/// Internal compilation kernel. `index_byte_start` is the byte offset within
|
||||||
|
|
@ -83,20 +150,37 @@ fn compile_source(
|
||||||
) -> Result<(String, String), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(String, String), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let index_source = &full_source[index_byte_start..];
|
let index_source = &full_source[index_byte_start..];
|
||||||
|
|
||||||
let engine = TypstEngine::builder()
|
let main_source_id = RootedPath::new(
|
||||||
.main_file((main_vpath, full_source.to_string()))
|
VirtualRoot::Project,
|
||||||
.search_fonts_with(
|
VirtualPath::new(main_vpath).map_err(|_| format!("invalid virtual path: {main_vpath}"))?,
|
||||||
TypstKitFontOptions::default()
|
|
||||||
.include_system_fonts(false)
|
|
||||||
.include_embedded_fonts(true),
|
|
||||||
)
|
)
|
||||||
.with_file_system_resolver(vfs_root)
|
.intern();
|
||||||
.build();
|
let mut book = FontBook::new();
|
||||||
|
let mut fonts = Vec::new();
|
||||||
|
for data in typst_assets::fonts() {
|
||||||
|
if let Some(font) = Font::new(typst::foundations::Bytes::new(data), 0) {
|
||||||
|
book.push(font.info().clone());
|
||||||
|
fonts.push(font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut doc: HtmlDocument = engine
|
let world = TypssgWorld {
|
||||||
.compile()
|
library: LazyHash::new(
|
||||||
.output
|
Library::builder()
|
||||||
.map_err(|e| format_typst_compile_error(e, full_source, index_byte_start, index_source))?;
|
.with_features([typst::Feature::Html].into_iter().collect())
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
book: LazyHash::new(book),
|
||||||
|
main_source_id,
|
||||||
|
main_source: Source::new(main_source_id, full_source.to_string()),
|
||||||
|
root: vfs_root.clone(),
|
||||||
|
fonts,
|
||||||
|
};
|
||||||
|
|
||||||
|
let Warned { output, .. } = typst::compile::<HtmlDocument>(&world);
|
||||||
|
let mut doc = output.map_err(|e| {
|
||||||
|
format_typst_compile_error(e.to_vec(), full_source, index_byte_start, index_source)
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut outline = EcoString::new();
|
let mut outline = EcoString::new();
|
||||||
let mut curr_level = 1u32;
|
let mut curr_level = 1u32;
|
||||||
|
|
@ -104,7 +188,7 @@ fn compile_source(
|
||||||
let mut title_h2_pending = !include_title;
|
let mut title_h2_pending = !include_title;
|
||||||
let mut first_outline_heading = true;
|
let mut first_outline_heading = true;
|
||||||
parse_outline(
|
parse_outline(
|
||||||
&mut doc.root,
|
doc.root_mut(),
|
||||||
&mut outline,
|
&mut outline,
|
||||||
&mut curr_level,
|
&mut curr_level,
|
||||||
&mut ul_depth,
|
&mut ul_depth,
|
||||||
|
|
@ -119,8 +203,9 @@ fn compile_source(
|
||||||
}
|
}
|
||||||
let outline_str = outline.to_string();
|
let outline_str = outline.to_string();
|
||||||
|
|
||||||
|
let body: HtmlElement = {
|
||||||
let mut body: Option<HtmlElement> = None;
|
let mut body: Option<HtmlElement> = None;
|
||||||
for child in &doc.root.children {
|
for child in &doc.root().children {
|
||||||
match child {
|
match child {
|
||||||
HtmlNode::Element(e) if e.tag.to_string().as_str() == "<body>" => {
|
HtmlNode::Element(e) if e.tag.to_string().as_str() == "<body>" => {
|
||||||
body = Some(e.clone());
|
body = Some(e.clone());
|
||||||
|
|
@ -128,11 +213,14 @@ fn compile_source(
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let body = body.ok_or("compiled HTML has no <body> element")?;
|
body.ok_or("compiled HTML has no <body> element")?
|
||||||
doc.root = body;
|
};
|
||||||
|
|
||||||
|
*doc.root_mut() = body;
|
||||||
|
|
||||||
let mut html: String =
|
let mut html: String =
|
||||||
typst_html::html(&doc).map_err(|e| format!("html generation failed: {e:?}"))?;
|
typst_html::html(&doc, &HtmlOptions { pretty: true })
|
||||||
|
.map_err(|e| format!("html generation failed: {e:?}"))?;
|
||||||
let lines = html
|
let lines = html
|
||||||
.lines()
|
.lines()
|
||||||
.map(|line| if line.len() >= 2 { &line[2..] } else { "" })
|
.map(|line| if line.len() >= 2 { &line[2..] } else { "" })
|
||||||
|
|
@ -355,7 +443,13 @@ pub fn compile_all(
|
||||||
include_title_in_outline: bool,
|
include_title_in_outline: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
info!("compiling all files at dir {:?}", root_dir);
|
info!("compiling all files at dir {:?}", root_dir);
|
||||||
let res = compile_all_at(root_dir, root_dir, prepend, plugins, include_title_in_outline);
|
let res = compile_all_at(
|
||||||
|
root_dir,
|
||||||
|
root_dir,
|
||||||
|
prepend,
|
||||||
|
plugins,
|
||||||
|
include_title_in_outline,
|
||||||
|
);
|
||||||
info!("done");
|
info!("done");
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
@ -375,7 +469,8 @@ fn compile_all_at(
|
||||||
compile_all_at(&path, root_dir, prepend, plugins, include_title_in_outline)?;
|
compile_all_at(&path, root_dir, prepend, plugins, include_title_in_outline)?;
|
||||||
} else if path.file_name().is_some_and(|n| n == "index.typ") {
|
} else if path.file_name().is_some_and(|n| n == "index.typ") {
|
||||||
let dir = path.parent().unwrap().to_path_buf();
|
let dir = path.parent().unwrap().to_path_buf();
|
||||||
if let Err(e) = compile_article(&dir, root_dir, prepend, plugins, include_title_in_outline)
|
if let Err(e) =
|
||||||
|
compile_article(&dir, root_dir, prepend, plugins, include_title_in_outline)
|
||||||
{
|
{
|
||||||
error!("{e}");
|
error!("{e}");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue