Add HTML render format to API and update version to 1.4.8

This commit is contained in:
2026-06-14 13:08:33 -04:00
parent 68ffa14622
commit 9644361bad
6 changed files with 84 additions and 18 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "server"
version = "1.4.7"
version = "1.4.8"
edition = "2021"
[dependencies]
@@ -25,6 +25,7 @@ typst-kit = { path = "../typst/crates/typst-kit", features = ["system-downloader
typst-pdf = { path = "../typst/crates/typst-pdf" }
typst-render = { path = "../typst/crates/typst-render" }
typst-svg = { path = "../typst/crates/typst-svg" }
typst-html = { path = "../typst/crates/typst-html" }
typst-layout = { path = "../typst/crates/typst-layout" }
yrs = "0.18.8"
+46 -5
View File
@@ -3,6 +3,7 @@ use serde::Serialize;
use std::collections::HashMap;
use typst::diag::{SourceDiagnostic, Warned};
use typst::layout::{Frame, FrameItem};
use typst_html::HtmlDocument;
use typst_layout::PagedDocument;
use typst_pdf::{pdf, PdfOptions};
use typst_render::{render, RenderOptions};
@@ -67,8 +68,8 @@ impl ProjectInput {
}
}
fn into_world(self) -> MemoryWorld {
MemoryWorld::new_project(self.entrypoint, self.files, self.packages)
fn into_world(self, enable_html: bool) -> MemoryWorld {
MemoryWorld::new_project(self.entrypoint, self.files, self.packages, enable_html)
}
}
@@ -86,7 +87,7 @@ impl TypstCompiler {
(Vec<String>, String, DocumentStats),
Vec<(SourceDiagnostic, Option<std::ops::Range<usize>>)>,
> {
let world = input.into_world();
let world = input.into_world(false);
match typst::compile::<PagedDocument>(&world) {
Warned {
output: Ok(doc),
@@ -127,7 +128,7 @@ impl TypstCompiler {
&self,
input: ProjectInput,
) -> Result<Vec<u8>, Vec<(SourceDiagnostic, Option<std::ops::Range<usize>>)>> {
let world = input.into_world();
let world = input.into_world(false);
match typst::compile::<PagedDocument>(&world) {
Warned {
output: Ok(doc),
@@ -159,7 +160,7 @@ impl TypstCompiler {
&self,
input: ProjectInput,
) -> Result<Vec<u8>, Vec<(SourceDiagnostic, Option<std::ops::Range<usize>>)>> {
let world = input.into_world();
let world = input.into_world(false);
match typst::compile::<PagedDocument>(&world) {
Warned {
output: Ok(doc),
@@ -192,4 +193,44 @@ impl TypstCompiler {
}
}
}
pub fn export_html(
&self,
input: ProjectInput,
) -> Result<String, Vec<(SourceDiagnostic, Option<std::ops::Range<usize>>)>> {
let world = input.into_world(true);
let document = match typst::compile::<HtmlDocument>(&world) {
Warned {
output: Ok(document),
warnings: _,
} => document,
Warned {
output: Err(errors),
warnings: _,
} => {
use typst::WorldExt;
return Err(errors
.into_iter()
.map(|d| {
let range = world.range(d.span);
(d, range)
})
.collect());
}
};
match typst_html::html(&document) {
Ok(html) => Ok(html),
Err(errors) => {
use typst::WorldExt;
Err(errors
.into_iter()
.map(|d| {
let range = world.range(d.span);
(d, range)
})
.collect())
}
}
}
}
+10 -3
View File
@@ -89,8 +89,8 @@ pub async fn render_handler(
None => return (StatusCode::UNAUTHORIZED, "Missing or invalid Authorization header. Use: Authorization: Bearer <api-key>").into_response(),
};
if payload.format != "png" && payload.format != "pdf" {
return (StatusCode::BAD_REQUEST, "Invalid format. Must be 'png' or 'pdf'").into_response();
if payload.format != "png" && payload.format != "pdf" && payload.format != "html" {
return (StatusCode::BAD_REQUEST, "Invalid format. Must be 'png', 'pdf', or 'html'").into_response();
}
if payload.code.trim().is_empty() {
@@ -170,7 +170,11 @@ pub async fn render_handler(
// Check cache
let cache_key = compute_cache_key(&payload.format, &payload.code, &payload.files);
let content_type: &'static str = if payload.format == "pdf" { "application/pdf" } else { "image/png" };
let content_type: &'static str = match payload.format.as_str() {
"pdf" => "application/pdf",
"html" => "text/html; charset=utf-8",
_ => "image/png",
};
if let Ok(Some((data, created_at))) = sqlx::query_as::<_, (Vec<u8>, String)>(
"SELECT data, created_at FROM api_render_cache WHERE content_hash = ? AND format = ?"
@@ -216,6 +220,9 @@ pub async fn render_handler(
let result = match payload.format.as_str() {
"pdf" => compiler.export_pdf(ProjectInput::single(payload.code.clone(), files_map)),
"png" => compiler.export_png(ProjectInput::single(payload.code.clone(), files_map)),
"html" => compiler
.export_html(ProjectInput::single(payload.code.clone(), files_map))
.map(|html| html.into_bytes()),
_ => unreachable!(),
};
drop(compiler);
+10 -1
View File
@@ -31,6 +31,7 @@ impl MemoryWorld {
entrypoint: String,
files: HashMap<String, Vec<u8>>,
local_packages: HashMap<String, HashMap<String, Vec<u8>>>,
enable_html: bool,
) -> Self {
let main = FileId::new(RootedPath::new(
VirtualRoot::Project,
@@ -62,8 +63,16 @@ impl MemoryWorld {
}
}
let library = if enable_html {
Library::builder()
.with_features([typst::Feature::Html].into_iter().collect())
.build()
} else {
Library::builder().build()
};
Self {
library: typst::utils::LazyHash::new(Library::builder().build()),
library: typst::utils::LazyHash::new(library),
main,
files,
local_packages,