1use std::collections::{BTreeMap, HashMap, HashSet};
59
60use serde_json::Value;
61
62use crate::document::{ContentLayers, DoclingDocument};
63use crate::markdown::ImageMode;
64
65pub const PANDOC_API_VERSION: [u32; 4] = [1, 23, 1, 1];
67
68pub type PandocOutput = (String, Vec<(String, Vec<u8>)>);
71
72#[derive(Debug, Clone)]
74pub struct PandocExportOptions {
75 pub image_mode: ImageMode,
77 pub artifacts_dir: String,
80 pub layers: ContentLayers,
82 pub api_version: Option<String>,
85}
86
87impl Default for PandocExportOptions {
88 fn default() -> Self {
89 Self {
90 image_mode: ImageMode::Placeholder,
91 artifacts_dir: "artifacts".to_string(),
92 layers: ContentLayers::BODY,
93 api_version: None,
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum PandocError {
101 UnsupportedApiVersion { requested: String },
103}
104
105impl std::fmt::Display for PandocError {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 match self {
108 PandocError::UnsupportedApiVersion { requested } => write!(
109 f,
110 "unsupported Pandoc API version '{requested}': only {} (pandoc-types {}, Pandoc 3.x) is supported",
111 version_string(&PANDOC_API_VERSION[..2]),
112 version_string(&PANDOC_API_VERSION),
113 ),
114 }
115 }
116}
117
118impl std::error::Error for PandocError {}
119
120fn version_string(parts: &[u32]) -> String {
121 parts
122 .iter()
123 .map(u32::to_string)
124 .collect::<Vec<_>>()
125 .join(".")
126}
127
128pub fn check_api_version(requested: &str) -> Result<(), PandocError> {
132 let err = || PandocError::UnsupportedApiVersion {
133 requested: requested.to_string(),
134 };
135 let parts: Vec<u32> = requested
136 .trim()
137 .split('.')
138 .map(|p| p.parse::<u32>().map_err(|_| err()))
139 .collect::<Result<_, _>>()?;
140 if parts.len() < 2 || parts[..2] != PANDOC_API_VERSION[..2] {
141 return Err(err());
142 }
143 if parts.len() > PANDOC_API_VERSION.len()
144 || parts
145 .iter()
146 .zip(PANDOC_API_VERSION.iter())
147 .skip(2)
148 .any(|(a, b)| a > b)
149 {
150 return Err(err());
152 }
153 Ok(())
154}
155
156pub fn to_pandoc(
160 doc: &DoclingDocument,
161 options: &PandocExportOptions,
162) -> Result<PandocOutput, PandocError> {
163 from_docling_json(&doc.export_to_json_value(), options)
164}
165
166pub fn from_docling_json(
170 json: &Value,
171 options: &PandocExportOptions,
172) -> Result<PandocOutput, PandocError> {
173 if let Some(v) = &options.api_version {
174 check_api_version(v)?;
175 }
176 let render = || {
179 let mut ser = Serializer::new(json, options);
180 let blocks = ser.body();
181 let doc = ast::document(blocks);
182 (
183 serde_json::to_string(&doc).expect("Pandoc JSON is always serializable"),
184 ser.artifacts,
185 )
186 };
187 #[cfg(not(target_arch = "wasm32"))]
188 {
189 Ok(std::thread::scope(|scope| {
190 std::thread::Builder::new()
191 .name("docling-pandoc".into())
192 .stack_size(256 << 20)
193 .spawn_scoped(scope, render)
194 .expect("spawn the pandoc serializer thread")
195 .join()
196 .expect("pandoc serializer thread panicked")
197 }))
198 }
199 #[cfg(target_arch = "wasm32")]
200 {
201 Ok(render())
202 }
203}
204
205pub mod ast {
208 use serde_json::{json, Value};
209
210 use super::PANDOC_API_VERSION;
211
212 pub fn document(blocks: Vec<Value>) -> Value {
213 json!({ "pandoc-api-version": PANDOC_API_VERSION, "meta": {}, "blocks": blocks })
214 }
215
216 pub fn attr(classes: &[&str]) -> Value {
218 json!(["", classes, []])
219 }
220
221 fn node(t: &str, c: Value) -> Value {
222 json!({ "t": t, "c": c })
223 }
224
225 pub fn para(inlines: Vec<Value>) -> Value {
228 node("Para", json!(inlines))
229 }
230 pub fn plain(inlines: Vec<Value>) -> Value {
231 node("Plain", json!(inlines))
232 }
233 pub fn header(level: usize, inlines: Vec<Value>) -> Value {
234 node("Header", json!([level, attr(&[]), inlines]))
235 }
236 pub fn code_block(language: Option<&str>, text: &str) -> Value {
237 let classes: Vec<&str> = language.into_iter().collect();
238 node("CodeBlock", json!([attr(&classes), text]))
239 }
240 pub fn bullet_list(items: Vec<Vec<Value>>) -> Value {
241 node("BulletList", json!(items))
242 }
243 pub fn ordered_list(start: u64, items: Vec<Vec<Value>>) -> Value {
244 node(
245 "OrderedList",
246 json!([[start, { "t": "Decimal" }, { "t": "Period" }], items]),
247 )
248 }
249 pub fn definition_list(entries: Vec<(Vec<Value>, Vec<Vec<Value>>)>) -> Value {
250 node(
251 "DefinitionList",
252 Value::Array(
253 entries
254 .into_iter()
255 .map(|(term, defs)| json!([term, defs]))
256 .collect(),
257 ),
258 )
259 }
260 pub fn raw_block(format: &str, text: &str) -> Value {
263 node("RawBlock", json!([format, text]))
264 }
265 pub fn div(classes: &[&str], blocks: Vec<Value>) -> Value {
266 node("Div", json!([attr(classes), blocks]))
267 }
268 pub fn caption(blocks: Vec<Value>) -> Value {
270 json!([null, blocks])
271 }
272 pub fn figure(classes: &[&str], caption: Value, blocks: Vec<Value>) -> Value {
273 node("Figure", json!([attr(classes), caption, blocks]))
274 }
275 pub fn cell(row_span: usize, col_span: usize, blocks: Vec<Value>) -> Value {
277 json!([attr(&[]), { "t": "AlignDefault" }, row_span, col_span, blocks])
278 }
279 pub fn row(cells: Vec<Value>) -> Value {
280 json!([attr(&[]), cells])
281 }
282 pub fn table(caption: Value, num_cols: usize, head: Vec<Value>, body: Vec<Value>) -> Value {
284 let colspecs: Vec<Value> = (0..num_cols)
285 .map(|_| json!([{ "t": "AlignDefault" }, { "t": "ColWidthDefault" }]))
286 .collect();
287 node(
288 "Table",
289 json!([
290 attr(&[]),
291 caption,
292 colspecs,
293 [attr(&[]), head],
294 [[attr(&[]), 0, [], body]],
295 [attr(&[]), []]
296 ]),
297 )
298 }
299
300 pub fn str_(s: &str) -> Value {
303 node("Str", json!(s))
304 }
305 pub fn space() -> Value {
306 json!({ "t": "Space" })
307 }
308 pub fn line_break() -> Value {
309 json!({ "t": "LineBreak" })
310 }
311 pub fn wrap(t: &str, inlines: Vec<Value>) -> Value {
313 node(t, json!(inlines))
314 }
315 pub fn code(text: &str) -> Value {
316 node("Code", json!([attr(&[]), text]))
317 }
318 pub fn math(display: bool, tex: &str) -> Value {
319 let kind = if display { "DisplayMath" } else { "InlineMath" };
320 node("Math", json!([{ "t": kind }, tex]))
321 }
322 pub fn link(inlines: Vec<Value>, url: &str) -> Value {
323 node("Link", json!([attr(&[]), inlines, [url, ""]]))
324 }
325 pub fn image(alt: Vec<Value>, src: &str) -> Value {
326 node("Image", json!([attr(&[]), alt, [src, ""]]))
327 }
328 pub fn note(blocks: Vec<Value>) -> Value {
329 node("Note", json!(blocks))
330 }
331}
332
333fn text_inlines(text: &str) -> Vec<Value> {
337 let mut out = Vec::new();
338 for (i, line) in text.trim().split('\n').enumerate() {
339 if i > 0 {
340 out.push(ast::line_break());
341 }
342 let mut first = true;
343 for word in line
344 .split(|c: char| c.is_whitespace() && c != '\u{a0}')
345 .filter(|w| !w.is_empty())
346 {
347 if !first {
348 out.push(ast::space());
349 }
350 out.push(ast::str_(word));
351 first = false;
352 }
353 }
354 out
355}
356
357fn join_inlines(parts: Vec<Vec<Value>>) -> Vec<Value> {
363 fn edge_char(inlines: &[Value], first: bool) -> Option<char> {
364 let node = if first {
365 inlines.first()
366 } else {
367 inlines.last()
368 }?;
369 match node.get("t").and_then(Value::as_str)? {
370 "Str" => {
371 let s = node.get("c")?.as_str()?;
372 if first {
373 s.chars().next()
374 } else {
375 s.chars().last()
376 }
377 }
378 "Strong" | "Emph" | "Underline" | "Strikeout" | "Subscript" | "Superscript" => {
380 edge_char(node.get("c")?.as_array()?, first)
381 }
382 "Link" => edge_char(node.get("c")?.get(1)?.as_array()?, first),
383 _ => None,
384 }
385 }
386 let mut out: Vec<Value> = Vec::new();
387 for part in parts.into_iter().filter(|p| !p.is_empty()) {
388 if !out.is_empty() {
389 let closes = edge_char(&part, true)
390 .is_some_and(|c| ".,;:!?)]}%\u{bb}\u{201d}\u{2019}".contains(c));
391 let opens = edge_char(&out, false).is_some_and(|c| "([{\u{ab}\u{201c}".contains(c));
392 if !closes && !opens {
393 out.push(ast::space());
394 }
395 }
396 out.extend(part);
397 }
398 out
399}
400
401fn marker_start(marker: &str) -> Option<u64> {
403 marker
404 .trim()
405 .trim_end_matches(['.', ')'])
406 .parse::<u64>()
407 .ok()
408}
409
410struct GridCell<'a> {
411 cell: &'a Value,
412 start_row: usize,
413 start_col: usize,
414 row_span: usize,
415 col_span: usize,
416}
417
418struct Serializer<'a> {
419 json: &'a Value,
420 image_mode: ImageMode,
421 artifacts_dir: String,
422 layers: ContentLayers,
423 artifacts: Vec<(String, Vec<u8>)>,
424 pic_index: usize,
425 visited: HashSet<String>,
426 owned_captions: HashSet<String>,
429 owned_footnotes: HashSet<String>,
430}
431
432impl<'a> Serializer<'a> {
433 fn new(json: &'a Value, options: &PandocExportOptions) -> Self {
434 let mut owned_captions = HashSet::new();
435 let mut owned_footnotes = HashSet::new();
436 for bucket in [
437 "pictures",
438 "tables",
439 "key_value_items",
440 "form_items",
441 "texts",
442 ] {
443 for item in Self::array(json.get(bucket)) {
444 for (key, set) in [
445 ("captions", &mut owned_captions),
446 ("footnotes", &mut owned_footnotes),
447 ] {
448 for r in Self::array(item.get(key)) {
449 if let Some(cref) = r.get("$ref").and_then(Value::as_str) {
450 set.insert(cref.to_string());
451 }
452 }
453 }
454 }
455 }
456 Self {
457 json,
458 image_mode: options.image_mode,
459 artifacts_dir: options.artifacts_dir.clone(),
460 layers: options.layers,
461 artifacts: Vec::new(),
462 pic_index: 0,
463 visited: HashSet::new(),
464 owned_captions,
465 owned_footnotes,
466 }
467 }
468
469 fn array(v: Option<&'a Value>) -> impl Iterator<Item = &'a Value> {
470 v.and_then(Value::as_array).into_iter().flatten()
471 }
472
473 fn resolve(&self, cref: &str) -> Option<&'a Value> {
474 let rest = cref.strip_prefix("#/")?;
475 if rest == "body" {
476 return self.json.get("body");
477 }
478 let (bucket, idx) = rest.split_once('/')?;
479 self.json.get(bucket)?.get(idx.parse::<usize>().ok()?)
480 }
481
482 fn refs(v: Option<&'a Value>) -> Vec<&'a str> {
483 Self::array(v)
484 .filter_map(|r| r.get("$ref").and_then(Value::as_str))
485 .collect()
486 }
487
488 fn children(item: &'a Value) -> Vec<&'a str> {
489 Self::refs(item.get("children"))
490 }
491
492 fn self_ref(item: &Value) -> &str {
493 item.get("self_ref").and_then(Value::as_str).unwrap_or("")
494 }
495
496 fn label(item: &Value) -> &str {
497 item.get("label").and_then(Value::as_str).unwrap_or("")
498 }
499
500 fn text(item: &Value) -> &str {
501 item.get("text").and_then(Value::as_str).unwrap_or("")
502 }
503
504 fn is_group(item: &Value) -> bool {
505 Self::self_ref(item).starts_with("#/groups/")
506 }
507
508 fn excluded(&self, item: &Value) -> bool {
509 let layer = item
510 .get("content_layer")
511 .and_then(Value::as_str)
512 .unwrap_or("body");
513 !self.layers.contains_name(layer)
514 }
515
516 fn descendants(&self, root: &'a Value, out: &mut Vec<&'a Value>) {
521 let picture = Self::self_ref(root).starts_with("#/pictures/");
522 let captions = if picture {
523 Self::refs(root.get("captions"))
524 } else {
525 Vec::new()
526 };
527 for cref in Self::children(root) {
528 if picture && !captions.contains(&cref) {
529 continue;
530 }
531 let Some(child) = self.resolve(cref) else {
532 continue;
533 };
534 if !self.excluded(child) {
535 out.push(child);
536 }
537 self.descendants(child, out);
538 }
539 }
540
541 fn blocks_of(&mut self, item: &'a Value) -> Vec<Value> {
545 let mut nodes = Vec::new();
546 self.descendants(item, &mut nodes);
547 let mut out = Vec::new();
548 for node in nodes {
549 if self.visited.insert(Self::self_ref(node).to_string()) {
550 out.extend(self.serialize(node));
551 }
552 }
553 out
554 }
555
556 fn body(&mut self) -> Vec<Value> {
557 let Some(body) = self.json.get("body") else {
558 return Vec::new();
559 };
560 self.visited.insert("#/body".to_string());
561 self.blocks_of(body)
562 }
563
564 fn serialize(&mut self, item: &'a Value) -> Vec<Value> {
566 let sref = Self::self_ref(item);
567 if Self::is_group(item) {
568 return match Self::label(item) {
569 "list" => self.list_group(item),
570 "inline" => {
571 let (inlines, mut rest) = self.inline_group(item);
572 let mut out = Vec::new();
573 if !inlines.is_empty() {
574 out.push(ast::para(inlines));
575 }
576 out.append(&mut rest);
577 out
578 }
579 _ => self.blocks_of(item),
580 };
581 }
582 if sref.starts_with("#/texts/") {
583 if self.owned_captions.contains(sref) || self.owned_footnotes.contains(sref) {
584 return Vec::new();
585 }
586 if self.excluded(item) {
587 return Vec::new();
588 }
589 return self.text_item(item);
590 }
591 if sref.starts_with("#/tables/") {
592 return self.table(item);
593 }
594 if sref.starts_with("#/pictures/") {
595 return self.picture(item);
596 }
597 if sref.starts_with("#/key_value_items/") {
598 return self.graph_item(item, "key-value-region");
599 }
600 if sref.starts_with("#/form_items/") {
601 return self.graph_item(item, "form-container");
602 }
603 if sref.starts_with("#/field_regions/") {
604 return self.field_region(item);
605 }
606 if sref.starts_with("#/field_items/") {
607 let entries = vec![self.field_item(item)];
608 return Self::field_blocks(entries);
609 }
610 Vec::new()
611 }
612
613 fn field_region(&mut self, region: &'a Value) -> Vec<Value> {
617 let mut entries = Vec::new();
618 for cref in Self::children(region) {
619 let Some(child) = self.resolve(cref) else {
620 continue;
621 };
622 if !self.visited.insert(cref.to_string()) {
623 continue;
624 }
625 if cref.starts_with("#/field_items/") {
626 entries.push(self.field_item(child));
627 } else {
628 let mut blocks = if self.excluded(child) {
629 Vec::new()
630 } else {
631 self.serialize(child)
632 };
633 blocks.extend(self.blocks_of(child));
634 entries.push((Vec::new(), blocks));
635 }
636 }
637 let blocks = Self::field_blocks(entries);
638 if blocks.is_empty() {
639 Vec::new()
640 } else {
641 vec![ast::div(&["field-region"], blocks)]
642 }
643 }
644
645 fn field_item(&mut self, field: &'a Value) -> (Vec<Value>, Vec<Value>) {
648 let mut nodes = Vec::new();
649 self.descendants(field, &mut nodes);
650 let (mut term, mut defs) = (Vec::new(), Vec::new());
651 for node in nodes {
652 if !self.visited.insert(Self::self_ref(node).to_string()) {
653 continue;
654 }
655 match Self::label(node) {
656 "marker" | "field_key" => {
657 let inlines = self.formatted(node);
658 if !inlines.is_empty() {
659 term = join_inlines(vec![term, inlines]);
660 }
661 }
662 "field_value" => {
663 let inlines = self.formatted(node);
664 if !inlines.is_empty() {
665 defs.push(ast::plain(inlines));
666 }
667 }
668 _ => defs.extend(self.serialize(node)),
669 }
670 }
671 (term, defs)
672 }
673
674 fn field_blocks(entries: Vec<(Vec<Value>, Vec<Value>)>) -> Vec<Value> {
677 let mut out = Vec::new();
678 let mut run: Vec<(Vec<Value>, Vec<Vec<Value>>)> = Vec::new();
679 for (term, defs) in entries {
680 if term.is_empty() {
681 if !run.is_empty() {
682 out.push(ast::definition_list(std::mem::take(&mut run)));
683 }
684 out.extend(defs);
685 } else {
686 let defs = defs.into_iter().map(|d| vec![d]).collect();
687 run.push((term, defs));
688 }
689 }
690 if !run.is_empty() {
691 out.push(ast::definition_list(run));
692 }
693 out
694 }
695
696 fn own_inlines(&mut self, item: &'a Value) -> (Vec<Value>, Vec<Value>, bool) {
701 let children = Self::children(item);
702 if Self::text(item).is_empty() && children.len() == 1 {
703 if let Some(group) = self
704 .resolve(children[0])
705 .filter(|c| Self::is_group(c) && Self::label(c) == "inline")
706 {
707 self.visited.insert(Self::self_ref(group).to_string());
708 let (inlines, rest) = self.inline_group(group);
709 return (inlines, rest, true);
710 }
711 }
712 (self.formatted(item), Vec::new(), false)
713 }
714
715 fn formatted(&self, item: &Value) -> Vec<Value> {
717 let base = match Self::label(item) {
718 "code" => vec![ast::code(Self::text(item))],
719 "formula" if !Self::text(item).is_empty() => {
720 vec![ast::math(false, Self::text(item).trim())]
721 }
722 _ => text_inlines(Self::text(item)),
723 };
724 Self::decorate(base, item)
725 }
726
727 fn decorate(mut inlines: Vec<Value>, item: &Value) -> Vec<Value> {
728 if inlines.is_empty() {
729 return inlines;
730 }
731 if let Some(f) = item.get("formatting") {
732 let on = |k: &str| f.get(k).and_then(Value::as_bool).unwrap_or(false);
733 if on("bold") {
734 inlines = vec![ast::wrap("Strong", inlines)];
735 }
736 if on("italic") {
737 inlines = vec![ast::wrap("Emph", inlines)];
738 }
739 if on("underline") {
740 inlines = vec![ast::wrap("Underline", inlines)];
741 }
742 if on("strikethrough") {
743 inlines = vec![ast::wrap("Strikeout", inlines)];
744 }
745 match f.get("script").and_then(Value::as_str) {
746 Some("sub") => inlines = vec![ast::wrap("Subscript", inlines)],
747 Some("super") => inlines = vec![ast::wrap("Superscript", inlines)],
748 _ => {}
749 }
750 }
751 if let Some(url) = item.get("hyperlink").and_then(Value::as_str) {
752 inlines = vec![ast::link(inlines, url)];
753 }
754 inlines
755 }
756
757 fn inline_group(&mut self, group: &'a Value) -> (Vec<Value>, Vec<Value>) {
761 let mut nodes = Vec::new();
762 self.descendants(group, &mut nodes);
763 let mut runs: Vec<Vec<Value>> = Vec::new();
764 let mut rest: Vec<Value> = Vec::new();
765 for node in nodes {
766 let r = Self::self_ref(node).to_string();
767 if self.visited.contains(&r) {
768 continue;
769 }
770 self.visited.insert(r.clone());
771 if r.starts_with("#/texts/") {
772 if self.owned_captions.contains(&r) || self.owned_footnotes.contains(&r) {
773 continue;
774 }
775 runs.push(self.formatted(node));
776 continue;
777 }
778 if Self::is_group(node) && Self::label(node) == "inline" {
779 let (inl, mut more) = self.inline_group(node);
780 runs.push(inl);
781 rest.append(&mut more);
782 continue;
783 }
784 rest.extend(self.serialize(node));
785 }
786 (join_inlines(runs), rest)
787 }
788
789 fn text_item(&mut self, item: &'a Value) -> Vec<Value> {
790 let label = Self::label(item);
791 let mut out = Vec::new();
792 match label {
793 "list_item" => {
794 let blocks = self.list_item_blocks(item);
796 if !blocks.is_empty() {
797 out.push(ast::bullet_list(vec![blocks]));
798 }
799 return out;
800 }
801 "code" => {
802 let lang = item
803 .get("code_language")
804 .and_then(Value::as_str)
805 .filter(|l| !l.is_empty() && *l != "unknown")
806 .map(str::to_ascii_lowercase);
807 out.push(ast::code_block(lang.as_deref(), Self::text(item)));
808 }
809 "formula" => {
810 let tex = Self::text(item).trim();
811 if !tex.is_empty() {
812 out.push(ast::para(vec![ast::math(true, tex)]));
813 }
814 }
815 _ => {
816 let (mut inlines, rest, _) = self.own_inlines(item);
817 match label {
818 "title" | "section_header" => {
819 let level = if label == "title" {
820 1
821 } else {
822 (item.get("level").and_then(Value::as_u64).unwrap_or(1) as usize + 1)
823 .min(6)
824 };
825 if !inlines.is_empty() {
826 out.push(ast::header(level, inlines));
827 }
828 }
829 "text" | "paragraph" => {
830 if !inlines.is_empty() {
831 out.push(ast::para(inlines));
832 }
833 }
834 "checkbox_selected" | "checkbox_unselected" => {
835 let mark = if label == "checkbox_selected" {
836 "☒"
837 } else {
838 "☐"
839 };
840 let mut with_mark = vec![ast::str_(mark)];
841 if !inlines.is_empty() {
842 with_mark.push(ast::space());
843 with_mark.append(&mut inlines);
844 }
845 out.push(ast::para(with_mark));
846 }
847 other => {
848 if !inlines.is_empty() {
849 let class = format!("docling-{other}");
850 out.push(ast::div(&[&class], vec![ast::para(inlines)]));
851 }
852 }
853 }
854 out.extend(rest);
855 }
856 }
857 out.extend(self.blocks_of(item));
859 out
860 }
861
862 fn list_item_blocks(&mut self, item: &'a Value) -> Vec<Value> {
865 let (inlines, rest, _) = self.own_inlines(item);
866 let mut blocks = Vec::new();
867 if !inlines.is_empty() {
868 blocks.push(ast::plain(inlines));
869 }
870 blocks.extend(rest);
871 blocks.extend(self.blocks_of(item));
872 blocks
873 }
874
875 fn list_group(&mut self, group: &'a Value) -> Vec<Value> {
876 let first = Self::children(group)
877 .first()
878 .and_then(|r| self.resolve(r))
879 .filter(|c| Self::label(c) == "list_item");
880 let enumerated = first.is_some_and(|c| {
881 c.get("enumerated")
882 .and_then(Value::as_bool)
883 .unwrap_or(false)
884 });
885 let start = first
886 .and_then(|c| c.get("marker").and_then(Value::as_str))
887 .and_then(marker_start)
888 .unwrap_or(1);
889
890 let mut nodes = Vec::new();
891 self.descendants(group, &mut nodes);
892 let mut items: Vec<Vec<Value>> = Vec::new();
893 for node in nodes {
894 let r = Self::self_ref(node).to_string();
895 if !self.visited.insert(r.clone()) {
896 continue;
897 }
898 if Self::label(node) == "list_item"
899 && r.starts_with("#/texts/")
900 && !self.owned_captions.contains(&r)
901 {
902 let blocks = self.list_item_blocks(node);
903 items.push(blocks);
904 continue;
905 }
906 let blocks = self.serialize(node);
909 if blocks.is_empty() {
910 continue;
911 }
912 match items.last_mut() {
913 Some(last) => last.extend(blocks),
914 None => items.push(blocks),
915 }
916 }
917 if items.is_empty() {
918 return Vec::new();
919 }
920 if enumerated {
921 vec![ast::ordered_list(start, items)]
922 } else {
923 vec![ast::bullet_list(items)]
924 }
925 }
926
927 fn caption(&self, item: &'a Value) -> Value {
930 let mut parts: Vec<Vec<Value>> = Vec::new();
931 for cref in Self::refs(item.get("captions")) {
932 if let Some(cap) = self.resolve(cref) {
933 if cref.starts_with("#/texts/") && !self.excluded(cap) {
934 parts.push(text_inlines(Self::text(cap)));
935 }
936 }
937 }
938 let mut inlines = join_inlines(parts);
939 for cref in Self::refs(item.get("footnotes")) {
940 if let Some(note) = self.resolve(cref) {
941 if cref.starts_with("#/texts/") && !self.excluded(note) {
942 let text = text_inlines(Self::text(note));
943 if !text.is_empty() {
944 inlines.push(ast::note(vec![ast::para(text)]));
945 }
946 }
947 }
948 }
949 if inlines.is_empty() {
950 ast::caption(Vec::new())
951 } else {
952 ast::caption(vec![ast::plain(inlines)])
953 }
954 }
955
956 fn grid(data: &'a Value) -> (usize, Vec<Vec<Option<GridCell<'a>>>>) {
958 let num_rows = data.get("num_rows").and_then(Value::as_u64).unwrap_or(0) as usize;
959 let num_cols = data.get("num_cols").and_then(Value::as_u64).unwrap_or(0) as usize;
960 let mut grid: Vec<Vec<Option<GridCell<'a>>>> = (0..num_rows)
961 .map(|_| (0..num_cols).map(|_| None).collect())
962 .collect();
963 for cell in Self::array(data.get("table_cells")) {
964 let get = |k: &str| cell.get(k).and_then(Value::as_u64).unwrap_or(0) as usize;
965 let (r0, c0) = (get("start_row_offset_idx"), get("start_col_offset_idx"));
966 let (r1, c1) = (get("end_row_offset_idx"), get("end_col_offset_idx"));
967 if r0 >= num_rows || c0 >= num_cols {
968 continue;
969 }
970 let (r1, c1) = (r1.clamp(r0 + 1, num_rows), c1.clamp(c0 + 1, num_cols));
971 for row in grid.iter_mut().take(r1).skip(r0) {
972 for slot in row.iter_mut().take(c1).skip(c0) {
973 *slot = Some(GridCell {
974 cell,
975 start_row: r0,
976 start_col: c0,
977 row_span: r1 - r0,
978 col_span: c1 - c0,
979 });
980 }
981 }
982 }
983 (num_cols, grid)
984 }
985
986 fn table_from_data(&mut self, data: &'a Value, caption: Value) -> Option<Value> {
987 let (num_cols, grid) = Self::grid(data);
988 if grid.is_empty() || num_cols == 0 {
989 return None;
990 }
991 let flag = |c: &Value, k: &str| c.get(k).and_then(Value::as_bool).unwrap_or(false);
992 let mut rows: Vec<(bool, Value)> = Vec::new();
993 for (i, row) in grid.iter().enumerate() {
994 let mut cells = Vec::new();
995 let mut all_header = true;
996 let mut anchored = 0;
997 for (j, slot) in row.iter().enumerate() {
998 let Some(g) = slot else {
999 cells.push(ast::cell(1, 1, Vec::new()));
1002 continue;
1003 };
1004 if g.start_row != i || g.start_col != j {
1005 continue;
1006 }
1007 anchored += 1;
1008 all_header &= flag(g.cell, "column_header");
1009 let blocks = match g
1010 .cell
1011 .get("ref")
1012 .and_then(|r| r.get("$ref"))
1013 .and_then(Value::as_str)
1014 {
1015 Some(cref) => match self.resolve(cref) {
1016 Some(target) => {
1017 self.visited.insert(cref.to_string());
1018 self.serialize(target)
1019 }
1020 None => Vec::new(),
1021 },
1022 None => {
1023 let inl =
1024 text_inlines(g.cell.get("text").and_then(Value::as_str).unwrap_or(""));
1025 if inl.is_empty() {
1026 Vec::new()
1027 } else {
1028 vec![ast::plain(inl)]
1029 }
1030 }
1031 };
1032 cells.push(ast::cell(g.row_span, g.col_span, blocks));
1033 }
1034 rows.push((all_header && anchored > 0, ast::row(cells)));
1035 }
1036 let head_len = rows.iter().take_while(|(h, _)| *h).count();
1038 let head_len = if head_len == rows.len() { 0 } else { head_len };
1039 let mut head = Vec::new();
1040 let mut body = Vec::new();
1041 for (i, (_, row)) in rows.into_iter().enumerate() {
1042 if i < head_len {
1043 head.push(row);
1044 } else {
1045 body.push(row);
1046 }
1047 }
1048 Some(ast::table(caption, num_cols, head, body))
1049 }
1050
1051 fn table(&mut self, item: &'a Value) -> Vec<Value> {
1052 let caption = self.caption(item);
1053 if self.excluded(item) {
1054 return Vec::new();
1055 }
1056 match item.get("data") {
1057 Some(data) => self.table_from_data(data, caption).into_iter().collect(),
1058 None => Vec::new(),
1059 }
1060 }
1061
1062 fn picture(&mut self, item: &'a Value) -> Vec<Value> {
1063 if self.excluded(item) {
1064 return Vec::new();
1065 }
1066 let caption = self.caption(item);
1067 let mut blocks = Vec::new();
1068 let uri = item
1069 .get("image")
1070 .and_then(|i| i.get("uri"))
1071 .and_then(Value::as_str);
1072 let src = match (self.image_mode, uri) {
1073 (ImageMode::Embedded, Some(uri)) if uri.starts_with("data:") => Some(uri.to_string()),
1074 (ImageMode::Referenced, Some(uri)) => self
1075 .reference_image(uri)
1076 .or_else(|| (!uri.starts_with("data:")).then(|| uri.to_string())),
1077 _ => None,
1078 };
1079 if let Some(src) = src {
1080 blocks.push(ast::plain(vec![ast::image(Vec::new(), &src)]));
1081 }
1082 if let Some(chart) = item
1084 .get("meta")
1085 .and_then(|m| m.get("tabular_chart"))
1086 .and_then(|t| t.get("chart_data"))
1087 {
1088 if let Some(t) = self.table_from_data(chart, ast::caption(Vec::new())) {
1089 blocks.push(t);
1090 }
1091 }
1092 let has_caption = caption
1093 .get(1)
1094 .and_then(Value::as_array)
1095 .is_some_and(|b| !b.is_empty());
1096 if blocks.is_empty() && !has_caption {
1097 return vec![ast::raw_block("html", "<!-- image -->")];
1102 }
1103 vec![ast::figure(&[], caption, blocks)]
1104 }
1105
1106 fn reference_image(&mut self, uri: &str) -> Option<String> {
1109 let rest = uri.strip_prefix("data:")?;
1110 let (mime, payload) = rest.split_once(";base64,")?;
1111 let bytes = crate::base64::decode(payload)?;
1112 let path = format!(
1113 "{}/image_{:06}.{}",
1114 self.artifacts_dir,
1115 self.pic_index,
1116 crate::markdown::ext_for(mime)
1117 );
1118 self.pic_index += 1;
1119 self.artifacts.push((path.clone(), bytes));
1120 Some(path)
1121 }
1122
1123 fn graph_item(&mut self, item: &'a Value, class: &str) -> Vec<Value> {
1128 let mut out = Vec::new();
1129 if !self.excluded(item) {
1130 if let Some(graph) = item.get("graph") {
1131 if let Some(block) = Self::graph(graph) {
1132 out.push(ast::div(&[class], vec![block]));
1133 }
1134 }
1135 }
1136 let caption = self.caption(item);
1138 for block in caption
1139 .get(1)
1140 .and_then(Value::as_array)
1141 .into_iter()
1142 .flatten()
1143 {
1144 if let Some(inlines) = block.get("c").and_then(Value::as_array) {
1145 out.push(ast::para(inlines.clone()));
1146 }
1147 }
1148 out
1149 }
1150
1151 fn graph(graph: &'a Value) -> Option<Value> {
1152 let cells: HashMap<u64, &Value> = Self::array(graph.get("cells"))
1153 .filter_map(|c| Some((c.get("cell_id")?.as_u64()?, c)))
1154 .collect();
1155 let order: Vec<u64> = Self::array(graph.get("cells"))
1156 .filter_map(|c| c.get("cell_id")?.as_u64())
1157 .collect();
1158 let mut child_links: HashMap<u64, Vec<u64>> = HashMap::new();
1159 let mut value_links: BTreeMap<u64, Vec<u64>> = BTreeMap::new();
1160 let mut value_order: Vec<u64> = Vec::new();
1161 let mut parents: HashSet<u64> = HashSet::new();
1162 for link in Self::array(graph.get("links")) {
1163 let (Some(s), Some(t)) = (
1164 link.get("source_cell_id").and_then(Value::as_u64),
1165 link.get("target_cell_id").and_then(Value::as_u64),
1166 ) else {
1167 continue;
1168 };
1169 if !cells.contains_key(&s) || !cells.contains_key(&t) {
1170 continue;
1171 }
1172 match link.get("label").and_then(Value::as_str) {
1173 Some("to_child") => {
1174 child_links.entry(s).or_default().push(t);
1175 parents.insert(t);
1176 }
1177 Some("to_value") => {
1178 if !value_links.contains_key(&s) {
1179 value_order.push(s);
1180 }
1181 value_links.entry(s).or_default().push(t);
1182 }
1183 _ => {}
1184 }
1185 }
1186 let cell_text =
1187 |id: u64| text_inlines(cells[&id].get("text").and_then(Value::as_str).unwrap_or(""));
1188 if child_links.is_empty() {
1189 if value_order.is_empty() {
1190 return None;
1191 }
1192 let entries = value_order
1193 .iter()
1194 .map(|&k| {
1195 let defs = value_links[&k]
1196 .iter()
1197 .map(|&v| vec![ast::plain(cell_text(v))])
1198 .collect();
1199 (cell_text(k), defs)
1200 })
1201 .collect();
1202 return Some(ast::definition_list(entries));
1203 }
1204 fn node(
1206 id: u64,
1207 cell_text: &dyn Fn(u64) -> Vec<Value>,
1208 child_links: &HashMap<u64, Vec<u64>>,
1209 value_links: &BTreeMap<u64, Vec<u64>>,
1210 path: &mut HashSet<u64>,
1211 ) -> Option<Vec<Value>> {
1212 if !path.insert(id) {
1213 return None;
1214 }
1215 let mut inlines = cell_text(id);
1216 if let Some(values) = value_links.get(&id) {
1217 let vals = join_inlines(values.iter().map(|&v| cell_text(v)).collect());
1218 inlines = vec![ast::wrap("Strong", inlines)];
1219 inlines.push(ast::str_(":"));
1220 if !vals.is_empty() {
1221 inlines.push(ast::space());
1222 inlines.extend(vals);
1223 }
1224 }
1225 let mut blocks = vec![ast::plain(inlines)];
1226 let kids: Vec<Vec<Value>> = child_links
1227 .get(&id)
1228 .into_iter()
1229 .flatten()
1230 .filter_map(|&c| node(c, cell_text, child_links, value_links, path))
1231 .collect();
1232 if !kids.is_empty() {
1233 blocks.push(ast::bullet_list(kids));
1234 }
1235 path.remove(&id);
1236 Some(blocks)
1237 }
1238 let mut path = HashSet::new();
1239 let items: Vec<Vec<Value>> = order
1240 .iter()
1241 .filter(|id| !parents.contains(id))
1242 .filter_map(|&id| node(id, &cell_text, &child_links, &value_links, &mut path))
1243 .collect();
1244 (!items.is_empty()).then(|| ast::bullet_list(items))
1245 }
1246}
1247
1248#[cfg(test)]
1249mod tests {
1250 use super::*;
1251 use serde_json::json;
1252
1253 fn doc(body: &[&str], texts: Value, groups: Value, tables: Value) -> Value {
1255 json!({
1256 "schema_name": "DoclingDocument",
1257 "name": "t",
1258 "body": {"self_ref": "#/body", "children": body.iter().map(|r| json!({"$ref": r})).collect::<Vec<_>>()},
1259 "texts": texts, "groups": groups, "tables": tables, "pictures": []
1260 })
1261 }
1262
1263 fn text(i: usize, label: &str, text: &str, extra: Value) -> Value {
1264 let mut v = json!({"self_ref": format!("#/texts/{i}"), "children": [], "label": label, "text": text, "content_layer": "body"});
1265 for (k, x) in extra.as_object().unwrap() {
1266 v[k] = x.clone();
1267 }
1268 v
1269 }
1270
1271 fn blocks(json: &Value) -> Vec<Value> {
1272 let (s, _) = from_docling_json(json, &PandocExportOptions::default()).unwrap();
1273 let v: Value = serde_json::from_str(&s).unwrap();
1274 assert_eq!(v["pandoc-api-version"], json!(PANDOC_API_VERSION));
1275 v["blocks"].as_array().unwrap().clone()
1276 }
1277
1278 #[test]
1279 fn text_splits_into_words_spaces_and_line_breaks() {
1280 assert_eq!(
1281 text_inlines(" two words\nnext\u{a0}line "),
1282 vec![
1283 ast::str_("two"),
1284 ast::space(),
1285 ast::str_("words"),
1286 ast::line_break(),
1287 ast::str_("next\u{a0}line"),
1288 ]
1289 );
1290 assert!(text_inlines(" ").is_empty());
1291 }
1292
1293 #[test]
1294 fn headings_paragraphs_and_lists() {
1295 let j = doc(
1296 &["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"],
1297 json!([
1298 text(0, "title", "Ducks", json!({})),
1299 text(1, "section_header", "Diet", json!({"level": 1})),
1300 text(
1301 2,
1302 "text",
1303 "Ducks eat plants.",
1304 json!({"formatting": {"bold": true}, "hyperlink": "https://x.org"})
1305 ),
1306 text(
1307 3,
1308 "list_item",
1309 "seeds",
1310 json!({"enumerated": true, "marker": "3."})
1311 ),
1312 text(
1313 4,
1314 "list_item",
1315 "insects",
1316 json!({"enumerated": true, "marker": "4."})
1317 ),
1318 ]),
1319 json!([{"self_ref": "#/groups/0", "children": [{"$ref": "#/texts/3"}, {"$ref": "#/texts/4"}], "label": "list", "name": "list"}]),
1320 json!([]),
1321 );
1322 let b = blocks(&j);
1323 assert_eq!(b[0], ast::header(1, vec![ast::str_("Ducks")]));
1324 assert_eq!(b[1], ast::header(2, vec![ast::str_("Diet")]));
1325 assert_eq!(b[2]["t"], "Para");
1327 assert_eq!(b[2]["c"][0]["t"], "Link");
1328 assert_eq!(b[2]["c"][0]["c"][1][0]["t"], "Strong");
1329 assert_eq!(b[3]["t"], "OrderedList");
1330 assert_eq!(b[3]["c"][0][0], 3, "start from the first marker");
1331 assert_eq!(
1332 b[3]["c"][1],
1333 json!([
1334 [ast::plain(vec![ast::str_("seeds")])],
1335 [ast::plain(vec![ast::str_("insects")])]
1336 ])
1337 );
1338 }
1339
1340 #[test]
1341 fn code_formulas_checkboxes_and_unmapped_labels() {
1342 let j = doc(
1343 &["#/texts/0", "#/texts/1", "#/texts/2", "#/texts/3"],
1344 json!([
1345 text(0, "code", "print(1)", json!({"code_language": "Python"})),
1346 text(1, "formula", "E = mc^2", json!({})),
1347 text(2, "checkbox_selected", "done", json!({})),
1348 text(3, "reference", "[1] Duck, D. (2020).", json!({})),
1349 ]),
1350 json!([]),
1351 json!([]),
1352 );
1353 let b = blocks(&j);
1354 assert_eq!(b[0], ast::code_block(Some("python"), "print(1)"));
1355 assert_eq!(b[1], ast::para(vec![ast::math(true, "E = mc^2")]));
1356 assert_eq!(b[2]["c"][0], ast::str_("☒"));
1357 assert_eq!(b[3]["t"], "Div");
1358 assert_eq!(b[3]["c"][0][1], json!(["docling-reference"]));
1359 }
1360
1361 #[test]
1362 fn tables_keep_spans_header_rows_and_captions() {
1363 let cell = |r0: usize, c0: usize, rs: usize, cs: usize, t: &str, header: bool| {
1364 json!({"text": t, "row_span": rs, "col_span": cs,
1365 "start_row_offset_idx": r0, "end_row_offset_idx": r0 + rs,
1366 "start_col_offset_idx": c0, "end_col_offset_idx": c0 + cs,
1367 "column_header": header})
1368 };
1369 let j = doc(
1370 &["#/tables/0"],
1371 json!([text(0, "caption", "Table 1: Ducks", json!({}))]),
1372 json!([]),
1373 json!([{"self_ref": "#/tables/0", "children": [{"$ref": "#/texts/0"}], "label": "table",
1374 "captions": [{"$ref": "#/texts/0"}],
1375 "data": {"num_rows": 2, "num_cols": 2, "table_cells": [
1376 cell(0, 0, 1, 2, "Species", true),
1377 cell(1, 0, 1, 1, "Mallard", false),
1378 cell(1, 1, 1, 1, "Anas", false)]}}]),
1379 );
1380 let b = blocks(&j);
1381 assert_eq!(b.len(), 1, "the caption is rendered by its table only");
1382 let t = &b[0];
1383 assert_eq!(t["t"], "Table");
1384 assert_eq!(
1385 t["c"][1][1][0],
1386 ast::plain(vec![
1387 ast::str_("Table"),
1388 ast::space(),
1389 ast::str_("1:"),
1390 ast::space(),
1391 ast::str_("Ducks")
1392 ])
1393 );
1394 assert_eq!(t["c"][2].as_array().unwrap().len(), 2);
1395 let head = t["c"][3][1].as_array().unwrap();
1396 assert_eq!(head.len(), 1);
1397 assert_eq!(head[0][1][0][3], 2, "colspan");
1398 assert_eq!(t["c"][4][0][3].as_array().unwrap().len(), 1, "one body row");
1399 }
1400
1401 #[test]
1402 fn api_version_is_checked() {
1403 for ok in ["1.23", "1.23.1", "1.23.1.1"] {
1404 assert!(check_api_version(ok).is_ok(), "{ok}");
1405 }
1406 for bad in ["1.22", "2.0", "1", "1.23.2", "1.23.1.2", "x.y", ""] {
1407 let e = check_api_version(bad).unwrap_err();
1408 assert!(e.to_string().contains("only 1.23"), "{bad}: {e}");
1409 }
1410 let err = to_pandoc(
1411 &DoclingDocument::new("t"),
1412 &PandocExportOptions {
1413 api_version: Some("1.22".into()),
1414 ..Default::default()
1415 },
1416 )
1417 .unwrap_err();
1418 assert_eq!(
1419 err,
1420 PandocError::UnsupportedApiVersion {
1421 requested: "1.22".into()
1422 }
1423 );
1424 }
1425
1426 #[test]
1427 fn empty_document_is_a_valid_pandoc_document() {
1428 let (s, artifacts) =
1429 to_pandoc(&DoclingDocument::new("e"), &PandocExportOptions::default()).unwrap();
1430 assert!(artifacts.is_empty());
1431 assert_eq!(
1432 s,
1433 r#"{"pandoc-api-version":[1,23,1,1],"meta":{},"blocks":[]}"#
1434 );
1435 assert_eq!(s, DoclingDocument::new("e").export_to_pandoc_json());
1436 }
1437}