1use serde_json::{json, Value};
12
13use crate::document::{CaptionParent, ContentLayer, DoclingDocument, Node, Table};
14
15const SCHEMA_VERSION: &str = "1.10.0";
16
17const CODE_LANGUAGES: &[&str] = &[
20 "Ada",
21 "Awk",
22 "Bash",
23 "bc",
24 "C",
25 "C#",
26 "C++",
27 "CMake",
28 "COBOL",
29 "CSS",
30 "Ceylon",
31 "Clojure",
32 "Crystal",
33 "Cuda",
34 "Cython",
35 "D",
36 "Dart",
37 "dc",
38 "Dockerfile",
39 "DocLang",
40 "Elixir",
41 "Erlang",
42 "FORTRAN",
43 "Forth",
44 "Go",
45 "HTML",
46 "Haskell",
47 "Haxe",
48 "Java",
49 "JavaScript",
50 "JSON",
51 "Julia",
52 "Kotlin",
53 "Latex",
54 "Lisp",
55 "Lua",
56 "Matlab",
57 "MoonScript",
58 "Nim",
59 "OCaml",
60 "ObjectiveC",
61 "Octave",
62 "PHP",
63 "Pascal",
64 "Perl",
65 "Prolog",
66 "Python",
67 "Racket",
68 "Ruby",
69 "Rust",
70 "SML",
71 "SQL",
72 "Scala",
73 "Scheme",
74 "Swift",
75 "Tikz",
76 "TypeScript",
77 "VisualBasic",
78 "XML",
79 "YAML",
80];
81
82pub fn code_language_label(lang: &str) -> &'static str {
88 code_language(Some(lang))
89}
90
91pub(crate) fn code_language(lang: Option<&str>) -> &'static str {
92 match lang {
93 Some(l) => CODE_LANGUAGES
94 .iter()
95 .find(|c| c.eq_ignore_ascii_case(l))
96 .copied()
97 .unwrap_or("unknown"),
98 None => "unknown",
99 }
100}
101
102fn formatting_json(f: &crate::tree::Formatting) -> Value {
104 json!({
105 "bold": f.bold,
106 "italic": f.italic,
107 "underline": f.underline,
108 "strikethrough": f.strikethrough,
109 "script": match f.script {
110 crate::Script::Baseline => "baseline",
111 crate::Script::Sub => "sub",
112 crate::Script::Super => "super",
113 },
114 })
115}
116
117pub fn to_json(doc: &DoclingDocument) -> Value {
119 let mut b = Builder::default();
120 let body = match &doc.tree {
124 Some(tree) => b.write_tree(tree),
125 None => b.walk_into(&doc.nodes, "#/body"),
126 };
127 b.link_comments();
128
129 let mut out = json!({
130 "schema_name": "DoclingDocument",
131 "version": SCHEMA_VERSION,
132 "name": doc.name,
133 "origin": {
134 "mimetype": "text/plain",
135 "binary_hash": fnv1a(&doc.name),
136 "filename": doc.name,
137 },
138 "furniture": {
139 "self_ref": "#/furniture",
140 "children": [],
141 "content_layer": "furniture",
142 "name": "_root_",
143 "label": "unspecified",
144 },
145 "body": {
146 "self_ref": "#/body",
147 "children": body,
148 "content_layer": "body",
149 "name": "_root_",
150 "label": "unspecified",
151 },
152 "groups": b.groups,
153 "texts": b.texts,
154 "pictures": b.pictures,
155 "tables": b.tables,
156 "key_value_items": [],
157 "form_items": [],
158 "pages": b.pages.iter().map(|(n, w, h)| {
159 let r2 = |v: f64| (v * 100.0).round() / 100.0;
160 (n.to_string(), json!({
161 "size": { "width": r2(*w), "height": r2(*h) },
162 "page_no": n,
163 }))
164 }).collect::<serde_json::Map<String, Value>>(),
165 });
166
167 if !b.field_regions.is_empty() {
172 if let Some(obj) = out.as_object_mut() {
173 let pages = obj.remove("pages");
174 obj.insert("field_regions".into(), Value::Array(b.field_regions));
175 obj.insert("field_items".into(), Value::Array(b.field_items));
176 if let Some(pages) = pages {
177 obj.insert("pages".into(), pages);
178 }
179 }
180 }
181 out
182}
183
184fn classification_meta(classes: &[crate::PictureClass]) -> Value {
191 json!({
192 "classification": {
193 "predictions": classes.iter().map(|c| json!({
194 "confidence": c.confidence as f64,
195 "created_by": "DocumentPictureClassifier",
196 "class_name": c.class_name,
197 })).collect::<Vec<_>>(),
198 },
199 "annotations": [{
200 "kind": "classification",
201 "provenance": "DocumentPictureClassifier",
202 "predicted_classes": classes.iter().map(|c| json!({
203 "class_name": c.class_name,
204 "confidence": c.confidence as f64,
205 })).collect::<Vec<_>>(),
206 }],
207 })
208}
209
210#[allow(clippy::too_many_arguments)]
221fn cell_value(
222 row_span: usize,
223 col_span: usize,
224 start_row: usize,
225 end_row: usize,
226 start_col: usize,
227 end_col: usize,
228 text: String,
229 column_header: bool,
230 row_header: bool,
231 row_section: bool,
232 bbox: Option<[f32; 4]>,
233) -> Value {
234 let mut m = serde_json::Map::with_capacity(12);
235 m.insert("row_span".into(), row_span.into());
236 m.insert("col_span".into(), col_span.into());
237 m.insert("start_row_offset_idx".into(), start_row.into());
238 m.insert("end_row_offset_idx".into(), end_row.into());
239 m.insert("start_col_offset_idx".into(), start_col.into());
240 m.insert("end_col_offset_idx".into(), end_col.into());
241 m.insert("text".into(), Value::String(text));
242 m.insert("column_header".into(), column_header.into());
243 m.insert("row_header".into(), row_header.into());
244 m.insert("row_section".into(), row_section.into());
245 m.insert("fillable".into(), false.into());
246 if let Some(b) = bbox {
247 m.insert(
248 "bbox".into(),
249 json!({
250 "l": b[0], "t": b[1], "r": b[2], "b": b[3],
251 "coord_origin": "TOPLEFT",
252 }),
253 );
254 }
255 Value::Object(m)
256}
257
258fn table_data(t: &Table) -> Value {
261 table_data_with(t, false)
262}
263
264fn table_data_with(t: &Table, raw: bool) -> Value {
267 let cell_text = |s: &str| {
268 if raw {
269 s.to_string()
270 } else {
271 unescape_text(&crate::markdown::strip_hard_breaks(s))
272 }
273 };
274 let num_rows = t.rows.len();
275 let num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
276 let mut grid = Vec::with_capacity(num_rows);
277 let mut cells = Vec::new();
278 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
282 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
283 for c in first_class {
284 let idx = cells.len();
285 cells.push(cell_value(
286 c.row_span,
287 c.col_span,
288 c.start_row,
289 c.start_row + c.row_span,
290 c.start_col,
291 c.start_col + c.col_span,
292 cell_text(&c.text),
293 c.column_header,
294 c.row_header,
295 c.row_section,
296 c.bbox,
297 ));
298 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
299 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
300 slot[r * num_cols + k] = Some(idx);
301 }
302 }
303 }
304 for r in 0..num_rows {
305 let mut grid_row = Vec::with_capacity(num_cols);
306 for c in 0..num_cols {
307 grid_row.push(match slot[r * num_cols + c] {
308 Some(i) => cells[i].clone(),
309 None => cell_value(
310 1,
311 1,
312 r,
313 r + 1,
314 c,
315 c + 1,
316 String::new(),
317 false,
318 false,
319 false,
320 None,
321 ),
322 });
323 }
324 grid.push(grid_row);
325 }
326 } else {
327 let s = t.structure.as_ref();
328 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
329 grid.and_then(|g| g.get(r))
330 .and_then(|row| row.get(c))
331 .copied()
332 .unwrap_or(false)
333 };
334 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
335 let (mut r0, mut c0) = (r, c);
336 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
337 c0 -= 1;
338 }
339 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
340 r0 -= 1;
341 }
342 (r0, c0)
343 };
344 let anchors: Vec<(usize, usize)> = (0..num_rows)
347 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
348 .map(|(r, c)| anchor_of(r, c))
349 .collect();
350 let mut extent: Vec<(usize, usize)> = (0..num_rows)
351 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
352 .collect();
353 for (i, &(ar, ac)) in anchors.iter().enumerate() {
354 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
355 let e = &mut extent[ar * num_cols + ac];
356 e.0 = e.0.max(r);
357 e.1 = e.1.max(c);
358 }
359 for (r, row) in t.rows.iter().enumerate() {
360 let mut grid_row = Vec::with_capacity(num_cols);
361 for c in 0..num_cols {
362 let (ar, ac) = anchors[r * num_cols + c];
363 if (ar, ac) == (r, c) {
364 let (er, ec) = extent[r * num_cols + c];
365 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
366 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
367 Some(s) => flag(Some(&s.col_header), r, c),
368 None => r == 0,
369 };
370 slot[r * num_cols + c] = Some(cells.len());
371 cells.push(cell_value(
372 er - r + 1,
373 ec - c + 1,
374 r,
375 er + 1,
376 c,
377 ec + 1,
378 text,
379 column_header,
380 flag(s.map(|s| &s.row_header), r, c),
381 false,
382 None,
383 ));
384 }
385 grid_row.push(match slot[ar * num_cols + ac] {
386 Some(i) => cells[i].clone(),
387 None => Value::Null,
388 });
389 }
390 grid.push(grid_row);
391 }
392 }
393 json!({
394 "table_cells": cells,
395 "num_rows": num_rows,
396 "num_cols": num_cols,
397 "orientation": "rot_0",
398 "grid": grid,
399 })
400}
401
402#[derive(Default)]
403struct Builder {
404 texts: Vec<Value>,
405 groups: Vec<Value>,
406 tables: Vec<Value>,
407 pictures: Vec<Value>,
408 field_regions: Vec<Value>,
409 field_items: Vec<Value>,
410 pages: Vec<(usize, f64, f64)>,
414 cur_page: usize,
416 cur_w: f64,
417 cur_h: f64,
418 pending_loc: Option<[u16; 4]>,
421 pending_exact: Option<(usize, [f32; 4], [usize; 2])>,
424 pending_siblings: Vec<Value>,
428 pending_after: Vec<Value>,
432 pending_body: Vec<Value>,
436 comment_groups: Vec<String>,
440 pending_comments: Vec<(String, Vec<usize>)>,
444}
445
446impl Builder {
447 fn take_prov(&mut self, char_len: usize) -> Value {
453 let prov = self.prov_json(char_len, false);
454 self.pending_exact = None;
455 self.pending_loc = None;
456 prov
457 }
458
459 fn prov_json(&self, char_len: usize, span_over_text: bool) -> Value {
464 let r2 = |v: f64| (v * 100.0).round() / 100.0;
465 if let Some((page_no, [l, t, r, b], charspan)) = self.pending_exact {
466 let charspan = if span_over_text {
467 [0, char_len]
468 } else {
469 charspan
470 };
471 return json!([{
472 "page_no": page_no,
473 "bbox": {
474 "l": r2(l as f64), "t": r2(t as f64), "r": r2(r as f64), "b": r2(b as f64),
475 "coord_origin": "TOPLEFT",
476 },
477 "charspan": charspan,
478 }]);
479 }
480 let Some([x0, y0, x1, y1]) = self.pending_loc else {
481 return json!([]);
482 };
483 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
487 return json!([{
488 "page_no": self.cur_page,
489 "bbox": { "l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT" },
490 "charspan": [0, char_len],
491 }]);
492 }
493 json!([{
494 "page_no": self.cur_page,
495 "bbox": {
496 "l": r2(x0 as f64 * self.cur_w / 512.0),
497 "t": r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
498 "r": r2(x1 as f64 * self.cur_w / 512.0),
499 "b": r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
500 "coord_origin": "BOTTOMLEFT",
501 },
502 "charspan": [0, char_len],
503 }])
504 }
505
506 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
509 if self.pending_loc.is_none() && self.cur_page > 0 {
510 self.pending_loc = loc;
511 }
512 }
513
514 fn link_comments(&mut self) {
522 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
523 .into_iter()
524 .map(|(item, comments)| {
525 let refs = comments
526 .iter()
527 .filter_map(|i| self.comment_groups.get(*i))
528 .map(|r| json!({ "$ref": r }))
529 .collect();
530 (item, refs)
531 })
532 .collect();
533 for (item, comment_refs) in refs {
534 if comment_refs.is_empty() {
535 continue;
536 }
537 let Some(target) = self.item_mut(&item) else {
538 continue;
539 };
540 let Some(obj) = target.as_object_mut() else {
541 continue;
542 };
543 let tail: Vec<(String, Value)> = obj
544 .iter()
545 .skip_while(|(k, _)| k.as_str() != "prov")
546 .skip(1)
547 .map(|(k, v)| (k.clone(), v.clone()))
548 .collect();
549 for (k, _) in &tail {
550 obj.shift_remove(k);
551 }
552 obj.insert("comments".into(), Value::Array(comment_refs));
553 for (k, v) in tail {
554 obj.insert(k, v);
555 }
556 }
557 }
558
559 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
561 let idx = ref_index(self_ref)?;
562 let bucket = if self_ref.starts_with("#/texts/") {
563 &mut self.texts
564 } else if self_ref.starts_with("#/tables/") {
565 &mut self.tables
566 } else if self_ref.starts_with("#/pictures/") {
567 &mut self.pictures
568 } else if self_ref.starts_with("#/groups/") {
569 &mut self.groups
570 } else {
571 return None;
572 };
573 bucket.get_mut(idx)
574 }
575
576 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
580 use crate::tree::TreeKind;
581 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
584 let (mut nt, mut ng, mut ntb, mut np, mut nf) = (0, 0, 0, 0, 0);
585 for item in &tree.items {
586 let r = match &item.kind {
587 TreeKind::Text { .. } | TreeKind::Code { .. } => {
588 nt += 1;
589 format!("#/texts/{}", nt - 1)
590 }
591 TreeKind::Group { .. } => {
592 ng += 1;
593 format!("#/groups/{}", ng - 1)
594 }
595 TreeKind::Table { .. } => {
596 ntb += 1;
597 format!("#/tables/{}", ntb - 1)
598 }
599 TreeKind::Picture { .. } => {
600 np += 1;
601 format!("#/pictures/{}", np - 1)
602 }
603 TreeKind::FieldRegion { items } => {
604 nt += items
607 .iter()
608 .map(|i| {
609 [&i.marker, &i.key, &i.value]
610 .iter()
611 .filter(|p| p.is_some())
612 .count()
613 })
614 .sum::<usize>();
615 nf += 1;
616 format!("#/field_regions/{}", nf - 1)
617 }
618 };
619 refs.push(r);
620 }
621 let ref_of = |id: usize| json!({ "$ref": refs[id] });
622 for (id, item) in tree.items.iter().enumerate() {
623 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
624 let children: Vec<Value> = item.children.iter().map(|&c| ref_of(c)).collect();
625 let layer = item.layer.map_or("body", |l| l.value());
626 let self_ref = match &item.kind {
627 TreeKind::Text {
628 label,
629 text,
630 orig,
631 formatting,
632 hyperlink,
633 level,
634 list,
635 } => {
636 let mut tail = serde_json::Map::new();
639 if let Some(f) = formatting {
640 tail.insert("formatting".into(), formatting_json(f));
641 }
642 if let Some(h) = hyperlink {
643 tail.insert("hyperlink".into(), json!(h));
644 }
645 if let Some(l) = level {
646 tail.insert("level".into(), json!(l));
647 }
648 if let Some(l) = list {
649 tail.insert("enumerated".into(), json!(l.enumerated));
650 tail.insert("marker".into(), json!(l.marker));
651 }
652 let r = format!("#/texts/{}", self.texts.len());
653 let mut item_json = json!({
654 "self_ref": r,
655 "parent": { "$ref": parent },
656 "children": children,
657 "content_layer": layer,
658 "label": label,
659 "prov": [],
660 "orig": orig.as_deref().unwrap_or(text),
661 "text": text,
662 });
663 merge(&mut item_json, Value::Object(tail));
664 self.texts.push(item_json);
665 r
666 }
667 TreeKind::Code {
668 text,
669 orig,
670 language,
671 formatting,
672 hyperlink,
673 } => {
674 let r = format!("#/texts/{}", self.texts.len());
675 let mut item_json = json!({
676 "self_ref": r,
677 "parent": { "$ref": parent },
678 "children": children,
679 "content_layer": layer,
680 "label": "code",
681 "prov": [],
682 "orig": orig.as_deref().unwrap_or(text),
683 "text": text,
684 });
685 if let Some(f) = formatting {
686 item_json["formatting"] = formatting_json(f);
687 }
688 if let Some(h) = hyperlink {
689 item_json["hyperlink"] = json!(h);
690 }
691 merge(
692 &mut item_json,
693 json!({
694 "captions": [],
695 "references": [],
696 "footnotes": [],
697 "code_language": code_language(language.as_deref()),
698 }),
699 );
700 self.texts.push(item_json);
701 r
702 }
703 TreeKind::Group { label, name } => {
704 let r = format!("#/groups/{}", self.groups.len());
705 self.groups.push(json!({
706 "self_ref": r,
707 "parent": { "$ref": parent },
708 "children": children,
709 "content_layer": layer,
710 "name": name,
711 "label": label,
712 }));
713 r
714 }
715 TreeKind::Table {
716 table,
717 rich_cells,
718 captions,
719 } => {
720 let r = self.add_table_with(table, parent, true);
722 let idx = ref_index(&r).expect("table ref");
723 let t = &mut self.tables[idx];
724 t["children"] = Value::Array(children);
725 t["content_layer"] = json!(layer);
726 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
727 for &(row, col, group) in rich_cells {
731 let cell_ref = ref_of(group);
732 let hit = |c: &Value| {
733 c["start_row_offset_idx"] == json!(row)
734 && c["start_col_offset_idx"] == json!(col)
735 };
736 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
737 for c in cells.iter_mut().filter(|c| hit(c)) {
738 c["ref"] = cell_ref.clone();
739 }
740 }
741 }
742 r
743 }
744 TreeKind::Picture {
745 captions,
746 image,
747 classification,
748 } => {
749 let meta = classification.as_ref().map(
750 |c| json!({ "classification": { "predictions": [{ "class_name": c }] } }),
751 );
752 let r = self.push_picture(
753 json!([]),
754 captions.iter().map(|&c| ref_of(c)).collect(),
755 children,
756 image.as_ref(),
757 meta,
758 parent,
759 );
760 if let Some(idx) = ref_index(&r) {
761 self.pictures[idx]["content_layer"] = json!(layer);
762 }
763 r
764 }
765 TreeKind::FieldRegion { items } => {
766 let r = self.add_field_region(items, parent);
767 if let Some(region) = self.field_regions.last_mut() {
768 region["content_layer"] = json!(layer);
769 }
770 r
771 }
772 };
773 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
774 }
775 tree.body.iter().map(|&c| ref_of(c)).collect()
776 }
777
778 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
779 match node {
780 Node::Heading { level: 1, text } => {
781 Some(self.add_text("title", text, parent, json!({})))
782 }
783 Node::Heading { level, text } => Some(self.add_text(
784 "section_header",
785 text,
786 parent,
787 json!({ "level": level.saturating_sub(1) }),
788 )),
789 Node::Caption { text, href } => {
790 let extra = match href {
791 Some(url) => json!({ "hyperlink": url }),
792 None => json!({}),
793 };
794 Some(self.add_text("caption", text, parent, extra))
795 }
796 Node::Paragraph { text } => {
797 let t = text.trim();
800 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
801 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
802 _ => Some(self.add_text("text", text, parent, json!({}))),
803 }
804 }
805 Node::CheckboxItem { checked, text } => {
806 let mark = if *checked { "- [x] " } else { "- [ ] " };
809 Some(self.add_text("text", &format!("{mark}{text}"), parent, json!({})))
810 }
811 Node::Code {
812 language,
813 text,
814 orig,
815 ..
816 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
817 Node::Formula {
820 latex,
821 orig,
822 location,
823 } => {
824 self.adopt_loc(*location);
825 Some(self.add_formula_item(latex, orig, parent))
826 }
827 Node::CommentSection {
833 name,
834 text,
835 refs_note_text,
836 grouped,
837 } => {
838 if !*grouped {
839 let child =
843 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
844 self.comment_groups.push(child.clone());
845 return Some(child);
846 }
847 let self_ref = format!("#/groups/{}", self.groups.len());
848 self.groups.push(Value::Null);
849 let child =
850 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
851 self.groups[group_index(&self_ref)] = json!({
852 "self_ref": self_ref,
853 "parent": { "$ref": parent },
854 "children": [{ "$ref": child }],
855 "content_layer": "notes",
856 "name": name,
857 "label": "comment_section",
858 });
859 self.comment_groups.push(if *refs_note_text {
860 child
861 } else {
862 self_ref.clone()
863 });
864 Some(self_ref)
865 }
866 Node::Commented { comments, inner } => {
869 let item = self.add_node(inner, parent)?;
870 if !comments.is_empty() {
871 self.pending_comments.push((item.clone(), comments.clone()));
872 }
873 Some(item)
874 }
875 Node::Table(t) => Some(self.add_table(t, parent)),
876 Node::Picture {
877 caption,
878 caption_href,
879 image,
880 classification,
881 caption_parent,
882 } => Some(self.add_picture(
883 caption.as_deref(),
884 caption_href.as_deref(),
885 image.as_ref(),
886 classification.as_deref().map(classification_meta),
887 parent,
888 *caption_parent,
889 )),
890 Node::Chart {
895 kind,
896 table,
897 caption,
898 location,
899 } => {
900 self.adopt_loc(*location);
901 let mut meta = json!({
902 "classification": { "predictions": [{ "class_name": kind }] },
903 });
904 if !table.rows.is_empty() {
905 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
906 }
907 let mut captions = Vec::new();
913 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
914 let prov = self.prov_json(unescape_text(cap).chars().count(), true);
915 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
916 self.pending_siblings.push(json!({ "$ref": cap_ref }));
917 captions.push(json!({ "$ref": cap_ref }));
918 }
919 let prov = self.take_prov(0);
920 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
921 }
922 Node::DoclangOnly(_) => None,
924 Node::Group {
925 label,
926 name,
927 layer,
928 children,
929 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
930 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
931 Node::InlineGroup { md_text, .. } => {
934 Some(self.add_text("text", md_text, parent, json!({})))
935 }
936 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
938 Node::Furniture {
944 layer: ContentLayer::Notes,
945 inner,
946 } => {
947 let item = self.add_node(inner, parent)?;
948 self.set_layer(&item, "notes");
949 Some(item)
950 }
951 Node::Furniture { .. } => None,
952 Node::PageFurniture { .. } => None,
953 Node::Located { location, inner } => {
958 if self.cur_page > 0 {
959 self.pending_loc = Some(*location);
960 }
961 let r = self.add_node(inner, parent);
962 self.pending_loc = None;
963 r
964 }
965 Node::Prov {
966 page_no,
967 bbox,
968 charspan,
969 inner,
970 ..
971 } => {
972 self.pending_exact = Some((*page_no, *bbox, *charspan));
973 let r = self.add_node(inner, parent);
974 self.pending_exact = None;
975 r
976 }
977 Node::PageBreak => None,
979 Node::PageInfo {
982 page_no,
983 width,
984 height,
985 } => {
986 self.cur_page = *page_no;
987 self.cur_w = *width as f64;
988 self.cur_h = *height as f64;
989 if *page_no > 0 {
990 self.pages.push((*page_no, self.cur_w, self.cur_h));
991 }
992 None
993 }
994 Node::ListItem { .. } => None,
996 }
997 }
998
999 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1003 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1004 self.field_regions.push(Value::Null);
1005 let region_index = self.field_regions.len() - 1;
1006 let mut item_refs = Vec::new();
1007 for item in items {
1008 item_refs.push(json!({ "$ref": self.add_field_item(item, &self_ref) }));
1009 }
1010 self.field_regions[region_index] = json!({
1011 "self_ref": self_ref,
1012 "parent": { "$ref": parent },
1013 "children": item_refs,
1014 "content_layer": "body",
1015 "label": "field_region",
1016 "prov": [],
1017 });
1018 self_ref
1019 }
1020
1021 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1022 let self_ref = format!("#/field_items/{}", self.field_items.len());
1023 self.field_items.push(Value::Null);
1024 let item_index = self.field_items.len() - 1;
1025 let mut child_refs = Vec::new();
1026 for (label, text) in [
1027 ("marker", &item.marker),
1028 ("field_key", &item.key),
1029 ("field_value", &item.value),
1030 ] {
1031 if let Some(text) = text {
1032 let extra = match (label, &item.value_kind) {
1035 ("field_value", Some(kind)) => json!({ "kind": kind }),
1036 _ => json!({}),
1037 };
1038 child_refs.push(json!({ "$ref": self.add_text(label, text, &self_ref, extra) }));
1039 }
1040 }
1041 self.field_items[item_index] = json!({
1042 "self_ref": self_ref,
1043 "parent": { "$ref": parent },
1044 "children": child_refs,
1045 "content_layer": "body",
1046 "label": "field_item",
1047 "prov": [],
1048 });
1049 self_ref
1050 }
1051
1052 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1056 let bucket = match self_ref.split('/').nth(1) {
1057 Some("texts") => &mut self.texts,
1058 Some("tables") => &mut self.tables,
1059 Some("pictures") => &mut self.pictures,
1060 Some("groups") => &mut self.groups,
1061 _ => return,
1062 };
1063 if let Some(item) = self_ref
1064 .rsplit('/')
1065 .next()
1066 .and_then(|i| i.parse::<usize>().ok())
1067 .and_then(|i| bucket.get_mut(i))
1068 {
1069 item["content_layer"] = json!(layer);
1070 }
1071 }
1072
1073 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1074 let prov = self.take_prov(unescape_text(text).chars().count());
1075 self.add_text_with(label, text, parent, extra, prov)
1076 }
1077
1078 fn add_text_with(
1081 &mut self,
1082 label: &str,
1083 text: &str,
1084 parent: &str,
1085 extra: Value,
1086 prov: Value,
1087 ) -> String {
1088 let self_ref = format!("#/texts/{}", self.texts.len());
1089 let raw = unescape_text(text);
1090 let mut item = json!({
1091 "self_ref": self_ref,
1092 "parent": { "$ref": parent },
1093 "children": [],
1094 "content_layer": "body",
1095 "label": label,
1096 "prov": prov,
1097 "orig": raw,
1098 "text": raw,
1099 });
1100 merge(&mut item, extra);
1101 self.texts.push(item);
1102 self_ref
1103 }
1104
1105 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
1108 let self_ref = format!("#/texts/{}", self.texts.len());
1109 let prov = self.take_prov(latex.chars().count());
1110 self.texts.push(json!({
1111 "self_ref": self_ref,
1112 "parent": { "$ref": parent },
1113 "children": [],
1114 "content_layer": "body",
1115 "label": "formula",
1116 "prov": prov,
1117 "orig": latex,
1118 "text": latex,
1119 }));
1120 self_ref
1121 }
1122
1123 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
1127 let self_ref = format!("#/texts/{}", self.texts.len());
1128 let prov = self.take_prov(latex.chars().count());
1129 self.texts.push(json!({
1130 "self_ref": self_ref,
1131 "parent": { "$ref": parent },
1132 "children": [],
1133 "content_layer": "body",
1134 "label": "formula",
1135 "prov": prov,
1136 "orig": orig,
1137 "text": latex,
1138 }));
1139 self_ref
1140 }
1141
1142 fn add_code(
1143 &mut self,
1144 text: &str,
1145 language: Option<&str>,
1146 orig: Option<&str>,
1147 parent: &str,
1148 ) -> String {
1149 let self_ref = format!("#/texts/{}", self.texts.len());
1150 let raw = unescape_text(text);
1151 let prov = self.take_prov(raw.chars().count());
1152 self.texts.push(json!({
1153 "self_ref": self_ref,
1154 "parent": { "$ref": parent },
1155 "children": [],
1156 "content_layer": "body",
1157 "label": "code",
1158 "prov": prov,
1159 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
1162 "text": raw,
1163 "captions": [],
1164 "references": [],
1165 "footnotes": [],
1166 "code_language": code_language(language),
1167 }));
1168 self_ref
1169 }
1170
1171 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
1174 let self_ref = format!("#/groups/{}", self.groups.len());
1175 self.groups.push(Value::Null);
1177 let base = level_of(&items[0]);
1178 let mut children = Vec::new();
1179 let mut i = 0;
1180 while i < items.len() {
1181 if !matches!(items[i], Node::ListItem { .. }) {
1184 i += 1;
1185 continue;
1186 }
1187 let lvl = level_of(&items[i]);
1188 if lvl > base {
1189 i += 1;
1191 continue;
1192 }
1193 let item_ref = self.add_list_item(&items[i], &self_ref);
1194 let mut j = i + 1;
1196 while j < items.len() && level_of(&items[j]) > base {
1197 j += 1;
1198 }
1199 if j > i + 1 {
1200 let mut nested = Vec::new();
1201 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
1202 if let Some(idx) = ref_index(&item_ref) {
1204 self.texts[idx]["children"]
1205 .as_array_mut()
1206 .unwrap()
1207 .extend(nested);
1208 }
1209 }
1210 children.push(json!({ "$ref": item_ref }));
1211 i = j;
1212 }
1213 self.groups[group_index(&self_ref)] = json!({
1214 "self_ref": self_ref,
1215 "parent": { "$ref": parent },
1216 "children": children,
1217 "content_layer": "body",
1218 "name": "list",
1219 "label": "list",
1220 });
1221 self_ref
1222 }
1223
1224 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
1225 let Node::ListItem {
1226 ordered,
1227 number,
1228 text,
1229 location,
1230 ..
1231 } = node
1232 else {
1233 unreachable!()
1234 };
1235 self.adopt_loc(*location);
1236 let self_ref = format!("#/texts/{}", self.texts.len());
1237 let raw = unescape_text(text);
1238 let prov = self.take_prov(raw.chars().count());
1239 let marker = if *ordered {
1240 format!("{number}.")
1241 } else {
1242 "-".to_string()
1243 };
1244 self.texts.push(json!({
1245 "self_ref": self_ref,
1246 "parent": { "$ref": parent },
1247 "children": [],
1248 "content_layer": "body",
1249 "label": "list_item",
1250 "prov": prov,
1251 "orig": raw,
1252 "text": raw,
1253 "enumerated": ordered,
1254 "marker": marker,
1255 }));
1256 self_ref
1257 }
1258
1259 fn add_table(&mut self, t: &Table, parent: &str) -> String {
1260 self.add_table_with(t, parent, false)
1261 }
1262
1263 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
1266 let self_ref = format!("#/tables/{}", self.tables.len());
1267 self.adopt_loc(t.location);
1268 let prov = self.take_prov(0);
1269 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
1273 Some(cap) => self.add_caption(cap, json!({}), &self_ref, parent, t.caption_parent),
1274 None => (Vec::new(), Vec::new()),
1275 };
1276 let data = table_data_with(t, raw);
1277 self.tables.push(json!({
1278 "self_ref": self_ref,
1279 "parent": { "$ref": parent },
1280 "children": children,
1281 "content_layer": "body",
1282 "label": "table",
1283 "prov": prov,
1284 "captions": captions,
1285 "references": [],
1286 "footnotes": [],
1287 "data": data,
1288 "annotations": [],
1289 }));
1290 self_ref
1291 }
1292
1293 fn add_caption(
1299 &mut self,
1300 text: &str,
1301 extra: Value,
1302 self_ref: &str,
1303 parent: &str,
1304 choice: CaptionParent,
1305 ) -> (Vec<Value>, Vec<Value>) {
1306 let cap_parent = match choice {
1311 CaptionParent::Item => self_ref,
1312 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
1313 CaptionParent::Body => "#/body",
1314 };
1315 let cap_ref = json!({ "$ref": self.add_text("caption", text, cap_parent, extra) });
1316 match choice {
1317 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
1318 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
1321 CaptionParent::Body if parent == "#/body" => {
1322 self.pending_siblings.push(cap_ref.clone())
1323 }
1324 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
1325 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
1328 }
1329 (vec![cap_ref], Vec::new())
1330 }
1331
1332 fn add_picture(
1335 &mut self,
1336 caption: Option<&str>,
1337 caption_href: Option<&str>,
1338 image: Option<&crate::PictureImage>,
1339 meta: Option<Value>,
1340 parent: &str,
1341 caption_parent: CaptionParent,
1342 ) -> String {
1343 let self_ref = format!("#/pictures/{}", self.pictures.len());
1344 let prov = self.take_prov(0);
1347 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
1348 Some(cap) => {
1349 let extra = match caption_href {
1353 Some(href) => json!({ "hyperlink": href }),
1354 None => json!({}),
1355 };
1356 self.add_caption(cap, extra, &self_ref, parent, caption_parent)
1357 }
1358 None => (Vec::new(), Vec::new()),
1359 };
1360 self.push_picture(prov, captions, children, image, meta, parent)
1361 }
1362
1363 fn push_picture(
1366 &mut self,
1367 prov: Value,
1368 captions: Vec<Value>,
1369 children: Vec<Value>,
1370 image: Option<&crate::PictureImage>,
1371 meta: Option<Value>,
1372 parent: &str,
1373 ) -> String {
1374 let self_ref = format!("#/pictures/{}", self.pictures.len());
1375 let annotations = meta
1379 .as_ref()
1380 .and_then(|m| m.get("annotations").cloned())
1381 .unwrap_or_else(|| json!([]));
1382 let meta = meta.map(|mut m| {
1383 if let Some(obj) = m.as_object_mut() {
1384 obj.remove("annotations");
1385 }
1386 m
1387 });
1388 let mut item = match meta {
1392 Some(meta) => json!({
1393 "self_ref": self_ref,
1394 "parent": { "$ref": parent },
1395 "children": children,
1396 "content_layer": "body",
1397 "meta": meta,
1398 "label": "picture",
1399 "prov": prov,
1400 "captions": captions,
1401 "references": [],
1402 "footnotes": [],
1403 "annotations": annotations,
1404 }),
1405 None => json!({
1406 "self_ref": self_ref,
1407 "parent": { "$ref": parent },
1408 "children": children,
1409 "content_layer": "body",
1410 "label": "picture",
1411 "prov": prov,
1412 "captions": captions,
1413 "references": [],
1414 "footnotes": [],
1415 "annotations": annotations,
1416 }),
1417 };
1418 if let Some(img) = image {
1423 let image = json!({
1424 "mimetype": img.mimetype,
1425 "dpi": 72,
1426 "size": { "width": img.width as f64, "height": img.height as f64 },
1427 "uri": img.data_uri(),
1428 });
1429 if let Some(obj) = item.as_object_mut() {
1430 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
1431 obj.insert("image".into(), image);
1432 obj.insert("annotations".into(), annotations);
1433 }
1434 }
1435 self.pictures.push(item);
1436 self_ref
1437 }
1438
1439 fn add_group(
1440 &mut self,
1441 label: &str,
1442 name: Option<&str>,
1443 layer: Option<ContentLayer>,
1444 nodes: &[Node],
1445 parent: &str,
1446 ) -> String {
1447 let self_ref = format!("#/groups/{}", self.groups.len());
1448 self.groups.push(Value::Null);
1449 let mark = (
1453 self.texts.len(),
1454 self.tables.len(),
1455 self.pictures.len(),
1456 self.groups.len(),
1457 );
1458 let children = self.walk_into(nodes, &self_ref);
1459 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
1460 let content_layer = layer.map_or("body", |l| l.value());
1461 self.groups[group_index(&self_ref)] = json!({
1462 "self_ref": self_ref,
1463 "parent": { "$ref": parent },
1464 "children": children,
1465 "content_layer": content_layer,
1466 "name": name,
1467 "label": label,
1468 });
1469 if layer.is_some() {
1470 let (t, tb, p, g) = mark;
1471 for item in self.texts[t..]
1472 .iter_mut()
1473 .chain(self.tables[tb..].iter_mut())
1474 .chain(self.pictures[p..].iter_mut())
1475 .chain(self.groups[g..].iter_mut())
1476 {
1477 if let Some(obj) = item.as_object_mut() {
1478 obj.insert("content_layer".into(), json!(content_layer));
1479 }
1480 }
1481 }
1482 self_ref
1483 }
1484
1485 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Value> {
1488 let seqs: Option<Vec<usize>> = nodes
1493 .iter()
1494 .map(|n| match n {
1495 Node::Prov { seq: Some(s), .. } => Some(*s),
1496 _ => None,
1497 })
1498 .collect();
1499 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
1500 let mut order: Vec<usize> = (0..nodes.len()).collect();
1501 order.sort_by_key(|&i| seqs[i]);
1502 let mut slots: Vec<Vec<Value>> = vec![Vec::new(); nodes.len()];
1503 for i in order {
1504 if let Some(r) = self.add_node(&nodes[i], parent) {
1505 slots[i].append(&mut self.pending_siblings);
1506 slots[i].push(json!({ "$ref": r }));
1507 slots[i].append(&mut self.pending_after);
1508 }
1509 if parent == "#/body" {
1510 slots[i].append(&mut self.pending_body);
1511 }
1512 }
1513 return slots.into_iter().flatten().collect();
1514 }
1515 let mut children = Vec::new();
1516 let mut i = 0;
1517 while i < nodes.len() {
1518 if matches!(nodes[i], Node::ListItem { .. }) {
1519 let start = i;
1520 i += 1;
1521 loop {
1522 match nodes.get(i) {
1523 Some(Node::ListItem { .. }) => i += 1,
1524 Some(Node::Paragraph { text })
1527 if text.is_empty()
1528 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
1529 {
1530 i += 1
1531 }
1532 _ => break,
1533 }
1534 }
1535 self.add_sibling_lists(&nodes[start..i], parent, &mut children);
1536 } else {
1537 if let Some(r) = self.add_node(&nodes[i], parent) {
1538 children.append(&mut self.pending_siblings);
1539 children.push(json!({ "$ref": r }));
1540 children.append(&mut self.pending_after);
1541 }
1542 i += 1;
1543 }
1544 if parent == "#/body" {
1547 children.append(&mut self.pending_body);
1548 }
1549 }
1550 children
1551 }
1552
1553 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
1558 let base = level_of(&run[0]);
1559 let mut seg = 0;
1560 for k in 0..run.len() {
1561 let Node::ListItem {
1562 first_in_list,
1563 level,
1564 ..
1565 } = &run[k]
1566 else {
1567 continue;
1568 };
1569 if *level != base {
1570 continue; }
1572 if k > seg && *first_in_list {
1573 out.push(json!({ "$ref": self.add_list(&run[seg..k], parent) }));
1574 seg = k;
1575 }
1576 }
1577 out.push(json!({ "$ref": self.add_list(&run[seg..], parent) }));
1578 }
1579}
1580
1581fn level_of(node: &Node) -> u8 {
1582 match node {
1583 Node::ListItem { level, .. } => *level,
1584 _ => 0,
1585 }
1586}
1587
1588fn group_index(self_ref: &str) -> usize {
1589 self_ref.rsplit('/').next().unwrap().parse().unwrap()
1590}
1591
1592fn ref_index(self_ref: &str) -> Option<usize> {
1593 self_ref.rsplit('/').next()?.parse().ok()
1594}
1595
1596fn merge(target: &mut Value, extra: Value) {
1598 if let (Some(t), Some(e)) = (target.as_object_mut(), extra.as_object()) {
1599 for (k, v) in e {
1600 t.insert(k.clone(), v.clone());
1601 }
1602 }
1603}
1604
1605fn unescape_text(s: &str) -> String {
1607 s.replace("<", "<")
1608 .replace(">", ">")
1609 .replace("&", "&")
1610 .replace("\\_", "_")
1611}
1612
1613fn fnv1a(s: &str) -> u64 {
1616 let mut h: u64 = 0xcbf29ce484222325;
1617 for b in s.bytes() {
1618 h ^= b as u64;
1619 h = h.wrapping_mul(0x100000001b3);
1620 }
1621 h
1622}
1623
1624#[cfg(test)]
1625mod tests {
1626 use crate::{
1627 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
1628 };
1629 use serde_json::Value;
1630
1631 fn doc_with_image() -> DoclingDocument {
1632 let mut doc = DoclingDocument::new("t");
1633 doc.push(Node::Picture {
1634 caption: Some("Fig 1".into()),
1635 caption_href: None,
1636 image: Some(PictureImage {
1637 mimetype: "image/png".into(),
1638 width: 4,
1639 height: 2,
1640 data: b"foobar".to_vec(),
1641 }),
1642 classification: None,
1643 caption_parent: Default::default(),
1644 });
1645 doc
1646 }
1647
1648 #[test]
1653 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
1654 let mut doc = DoclingDocument::new("t");
1655 doc.push(Node::Heading {
1656 level: 1,
1657 text: "Slide One".into(),
1658 });
1659 doc.push(Node::Furniture {
1660 layer: ContentLayer::Notes,
1661 inner: Box::new(Node::Located {
1662 location: [0, 0, 0, 0],
1663 inner: Box::new(Node::Paragraph {
1664 text: "Speaker note for slide 1.".into(),
1665 }),
1666 }),
1667 });
1668 doc.push(Node::Furniture {
1669 layer: ContentLayer::Furniture,
1670 inner: Box::new(Node::Paragraph {
1671 text: "page header".into(),
1672 }),
1673 });
1674
1675 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1676 let texts = v["texts"].as_array().unwrap();
1677 assert_eq!(
1678 texts
1679 .iter()
1680 .map(|t| (
1681 t["label"].as_str().unwrap(),
1682 t["content_layer"].as_str().unwrap(),
1683 t["text"].as_str().unwrap()
1684 ))
1685 .collect::<Vec<_>>(),
1686 vec![
1687 ("title", "body", "Slide One"),
1688 ("text", "notes", "Speaker note for slide 1."),
1689 ],
1690 "the note is carried on its own layer; the furniture is not carried"
1691 );
1692 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
1694 }
1695
1696 #[test]
1702 fn continuation_flags_become_spanning_cells() {
1703 let mut doc = DoclingDocument::new("t");
1704 let rows = vec![
1706 vec!["merged".to_string(), "merged".into(), "merged".into()],
1707 vec!["merged".to_string(), "merged".into(), "merged".into()],
1708 vec!["a".to_string(), "b".into(), "c".into()],
1709 ];
1710 doc.push(Node::Table(crate::Table {
1711 rows,
1712 location: None,
1713 structure: Some(crate::TableStructure {
1714 header_row: vec![true, false, false],
1715 col_continuation: vec![
1716 vec![false, true, true],
1717 vec![false, true, true],
1718 vec![false, false, false],
1719 ],
1720 row_continuation: vec![
1721 vec![false, false, false],
1722 vec![true, true, true],
1723 vec![false, false, false],
1724 ],
1725 row_header: Vec::new(),
1726 col_header: Vec::new(),
1727 }),
1728 cell_blocks: None,
1729 cells: None,
1730 caption: None,
1731 caption_parent: Default::default(),
1732 }));
1733 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1734 let data = &v["tables"][0]["data"];
1735 assert_eq!(data["num_rows"], 3);
1736 assert_eq!(data["num_cols"], 3);
1737 let cells = data["table_cells"].as_array().unwrap();
1738 assert_eq!(
1739 cells.len(),
1740 4,
1741 "one cell for the range, three for the plain row"
1742 );
1743 assert_eq!(
1744 cells[0],
1745 serde_json::json!({
1746 "row_span": 2, "col_span": 3,
1747 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
1748 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
1749 "text": "merged", "column_header": true, "row_header": false,
1750 "row_section": false, "fillable": false,
1751 })
1752 );
1753 assert_eq!(cells[1]["text"], "a");
1754 assert_eq!(cells[1]["row_span"], 1);
1755 assert_eq!(cells[1]["column_header"], false);
1756 let grid = data["grid"].as_array().unwrap();
1758 assert_eq!(grid.len(), 3);
1759 for (r, row) in grid.iter().take(2).enumerate() {
1760 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
1761 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
1762 }
1763 }
1764 assert_eq!(grid[2][2]["text"], "c");
1765 }
1766
1767 #[test]
1773 fn exact_provenance_pages_and_chart_captions_follow_docling() {
1774 let mut doc = DoclingDocument::new("t");
1775 doc.push(Node::PageInfo {
1776 page_no: 1,
1777 width: 3.0,
1778 height: 4.0,
1779 });
1780 let table = crate::Table {
1781 rows: vec![vec!["a".to_string(), "b".into()]],
1782 ..Default::default()
1783 };
1784 doc.push(Node::Group {
1785 label: "sheet".into(),
1786 name: Some("Data".into()),
1787 layer: None,
1788 children: vec![
1789 Node::Prov {
1794 page_no: 1,
1795 bbox: [0.0, 0.0, 3.0, 4.0],
1796 charspan: [0, 0],
1797 seq: Some(1),
1798 inner: Box::new(Node::Table(table.clone())),
1799 },
1800 Node::Prov {
1801 page_no: 1,
1802 bbox: [0.0, 1.0, 1.0, 1.0],
1803 charspan: [0, 0],
1804 seq: Some(0),
1805 inner: Box::new(Node::Chart {
1806 kind: "bar_chart".into(),
1807 table,
1808 caption: Some("Sales".into()),
1809 location: Some([0, 128, 170, 128]),
1810 }),
1811 },
1812 ],
1813 });
1814 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1815 assert_eq!(
1816 v["pages"],
1817 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
1818 );
1819 assert_eq!(
1820 v["tables"][0]["prov"],
1821 serde_json::json!([{
1822 "page_no": 1,
1823 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
1824 "charspan": [0, 0],
1825 }])
1826 );
1827 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
1828 let sheet = &v["groups"][0];
1831 assert_eq!(
1832 sheet["children"],
1833 serde_json::json!([
1834 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
1835 ])
1836 );
1837 let cap = &v["texts"][0];
1838 assert_eq!(cap["label"], "caption");
1839 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
1840 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
1841 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
1842 let pic = &v["pictures"][0];
1843 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
1844 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
1845 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
1846 assert_eq!(
1847 pic["meta"]["classification"]["predictions"][0]["class_name"],
1848 "bar_chart"
1849 );
1850 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
1851 }
1852
1853 #[test]
1857 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
1858 let mut doc = DoclingDocument::new("t");
1859 doc.push(Node::PageInfo {
1860 page_no: 1,
1861 width: 12192000.0,
1862 height: 6858000.0,
1863 });
1864 doc.push(Node::Furniture {
1865 layer: ContentLayer::Notes,
1866 inner: Box::new(Node::Located {
1867 location: [0, 0, 0, 0],
1868 inner: Box::new(Node::Paragraph {
1869 text: "a note".into(),
1870 }),
1871 }),
1872 });
1873 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1874 let prov = &v["texts"][0]["prov"][0];
1875 assert_eq!(prov["page_no"], 1);
1876 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
1877 assert_eq!(
1878 prov["bbox"],
1879 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
1880 );
1881 assert_eq!(
1883 v["pages"]["1"]["size"],
1884 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
1885 );
1886 }
1887
1888 #[test]
1894 fn page_markers_produce_pages_and_prov() {
1895 let mut doc = DoclingDocument::new("t");
1896 doc.push(Node::PageInfo {
1897 page_no: 1,
1898 width: 512.0,
1899 height: 1024.0,
1900 });
1901 doc.push(Node::Located {
1902 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
1904 text: "hello".into(),
1905 }),
1906 });
1907 doc.push(Node::Table(Table {
1908 rows: vec![vec!["a".into()]],
1909 location: Some([0, 0, 512, 512]),
1910 ..Table::default()
1911 }));
1912 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1913 assert_eq!(v["pages"]["1"]["page_no"], 1);
1914 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
1915 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
1916 let prov = &v["texts"][0]["prov"][0];
1919 assert_eq!(prov["page_no"], 1);
1920 assert_eq!(prov["bbox"]["l"], 128.0);
1921 assert_eq!(prov["bbox"]["t"], 896.0);
1922 assert_eq!(prov["bbox"]["r"], 256.0);
1923 assert_eq!(prov["bbox"]["b"], 768.0);
1924 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
1925 assert_eq!(prov["charspan"][1], 5);
1926 let tprov = &v["tables"][0]["prov"][0];
1928 assert_eq!(tprov["bbox"]["t"], 1024.0);
1929 assert_eq!(tprov["bbox"]["b"], 0.0);
1930 assert_eq!(tprov["charspan"][1], 0);
1931
1932 let mut plain = DoclingDocument::new("t");
1934 plain.push(Node::Located {
1935 location: [1, 2, 3, 4],
1936 inner: Box::new(Node::Paragraph { text: "x".into() }),
1937 });
1938 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
1939 assert_eq!(v["pages"], serde_json::json!({}));
1940 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
1941 }
1942
1943 #[test]
1944 fn picture_image_in_markdown_modes_and_json() {
1945 let doc = doc_with_image();
1946 assert!(doc.export_to_markdown().contains("<!-- image -->"));
1948 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
1950 assert!(
1951 md.contains(""),
1952 "got:\n{md}"
1953 );
1954 assert!(files.is_empty());
1955 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
1957 assert!(
1958 md.contains(""),
1959 "got:\n{md}"
1960 );
1961 assert_eq!(
1962 files,
1963 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
1964 );
1965 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1968 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
1969 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
1970 let keys: Vec<&str> = v["pictures"][0]
1971 .as_object()
1972 .unwrap()
1973 .keys()
1974 .map(String::as_str)
1975 .collect();
1976 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
1977 assert_eq!(
1978 v["pictures"][0]["image"]["uri"],
1979 "data:image/png;base64,Zm9vYmFy"
1980 );
1981 }
1982
1983 #[test]
1984 fn exports_docling_schema() {
1985 let mut doc = DoclingDocument::new("t");
1986 doc.push(Node::Heading {
1987 level: 1,
1988 text: "Title".into(),
1989 });
1990 doc.push(Node::Heading {
1991 level: 2,
1992 text: "Sec".into(),
1993 });
1994 doc.push(Node::Paragraph {
1995 text: "Body & more".into(),
1996 }); doc.push(Node::ListItem {
1998 ordered: false,
1999 number: 0,
2000 first_in_list: true,
2001 text: "one".into(),
2002 level: 0,
2003 marker: None,
2004 location: None,
2005 dclx: None,
2006 href: None,
2007 layer: None,
2008 });
2009 doc.push(Node::ListItem {
2010 ordered: false,
2011 number: 0,
2012 first_in_list: false,
2013 text: "two".into(),
2014 level: 0,
2015 marker: None,
2016 location: None,
2017 dclx: None,
2018 href: None,
2019 layer: None,
2020 });
2021 doc.push(Node::Table(Table {
2022 rows: vec![vec!["A".into(), "B".into()]],
2023 location: None,
2024 structure: None,
2025 cell_blocks: None,
2026 cells: None,
2027 caption: None,
2028 caption_parent: Default::default(),
2029 }));
2030
2031 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2032 assert_eq!(v["schema_name"], "DoclingDocument");
2033 assert_eq!(v["version"], "1.10.0");
2034 assert_eq!(v["texts"][0]["label"], "title");
2035 assert_eq!(v["texts"][1]["label"], "section_header");
2036 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
2040 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
2041 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
2042 assert_eq!(v["texts"][3]["marker"], "-");
2043 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
2045 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
2046 }
2047 #[test]
2051 fn a_layered_group_stamps_its_whole_subtree() {
2052 let doc = DoclingDocument {
2053 name: "s".into(),
2054 nodes: vec![
2055 Node::Group {
2056 label: "sheet".into(),
2057 name: Some("Sheet1".into()),
2058 layer: None,
2059 children: vec![Node::Paragraph {
2060 text: "visible".into(),
2061 }],
2062 },
2063 Node::Group {
2064 label: "sheet".into(),
2065 name: Some("Sheet2".into()),
2066 layer: Some(ContentLayer::Invisible),
2067 children: vec![Node::Paragraph {
2068 text: "hidden".into(),
2069 }],
2070 },
2071 ],
2072 ..DoclingDocument::new("s")
2073 };
2074 let v = crate::json::to_json(&doc);
2075 assert_eq!(v["groups"][0]["label"], "sheet");
2076 assert_eq!(v["groups"][0]["name"], "Sheet1");
2077 assert_eq!(v["groups"][0]["content_layer"], "body");
2078 assert_eq!(v["texts"][0]["content_layer"], "body");
2079 assert_eq!(v["groups"][1]["name"], "Sheet2");
2080 assert_eq!(v["groups"][1]["content_layer"], "invisible");
2081 assert_eq!(v["texts"][1]["content_layer"], "invisible");
2082 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
2084 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
2085 }
2086
2087 #[test]
2093 fn a_backend_item_tree_is_written_verbatim() {
2094 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
2095 let mut t = ItemTree::default();
2096 let text = |label: &str, txt: &str| TreeKind::Text {
2097 label: label.into(),
2098 text: txt.into(),
2099 orig: None,
2100 formatting: None,
2101 hyperlink: None,
2102 level: None,
2103 list: None,
2104 };
2105 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
2106 let h = t.add(None, None, text("title", "Heading"));
2107 let group = t.add(
2108 Some(h),
2109 None,
2110 TreeKind::Group {
2111 label: "inline".into(),
2112 name: "group".into(),
2113 },
2114 );
2115 t.add(
2116 Some(group),
2117 None,
2118 TreeKind::Text {
2119 label: "text".into(),
2120 text: "bold".into(),
2121 orig: None,
2122 formatting: Some(Formatting {
2123 bold: true,
2124 ..Formatting::default()
2125 }),
2126 hyperlink: Some("https://example.com/".into()),
2127 level: None,
2128 list: None,
2129 },
2130 );
2131 t.add(
2132 Some(group),
2133 None,
2134 TreeKind::Code {
2135 text: "x = 1".into(),
2136 orig: None,
2137 language: Some("python".into()),
2138 formatting: None,
2139 hyperlink: None,
2140 },
2141 );
2142 let sub = t.add(
2143 Some(h),
2144 None,
2145 TreeKind::Text {
2146 label: "section_header".into(),
2147 text: "Sub".into(),
2148 orig: Some("Sub\u{2019}".into()),
2149 formatting: None,
2150 hyperlink: None,
2151 level: Some(1),
2152 list: None,
2153 },
2154 );
2155 t.add(
2156 Some(sub),
2157 None,
2158 TreeKind::Text {
2159 label: "list_item".into(),
2160 text: "item".into(),
2161 orig: None,
2162 formatting: None,
2163 hyperlink: None,
2164 level: None,
2165 list: Some(ListMeta {
2166 enumerated: true,
2167 marker: "3.".into(),
2168 }),
2169 },
2170 );
2171 let _region = t.add(
2172 Some(sub),
2173 None,
2174 TreeKind::FieldRegion {
2175 items: vec![crate::FieldItem {
2176 marker: None,
2177 key: Some("Name".into()),
2178 value: Some("Duck".into()),
2179 value_kind: Some("read_only".into()),
2180 }],
2181 },
2182 );
2183 let table = t.add(
2184 Some(sub),
2185 None,
2186 TreeKind::Table {
2187 table: Table {
2188 rows: vec![vec!["a \n<".into(), "b".into()]],
2189 cells: Some(vec![
2190 crate::TableCell {
2191 text: "a \n<".into(),
2192 bbox: None,
2193 start_row: 0,
2194 start_col: 0,
2195 row_span: 3,
2196 col_span: 1,
2197 column_header: false,
2198 row_header: true,
2199 row_section: false,
2200 },
2201 crate::TableCell {
2202 text: "b".into(),
2203 bbox: None,
2204 start_row: 0,
2205 start_col: 1,
2206 row_span: 1,
2207 col_span: 1,
2208 column_header: false,
2209 row_header: false,
2210 row_section: false,
2211 },
2212 ]),
2213 ..Table::default()
2214 },
2215 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
2217 },
2218 );
2219 let cell_group = t.add(
2220 Some(table),
2221 None,
2222 TreeKind::Group {
2223 label: "unspecified".into(),
2224 name: "rich_cell_group_1_0_0".into(),
2225 },
2226 );
2227 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
2228 *rich_cells = vec![(0, 1, cell_group)];
2229 }
2230 let after = t.add(Some(sub), None, text("text", "after the region"));
2231 let _ = (title, after);
2232
2233 let doc = DoclingDocument {
2234 tree: Some(t),
2235 ..DoclingDocument::new("t")
2236 };
2237 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2238 let texts: Vec<&str> = v["texts"]
2240 .as_array()
2241 .unwrap()
2242 .iter()
2243 .map(|t| t["text"].as_str().unwrap())
2244 .collect();
2245 assert_eq!(
2246 texts,
2247 [
2248 "Page",
2249 "Heading",
2250 "bold",
2251 "x = 1",
2252 "Sub",
2253 "item",
2254 "Name",
2255 "Duck",
2256 "after the region"
2257 ]
2258 );
2259 assert_eq!(
2260 v["body"]["children"],
2261 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
2262 );
2263 assert_eq!(v["texts"][0]["content_layer"], "furniture");
2264 assert_eq!(
2265 v["texts"][1]["children"],
2266 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
2267 );
2268 let bold = &v["texts"][2];
2269 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
2270 let keys: Vec<&str> = bold
2271 .as_object()
2272 .unwrap()
2273 .keys()
2274 .map(String::as_str)
2275 .collect();
2276 assert_eq!(
2277 keys,
2278 [
2279 "self_ref",
2280 "parent",
2281 "children",
2282 "content_layer",
2283 "label",
2284 "prov",
2285 "orig",
2286 "text",
2287 "formatting",
2288 "hyperlink"
2289 ]
2290 );
2291 assert_eq!(
2292 bold["formatting"],
2293 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
2294 );
2295 let code = &v["texts"][3];
2296 assert_eq!(code["label"], "code");
2297 assert_eq!(code["code_language"], "Python");
2298 let sub = &v["texts"][4];
2299 assert_eq!(sub["orig"], "Sub\u{2019}");
2300 assert_eq!(sub["level"], 1);
2301 let item = &v["texts"][5];
2302 let keys: Vec<&str> = item
2303 .as_object()
2304 .unwrap()
2305 .keys()
2306 .map(String::as_str)
2307 .collect();
2308 assert_eq!(
2309 keys,
2310 [
2311 "self_ref",
2312 "parent",
2313 "children",
2314 "content_layer",
2315 "label",
2316 "prov",
2317 "orig",
2318 "text",
2319 "enumerated",
2320 "marker"
2321 ]
2322 );
2323 assert_eq!(item["marker"], "3.");
2324 assert_eq!(v["texts"][7]["kind"], "read_only");
2325 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
2326 let table = &v["tables"][0];
2327 assert_eq!(
2328 table["children"],
2329 serde_json::json!([{"$ref": "#/groups/1"}])
2330 );
2331 let cells = table["data"]["table_cells"].as_array().unwrap();
2332 assert_eq!(
2333 cells[0]["text"], "a \n<",
2334 "raw cell text is written verbatim"
2335 );
2336 assert_eq!(
2337 cells[0]["end_row_offset_idx"], 3,
2338 "declared spans are not clamped"
2339 );
2340 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
2341 assert!(cells[0].get("ref").is_none());
2342 assert!(
2343 table["data"]["grid"][0][1].get("ref").is_none(),
2344 "the grid shows plain cells"
2345 );
2346 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
2347 }
2348
2349 #[test]
2353 fn a_comment_section_can_be_referenced_by_its_note_text() {
2354 let doc = DoclingDocument {
2355 name: "c".into(),
2356 nodes: vec![
2357 Node::Commented {
2358 comments: vec![0],
2359 inner: Box::new(Node::Paragraph {
2360 text: "annotated".into(),
2361 }),
2362 },
2363 Node::CommentSection {
2364 name: "comment-Sheet1-A1".into(),
2365 text: "[author: A]: note".into(),
2366 refs_note_text: true,
2367 grouped: true,
2368 },
2369 ],
2370 ..DoclingDocument::new("c")
2371 };
2372 let v = crate::json::to_json(&doc);
2373 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
2374 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
2375 }
2376
2377 #[test]
2381 fn comment_sections_link_back_to_their_items() {
2382 let doc = DoclingDocument {
2383 name: "c".into(),
2384 nodes: vec![
2385 Node::Commented {
2386 comments: vec![0],
2387 inner: Box::new(Node::Paragraph {
2388 text: "annotated".into(),
2389 }),
2390 },
2391 Node::Paragraph {
2392 text: "plain".into(),
2393 },
2394 Node::CommentSection {
2395 name: "comment-7".into(),
2396 text: "[time: t]: note".into(),
2397 refs_note_text: false,
2398 grouped: true,
2399 },
2400 ],
2401 ..DoclingDocument::new("c")
2402 };
2403 let v = crate::json::to_json(&doc);
2404 assert_eq!(v["groups"][0]["label"], "comment_section");
2406 assert_eq!(v["groups"][0]["name"], "comment-7");
2407 assert_eq!(v["groups"][0]["content_layer"], "notes");
2408 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
2409 assert_eq!(v["texts"][2]["content_layer"], "notes");
2410 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
2412 assert!(v["texts"][1].get("comments").is_none());
2413 let keys: Vec<&str> = v["texts"][0]
2415 .as_object()
2416 .unwrap()
2417 .keys()
2418 .map(String::as_str)
2419 .collect();
2420 assert_eq!(
2421 &keys[keys.len() - 4..],
2422 &["prov", "comments", "orig", "text"]
2423 );
2424 }
2425
2426 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
2427 Node::Picture {
2428 caption: Some(caption.into()),
2429 caption_href: None,
2430 image: None,
2431 classification: None,
2432 caption_parent,
2433 }
2434 }
2435
2436 fn group(children: Vec<Node>) -> Node {
2437 Node::Group {
2438 label: "section".into(),
2439 name: None,
2440 layer: None,
2441 children,
2442 }
2443 }
2444
2445 fn refs(v: &Value) -> Vec<&str> {
2446 v.as_array()
2447 .unwrap()
2448 .iter()
2449 .map(|r| r["$ref"].as_str().unwrap())
2450 .collect()
2451 }
2452
2453 #[test]
2458 fn a_body_caption_follows_the_enclosing_top_level_item() {
2459 let mut doc = DoclingDocument::new("t");
2460 doc.push(picture("top", CaptionParent::Body));
2461 doc.push(group(vec![
2462 Node::Paragraph { text: "p".into() },
2463 picture("nested", CaptionParent::Body),
2464 ]));
2465 doc.push(Node::Paragraph {
2466 text: "after".into(),
2467 });
2468 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2469 assert_eq!(
2470 refs(&v["body"]["children"]),
2471 [
2472 "#/texts/0",
2473 "#/pictures/0",
2474 "#/groups/0",
2475 "#/texts/2",
2476 "#/texts/3"
2477 ]
2478 );
2479 assert_eq!(
2480 refs(&v["groups"][0]["children"]),
2481 ["#/texts/1", "#/pictures/1"]
2482 );
2483 for (cap, pic) in [(0, 0), (2, 1)] {
2484 assert_eq!(v["texts"][cap]["label"], "caption");
2485 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
2486 assert_eq!(
2487 refs(&v["pictures"][pic]["captions"]),
2488 [format!("#/texts/{cap}")]
2489 );
2490 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
2491 }
2492 }
2493
2494 #[test]
2497 fn an_item_caption_is_the_items_first_child() {
2498 let mut doc = DoclingDocument::new("t");
2499 doc.push(picture("fig", CaptionParent::Item));
2500 doc.push(Node::Table(Table {
2501 rows: vec![vec!["a".into()]],
2502 caption: Some("tab".into()),
2503 caption_parent: CaptionParent::Item,
2504 ..Table::default()
2505 }));
2506 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2507 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
2508 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
2509 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
2510 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
2511 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
2512 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
2513 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
2514 }
2515
2516 #[test]
2520 fn a_container_caption_is_the_items_sibling() {
2521 let mut doc = DoclingDocument::new("t");
2522 doc.push(group(vec![
2523 picture("chart", CaptionParent::Container),
2524 Node::Table(Table {
2525 rows: vec![vec!["a".into()]],
2526 caption: Some("figcaption".into()),
2527 caption_parent: CaptionParent::ContainerAfter,
2528 ..Table::default()
2529 }),
2530 ]));
2531 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2532 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
2533 assert_eq!(
2534 refs(&v["groups"][0]["children"]),
2535 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
2536 );
2537 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
2538 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
2539 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
2540 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
2541 }
2542}