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
102#[derive(Clone, Copy)]
106struct ExactProv {
107 page_no: usize,
108 bbox: [f64; 4],
109 bottom_left: bool,
110 charspan: [usize; 2],
111}
112
113impl From<&crate::tree::TreeProv> for ExactProv {
114 fn from(p: &crate::tree::TreeProv) -> Self {
115 ExactProv {
116 page_no: p.page_no,
117 bbox: p.bbox,
118 bottom_left: p.bottom_left,
119 charspan: p.charspan,
120 }
121 }
122}
123
124fn formatting_json(f: &crate::tree::Formatting) -> Value {
126 json!({
127 "bold": f.bold,
128 "italic": f.italic,
129 "underline": f.underline,
130 "strikethrough": f.strikethrough,
131 "script": match f.script {
132 crate::Script::Baseline => "baseline",
133 crate::Script::Sub => "sub",
134 crate::Script::Super => "super",
135 },
136 })
137}
138
139pub fn to_json(doc: &DoclingDocument) -> Value {
141 let mut b = Builder::default();
142 let body = match &doc.tree {
146 Some(tree) => {
147 for n in &doc.nodes {
150 if let Node::PageInfo {
151 page_no,
152 width,
153 height,
154 } = n
155 {
156 if *page_no > 0 {
157 b.pages.push((*page_no, *width as f64, *height as f64));
158 }
159 }
160 }
161 b.write_tree(tree)
162 }
163 None => b.walk_into(&doc.nodes, "#/body"),
164 };
165 b.link_comments();
166
167 let mut out = json!({
168 "schema_name": "DoclingDocument",
169 "version": SCHEMA_VERSION,
170 "name": doc.name,
171 "origin": {
172 "mimetype": "text/plain",
173 "binary_hash": fnv1a(&doc.name),
174 "filename": doc.name,
175 },
176 "furniture": {
177 "self_ref": "#/furniture",
178 "children": [],
179 "content_layer": "furniture",
180 "name": "_root_",
181 "label": "unspecified",
182 },
183 "body": {
184 "self_ref": "#/body",
185 "children": body,
186 "content_layer": "body",
187 "name": "_root_",
188 "label": "unspecified",
189 },
190 "groups": b.groups,
191 "texts": b.texts,
192 "pictures": b.pictures,
193 "tables": b.tables,
194 "key_value_items": [],
195 "form_items": [],
196 "pages": b.pages.iter().map(|(n, w, h)| {
197 let r2 = |v: f64| (v * 100.0).round() / 100.0;
198 (n.to_string(), json!({
199 "size": { "width": r2(*w), "height": r2(*h) },
200 "page_no": n,
201 }))
202 }).collect::<serde_json::Map<String, Value>>(),
203 });
204
205 if !b.field_regions.is_empty() {
210 if let Some(obj) = out.as_object_mut() {
211 let pages = obj.remove("pages");
212 obj.insert("field_regions".into(), Value::Array(b.field_regions));
213 obj.insert("field_items".into(), Value::Array(b.field_items));
214 if let Some(pages) = pages {
215 obj.insert("pages".into(), pages);
216 }
217 }
218 }
219 out
220}
221
222fn classification_meta(classes: &[crate::PictureClass]) -> Value {
229 json!({
230 "classification": {
231 "predictions": classes.iter().map(|c| json!({
232 "confidence": c.confidence as f64,
233 "created_by": "DocumentPictureClassifier",
234 "class_name": c.class_name,
235 })).collect::<Vec<_>>(),
236 },
237 "annotations": [{
238 "kind": "classification",
239 "provenance": "DocumentPictureClassifier",
240 "predicted_classes": classes.iter().map(|c| json!({
241 "class_name": c.class_name,
242 "confidence": c.confidence as f64,
243 })).collect::<Vec<_>>(),
244 }],
245 })
246}
247
248#[allow(clippy::too_many_arguments)]
259fn cell_value(
260 row_span: usize,
261 col_span: usize,
262 start_row: usize,
263 end_row: usize,
264 start_col: usize,
265 end_col: usize,
266 text: String,
267 column_header: bool,
268 row_header: bool,
269 row_section: bool,
270 bbox: Option<[f32; 4]>,
271) -> Value {
272 let mut m = serde_json::Map::with_capacity(12);
273 m.insert("row_span".into(), row_span.into());
274 m.insert("col_span".into(), col_span.into());
275 m.insert("start_row_offset_idx".into(), start_row.into());
276 m.insert("end_row_offset_idx".into(), end_row.into());
277 m.insert("start_col_offset_idx".into(), start_col.into());
278 m.insert("end_col_offset_idx".into(), end_col.into());
279 m.insert("text".into(), Value::String(text));
280 m.insert("column_header".into(), column_header.into());
281 m.insert("row_header".into(), row_header.into());
282 m.insert("row_section".into(), row_section.into());
283 m.insert("fillable".into(), false.into());
284 if let Some(b) = bbox {
285 m.insert(
286 "bbox".into(),
287 json!({
288 "l": b[0], "t": b[1], "r": b[2], "b": b[3],
289 "coord_origin": "TOPLEFT",
290 }),
291 );
292 }
293 Value::Object(m)
294}
295
296fn table_data(t: &Table) -> Value {
299 table_data_with(t, false)
300}
301
302fn table_data_with(t: &Table, raw: bool) -> Value {
305 let cell_text = |s: &str| {
306 if raw {
307 s.to_string()
308 } else {
309 unescape_text(&crate::markdown::strip_hard_breaks(s))
310 }
311 };
312 let num_rows = t.rows.len();
313 let num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
314 let mut grid = Vec::with_capacity(num_rows);
315 let mut cells = Vec::new();
316 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
320 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
321 for c in first_class {
322 let idx = cells.len();
323 cells.push(cell_value(
324 c.row_span,
325 c.col_span,
326 c.start_row,
327 c.start_row + c.row_span,
328 c.start_col,
329 c.start_col + c.col_span,
330 cell_text(&c.text),
331 c.column_header,
332 c.row_header,
333 c.row_section,
334 c.bbox,
335 ));
336 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
337 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
338 slot[r * num_cols + k] = Some(idx);
339 }
340 }
341 }
342 for r in 0..num_rows {
343 let mut grid_row = Vec::with_capacity(num_cols);
344 for c in 0..num_cols {
345 grid_row.push(match slot[r * num_cols + c] {
346 Some(i) => cells[i].clone(),
347 None => cell_value(
348 1,
349 1,
350 r,
351 r + 1,
352 c,
353 c + 1,
354 String::new(),
355 false,
356 false,
357 false,
358 None,
359 ),
360 });
361 }
362 grid.push(grid_row);
363 }
364 } else {
365 let s = t.structure.as_ref();
366 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
367 grid.and_then(|g| g.get(r))
368 .and_then(|row| row.get(c))
369 .copied()
370 .unwrap_or(false)
371 };
372 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
373 let (mut r0, mut c0) = (r, c);
374 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
375 c0 -= 1;
376 }
377 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
378 r0 -= 1;
379 }
380 (r0, c0)
381 };
382 let anchors: Vec<(usize, usize)> = (0..num_rows)
385 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
386 .map(|(r, c)| anchor_of(r, c))
387 .collect();
388 let mut extent: Vec<(usize, usize)> = (0..num_rows)
389 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
390 .collect();
391 for (i, &(ar, ac)) in anchors.iter().enumerate() {
392 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
393 let e = &mut extent[ar * num_cols + ac];
394 e.0 = e.0.max(r);
395 e.1 = e.1.max(c);
396 }
397 for (r, row) in t.rows.iter().enumerate() {
398 let mut grid_row = Vec::with_capacity(num_cols);
399 for c in 0..num_cols {
400 let (ar, ac) = anchors[r * num_cols + c];
401 if (ar, ac) == (r, c) {
402 let (er, ec) = extent[r * num_cols + c];
403 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
404 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
405 Some(s) => flag(Some(&s.col_header), r, c),
406 None => r == 0,
407 };
408 slot[r * num_cols + c] = Some(cells.len());
409 cells.push(cell_value(
410 er - r + 1,
411 ec - c + 1,
412 r,
413 er + 1,
414 c,
415 ec + 1,
416 text,
417 column_header,
418 flag(s.map(|s| &s.row_header), r, c),
419 false,
420 None,
421 ));
422 }
423 grid_row.push(match slot[ar * num_cols + ac] {
424 Some(i) => cells[i].clone(),
425 None => Value::Null,
426 });
427 }
428 grid.push(grid_row);
429 }
430 }
431 json!({
432 "table_cells": cells,
433 "num_rows": num_rows,
434 "num_cols": num_cols,
435 "orientation": "rot_0",
436 "grid": grid,
437 })
438}
439
440#[derive(Default)]
441struct Builder {
442 texts: Vec<Value>,
443 groups: Vec<Value>,
444 tables: Vec<Value>,
445 pictures: Vec<Value>,
446 field_regions: Vec<Value>,
447 field_items: Vec<Value>,
448 pages: Vec<(usize, f64, f64)>,
452 cur_page: usize,
454 cur_w: f64,
455 cur_h: f64,
456 pending_loc: Option<[u16; 4]>,
459 pending_exact: Option<ExactProv>,
462 pending_siblings: Vec<Value>,
466 pending_after: Vec<Value>,
470 pending_body: Vec<Value>,
474 comment_groups: Vec<String>,
478 pending_comments: Vec<(String, Vec<usize>)>,
482}
483
484impl Builder {
485 fn take_prov(&mut self, char_len: usize) -> Value {
491 let prov = self.prov_json(char_len, false);
492 self.pending_exact = None;
493 self.pending_loc = None;
494 prov
495 }
496
497 fn prov_json(&self, char_len: usize, span_over_text: bool) -> Value {
502 let r2 = |v: f64| (v * 100.0).round() / 100.0;
503 if let Some(ExactProv {
504 page_no,
505 bbox: [l, t, r, b],
506 bottom_left,
507 charspan,
508 }) = self.pending_exact
509 {
510 let charspan = if span_over_text {
511 [0, char_len]
512 } else {
513 charspan
514 };
515 return json!([{
516 "page_no": page_no,
517 "bbox": {
518 "l": r2(l), "t": r2(t), "r": r2(r), "b": r2(b),
519 "coord_origin": if bottom_left { "BOTTOMLEFT" } else { "TOPLEFT" },
520 },
521 "charspan": charspan,
522 }]);
523 }
524 let Some([x0, y0, x1, y1]) = self.pending_loc else {
525 return json!([]);
526 };
527 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
531 return json!([{
532 "page_no": self.cur_page,
533 "bbox": { "l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT" },
534 "charspan": [0, char_len],
535 }]);
536 }
537 json!([{
538 "page_no": self.cur_page,
539 "bbox": {
540 "l": r2(x0 as f64 * self.cur_w / 512.0),
541 "t": r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
542 "r": r2(x1 as f64 * self.cur_w / 512.0),
543 "b": r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
544 "coord_origin": "BOTTOMLEFT",
545 },
546 "charspan": [0, char_len],
547 }])
548 }
549
550 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
553 if self.pending_loc.is_none() && self.cur_page > 0 {
554 self.pending_loc = loc;
555 }
556 }
557
558 fn link_comments(&mut self) {
566 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
567 .into_iter()
568 .map(|(item, comments)| {
569 let refs = comments
570 .iter()
571 .filter_map(|i| self.comment_groups.get(*i))
572 .map(|r| json!({ "$ref": r }))
573 .collect();
574 (item, refs)
575 })
576 .collect();
577 for (item, comment_refs) in refs {
578 if comment_refs.is_empty() {
579 continue;
580 }
581 let Some(target) = self.item_mut(&item) else {
582 continue;
583 };
584 let Some(obj) = target.as_object_mut() else {
585 continue;
586 };
587 let tail: Vec<(String, Value)> = obj
588 .iter()
589 .skip_while(|(k, _)| k.as_str() != "prov")
590 .skip(1)
591 .map(|(k, v)| (k.clone(), v.clone()))
592 .collect();
593 for (k, _) in &tail {
594 obj.shift_remove(k);
595 }
596 obj.insert("comments".into(), Value::Array(comment_refs));
597 for (k, v) in tail {
598 obj.insert(k, v);
599 }
600 }
601 }
602
603 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
605 let idx = ref_index(self_ref)?;
606 let bucket = if self_ref.starts_with("#/texts/") {
607 &mut self.texts
608 } else if self_ref.starts_with("#/tables/") {
609 &mut self.tables
610 } else if self_ref.starts_with("#/pictures/") {
611 &mut self.pictures
612 } else if self_ref.starts_with("#/groups/") {
613 &mut self.groups
614 } else {
615 return None;
616 };
617 bucket.get_mut(idx)
618 }
619
620 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
624 use crate::tree::TreeKind;
625 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
628 let (mut nt, mut ng, mut ntb, mut np, mut nf) = (0, 0, 0, 0, 0);
629 for item in &tree.items {
630 if item.deleted {
631 refs.push(String::new());
632 continue;
633 }
634 let r = match &item.kind {
635 TreeKind::Text { .. } | TreeKind::Code { .. } => {
636 nt += 1;
637 format!("#/texts/{}", nt - 1)
638 }
639 TreeKind::Group { .. } => {
640 ng += 1;
641 format!("#/groups/{}", ng - 1)
642 }
643 TreeKind::Table { .. } => {
644 ntb += 1;
645 format!("#/tables/{}", ntb - 1)
646 }
647 TreeKind::Picture { .. } => {
648 np += 1;
649 format!("#/pictures/{}", np - 1)
650 }
651 TreeKind::FieldRegion { items } => {
652 nt += items
655 .iter()
656 .map(|i| {
657 [&i.marker, &i.key, &i.value]
658 .iter()
659 .filter(|p| p.is_some())
660 .count()
661 })
662 .sum::<usize>();
663 nf += 1;
664 format!("#/field_regions/{}", nf - 1)
665 }
666 };
667 refs.push(r);
668 }
669 let ref_of = |id: usize| json!({ "$ref": refs[id] });
670 for (id, item) in tree.items.iter().enumerate() {
671 if item.deleted {
672 continue;
673 }
674 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
675 let children: Vec<Value> = item.children.iter().map(|&c| ref_of(c)).collect();
676 let layer = item.layer.map_or("body", |l| l.value());
677 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
680 let self_ref = match &item.kind {
681 TreeKind::Text {
682 label,
683 text,
684 orig,
685 formatting,
686 hyperlink,
687 level,
688 list,
689 } => {
690 let mut tail = serde_json::Map::new();
693 if let Some(f) = formatting {
694 tail.insert("formatting".into(), formatting_json(f));
695 }
696 if let Some(h) = hyperlink {
697 tail.insert("hyperlink".into(), json!(h));
698 }
699 if let Some(l) = level {
700 tail.insert("level".into(), json!(l));
701 }
702 if let Some(l) = list {
703 tail.insert("enumerated".into(), json!(l.enumerated));
704 tail.insert("marker".into(), json!(l.marker));
705 }
706 let r = format!("#/texts/{}", self.texts.len());
707 let prov = self.take_prov(text.chars().count());
708 let mut item_json = json!({
709 "self_ref": r,
710 "parent": { "$ref": parent },
711 "children": children,
712 "content_layer": layer,
713 "label": label,
714 "prov": prov,
715 });
716 if !item.comments.is_empty() {
719 item_json["comments"] =
720 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
721 }
722 merge(
723 &mut item_json,
724 json!({
725 "orig": orig.as_deref().unwrap_or(text),
726 "text": text,
727 }),
728 );
729 merge(&mut item_json, Value::Object(tail));
730 self.texts.push(item_json);
731 r
732 }
733 TreeKind::Code {
734 text,
735 orig,
736 language,
737 formatting,
738 hyperlink,
739 } => {
740 let r = format!("#/texts/{}", self.texts.len());
741 let prov = self.take_prov(text.chars().count());
742 let mut item_json = json!({
743 "self_ref": r,
744 "parent": { "$ref": parent },
745 "children": children,
746 "content_layer": layer,
747 "label": "code",
748 "prov": prov,
749 });
750 if !item.comments.is_empty() {
751 item_json["comments"] =
752 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
753 }
754 merge(
755 &mut item_json,
756 json!({
757 "orig": orig.as_deref().unwrap_or(text),
758 "text": text,
759 }),
760 );
761 if let Some(f) = formatting {
762 item_json["formatting"] = formatting_json(f);
763 }
764 if let Some(h) = hyperlink {
765 item_json["hyperlink"] = json!(h);
766 }
767 merge(
768 &mut item_json,
769 json!({
770 "captions": [],
771 "references": [],
772 "footnotes": [],
773 "code_language": code_language(language.as_deref()),
774 }),
775 );
776 self.texts.push(item_json);
777 r
778 }
779 TreeKind::Group { label, name } => {
780 self.pending_exact = None;
781 let r = format!("#/groups/{}", self.groups.len());
782 self.groups.push(json!({
783 "self_ref": r,
784 "parent": { "$ref": parent },
785 "children": children,
786 "content_layer": layer,
787 "name": name,
788 "label": label,
789 }));
790 r
791 }
792 TreeKind::Table {
793 table,
794 rich_cells,
795 captions,
796 } => {
797 let r = self.add_table_with(table, parent, true);
799 let idx = ref_index(&r).expect("table ref");
800 let t = &mut self.tables[idx];
801 t["children"] = Value::Array(children);
802 t["content_layer"] = json!(layer);
803 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
804 for &(row, col, group) in rich_cells {
808 let cell_ref = ref_of(group);
809 let hit = |c: &Value| {
810 c["start_row_offset_idx"] == json!(row)
811 && c["start_col_offset_idx"] == json!(col)
812 };
813 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
814 for c in cells.iter_mut().filter(|c| hit(c)) {
815 c["ref"] = cell_ref.clone();
816 }
817 }
818 }
819 r
820 }
821 TreeKind::Picture {
822 captions,
823 image,
824 classification,
825 chart,
826 dpi,
827 } => {
828 let mut meta = classification.as_ref().map(
831 |c| json!({ "classification": { "predictions": [{ "class_name": c }] } }),
832 );
833 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
834 if !t.rows.is_empty() {
835 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
836 }
837 }
838 let prov = self.take_prov(0);
839 let r = self.push_picture(
840 prov,
841 captions.iter().map(|&c| ref_of(c)).collect(),
842 children,
843 image.as_ref(),
844 meta,
845 parent,
846 );
847 if let Some(idx) = ref_index(&r) {
848 self.pictures[idx]["content_layer"] = json!(layer);
849 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
852 img["dpi"] = json!(dpi);
853 }
854 }
855 r
856 }
857 TreeKind::FieldRegion { items } => {
858 self.pending_exact = None;
859 let r = self.add_field_region(items, parent);
860 if let Some(region) = self.field_regions.last_mut() {
861 region["content_layer"] = json!(layer);
862 }
863 r
864 }
865 };
866 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
867 }
868 tree.body.iter().map(|&c| ref_of(c)).collect()
869 }
870
871 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
872 match node {
873 Node::Heading { level: 1, text } => {
874 Some(self.add_text("title", text, parent, json!({})))
875 }
876 Node::Heading { level, text } => Some(self.add_text(
877 "section_header",
878 text,
879 parent,
880 json!({ "level": level.saturating_sub(1) }),
881 )),
882 Node::Caption { text, href } => {
883 let extra = match href {
884 Some(url) => json!({ "hyperlink": url }),
885 None => json!({}),
886 };
887 Some(self.add_text("caption", text, parent, extra))
888 }
889 Node::Paragraph { text } => {
890 let t = text.trim();
893 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
894 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
895 _ => Some(self.add_text("text", text, parent, json!({}))),
896 }
897 }
898 Node::CheckboxItem { checked, text } => {
899 let mark = if *checked { "- [x] " } else { "- [ ] " };
902 Some(self.add_text("text", &format!("{mark}{text}"), parent, json!({})))
903 }
904 Node::Code {
905 language,
906 text,
907 orig,
908 ..
909 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
910 Node::Formula {
913 latex,
914 orig,
915 location,
916 } => {
917 self.adopt_loc(*location);
918 Some(self.add_formula_item(latex, orig, parent))
919 }
920 Node::CommentSection {
926 name,
927 text,
928 refs_note_text,
929 grouped,
930 } => {
931 if !*grouped {
932 let child =
936 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
937 self.comment_groups.push(child.clone());
938 return Some(child);
939 }
940 let self_ref = format!("#/groups/{}", self.groups.len());
941 self.groups.push(Value::Null);
942 let child =
943 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
944 self.groups[group_index(&self_ref)] = json!({
945 "self_ref": self_ref,
946 "parent": { "$ref": parent },
947 "children": [{ "$ref": child }],
948 "content_layer": "notes",
949 "name": name,
950 "label": "comment_section",
951 });
952 self.comment_groups.push(if *refs_note_text {
953 child
954 } else {
955 self_ref.clone()
956 });
957 Some(self_ref)
958 }
959 Node::Commented { comments, inner } => {
962 let item = self.add_node(inner, parent)?;
963 if !comments.is_empty() {
964 self.pending_comments.push((item.clone(), comments.clone()));
965 }
966 Some(item)
967 }
968 Node::Table(t) => Some(self.add_table(t, parent)),
969 Node::Picture {
970 caption,
971 caption_href,
972 image,
973 classification,
974 caption_parent,
975 } => Some(self.add_picture(
976 caption.as_deref(),
977 caption_href.as_deref(),
978 image.as_ref(),
979 classification.as_deref().map(classification_meta),
980 parent,
981 *caption_parent,
982 )),
983 Node::Chart {
988 kind,
989 table,
990 caption,
991 location,
992 } => {
993 self.adopt_loc(*location);
994 let mut meta = json!({
995 "classification": { "predictions": [{ "class_name": kind }] },
996 });
997 if !table.rows.is_empty() {
998 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
999 }
1000 let mut captions = Vec::new();
1006 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1007 let prov = self.prov_json(unescape_text(cap).chars().count(), true);
1008 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1009 self.pending_siblings.push(json!({ "$ref": cap_ref }));
1010 captions.push(json!({ "$ref": cap_ref }));
1011 }
1012 let prov = self.take_prov(0);
1013 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1014 }
1015 Node::DoclangOnly(_) => None,
1017 Node::Group {
1018 label,
1019 name,
1020 layer,
1021 children,
1022 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1023 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1024 Node::InlineGroup { md_text, .. } => {
1027 Some(self.add_text("text", md_text, parent, json!({})))
1028 }
1029 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1031 Node::Furniture {
1037 layer: ContentLayer::Notes,
1038 inner,
1039 } => {
1040 let item = self.add_node(inner, parent)?;
1041 self.set_layer(&item, "notes");
1042 Some(item)
1043 }
1044 Node::Furniture { .. } => None,
1045 Node::PageFurniture { .. } => None,
1046 Node::Located { location, inner } => {
1051 if self.cur_page > 0 {
1052 self.pending_loc = Some(*location);
1053 }
1054 let r = self.add_node(inner, parent);
1055 self.pending_loc = None;
1056 r
1057 }
1058 Node::Prov {
1059 page_no,
1060 bbox,
1061 charspan,
1062 inner,
1063 ..
1064 } => {
1065 self.pending_exact = Some(ExactProv {
1066 page_no: *page_no,
1067 bbox: bbox.map(f64::from),
1068 bottom_left: false,
1069 charspan: *charspan,
1070 });
1071 let r = self.add_node(inner, parent);
1072 self.pending_exact = None;
1073 r
1074 }
1075 Node::PageBreak => None,
1077 Node::PageInfo {
1080 page_no,
1081 width,
1082 height,
1083 } => {
1084 self.cur_page = *page_no;
1085 self.cur_w = *width as f64;
1086 self.cur_h = *height as f64;
1087 if *page_no > 0 {
1088 self.pages.push((*page_no, self.cur_w, self.cur_h));
1089 }
1090 None
1091 }
1092 Node::ListItem { .. } => None,
1094 }
1095 }
1096
1097 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1101 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1102 self.field_regions.push(Value::Null);
1103 let region_index = self.field_regions.len() - 1;
1104 let mut item_refs = Vec::new();
1105 for item in items {
1106 item_refs.push(json!({ "$ref": self.add_field_item(item, &self_ref) }));
1107 }
1108 self.field_regions[region_index] = json!({
1109 "self_ref": self_ref,
1110 "parent": { "$ref": parent },
1111 "children": item_refs,
1112 "content_layer": "body",
1113 "label": "field_region",
1114 "prov": [],
1115 });
1116 self_ref
1117 }
1118
1119 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1120 let self_ref = format!("#/field_items/{}", self.field_items.len());
1121 self.field_items.push(Value::Null);
1122 let item_index = self.field_items.len() - 1;
1123 let mut child_refs = Vec::new();
1124 for (label, text) in [
1125 ("marker", &item.marker),
1126 ("field_key", &item.key),
1127 ("field_value", &item.value),
1128 ] {
1129 if let Some(text) = text {
1130 let extra = match (label, &item.value_kind) {
1133 ("field_value", Some(kind)) => json!({ "kind": kind }),
1134 _ => json!({}),
1135 };
1136 child_refs.push(json!({ "$ref": self.add_text(label, text, &self_ref, extra) }));
1137 }
1138 }
1139 self.field_items[item_index] = json!({
1140 "self_ref": self_ref,
1141 "parent": { "$ref": parent },
1142 "children": child_refs,
1143 "content_layer": "body",
1144 "label": "field_item",
1145 "prov": [],
1146 });
1147 self_ref
1148 }
1149
1150 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1154 let bucket = match self_ref.split('/').nth(1) {
1155 Some("texts") => &mut self.texts,
1156 Some("tables") => &mut self.tables,
1157 Some("pictures") => &mut self.pictures,
1158 Some("groups") => &mut self.groups,
1159 _ => return,
1160 };
1161 if let Some(item) = self_ref
1162 .rsplit('/')
1163 .next()
1164 .and_then(|i| i.parse::<usize>().ok())
1165 .and_then(|i| bucket.get_mut(i))
1166 {
1167 item["content_layer"] = json!(layer);
1168 }
1169 }
1170
1171 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1172 let prov = self.take_prov(unescape_text(text).chars().count());
1173 self.add_text_with(label, text, parent, extra, prov)
1174 }
1175
1176 fn add_text_with(
1179 &mut self,
1180 label: &str,
1181 text: &str,
1182 parent: &str,
1183 extra: Value,
1184 prov: Value,
1185 ) -> String {
1186 let self_ref = format!("#/texts/{}", self.texts.len());
1187 let raw = unescape_text(text);
1188 let mut item = json!({
1189 "self_ref": self_ref,
1190 "parent": { "$ref": parent },
1191 "children": [],
1192 "content_layer": "body",
1193 "label": label,
1194 "prov": prov,
1195 "orig": raw,
1196 "text": raw,
1197 });
1198 merge(&mut item, extra);
1199 self.texts.push(item);
1200 self_ref
1201 }
1202
1203 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
1206 let self_ref = format!("#/texts/{}", self.texts.len());
1207 let prov = self.take_prov(latex.chars().count());
1208 self.texts.push(json!({
1209 "self_ref": self_ref,
1210 "parent": { "$ref": parent },
1211 "children": [],
1212 "content_layer": "body",
1213 "label": "formula",
1214 "prov": prov,
1215 "orig": latex,
1216 "text": latex,
1217 }));
1218 self_ref
1219 }
1220
1221 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
1225 let self_ref = format!("#/texts/{}", self.texts.len());
1226 let prov = self.take_prov(latex.chars().count());
1227 self.texts.push(json!({
1228 "self_ref": self_ref,
1229 "parent": { "$ref": parent },
1230 "children": [],
1231 "content_layer": "body",
1232 "label": "formula",
1233 "prov": prov,
1234 "orig": orig,
1235 "text": latex,
1236 }));
1237 self_ref
1238 }
1239
1240 fn add_code(
1241 &mut self,
1242 text: &str,
1243 language: Option<&str>,
1244 orig: Option<&str>,
1245 parent: &str,
1246 ) -> String {
1247 let self_ref = format!("#/texts/{}", self.texts.len());
1248 let raw = unescape_text(text);
1249 let prov = self.take_prov(raw.chars().count());
1250 self.texts.push(json!({
1251 "self_ref": self_ref,
1252 "parent": { "$ref": parent },
1253 "children": [],
1254 "content_layer": "body",
1255 "label": "code",
1256 "prov": prov,
1257 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
1260 "text": raw,
1261 "captions": [],
1262 "references": [],
1263 "footnotes": [],
1264 "code_language": code_language(language),
1265 }));
1266 self_ref
1267 }
1268
1269 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
1272 let self_ref = format!("#/groups/{}", self.groups.len());
1273 self.groups.push(Value::Null);
1275 let base = level_of(&items[0]);
1276 let mut children = Vec::new();
1277 let mut i = 0;
1278 while i < items.len() {
1279 if !matches!(items[i], Node::ListItem { .. }) {
1282 i += 1;
1283 continue;
1284 }
1285 let lvl = level_of(&items[i]);
1286 if lvl > base {
1287 i += 1;
1289 continue;
1290 }
1291 let item_ref = self.add_list_item(&items[i], &self_ref);
1292 let mut j = i + 1;
1294 while j < items.len() && level_of(&items[j]) > base {
1295 j += 1;
1296 }
1297 if j > i + 1 {
1298 let mut nested = Vec::new();
1299 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
1300 if let Some(idx) = ref_index(&item_ref) {
1302 self.texts[idx]["children"]
1303 .as_array_mut()
1304 .unwrap()
1305 .extend(nested);
1306 }
1307 }
1308 children.push(json!({ "$ref": item_ref }));
1309 i = j;
1310 }
1311 self.groups[group_index(&self_ref)] = json!({
1312 "self_ref": self_ref,
1313 "parent": { "$ref": parent },
1314 "children": children,
1315 "content_layer": "body",
1316 "name": "list",
1317 "label": "list",
1318 });
1319 self_ref
1320 }
1321
1322 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
1323 let Node::ListItem {
1324 ordered,
1325 number,
1326 text,
1327 location,
1328 ..
1329 } = node
1330 else {
1331 unreachable!()
1332 };
1333 self.adopt_loc(*location);
1334 let self_ref = format!("#/texts/{}", self.texts.len());
1335 let raw = unescape_text(text);
1336 let prov = self.take_prov(raw.chars().count());
1337 let marker = if *ordered {
1338 format!("{number}.")
1339 } else {
1340 "-".to_string()
1341 };
1342 self.texts.push(json!({
1343 "self_ref": self_ref,
1344 "parent": { "$ref": parent },
1345 "children": [],
1346 "content_layer": "body",
1347 "label": "list_item",
1348 "prov": prov,
1349 "orig": raw,
1350 "text": raw,
1351 "enumerated": ordered,
1352 "marker": marker,
1353 }));
1354 self_ref
1355 }
1356
1357 fn add_table(&mut self, t: &Table, parent: &str) -> String {
1358 self.add_table_with(t, parent, false)
1359 }
1360
1361 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
1364 let self_ref = format!("#/tables/{}", self.tables.len());
1365 self.adopt_loc(t.location);
1366 let prov = self.take_prov(0);
1367 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
1371 Some(cap) => self.add_caption(cap, json!({}), &self_ref, parent, t.caption_parent),
1372 None => (Vec::new(), Vec::new()),
1373 };
1374 let data = table_data_with(t, raw);
1375 self.tables.push(json!({
1376 "self_ref": self_ref,
1377 "parent": { "$ref": parent },
1378 "children": children,
1379 "content_layer": "body",
1380 "label": "table",
1381 "prov": prov,
1382 "captions": captions,
1383 "references": [],
1384 "footnotes": [],
1385 "data": data,
1386 "annotations": [],
1387 }));
1388 self_ref
1389 }
1390
1391 fn add_caption(
1397 &mut self,
1398 text: &str,
1399 extra: Value,
1400 self_ref: &str,
1401 parent: &str,
1402 choice: CaptionParent,
1403 ) -> (Vec<Value>, Vec<Value>) {
1404 let cap_parent = match choice {
1409 CaptionParent::Item => self_ref,
1410 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
1411 CaptionParent::Body => "#/body",
1412 };
1413 let cap_ref = json!({ "$ref": self.add_text("caption", text, cap_parent, extra) });
1414 match choice {
1415 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
1416 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
1419 CaptionParent::Body if parent == "#/body" => {
1420 self.pending_siblings.push(cap_ref.clone())
1421 }
1422 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
1423 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
1426 }
1427 (vec![cap_ref], Vec::new())
1428 }
1429
1430 fn add_picture(
1433 &mut self,
1434 caption: Option<&str>,
1435 caption_href: Option<&str>,
1436 image: Option<&crate::PictureImage>,
1437 meta: Option<Value>,
1438 parent: &str,
1439 caption_parent: CaptionParent,
1440 ) -> String {
1441 let self_ref = format!("#/pictures/{}", self.pictures.len());
1442 let prov = self.take_prov(0);
1445 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
1446 Some(cap) => {
1447 let extra = match caption_href {
1451 Some(href) => json!({ "hyperlink": href }),
1452 None => json!({}),
1453 };
1454 self.add_caption(cap, extra, &self_ref, parent, caption_parent)
1455 }
1456 None => (Vec::new(), Vec::new()),
1457 };
1458 self.push_picture(prov, captions, children, image, meta, parent)
1459 }
1460
1461 fn push_picture(
1464 &mut self,
1465 prov: Value,
1466 captions: Vec<Value>,
1467 children: Vec<Value>,
1468 image: Option<&crate::PictureImage>,
1469 meta: Option<Value>,
1470 parent: &str,
1471 ) -> String {
1472 let self_ref = format!("#/pictures/{}", self.pictures.len());
1473 let annotations = meta
1477 .as_ref()
1478 .and_then(|m| m.get("annotations").cloned())
1479 .unwrap_or_else(|| json!([]));
1480 let meta = meta.map(|mut m| {
1481 if let Some(obj) = m.as_object_mut() {
1482 obj.remove("annotations");
1483 }
1484 m
1485 });
1486 let mut item = match meta {
1490 Some(meta) => json!({
1491 "self_ref": self_ref,
1492 "parent": { "$ref": parent },
1493 "children": children,
1494 "content_layer": "body",
1495 "meta": meta,
1496 "label": "picture",
1497 "prov": prov,
1498 "captions": captions,
1499 "references": [],
1500 "footnotes": [],
1501 "annotations": annotations,
1502 }),
1503 None => json!({
1504 "self_ref": self_ref,
1505 "parent": { "$ref": parent },
1506 "children": children,
1507 "content_layer": "body",
1508 "label": "picture",
1509 "prov": prov,
1510 "captions": captions,
1511 "references": [],
1512 "footnotes": [],
1513 "annotations": annotations,
1514 }),
1515 };
1516 if let Some(img) = image {
1521 let image = json!({
1522 "mimetype": img.mimetype,
1523 "dpi": 72,
1524 "size": { "width": img.width as f64, "height": img.height as f64 },
1525 "uri": img.data_uri(),
1526 });
1527 if let Some(obj) = item.as_object_mut() {
1528 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
1529 obj.insert("image".into(), image);
1530 obj.insert("annotations".into(), annotations);
1531 }
1532 }
1533 self.pictures.push(item);
1534 self_ref
1535 }
1536
1537 fn add_group(
1538 &mut self,
1539 label: &str,
1540 name: Option<&str>,
1541 layer: Option<ContentLayer>,
1542 nodes: &[Node],
1543 parent: &str,
1544 ) -> String {
1545 let self_ref = format!("#/groups/{}", self.groups.len());
1546 self.groups.push(Value::Null);
1547 let mark = (
1551 self.texts.len(),
1552 self.tables.len(),
1553 self.pictures.len(),
1554 self.groups.len(),
1555 );
1556 let children = self.walk_into(nodes, &self_ref);
1557 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
1558 let content_layer = layer.map_or("body", |l| l.value());
1559 self.groups[group_index(&self_ref)] = json!({
1560 "self_ref": self_ref,
1561 "parent": { "$ref": parent },
1562 "children": children,
1563 "content_layer": content_layer,
1564 "name": name,
1565 "label": label,
1566 });
1567 if layer.is_some() {
1568 let (t, tb, p, g) = mark;
1569 for item in self.texts[t..]
1570 .iter_mut()
1571 .chain(self.tables[tb..].iter_mut())
1572 .chain(self.pictures[p..].iter_mut())
1573 .chain(self.groups[g..].iter_mut())
1574 {
1575 if let Some(obj) = item.as_object_mut() {
1576 obj.insert("content_layer".into(), json!(content_layer));
1577 }
1578 }
1579 }
1580 self_ref
1581 }
1582
1583 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Value> {
1586 let seqs: Option<Vec<usize>> = nodes
1591 .iter()
1592 .map(|n| match n {
1593 Node::Prov { seq: Some(s), .. } => Some(*s),
1594 _ => None,
1595 })
1596 .collect();
1597 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
1598 let mut order: Vec<usize> = (0..nodes.len()).collect();
1599 order.sort_by_key(|&i| seqs[i]);
1600 let mut slots: Vec<Vec<Value>> = vec![Vec::new(); nodes.len()];
1601 for i in order {
1602 if let Some(r) = self.add_node(&nodes[i], parent) {
1603 slots[i].append(&mut self.pending_siblings);
1604 slots[i].push(json!({ "$ref": r }));
1605 slots[i].append(&mut self.pending_after);
1606 }
1607 if parent == "#/body" {
1608 slots[i].append(&mut self.pending_body);
1609 }
1610 }
1611 return slots.into_iter().flatten().collect();
1612 }
1613 let mut children = Vec::new();
1614 let mut i = 0;
1615 while i < nodes.len() {
1616 if matches!(nodes[i], Node::ListItem { .. }) {
1617 let start = i;
1618 i += 1;
1619 loop {
1620 match nodes.get(i) {
1621 Some(Node::ListItem { .. }) => i += 1,
1622 Some(Node::Paragraph { text })
1625 if text.is_empty()
1626 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
1627 {
1628 i += 1
1629 }
1630 _ => break,
1631 }
1632 }
1633 self.add_sibling_lists(&nodes[start..i], parent, &mut children);
1634 } else {
1635 if let Some(r) = self.add_node(&nodes[i], parent) {
1636 children.append(&mut self.pending_siblings);
1637 children.push(json!({ "$ref": r }));
1638 children.append(&mut self.pending_after);
1639 }
1640 i += 1;
1641 }
1642 if parent == "#/body" {
1645 children.append(&mut self.pending_body);
1646 }
1647 }
1648 children
1649 }
1650
1651 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
1656 let base = level_of(&run[0]);
1657 let mut seg = 0;
1658 for k in 0..run.len() {
1659 let Node::ListItem {
1660 first_in_list,
1661 level,
1662 ..
1663 } = &run[k]
1664 else {
1665 continue;
1666 };
1667 if *level != base {
1668 continue; }
1670 if k > seg && *first_in_list {
1671 out.push(json!({ "$ref": self.add_list(&run[seg..k], parent) }));
1672 seg = k;
1673 }
1674 }
1675 out.push(json!({ "$ref": self.add_list(&run[seg..], parent) }));
1676 }
1677}
1678
1679fn level_of(node: &Node) -> u8 {
1680 match node {
1681 Node::ListItem { level, .. } => *level,
1682 _ => 0,
1683 }
1684}
1685
1686fn group_index(self_ref: &str) -> usize {
1687 self_ref.rsplit('/').next().unwrap().parse().unwrap()
1688}
1689
1690fn ref_index(self_ref: &str) -> Option<usize> {
1691 self_ref.rsplit('/').next()?.parse().ok()
1692}
1693
1694fn merge(target: &mut Value, extra: Value) {
1696 if let (Some(t), Some(e)) = (target.as_object_mut(), extra.as_object()) {
1697 for (k, v) in e {
1698 t.insert(k.clone(), v.clone());
1699 }
1700 }
1701}
1702
1703fn unescape_text(s: &str) -> String {
1705 s.replace("<", "<")
1706 .replace(">", ">")
1707 .replace("&", "&")
1708 .replace("\\_", "_")
1709}
1710
1711fn fnv1a(s: &str) -> u64 {
1714 let mut h: u64 = 0xcbf29ce484222325;
1715 for b in s.bytes() {
1716 h ^= b as u64;
1717 h = h.wrapping_mul(0x100000001b3);
1718 }
1719 h
1720}
1721
1722#[cfg(test)]
1723mod tests {
1724 use crate::{
1725 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
1726 };
1727 use serde_json::Value;
1728
1729 fn doc_with_image() -> DoclingDocument {
1730 let mut doc = DoclingDocument::new("t");
1731 doc.push(Node::Picture {
1732 caption: Some("Fig 1".into()),
1733 caption_href: None,
1734 image: Some(PictureImage {
1735 mimetype: "image/png".into(),
1736 width: 4,
1737 height: 2,
1738 data: b"foobar".to_vec(),
1739 }),
1740 classification: None,
1741 caption_parent: Default::default(),
1742 });
1743 doc
1744 }
1745
1746 #[test]
1751 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
1752 let mut doc = DoclingDocument::new("t");
1753 doc.push(Node::Heading {
1754 level: 1,
1755 text: "Slide One".into(),
1756 });
1757 doc.push(Node::Furniture {
1758 layer: ContentLayer::Notes,
1759 inner: Box::new(Node::Located {
1760 location: [0, 0, 0, 0],
1761 inner: Box::new(Node::Paragraph {
1762 text: "Speaker note for slide 1.".into(),
1763 }),
1764 }),
1765 });
1766 doc.push(Node::Furniture {
1767 layer: ContentLayer::Furniture,
1768 inner: Box::new(Node::Paragraph {
1769 text: "page header".into(),
1770 }),
1771 });
1772
1773 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1774 let texts = v["texts"].as_array().unwrap();
1775 assert_eq!(
1776 texts
1777 .iter()
1778 .map(|t| (
1779 t["label"].as_str().unwrap(),
1780 t["content_layer"].as_str().unwrap(),
1781 t["text"].as_str().unwrap()
1782 ))
1783 .collect::<Vec<_>>(),
1784 vec![
1785 ("title", "body", "Slide One"),
1786 ("text", "notes", "Speaker note for slide 1."),
1787 ],
1788 "the note is carried on its own layer; the furniture is not carried"
1789 );
1790 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
1792 }
1793
1794 #[test]
1800 fn continuation_flags_become_spanning_cells() {
1801 let mut doc = DoclingDocument::new("t");
1802 let rows = vec![
1804 vec!["merged".to_string(), "merged".into(), "merged".into()],
1805 vec!["merged".to_string(), "merged".into(), "merged".into()],
1806 vec!["a".to_string(), "b".into(), "c".into()],
1807 ];
1808 doc.push(Node::Table(crate::Table {
1809 rows,
1810 location: None,
1811 structure: Some(crate::TableStructure {
1812 header_row: vec![true, false, false],
1813 col_continuation: vec![
1814 vec![false, true, true],
1815 vec![false, true, true],
1816 vec![false, false, false],
1817 ],
1818 row_continuation: vec![
1819 vec![false, false, false],
1820 vec![true, true, true],
1821 vec![false, false, false],
1822 ],
1823 row_header: Vec::new(),
1824 col_header: Vec::new(),
1825 }),
1826 cell_blocks: None,
1827 cells: None,
1828 caption: None,
1829 caption_parent: Default::default(),
1830 }));
1831 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1832 let data = &v["tables"][0]["data"];
1833 assert_eq!(data["num_rows"], 3);
1834 assert_eq!(data["num_cols"], 3);
1835 let cells = data["table_cells"].as_array().unwrap();
1836 assert_eq!(
1837 cells.len(),
1838 4,
1839 "one cell for the range, three for the plain row"
1840 );
1841 assert_eq!(
1842 cells[0],
1843 serde_json::json!({
1844 "row_span": 2, "col_span": 3,
1845 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
1846 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
1847 "text": "merged", "column_header": true, "row_header": false,
1848 "row_section": false, "fillable": false,
1849 })
1850 );
1851 assert_eq!(cells[1]["text"], "a");
1852 assert_eq!(cells[1]["row_span"], 1);
1853 assert_eq!(cells[1]["column_header"], false);
1854 let grid = data["grid"].as_array().unwrap();
1856 assert_eq!(grid.len(), 3);
1857 for (r, row) in grid.iter().take(2).enumerate() {
1858 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
1859 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
1860 }
1861 }
1862 assert_eq!(grid[2][2]["text"], "c");
1863 }
1864
1865 #[test]
1871 fn exact_provenance_pages_and_chart_captions_follow_docling() {
1872 let mut doc = DoclingDocument::new("t");
1873 doc.push(Node::PageInfo {
1874 page_no: 1,
1875 width: 3.0,
1876 height: 4.0,
1877 });
1878 let table = crate::Table {
1879 rows: vec![vec!["a".to_string(), "b".into()]],
1880 ..Default::default()
1881 };
1882 doc.push(Node::Group {
1883 label: "sheet".into(),
1884 name: Some("Data".into()),
1885 layer: None,
1886 children: vec![
1887 Node::Prov {
1892 page_no: 1,
1893 bbox: [0.0, 0.0, 3.0, 4.0],
1894 charspan: [0, 0],
1895 seq: Some(1),
1896 inner: Box::new(Node::Table(table.clone())),
1897 },
1898 Node::Prov {
1899 page_no: 1,
1900 bbox: [0.0, 1.0, 1.0, 1.0],
1901 charspan: [0, 0],
1902 seq: Some(0),
1903 inner: Box::new(Node::Chart {
1904 kind: "bar_chart".into(),
1905 table,
1906 caption: Some("Sales".into()),
1907 location: Some([0, 128, 170, 128]),
1908 }),
1909 },
1910 ],
1911 });
1912 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1913 assert_eq!(
1914 v["pages"],
1915 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
1916 );
1917 assert_eq!(
1918 v["tables"][0]["prov"],
1919 serde_json::json!([{
1920 "page_no": 1,
1921 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
1922 "charspan": [0, 0],
1923 }])
1924 );
1925 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
1926 let sheet = &v["groups"][0];
1929 assert_eq!(
1930 sheet["children"],
1931 serde_json::json!([
1932 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
1933 ])
1934 );
1935 let cap = &v["texts"][0];
1936 assert_eq!(cap["label"], "caption");
1937 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
1938 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
1939 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
1940 let pic = &v["pictures"][0];
1941 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
1942 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
1943 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
1944 assert_eq!(
1945 pic["meta"]["classification"]["predictions"][0]["class_name"],
1946 "bar_chart"
1947 );
1948 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
1949 }
1950
1951 #[test]
1955 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
1956 let mut doc = DoclingDocument::new("t");
1957 doc.push(Node::PageInfo {
1958 page_no: 1,
1959 width: 12192000.0,
1960 height: 6858000.0,
1961 });
1962 doc.push(Node::Furniture {
1963 layer: ContentLayer::Notes,
1964 inner: Box::new(Node::Located {
1965 location: [0, 0, 0, 0],
1966 inner: Box::new(Node::Paragraph {
1967 text: "a note".into(),
1968 }),
1969 }),
1970 });
1971 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1972 let prov = &v["texts"][0]["prov"][0];
1973 assert_eq!(prov["page_no"], 1);
1974 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
1975 assert_eq!(
1976 prov["bbox"],
1977 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
1978 );
1979 assert_eq!(
1981 v["pages"]["1"]["size"],
1982 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
1983 );
1984 }
1985
1986 #[test]
1992 fn page_markers_produce_pages_and_prov() {
1993 let mut doc = DoclingDocument::new("t");
1994 doc.push(Node::PageInfo {
1995 page_no: 1,
1996 width: 512.0,
1997 height: 1024.0,
1998 });
1999 doc.push(Node::Located {
2000 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
2002 text: "hello".into(),
2003 }),
2004 });
2005 doc.push(Node::Table(Table {
2006 rows: vec![vec!["a".into()]],
2007 location: Some([0, 0, 512, 512]),
2008 ..Table::default()
2009 }));
2010 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2011 assert_eq!(v["pages"]["1"]["page_no"], 1);
2012 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
2013 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
2014 let prov = &v["texts"][0]["prov"][0];
2017 assert_eq!(prov["page_no"], 1);
2018 assert_eq!(prov["bbox"]["l"], 128.0);
2019 assert_eq!(prov["bbox"]["t"], 896.0);
2020 assert_eq!(prov["bbox"]["r"], 256.0);
2021 assert_eq!(prov["bbox"]["b"], 768.0);
2022 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
2023 assert_eq!(prov["charspan"][1], 5);
2024 let tprov = &v["tables"][0]["prov"][0];
2026 assert_eq!(tprov["bbox"]["t"], 1024.0);
2027 assert_eq!(tprov["bbox"]["b"], 0.0);
2028 assert_eq!(tprov["charspan"][1], 0);
2029
2030 let mut plain = DoclingDocument::new("t");
2032 plain.push(Node::Located {
2033 location: [1, 2, 3, 4],
2034 inner: Box::new(Node::Paragraph { text: "x".into() }),
2035 });
2036 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2037 assert_eq!(v["pages"], serde_json::json!({}));
2038 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2039 }
2040
2041 #[test]
2042 fn picture_image_in_markdown_modes_and_json() {
2043 let doc = doc_with_image();
2044 assert!(doc.export_to_markdown().contains("<!-- image -->"));
2046 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
2048 assert!(
2049 md.contains(""),
2050 "got:\n{md}"
2051 );
2052 assert!(files.is_empty());
2053 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
2055 assert!(
2056 md.contains(""),
2057 "got:\n{md}"
2058 );
2059 assert_eq!(
2060 files,
2061 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
2062 );
2063 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2066 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
2067 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
2068 let keys: Vec<&str> = v["pictures"][0]
2069 .as_object()
2070 .unwrap()
2071 .keys()
2072 .map(String::as_str)
2073 .collect();
2074 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
2075 assert_eq!(
2076 v["pictures"][0]["image"]["uri"],
2077 "data:image/png;base64,Zm9vYmFy"
2078 );
2079 }
2080
2081 #[test]
2082 fn exports_docling_schema() {
2083 let mut doc = DoclingDocument::new("t");
2084 doc.push(Node::Heading {
2085 level: 1,
2086 text: "Title".into(),
2087 });
2088 doc.push(Node::Heading {
2089 level: 2,
2090 text: "Sec".into(),
2091 });
2092 doc.push(Node::Paragraph {
2093 text: "Body & more".into(),
2094 }); doc.push(Node::ListItem {
2096 ordered: false,
2097 number: 0,
2098 first_in_list: true,
2099 text: "one".into(),
2100 level: 0,
2101 marker: None,
2102 location: None,
2103 dclx: None,
2104 href: None,
2105 layer: None,
2106 });
2107 doc.push(Node::ListItem {
2108 ordered: false,
2109 number: 0,
2110 first_in_list: false,
2111 text: "two".into(),
2112 level: 0,
2113 marker: None,
2114 location: None,
2115 dclx: None,
2116 href: None,
2117 layer: None,
2118 });
2119 doc.push(Node::Table(Table {
2120 rows: vec![vec!["A".into(), "B".into()]],
2121 location: None,
2122 structure: None,
2123 cell_blocks: None,
2124 cells: None,
2125 caption: None,
2126 caption_parent: Default::default(),
2127 }));
2128
2129 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2130 assert_eq!(v["schema_name"], "DoclingDocument");
2131 assert_eq!(v["version"], "1.10.0");
2132 assert_eq!(v["texts"][0]["label"], "title");
2133 assert_eq!(v["texts"][1]["label"], "section_header");
2134 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
2138 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
2139 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
2140 assert_eq!(v["texts"][3]["marker"], "-");
2141 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
2143 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
2144 }
2145 #[test]
2149 fn a_layered_group_stamps_its_whole_subtree() {
2150 let doc = DoclingDocument {
2151 name: "s".into(),
2152 nodes: vec![
2153 Node::Group {
2154 label: "sheet".into(),
2155 name: Some("Sheet1".into()),
2156 layer: None,
2157 children: vec![Node::Paragraph {
2158 text: "visible".into(),
2159 }],
2160 },
2161 Node::Group {
2162 label: "sheet".into(),
2163 name: Some("Sheet2".into()),
2164 layer: Some(ContentLayer::Invisible),
2165 children: vec![Node::Paragraph {
2166 text: "hidden".into(),
2167 }],
2168 },
2169 ],
2170 ..DoclingDocument::new("s")
2171 };
2172 let v = crate::json::to_json(&doc);
2173 assert_eq!(v["groups"][0]["label"], "sheet");
2174 assert_eq!(v["groups"][0]["name"], "Sheet1");
2175 assert_eq!(v["groups"][0]["content_layer"], "body");
2176 assert_eq!(v["texts"][0]["content_layer"], "body");
2177 assert_eq!(v["groups"][1]["name"], "Sheet2");
2178 assert_eq!(v["groups"][1]["content_layer"], "invisible");
2179 assert_eq!(v["texts"][1]["content_layer"], "invisible");
2180 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
2182 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
2183 }
2184
2185 #[test]
2195 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
2196 use crate::tree::{ItemTree, TreeKind};
2197 let mut t = ItemTree::default();
2198 let text = |txt: &str| TreeKind::Text {
2199 label: "text".into(),
2200 text: txt.into(),
2201 orig: None,
2202 formatting: None,
2203 hyperlink: None,
2204 level: None,
2205 list: None,
2206 };
2207 let a = t.add(None, None, text("a"));
2208 let blank = t.add(None, None, text(""));
2209 let b = t.add(None, None, text("b"));
2210 t.delete(blank);
2211 let group = t.add(
2212 None,
2213 Some(ContentLayer::Notes),
2214 TreeKind::Group {
2215 label: "comment_section".into(),
2216 name: "comment-0".into(),
2217 },
2218 );
2219 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
2220 t.items[a].comments.push(group);
2221 t.add(
2222 None,
2223 None,
2224 TreeKind::Picture {
2225 captions: Vec::new(),
2226 image: None,
2227 classification: Some("bar_chart".into()),
2228 chart: Some(Table {
2229 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
2230 ..Table::default()
2231 }),
2232 dpi: None,
2233 },
2234 );
2235 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
2236 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
2237 let mut doc = DoclingDocument::new("t");
2238 doc.tree = Some(t);
2239 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2240 let texts = v["texts"].as_array().unwrap();
2241 assert_eq!(texts.len(), 3);
2242 assert_eq!(texts[1]["text"], "b");
2243 assert_eq!(texts[1]["self_ref"], "#/texts/1");
2244 assert_eq!(
2245 v["body"]["children"],
2246 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
2247 );
2248 let keys: Vec<&str> = texts[0]
2249 .as_object()
2250 .unwrap()
2251 .keys()
2252 .map(String::as_str)
2253 .collect();
2254 assert_eq!(
2255 keys,
2256 vec![
2257 "self_ref",
2258 "parent",
2259 "children",
2260 "content_layer",
2261 "label",
2262 "prov",
2263 "comments",
2264 "orig",
2265 "text"
2266 ]
2267 );
2268 assert_eq!(
2269 texts[0]["comments"],
2270 serde_json::json!([{"$ref": "#/groups/0"}])
2271 );
2272 assert!(texts[1].get("comments").is_none());
2273 let meta = &v["pictures"][0]["meta"];
2274 assert_eq!(
2275 meta["classification"]["predictions"][0]["class_name"],
2276 "bar_chart"
2277 );
2278 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
2279 }
2280
2281 #[test]
2287 fn tree_items_carry_exact_provenance_and_dpi() {
2288 use crate::tree::{ItemTree, TreeKind, TreeProv};
2289 let text = |label: &str, t: &str| TreeKind::Text {
2290 label: label.into(),
2291 text: t.into(),
2292 orig: None,
2293 formatting: None,
2294 hyperlink: None,
2295 level: None,
2296 list: None,
2297 };
2298 let mut t = ItemTree::default();
2299 let slide = t.add(
2300 None,
2301 None,
2302 TreeKind::Group {
2303 label: "chapter".into(),
2304 name: "slide-0".into(),
2305 },
2306 );
2307 t.add_with_prov(
2308 Some(slide),
2309 None,
2310 text("paragraph", "héllo"),
2311 TreeProv {
2312 page_no: 1,
2313 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
2314 bottom_left: true,
2315 charspan: [0, 5],
2316 },
2317 );
2318 t.add_with_prov(
2319 Some(slide),
2320 None,
2321 TreeKind::Picture {
2322 captions: Vec::new(),
2323 image: Some(crate::PictureImage {
2324 mimetype: "image/png".into(),
2325 width: 2,
2326 height: 2,
2327 data: vec![0],
2328 }),
2329 classification: None,
2330 chart: None,
2331 dpi: Some(300),
2332 },
2333 TreeProv {
2334 page_no: 1,
2335 bbox: [0.0; 4],
2336 bottom_left: false,
2337 charspan: [0, 0],
2338 },
2339 );
2340 t.add(
2341 Some(slide),
2342 Some(ContentLayer::Notes),
2343 text("text", "no geometry"),
2344 );
2345 let mut doc = DoclingDocument::new("t");
2346 doc.push(Node::PageInfo {
2347 page_no: 1,
2348 width: 9144000.0,
2349 height: 6858000.0,
2350 });
2351 doc.tree = Some(t);
2352 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2353 assert_eq!(
2354 v["texts"][0]["prov"],
2355 serde_json::json!([{
2356 "page_no": 1,
2357 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
2358 "charspan": [0, 5],
2359 }])
2360 );
2361 assert_eq!(v["texts"][0]["label"], "paragraph");
2362 assert_eq!(
2363 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
2364 "TOPLEFT"
2365 );
2366 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
2367 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
2368 assert_eq!(v["texts"][1]["content_layer"], "notes");
2369 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
2370 assert_eq!(v["pages"]["1"]["page_no"], 1);
2371 }
2372
2373 #[test]
2374 fn a_backend_item_tree_is_written_verbatim() {
2375 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
2376 let mut t = ItemTree::default();
2377 let text = |label: &str, txt: &str| TreeKind::Text {
2378 label: label.into(),
2379 text: txt.into(),
2380 orig: None,
2381 formatting: None,
2382 hyperlink: None,
2383 level: None,
2384 list: None,
2385 };
2386 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
2387 let h = t.add(None, None, text("title", "Heading"));
2388 let group = t.add(
2389 Some(h),
2390 None,
2391 TreeKind::Group {
2392 label: "inline".into(),
2393 name: "group".into(),
2394 },
2395 );
2396 t.add(
2397 Some(group),
2398 None,
2399 TreeKind::Text {
2400 label: "text".into(),
2401 text: "bold".into(),
2402 orig: None,
2403 formatting: Some(Formatting {
2404 bold: true,
2405 ..Formatting::default()
2406 }),
2407 hyperlink: Some("https://example.com/".into()),
2408 level: None,
2409 list: None,
2410 },
2411 );
2412 t.add(
2413 Some(group),
2414 None,
2415 TreeKind::Code {
2416 text: "x = 1".into(),
2417 orig: None,
2418 language: Some("python".into()),
2419 formatting: None,
2420 hyperlink: None,
2421 },
2422 );
2423 let sub = t.add(
2424 Some(h),
2425 None,
2426 TreeKind::Text {
2427 label: "section_header".into(),
2428 text: "Sub".into(),
2429 orig: Some("Sub\u{2019}".into()),
2430 formatting: None,
2431 hyperlink: None,
2432 level: Some(1),
2433 list: None,
2434 },
2435 );
2436 t.add(
2437 Some(sub),
2438 None,
2439 TreeKind::Text {
2440 label: "list_item".into(),
2441 text: "item".into(),
2442 orig: None,
2443 formatting: None,
2444 hyperlink: None,
2445 level: None,
2446 list: Some(ListMeta {
2447 enumerated: true,
2448 marker: "3.".into(),
2449 }),
2450 },
2451 );
2452 let _region = t.add(
2453 Some(sub),
2454 None,
2455 TreeKind::FieldRegion {
2456 items: vec![crate::FieldItem {
2457 marker: None,
2458 key: Some("Name".into()),
2459 value: Some("Duck".into()),
2460 value_kind: Some("read_only".into()),
2461 }],
2462 },
2463 );
2464 let table = t.add(
2465 Some(sub),
2466 None,
2467 TreeKind::Table {
2468 table: Table {
2469 rows: vec![vec!["a \n<".into(), "b".into()]],
2470 cells: Some(vec![
2471 crate::TableCell {
2472 text: "a \n<".into(),
2473 bbox: None,
2474 start_row: 0,
2475 start_col: 0,
2476 row_span: 3,
2477 col_span: 1,
2478 column_header: false,
2479 row_header: true,
2480 row_section: false,
2481 },
2482 crate::TableCell {
2483 text: "b".into(),
2484 bbox: None,
2485 start_row: 0,
2486 start_col: 1,
2487 row_span: 1,
2488 col_span: 1,
2489 column_header: false,
2490 row_header: false,
2491 row_section: false,
2492 },
2493 ]),
2494 ..Table::default()
2495 },
2496 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
2498 },
2499 );
2500 let cell_group = t.add(
2501 Some(table),
2502 None,
2503 TreeKind::Group {
2504 label: "unspecified".into(),
2505 name: "rich_cell_group_1_0_0".into(),
2506 },
2507 );
2508 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
2509 *rich_cells = vec![(0, 1, cell_group)];
2510 }
2511 let after = t.add(Some(sub), None, text("text", "after the region"));
2512 let _ = (title, after);
2513
2514 let doc = DoclingDocument {
2515 tree: Some(t),
2516 ..DoclingDocument::new("t")
2517 };
2518 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2519 let texts: Vec<&str> = v["texts"]
2521 .as_array()
2522 .unwrap()
2523 .iter()
2524 .map(|t| t["text"].as_str().unwrap())
2525 .collect();
2526 assert_eq!(
2527 texts,
2528 [
2529 "Page",
2530 "Heading",
2531 "bold",
2532 "x = 1",
2533 "Sub",
2534 "item",
2535 "Name",
2536 "Duck",
2537 "after the region"
2538 ]
2539 );
2540 assert_eq!(
2541 v["body"]["children"],
2542 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
2543 );
2544 assert_eq!(v["texts"][0]["content_layer"], "furniture");
2545 assert_eq!(
2546 v["texts"][1]["children"],
2547 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
2548 );
2549 let bold = &v["texts"][2];
2550 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
2551 let keys: Vec<&str> = bold
2552 .as_object()
2553 .unwrap()
2554 .keys()
2555 .map(String::as_str)
2556 .collect();
2557 assert_eq!(
2558 keys,
2559 [
2560 "self_ref",
2561 "parent",
2562 "children",
2563 "content_layer",
2564 "label",
2565 "prov",
2566 "orig",
2567 "text",
2568 "formatting",
2569 "hyperlink"
2570 ]
2571 );
2572 assert_eq!(
2573 bold["formatting"],
2574 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
2575 );
2576 let code = &v["texts"][3];
2577 assert_eq!(code["label"], "code");
2578 assert_eq!(code["code_language"], "Python");
2579 let sub = &v["texts"][4];
2580 assert_eq!(sub["orig"], "Sub\u{2019}");
2581 assert_eq!(sub["level"], 1);
2582 let item = &v["texts"][5];
2583 let keys: Vec<&str> = item
2584 .as_object()
2585 .unwrap()
2586 .keys()
2587 .map(String::as_str)
2588 .collect();
2589 assert_eq!(
2590 keys,
2591 [
2592 "self_ref",
2593 "parent",
2594 "children",
2595 "content_layer",
2596 "label",
2597 "prov",
2598 "orig",
2599 "text",
2600 "enumerated",
2601 "marker"
2602 ]
2603 );
2604 assert_eq!(item["marker"], "3.");
2605 assert_eq!(v["texts"][7]["kind"], "read_only");
2606 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
2607 let table = &v["tables"][0];
2608 assert_eq!(
2609 table["children"],
2610 serde_json::json!([{"$ref": "#/groups/1"}])
2611 );
2612 let cells = table["data"]["table_cells"].as_array().unwrap();
2613 assert_eq!(
2614 cells[0]["text"], "a \n<",
2615 "raw cell text is written verbatim"
2616 );
2617 assert_eq!(
2618 cells[0]["end_row_offset_idx"], 3,
2619 "declared spans are not clamped"
2620 );
2621 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
2622 assert!(cells[0].get("ref").is_none());
2623 assert!(
2624 table["data"]["grid"][0][1].get("ref").is_none(),
2625 "the grid shows plain cells"
2626 );
2627 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
2628 }
2629
2630 #[test]
2634 fn a_comment_section_can_be_referenced_by_its_note_text() {
2635 let doc = DoclingDocument {
2636 name: "c".into(),
2637 nodes: vec![
2638 Node::Commented {
2639 comments: vec![0],
2640 inner: Box::new(Node::Paragraph {
2641 text: "annotated".into(),
2642 }),
2643 },
2644 Node::CommentSection {
2645 name: "comment-Sheet1-A1".into(),
2646 text: "[author: A]: note".into(),
2647 refs_note_text: true,
2648 grouped: true,
2649 },
2650 ],
2651 ..DoclingDocument::new("c")
2652 };
2653 let v = crate::json::to_json(&doc);
2654 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
2655 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
2656 }
2657
2658 #[test]
2662 fn comment_sections_link_back_to_their_items() {
2663 let doc = DoclingDocument {
2664 name: "c".into(),
2665 nodes: vec![
2666 Node::Commented {
2667 comments: vec![0],
2668 inner: Box::new(Node::Paragraph {
2669 text: "annotated".into(),
2670 }),
2671 },
2672 Node::Paragraph {
2673 text: "plain".into(),
2674 },
2675 Node::CommentSection {
2676 name: "comment-7".into(),
2677 text: "[time: t]: note".into(),
2678 refs_note_text: false,
2679 grouped: true,
2680 },
2681 ],
2682 ..DoclingDocument::new("c")
2683 };
2684 let v = crate::json::to_json(&doc);
2685 assert_eq!(v["groups"][0]["label"], "comment_section");
2687 assert_eq!(v["groups"][0]["name"], "comment-7");
2688 assert_eq!(v["groups"][0]["content_layer"], "notes");
2689 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
2690 assert_eq!(v["texts"][2]["content_layer"], "notes");
2691 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
2693 assert!(v["texts"][1].get("comments").is_none());
2694 let keys: Vec<&str> = v["texts"][0]
2696 .as_object()
2697 .unwrap()
2698 .keys()
2699 .map(String::as_str)
2700 .collect();
2701 assert_eq!(
2702 &keys[keys.len() - 4..],
2703 &["prov", "comments", "orig", "text"]
2704 );
2705 }
2706
2707 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
2708 Node::Picture {
2709 caption: Some(caption.into()),
2710 caption_href: None,
2711 image: None,
2712 classification: None,
2713 caption_parent,
2714 }
2715 }
2716
2717 fn group(children: Vec<Node>) -> Node {
2718 Node::Group {
2719 label: "section".into(),
2720 name: None,
2721 layer: None,
2722 children,
2723 }
2724 }
2725
2726 fn refs(v: &Value) -> Vec<&str> {
2727 v.as_array()
2728 .unwrap()
2729 .iter()
2730 .map(|r| r["$ref"].as_str().unwrap())
2731 .collect()
2732 }
2733
2734 #[test]
2739 fn a_body_caption_follows_the_enclosing_top_level_item() {
2740 let mut doc = DoclingDocument::new("t");
2741 doc.push(picture("top", CaptionParent::Body));
2742 doc.push(group(vec![
2743 Node::Paragraph { text: "p".into() },
2744 picture("nested", CaptionParent::Body),
2745 ]));
2746 doc.push(Node::Paragraph {
2747 text: "after".into(),
2748 });
2749 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2750 assert_eq!(
2751 refs(&v["body"]["children"]),
2752 [
2753 "#/texts/0",
2754 "#/pictures/0",
2755 "#/groups/0",
2756 "#/texts/2",
2757 "#/texts/3"
2758 ]
2759 );
2760 assert_eq!(
2761 refs(&v["groups"][0]["children"]),
2762 ["#/texts/1", "#/pictures/1"]
2763 );
2764 for (cap, pic) in [(0, 0), (2, 1)] {
2765 assert_eq!(v["texts"][cap]["label"], "caption");
2766 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
2767 assert_eq!(
2768 refs(&v["pictures"][pic]["captions"]),
2769 [format!("#/texts/{cap}")]
2770 );
2771 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
2772 }
2773 }
2774
2775 #[test]
2778 fn an_item_caption_is_the_items_first_child() {
2779 let mut doc = DoclingDocument::new("t");
2780 doc.push(picture("fig", CaptionParent::Item));
2781 doc.push(Node::Table(Table {
2782 rows: vec![vec!["a".into()]],
2783 caption: Some("tab".into()),
2784 caption_parent: CaptionParent::Item,
2785 ..Table::default()
2786 }));
2787 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2788 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
2789 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
2790 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
2791 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
2792 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
2793 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
2794 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
2795 }
2796
2797 #[test]
2801 fn a_container_caption_is_the_items_sibling() {
2802 let mut doc = DoclingDocument::new("t");
2803 doc.push(group(vec![
2804 picture("chart", CaptionParent::Container),
2805 Node::Table(Table {
2806 rows: vec![vec!["a".into()]],
2807 caption: Some("figcaption".into()),
2808 caption_parent: CaptionParent::ContainerAfter,
2809 ..Table::default()
2810 }),
2811 ]));
2812 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2813 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
2814 assert_eq!(
2815 refs(&v["groups"][0]["children"]),
2816 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
2817 );
2818 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
2819 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
2820 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
2821 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
2822 }
2823}