Add HTML render format to API and update version to 1.4.8
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "typstdrive",
|
||||
"private": true,
|
||||
"version": "1.4.7",
|
||||
"version": "1.4.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev --host",
|
||||
|
||||
+2
-1
@@ -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
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -31,6 +31,12 @@
|
||||
-d '{"code":"= My Report\\n\\nSome body text.","format":"pdf"}' \\
|
||||
--output report.pdf`);
|
||||
|
||||
let curlHtml = $derived(`curl -X POST ${baseUrl}/v1/render \\
|
||||
-H "Authorization: Bearer td_your_api_key_here" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"code":"= My Report\\n\\nSome body text.","format":"html"}' \\
|
||||
--output report.html`);
|
||||
|
||||
let jsExample = $derived(`const response = await fetch('${baseUrl}/v1/render', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -87,12 +93,12 @@ with open("output.png", "wb") as f:
|
||||
f.write(response.content)`);
|
||||
|
||||
const requestSchemaJson = `{
|
||||
"code": "string", // Typst markup (required)
|
||||
"format": "png" | "pdf", // Output format (required)
|
||||
"files": [ // Optional inline assets
|
||||
"code": "string", // Typst markup (required)
|
||||
"format": "png" | "pdf" | "html", // Output format (required)
|
||||
"files": [ // Optional inline assets
|
||||
{
|
||||
"name": "string", // Filename used in Typst code
|
||||
"data": "string" // Base64-encoded file content
|
||||
"name": "string", // Filename used in Typst code
|
||||
"data": "string" // Base64-encoded file content
|
||||
}
|
||||
]
|
||||
}`;
|
||||
@@ -112,6 +118,7 @@ with open("output.png", "wb") as f:
|
||||
// Highlighted versions (derived so they update if baseUrl changes)
|
||||
let hCurlPng = $derived(hljs.highlight(curlPng, { language: 'bash' }).value);
|
||||
let hCurlPdf = $derived(hljs.highlight(curlPdf, { language: 'bash' }).value);
|
||||
let hCurlHtml = $derived(hljs.highlight(curlHtml, { language: 'bash' }).value);
|
||||
let hJs = $derived(hljs.highlight(jsExample, { language: 'javascript' }).value);
|
||||
let hPython = $derived(hljs.highlight(pythonExample, { language: 'python' }).value);
|
||||
let hFiles = $derived(hljs.highlight(filesExample, { language: 'python' }).value);
|
||||
@@ -290,7 +297,7 @@ with open("output.png", "wb") as f:
|
||||
<code class="font-mono text-sm text-gray-800 dark:text-gray-200">/v1/render</code>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Compile Typst markup and return rendered binary output as PNG or PDF.
|
||||
Compile Typst markup and return rendered output as PNG, PDF, or HTML.
|
||||
Results are cached for 1 hour — identical inputs return the cached result without recompiling.
|
||||
</p>
|
||||
</div>
|
||||
@@ -330,7 +337,7 @@ with open("output.png", "wb") as f:
|
||||
<p class="text-sm font-bold text-gray-800 dark:text-gray-200 mb-3">Response</p>
|
||||
<div class="p-3 rounded-lg bg-green-50 dark:bg-green-900/10 border border-green-100 dark:border-green-800/30 text-sm">
|
||||
<span class="font-mono text-xs font-bold text-green-700 dark:text-green-400">200 OK</span>
|
||||
<span class="text-gray-600 dark:text-gray-400 ml-2">Binary body with <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">Content-Type: image/png</code> or <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">application/pdf</code></span>
|
||||
<span class="text-gray-600 dark:text-gray-400 ml-2">Response body with <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">Content-Type: image/png</code>, <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">application/pdf</code>, or <code class="font-mono text-xs bg-gray-100 dark:bg-white/10 px-2 rounded">text/html</code></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -360,6 +367,7 @@ with open("output.png", "wb") as f:
|
||||
{#each [
|
||||
{ id: 'curl-png', label: 'cURL — render PNG', icon: 'mdi:bash', iconColor: 'text-gray-400', code: hCurlPng, raw: curlPng },
|
||||
{ id: 'curl-pdf', label: 'cURL — render PDF', icon: 'mdi:bash', iconColor: 'text-gray-400', code: hCurlPdf, raw: curlPdf },
|
||||
{ id: 'curl-html', label: 'cURL — render HTML', icon: 'mdi:bash', iconColor: 'text-gray-400', code: hCurlHtml, raw: curlHtml },
|
||||
{ id: 'js', label: 'JavaScript / TypeScript', icon: 'mdi:language-javascript', iconColor: 'text-yellow-400', code: hJs, raw: jsExample },
|
||||
{ id: 'python', label: 'Python (httpx)', icon: 'mdi:language-python', iconColor: 'text-blue-400', code: hPython, raw: pythonExample},
|
||||
{ id: 'files', label: 'Python — with inline files', icon: 'mdi:file-image-outline', iconColor: 'text-purple-400', code: hFiles, raw: filesExample },
|
||||
|
||||
Reference in New Issue
Block a user