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",
|
"name": "typstdrive",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.7",
|
"version": "1.4.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host",
|
"dev": "vite dev --host",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "server"
|
name = "server"
|
||||||
version = "1.4.7"
|
version = "1.4.8"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -25,6 +25,7 @@ typst-kit = { path = "../typst/crates/typst-kit", features = ["system-downloader
|
|||||||
typst-pdf = { path = "../typst/crates/typst-pdf" }
|
typst-pdf = { path = "../typst/crates/typst-pdf" }
|
||||||
typst-render = { path = "../typst/crates/typst-render" }
|
typst-render = { path = "../typst/crates/typst-render" }
|
||||||
typst-svg = { path = "../typst/crates/typst-svg" }
|
typst-svg = { path = "../typst/crates/typst-svg" }
|
||||||
|
typst-html = { path = "../typst/crates/typst-html" }
|
||||||
typst-layout = { path = "../typst/crates/typst-layout" }
|
typst-layout = { path = "../typst/crates/typst-layout" }
|
||||||
|
|
||||||
yrs = "0.18.8"
|
yrs = "0.18.8"
|
||||||
|
|||||||
+46
-5
@@ -3,6 +3,7 @@ use serde::Serialize;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use typst::diag::{SourceDiagnostic, Warned};
|
use typst::diag::{SourceDiagnostic, Warned};
|
||||||
use typst::layout::{Frame, FrameItem};
|
use typst::layout::{Frame, FrameItem};
|
||||||
|
use typst_html::HtmlDocument;
|
||||||
use typst_layout::PagedDocument;
|
use typst_layout::PagedDocument;
|
||||||
use typst_pdf::{pdf, PdfOptions};
|
use typst_pdf::{pdf, PdfOptions};
|
||||||
use typst_render::{render, RenderOptions};
|
use typst_render::{render, RenderOptions};
|
||||||
@@ -67,8 +68,8 @@ impl ProjectInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_world(self) -> MemoryWorld {
|
fn into_world(self, enable_html: bool) -> MemoryWorld {
|
||||||
MemoryWorld::new_project(self.entrypoint, self.files, self.packages)
|
MemoryWorld::new_project(self.entrypoint, self.files, self.packages, enable_html)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ impl TypstCompiler {
|
|||||||
(Vec<String>, String, DocumentStats),
|
(Vec<String>, String, DocumentStats),
|
||||||
Vec<(SourceDiagnostic, Option<std::ops::Range<usize>>)>,
|
Vec<(SourceDiagnostic, Option<std::ops::Range<usize>>)>,
|
||||||
> {
|
> {
|
||||||
let world = input.into_world();
|
let world = input.into_world(false);
|
||||||
match typst::compile::<PagedDocument>(&world) {
|
match typst::compile::<PagedDocument>(&world) {
|
||||||
Warned {
|
Warned {
|
||||||
output: Ok(doc),
|
output: Ok(doc),
|
||||||
@@ -127,7 +128,7 @@ impl TypstCompiler {
|
|||||||
&self,
|
&self,
|
||||||
input: ProjectInput,
|
input: ProjectInput,
|
||||||
) -> Result<Vec<u8>, Vec<(SourceDiagnostic, Option<std::ops::Range<usize>>)>> {
|
) -> 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) {
|
match typst::compile::<PagedDocument>(&world) {
|
||||||
Warned {
|
Warned {
|
||||||
output: Ok(doc),
|
output: Ok(doc),
|
||||||
@@ -159,7 +160,7 @@ impl TypstCompiler {
|
|||||||
&self,
|
&self,
|
||||||
input: ProjectInput,
|
input: ProjectInput,
|
||||||
) -> Result<Vec<u8>, Vec<(SourceDiagnostic, Option<std::ops::Range<usize>>)>> {
|
) -> 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) {
|
match typst::compile::<PagedDocument>(&world) {
|
||||||
Warned {
|
Warned {
|
||||||
output: Ok(doc),
|
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(),
|
None => return (StatusCode::UNAUTHORIZED, "Missing or invalid Authorization header. Use: Authorization: Bearer <api-key>").into_response(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if payload.format != "png" && payload.format != "pdf" {
|
if payload.format != "png" && payload.format != "pdf" && payload.format != "html" {
|
||||||
return (StatusCode::BAD_REQUEST, "Invalid format. Must be 'png' or 'pdf'").into_response();
|
return (StatusCode::BAD_REQUEST, "Invalid format. Must be 'png', 'pdf', or 'html'").into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.code.trim().is_empty() {
|
if payload.code.trim().is_empty() {
|
||||||
@@ -170,7 +170,11 @@ pub async fn render_handler(
|
|||||||
|
|
||||||
// Check cache
|
// Check cache
|
||||||
let cache_key = compute_cache_key(&payload.format, &payload.code, &payload.files);
|
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)>(
|
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 = ?"
|
"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() {
|
let result = match payload.format.as_str() {
|
||||||
"pdf" => compiler.export_pdf(ProjectInput::single(payload.code.clone(), files_map)),
|
"pdf" => compiler.export_pdf(ProjectInput::single(payload.code.clone(), files_map)),
|
||||||
"png" => compiler.export_png(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!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
drop(compiler);
|
drop(compiler);
|
||||||
|
|||||||
+10
-1
@@ -31,6 +31,7 @@ impl MemoryWorld {
|
|||||||
entrypoint: String,
|
entrypoint: String,
|
||||||
files: HashMap<String, Vec<u8>>,
|
files: HashMap<String, Vec<u8>>,
|
||||||
local_packages: HashMap<String, HashMap<String, Vec<u8>>>,
|
local_packages: HashMap<String, HashMap<String, Vec<u8>>>,
|
||||||
|
enable_html: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let main = FileId::new(RootedPath::new(
|
let main = FileId::new(RootedPath::new(
|
||||||
VirtualRoot::Project,
|
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 {
|
Self {
|
||||||
library: typst::utils::LazyHash::new(Library::builder().build()),
|
library: typst::utils::LazyHash::new(library),
|
||||||
main,
|
main,
|
||||||
files,
|
files,
|
||||||
local_packages,
|
local_packages,
|
||||||
|
|||||||
@@ -31,6 +31,12 @@
|
|||||||
-d '{"code":"= My Report\\n\\nSome body text.","format":"pdf"}' \\
|
-d '{"code":"= My Report\\n\\nSome body text.","format":"pdf"}' \\
|
||||||
--output report.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', {
|
let jsExample = $derived(`const response = await fetch('${baseUrl}/v1/render', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -87,12 +93,12 @@ with open("output.png", "wb") as f:
|
|||||||
f.write(response.content)`);
|
f.write(response.content)`);
|
||||||
|
|
||||||
const requestSchemaJson = `{
|
const requestSchemaJson = `{
|
||||||
"code": "string", // Typst markup (required)
|
"code": "string", // Typst markup (required)
|
||||||
"format": "png" | "pdf", // Output format (required)
|
"format": "png" | "pdf" | "html", // Output format (required)
|
||||||
"files": [ // Optional inline assets
|
"files": [ // Optional inline assets
|
||||||
{
|
{
|
||||||
"name": "string", // Filename used in Typst code
|
"name": "string", // Filename used in Typst code
|
||||||
"data": "string" // Base64-encoded file content
|
"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)
|
// Highlighted versions (derived so they update if baseUrl changes)
|
||||||
let hCurlPng = $derived(hljs.highlight(curlPng, { language: 'bash' }).value);
|
let hCurlPng = $derived(hljs.highlight(curlPng, { language: 'bash' }).value);
|
||||||
let hCurlPdf = $derived(hljs.highlight(curlPdf, { 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 hJs = $derived(hljs.highlight(jsExample, { language: 'javascript' }).value);
|
||||||
let hPython = $derived(hljs.highlight(pythonExample, { language: 'python' }).value);
|
let hPython = $derived(hljs.highlight(pythonExample, { language: 'python' }).value);
|
||||||
let hFiles = $derived(hljs.highlight(filesExample, { 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>
|
<code class="font-mono text-sm text-gray-800 dark:text-gray-200">/v1/render</code>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<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.
|
Results are cached for 1 hour — identical inputs return the cached result without recompiling.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
<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">
|
<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="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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -360,6 +367,7 @@ with open("output.png", "wb") as f:
|
|||||||
{#each [
|
{#each [
|
||||||
{ id: 'curl-png', label: 'cURL — render PNG', icon: 'mdi:bash', iconColor: 'text-gray-400', code: hCurlPng, raw: curlPng },
|
{ 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-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: '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: '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 },
|
{ 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