1pub mod compile;
33pub mod convert;
34mod error_mapping;
35
36pub mod helper;
37mod sig_overlay;
38mod world;
39
40#[doc(hidden)]
43pub mod fuzz_utils {
44 pub use super::helper::inject_json;
45}
46
47use convert::mark_to_typst;
48use quillmark_core::{
49 quill::build_transform_schema, session::SessionHandle, Backend, Diagnostic, OutputFormat,
50 QuillSource, QuillValue, RenderError, RenderOptions, RenderResult, RenderSession, Severity,
51};
52use std::any::Any;
53use std::collections::HashMap;
54
55#[derive(Debug)]
57pub struct TypstBackend;
58
59const SUPPORTED_FORMATS: &[OutputFormat] =
60 &[OutputFormat::Pdf, OutputFormat::Svg, OutputFormat::Png];
61
62#[derive(Debug)]
69pub struct TypstSession {
70 document: typst::layout::PagedDocument,
71 page_count: usize,
72 sig_placements: Vec<sig_overlay::SigPlacement>,
74}
75
76impl TypstSession {
77 pub fn page_size_pt(&self, page: usize) -> Option<(f32, f32)> {
81 let frame = &self.document.pages.get(page)?.frame;
82 let size = frame.size();
83 Some((size.x.to_pt() as f32, size.y.to_pt() as f32))
84 }
85
86 pub fn render_rgba(&self, page: usize, scale: f32) -> Option<(u32, u32, Vec<u8>)> {
93 let p = self.document.pages.get(page)?;
94 let pixmap = typst_render::render(p, scale);
95 let width = pixmap.width();
96 let height = pixmap.height();
97 let mut rgba = Vec::with_capacity((width as usize) * (height as usize) * 4);
98 for px in pixmap.pixels() {
99 let c = px.demultiply();
100 rgba.push(c.red());
101 rgba.push(c.green());
102 rgba.push(c.blue());
103 rgba.push(c.alpha());
104 }
105 Some((width, height, rgba))
106 }
107}
108
109impl SessionHandle for TypstSession {
110 fn render(&self, opts: &RenderOptions) -> Result<RenderResult, RenderError> {
111 let format = opts.output_format.unwrap_or(OutputFormat::Pdf);
112
113 if !SUPPORTED_FORMATS.contains(&format) {
114 return Err(RenderError::FormatNotSupported {
115 diag: Box::new(
116 Diagnostic::new(
117 Severity::Error,
118 format!("{:?} not supported by typst backend", format),
119 )
120 .with_code("backend::format_not_supported".to_string())
121 .with_hint(format!("Supported formats: {:?}", SUPPORTED_FORMATS)),
122 ),
123 });
124 }
125
126 compile::render_document_pages(
127 &self.document,
128 opts.pages.as_deref(),
129 format,
130 opts.ppi,
131 &self.sig_placements,
132 )
133 }
134
135 fn page_count(&self) -> usize {
136 self.page_count
137 }
138
139 fn as_any(&self) -> &dyn Any {
140 self
141 }
142}
143
144pub fn typst_session_of(session: &RenderSession) -> Option<&TypstSession> {
151 session.handle().as_any().downcast_ref::<TypstSession>()
152}
153
154impl Backend for TypstBackend {
155 fn id(&self) -> &'static str {
156 "typst"
157 }
158
159 fn supported_formats(&self) -> &'static [OutputFormat] {
160 SUPPORTED_FORMATS
161 }
162
163 fn open(
164 &self,
165 plate_content: &str,
166 source: &QuillSource,
167 json_data: &serde_json::Value,
168 ) -> Result<RenderSession, RenderError> {
169 let fields = json_data.as_object().map_or_else(HashMap::new, |obj| {
170 obj.iter()
171 .map(|(key, value)| (key.clone(), QuillValue::from_json(value.clone())))
172 .collect::<HashMap<_, _>>()
173 });
174
175 let transformed_fields =
176 transform_markdown_fields(&fields, &build_transform_schema(source.config()));
177 let transformed_json = serde_json::Value::Object(
178 transformed_fields
179 .into_iter()
180 .map(|(key, value)| (key, value.into_json()))
181 .collect(),
182 );
183
184 let json_str =
185 serde_json::to_string(&transformed_json).unwrap_or_else(|_| "{}".to_string());
186 let document = compile::compile_to_document(source, plate_content, &json_str)?;
187 let page_count = document.pages.len();
188 let sig_placements = sig_overlay::extract(&document)?;
189 let session = TypstSession {
190 document,
191 page_count,
192 sig_placements,
193 };
194 Ok(RenderSession::new(Box::new(session)))
195 }
196}
197
198impl Default for TypstBackend {
199 fn default() -> Self {
201 Self
202 }
203}
204
205fn is_markdown_field(field_schema: &serde_json::Value) -> bool {
210 field_schema
211 .get("contentMediaType")
212 .and_then(|v| v.as_str())
213 .map(|s| s == "text/markdown")
214 .unwrap_or(false)
215}
216
217fn is_date_field(field_schema: &serde_json::Value) -> bool {
223 let is_string = field_schema
224 .get("type")
225 .and_then(|v| v.as_str())
226 .map(|s| s == "string")
227 .unwrap_or(false);
228
229 let is_date_format = field_schema
230 .get("format")
231 .and_then(|v| v.as_str())
232 .map(|s| s == "date")
233 .unwrap_or(false);
234
235 is_string && is_date_format
236}
237
238fn transform_markdown_fields(
248 fields: &HashMap<String, QuillValue>,
249 schema: &QuillValue,
250) -> HashMap<String, QuillValue> {
251 let mut result = fields.clone();
252 let schema_json = schema.as_json();
253
254 let properties_obj = match schema_json.get("properties").and_then(|v| v.as_object()) {
256 Some(obj) => obj,
257 None => return result,
258 };
259
260 let mut content_field_names: Vec<&str> = Vec::new();
262 for (field_name, field_value) in fields {
263 if let Some(field_schema) = properties_obj.get(field_name) {
264 if is_markdown_field(field_schema) {
265 if let Some(content) = field_value.as_str() {
266 if let Ok(typst_markup) = mark_to_typst(content) {
267 result.insert(
268 field_name.clone(),
269 QuillValue::from_json(serde_json::json!(typst_markup)),
270 );
271 content_field_names.push(field_name);
272 }
273 }
274 }
275 }
276 }
277
278 let date_fields: Vec<&str> = properties_obj
279 .iter()
280 .filter(|(_, fs)| is_date_field(fs))
281 .map(|(name, _)| name.as_str())
282 .collect();
283
284 if let Some(cards_value) = result.get("CARDS") {
286 if let Some(cards_array) = cards_value.as_array() {
287 let transformed_cards = transform_cards_array(schema, cards_array);
288 result.insert(
289 "CARDS".to_string(),
290 QuillValue::from_json(serde_json::Value::Array(transformed_cards)),
291 );
292 }
293 }
294
295 let mut card_content_fields = serde_json::Map::new();
297 let mut card_date_fields = serde_json::Map::new();
298 if let Some(defs) = schema_json.get("$defs").and_then(|v| v.as_object()) {
299 for (def_name, def_schema) in defs {
300 if let Some(card_type) = def_name.strip_suffix("_card") {
301 let card_fields: Vec<&str> = def_schema
302 .get("properties")
303 .and_then(|v| v.as_object())
304 .map(|props| {
305 props
306 .iter()
307 .filter(|(_, fs)| is_markdown_field(fs))
308 .map(|(name, _)| name.as_str())
309 .collect()
310 })
311 .unwrap_or_default();
312 if !card_fields.is_empty() {
313 card_content_fields.insert(
314 card_type.to_string(),
315 serde_json::Value::Array(
316 card_fields
317 .into_iter()
318 .map(|s| serde_json::Value::String(s.to_string()))
319 .collect(),
320 ),
321 );
322 }
323
324 let date_fields: Vec<&str> = def_schema
325 .get("properties")
326 .and_then(|v| v.as_object())
327 .map(|props| {
328 props
329 .iter()
330 .filter(|(_, fs)| is_date_field(fs))
331 .map(|(name, _)| name.as_str())
332 .collect()
333 })
334 .unwrap_or_default();
335 if !date_fields.is_empty() {
336 card_date_fields.insert(
337 card_type.to_string(),
338 serde_json::Value::Array(
339 date_fields
340 .into_iter()
341 .map(|s| serde_json::Value::String(s.to_string()))
342 .collect(),
343 ),
344 );
345 }
346 }
347 }
348 }
349
350 result.insert(
352 "__meta__".to_string(),
353 QuillValue::from_json(serde_json::json!({
354 "content_fields": content_field_names,
355 "card_content_fields": card_content_fields,
356 "date_fields": date_fields,
357 "card_date_fields": card_date_fields,
358 })),
359 );
360
361 result
362}
363
364fn transform_cards_array(
366 document_schema: &QuillValue,
367 cards_array: &[serde_json::Value],
368) -> Vec<serde_json::Value> {
369 let mut transformed_cards = Vec::new();
370
371 let defs = document_schema
373 .as_json()
374 .get("$defs")
375 .and_then(|v| v.as_object());
376
377 for card in cards_array {
378 if let Some(card_obj) = card.as_object() {
379 if let Some(card_type) = card_obj.get("CARD").and_then(|v| v.as_str()) {
380 let def_name = format!("{}_card", card_type);
382
383 if let Some(card_schema_json) = defs.and_then(|d| d.get(&def_name)) {
385 let mut card_fields: HashMap<String, QuillValue> = HashMap::new();
387 for (k, v) in card_obj {
388 card_fields.insert(k.clone(), QuillValue::from_json(v.clone()));
389 }
390
391 let transformed_card_fields = transform_markdown_fields(
393 &card_fields,
394 &QuillValue::from_json(card_schema_json.clone()),
395 );
396
397 let mut transformed_card_obj = serde_json::Map::new();
399 for (k, v) in transformed_card_fields {
400 transformed_card_obj.insert(k, v.into_json());
401 }
402
403 transformed_cards.push(serde_json::Value::Object(transformed_card_obj));
404 continue;
405 }
406 }
407 }
408
409 transformed_cards.push(card.clone());
411 }
412
413 transformed_cards
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use serde_json::json;
420
421 #[test]
422 fn test_backend_info() {
423 let backend = TypstBackend;
424 assert_eq!(backend.id(), "typst");
425 assert!(backend.supported_formats().contains(&OutputFormat::Pdf));
426 assert!(backend.supported_formats().contains(&OutputFormat::Svg));
427 }
428
429 #[test]
430 fn test_is_markdown_field() {
431 let markdown_schema = json!({
432 "type": "string",
433 "contentMediaType": "text/markdown"
434 });
435 assert!(is_markdown_field(&markdown_schema));
436
437 let string_schema = json!({
438 "type": "string"
439 });
440 assert!(!is_markdown_field(&string_schema));
441
442 let other_media_type = json!({
443 "type": "string",
444 "contentMediaType": "text/plain"
445 });
446 assert!(!is_markdown_field(&other_media_type));
447 }
448
449 #[test]
450 fn test_is_date_field() {
451 let date_schema = json!({
452 "type": "string",
453 "format": "date"
454 });
455 assert!(is_date_field(&date_schema));
456
457 let date_time_schema = json!({
458 "type": "string",
459 "format": "date-time"
460 });
461 assert!(!is_date_field(&date_time_schema));
462
463 let non_string_date_schema = json!({
464 "type": "number",
465 "format": "date"
466 });
467 assert!(!is_date_field(&non_string_date_schema));
468 }
469
470 #[test]
471 fn test_transform_markdown_fields_basic() {
472 let schema = QuillValue::from_json(json!({
473 "type": "object",
474 "properties": {
475 "title": { "type": "string" },
476 "BODY": { "type": "string", "contentMediaType": "text/markdown" }
477 }
478 }));
479
480 let mut fields = HashMap::new();
481 fields.insert(
482 "title".to_string(),
483 QuillValue::from_json(json!("My Title")),
484 );
485 fields.insert(
486 "BODY".to_string(),
487 QuillValue::from_json(json!("This is **bold** text.")),
488 );
489
490 let result = transform_markdown_fields(&fields, &schema);
491
492 assert_eq!(result.get("title").unwrap().as_str(), Some("My Title"));
494
495 let body = result.get("BODY").unwrap().as_str().unwrap();
497 assert!(body.contains("#strong[bold]"));
498 }
499
500 #[test]
501 fn test_transform_markdown_fields_no_markdown() {
502 let schema = QuillValue::from_json(json!({
503 "type": "object",
504 "properties": {
505 "title": { "type": "string" },
506 "count": { "type": "number" }
507 }
508 }));
509
510 let mut fields = HashMap::new();
511 fields.insert(
512 "title".to_string(),
513 QuillValue::from_json(json!("My Title")),
514 );
515 fields.insert("count".to_string(), QuillValue::from_json(json!(42)));
516
517 let result = transform_markdown_fields(&fields, &schema);
518
519 assert_eq!(result.get("title").unwrap().as_str(), Some("My Title"));
521 assert_eq!(result.get("count").unwrap().as_i64(), Some(42));
522 }
523
524 #[test]
525 fn test_transform_markdown_fields_wrapper() {
526 let schema = QuillValue::from_json(json!({
527 "type": "object",
528 "properties": {
529 "BODY": { "type": "string", "contentMediaType": "text/markdown" }
530 }
531 }));
532
533 let mut fields = HashMap::new();
534 fields.insert(
535 "BODY".to_string(),
536 QuillValue::from_json(json!("_italic_ text")),
537 );
538
539 let result = transform_markdown_fields(&fields, &schema);
540
541 let body = result.get("BODY").unwrap().as_str().unwrap();
542 assert!(body.contains("#emph[italic]"));
543 }
544
545 #[test]
546 fn test_transform_markdown_fields_collects_top_level_date_metadata() {
547 let schema = QuillValue::from_json(json!({
548 "type": "object",
549 "properties": {
550 "title": { "type": "string" },
551 "date": { "type": "string", "format": "date" },
552 "timestamp": { "type": "string", "format": "date-time" }
553 }
554 }));
555
556 let mut fields = HashMap::new();
557 fields.insert(
558 "title".to_string(),
559 QuillValue::from_json(json!("My Title")),
560 );
561
562 let result = transform_markdown_fields(&fields, &schema);
563 let meta = result.get("__meta__").expect("missing __meta__").as_json();
564
565 assert_eq!(meta["date_fields"], json!(["date"]));
566 }
567
568 #[test]
569 fn test_transform_markdown_fields_collects_card_date_metadata() {
570 let schema = QuillValue::from_json(json!({
571 "type": "object",
572 "properties": {},
573 "$defs": {
574 "indorsement_card": {
575 "type": "object",
576 "properties": {
577 "date": { "type": "string", "format": "date" },
578 "created_at": { "type": "string", "format": "date-time" },
579 "BODY": { "type": "string", "contentMediaType": "text/markdown" }
580 }
581 }
582 }
583 }));
584
585 let fields = HashMap::new();
586 let result = transform_markdown_fields(&fields, &schema);
587 let meta = result.get("__meta__").expect("missing __meta__").as_json();
588
589 assert_eq!(meta["card_date_fields"]["indorsement"], json!(["date"]));
590 }
591}