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