1use std::str::FromStr;
4use std::sync::Arc;
5
6use comemo::Track;
7use ecow::EcoString;
8use tinymist_std::error::prelude::*;
9use tinymist_std::typst::{TypstDocument, TypstHtmlDocument, TypstPagedDocument};
10use tinymist_world::{CompileSnapshot, CompilerFeat, ExportComputation, WorldComputeGraph};
11use typst::World;
12use typst::diag::{SourceResult, StrResult};
13use typst::foundations::{Bytes, Content, IntoValue, LocatableSelector, Scope, Value};
14use typst::layout::Abs;
15use typst::routines::EvalMode;
16use typst::syntax::{Span, SyntaxNode, ast};
17use typst::visualize::Color;
18use typst_eval::eval_string;
19
20use crate::model::{ExportHtmlTask, ExportPngTask, ExportSvgTask};
21use crate::primitives::TaskWhen;
22use crate::{ExportTransform, Pages, QueryTask};
23
24#[cfg(feature = "pdf")]
25pub mod pdf;
26#[cfg(feature = "pdf")]
27pub use pdf::*;
28#[cfg(feature = "text")]
29pub mod text;
30#[cfg(feature = "text")]
31pub use text::*;
32
33pub struct SvgFlag;
35pub struct PngFlag;
37pub struct HtmlFlag;
39
40pub struct ExportTimings;
42
43impl ExportTimings {
44 pub fn needs_run<F: CompilerFeat, D: typst::Document>(
46 snap: &CompileSnapshot<F>,
47 timing: Option<&TaskWhen>,
48 docs: Option<&D>,
49 ) -> Option<bool> {
50 snap.signal
51 .should_run_task(timing.unwrap_or(&TaskWhen::Never), docs)
52 }
53}
54
55pub struct SvgExport;
57
58impl<F: CompilerFeat> ExportComputation<F, TypstPagedDocument> for SvgExport {
59 type Output = String;
60 type Config = ExportSvgTask;
61
62 fn run(
63 _graph: &Arc<WorldComputeGraph<F>>,
64 doc: &Arc<TypstPagedDocument>,
65 config: &ExportSvgTask,
66 ) -> Result<String> {
67 let (is_first, merged_gap) = get_page_selection(&config.export)?;
68
69 let first_page = doc.pages.first();
70
71 Ok(if is_first {
72 if let Some(first_page) = first_page {
73 typst_svg::svg(first_page)
74 } else {
75 typst_svg::svg_merged(doc, merged_gap)
76 }
77 } else {
78 typst_svg::svg_merged(doc, merged_gap)
79 })
80 }
81}
82
83pub struct PngExport;
93
94impl<F: CompilerFeat> ExportComputation<F, TypstPagedDocument> for PngExport {
95 type Output = Bytes;
96 type Config = ExportPngTask;
97
98 fn run(
99 _graph: &Arc<WorldComputeGraph<F>>,
100 doc: &Arc<TypstPagedDocument>,
101 config: &ExportPngTask,
102 ) -> Result<Bytes> {
103 let ppi = config.ppi.to_f32();
104 if ppi <= 1e-6 {
105 tinymist_std::bail!("invalid ppi: {ppi}");
106 }
107
108 let fill = if let Some(fill) = &config.fill {
109 parse_color(fill.clone()).map_err(|err| anyhow::anyhow!("invalid fill ({err})"))?
110 } else {
111 Color::WHITE
112 };
113
114 let (is_first, merged_gap) = get_page_selection(&config.export)?;
115
116 let ppp = ppi / 72.;
117 let pixmap = if is_first {
118 if let Some(first_page) = doc.pages.first() {
119 typst_render::render(first_page, ppp)
120 } else {
121 typst_render::render_merged(doc, ppp, merged_gap, Some(fill))
122 }
123 } else {
124 typst_render::render_merged(doc, ppp, merged_gap, Some(fill))
125 };
126
127 pixmap
128 .encode_png()
129 .map(Bytes::new)
130 .context_ut("failed to encode PNG")
131 }
132}
133
134pub struct HtmlExport;
144
145impl<F: CompilerFeat> ExportComputation<F, TypstHtmlDocument> for HtmlExport {
146 type Output = String;
147 type Config = ExportHtmlTask;
148
149 fn run(
150 _graph: &Arc<WorldComputeGraph<F>>,
151 doc: &Arc<TypstHtmlDocument>,
152 _config: &ExportHtmlTask,
153 ) -> Result<String> {
154 Ok(typst_html::html(doc)?)
155 }
156}
157
158pub struct DocumentQuery;
168
169impl DocumentQuery {
170 pub fn retrieve<D: typst::Document>(
173 world: &dyn World,
174 selector: &str,
175 document: &D,
176 ) -> StrResult<Vec<Content>> {
177 let selector = eval_string(
178 &typst::ROUTINES,
179 world.track(),
180 selector,
181 Span::detached(),
182 EvalMode::Code,
183 Scope::default(),
184 )
185 .map_err(|errors| {
186 let mut message = EcoString::from("failed to evaluate selector");
187 for (i, error) in errors.into_iter().enumerate() {
188 message.push_str(if i == 0 { ": " } else { ", " });
189 message.push_str(&error.message);
190 }
191 message
192 })?
193 .cast::<LocatableSelector>()
194 .map_err(|e| EcoString::from(format!("failed to cast: {}", e.message())))?;
195
196 Ok(document
197 .introspector()
198 .query(&selector.0)
199 .into_iter()
200 .collect::<Vec<_>>())
201 }
202
203 fn run_inner<F: CompilerFeat, D: typst::Document>(
204 g: &Arc<WorldComputeGraph<F>>,
205 doc: &Arc<D>,
206 config: &QueryTask,
207 ) -> Result<Vec<Value>> {
208 let selector = &config.selector;
209 let elements = Self::retrieve(&g.snap.world, selector, doc.as_ref())
210 .map_err(|e| anyhow::anyhow!("failed to retrieve: {e}"))?;
211 if config.one && elements.len() != 1 {
212 bail!("expected exactly one element, found {}", elements.len());
213 }
214
215 Ok(elements
216 .into_iter()
217 .filter_map(|c| match &config.field {
218 Some(field) => c.get_by_name(field).ok(),
219 _ => Some(c.into_value()),
220 })
221 .collect())
222 }
223
224 pub fn doc_get_as_value<F: CompilerFeat>(
226 g: &Arc<WorldComputeGraph<F>>,
227 doc: &TypstDocument,
228 config: &QueryTask,
229 ) -> Result<serde_json::Value> {
230 match doc {
231 TypstDocument::Paged(doc) => Self::get_as_value(g, doc, config),
232 TypstDocument::Html(doc) => Self::get_as_value(g, doc, config),
233 }
234 }
235
236 pub fn get_as_value<F: CompilerFeat, D: typst::Document>(
238 g: &Arc<WorldComputeGraph<F>>,
239 doc: &Arc<D>,
240 config: &QueryTask,
241 ) -> Result<serde_json::Value> {
242 let mapped = Self::run_inner(g, doc, config)?;
243
244 let res = if config.one {
245 let Some(value) = mapped.first() else {
246 bail!("no such field found for element");
247 };
248 serde_json::to_value(value)
249 } else {
250 serde_json::to_value(&mapped)
251 };
252
253 res.context("failed to serialize")
254 }
255}
256
257impl<F: CompilerFeat, D: typst::Document> ExportComputation<F, D> for DocumentQuery {
258 type Output = SourceResult<String>;
259 type Config = QueryTask;
260
261 fn run(
262 g: &Arc<WorldComputeGraph<F>>,
263 doc: &Arc<D>,
264 config: &QueryTask,
265 ) -> Result<SourceResult<String>> {
266 let pretty = false;
267 let mapped = Self::run_inner(g, doc, config)?;
268
269 let res = if config.one {
270 let Some(value) = mapped.first() else {
271 bail!("no such field found for element");
272 };
273 serialize(value, &config.format, pretty)
274 } else {
275 serialize(&mapped, &config.format, pretty)
276 };
277
278 res.map(Ok)
279 }
280}
281
282fn serialize(data: &impl serde::Serialize, format: &str, pretty: bool) -> Result<String> {
284 Ok(match format {
285 "json" if pretty => serde_json::to_string_pretty(data).context("serialize query")?,
286 "json" => serde_json::to_string(data).context("serialize query")?,
287 "yaml" => serde_yaml::to_string(&data).context_ut("serialize query")?,
288 "txt" => {
289 use serde_json::Value::*;
290 let value = serde_json::to_value(data).context("serialize query")?;
291 match value {
292 String(s) => s,
293 _ => {
294 let kind = match value {
295 Null => "null",
296 Bool(_) => "boolean",
297 Number(_) => "number",
298 String(_) => "string",
299 Array(_) => "array",
300 Object(_) => "object",
301 };
302 bail!("expected a string value for format: {format}, got {kind}")
303 }
304 }
305 }
306 _ => bail!("unsupported format for query: {format}"),
307 })
308}
309
310pub fn get_page_selection(task: &crate::ExportTask) -> Result<(bool, Abs)> {
312 let is_first = task
313 .transform
314 .iter()
315 .any(|t| matches!(t, ExportTransform::Pages { ranges, .. } if ranges == &[Pages::FIRST]));
316
317 let mut gap_res = Abs::default();
318 if !is_first {
319 for trans in &task.transform {
320 if let ExportTransform::Merge { gap } = trans {
321 let gap = gap
322 .as_deref()
323 .map(parse_length)
324 .transpose()
325 .context_ut("failed to parse gap")?;
326 gap_res = gap.unwrap_or_default();
327 }
328 }
329 }
330
331 Ok((is_first, gap_res))
332}
333
334fn parse_length(gap: &str) -> Result<Abs> {
335 let length = typst::syntax::parse_code(gap);
336 if length.erroneous() {
337 bail!("invalid length: {gap}, errors: {:?}", length.errors());
338 }
339
340 let length: Option<ast::Numeric> = descendants(&length).into_iter().find_map(SyntaxNode::cast);
341
342 let Some(length) = length else {
343 bail!("not a length: {gap}");
344 };
345
346 let (value, unit) = length.get();
347 match unit {
348 ast::Unit::Pt => Ok(Abs::pt(value)),
349 ast::Unit::Mm => Ok(Abs::mm(value)),
350 ast::Unit::Cm => Ok(Abs::cm(value)),
351 ast::Unit::In => Ok(Abs::inches(value)),
352 _ => bail!("invalid unit: {unit:?} in {gap}"),
353 }
354}
355
356fn descendants(node: &SyntaxNode) -> impl IntoIterator<Item = &SyntaxNode> + '_ {
358 let mut res = vec![];
359 for child in node.children() {
360 res.push(child);
361 res.extend(descendants(child));
362 }
363
364 res
365}
366
367fn parse_color(fill: String) -> anyhow::Result<Color> {
368 match fill.as_str() {
369 "black" => Ok(Color::BLACK),
370 "white" => Ok(Color::WHITE),
371 "red" => Ok(Color::RED),
372 "green" => Ok(Color::GREEN),
373 "blue" => Ok(Color::BLUE),
374 hex if hex.starts_with('#') => {
375 Color::from_str(&hex[1..]).map_err(|e| anyhow::anyhow!("failed to parse color: {e}"))
376 }
377 _ => anyhow::bail!("invalid color: {fill}"),
378 }
379}
380
381#[cfg(test)]
382mod tests {
383
384 use super::*;
385
386 #[test]
387 fn test_parse_color() {
388 assert_eq!(parse_color("black".to_owned()).unwrap(), Color::BLACK);
389 assert_eq!(parse_color("white".to_owned()).unwrap(), Color::WHITE);
390 assert_eq!(parse_color("red".to_owned()).unwrap(), Color::RED);
391 assert_eq!(parse_color("green".to_owned()).unwrap(), Color::GREEN);
392 assert_eq!(parse_color("blue".to_owned()).unwrap(), Color::BLUE);
393 assert_eq!(
394 parse_color("#000000".to_owned()).unwrap().to_hex(),
395 "#000000"
396 );
397 assert_eq!(
398 parse_color("#ffffff".to_owned()).unwrap().to_hex(),
399 "#ffffff"
400 );
401 assert_eq!(
402 parse_color("#000000cc".to_owned()).unwrap().to_hex(),
403 "#000000cc"
404 );
405 assert!(parse_color("invalid".to_owned()).is_err());
406 }
407
408 #[test]
409 fn test_parse_length() {
410 assert_eq!(parse_length("1pt").unwrap(), Abs::pt(1.));
411 assert_eq!(parse_length("1mm").unwrap(), Abs::mm(1.));
412 assert_eq!(parse_length("1cm").unwrap(), Abs::cm(1.));
413 assert_eq!(parse_length("1in").unwrap(), Abs::inches(1.));
414 assert!(parse_length("1").is_err());
415 assert!(parse_length("1px").is_err());
416 }
417}