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 clamp_boxes_to_pages(out: &mut Value, pages: &[(usize, f64, f64)]) {
129 if pages.is_empty() {
130 return;
131 }
132 let size = |page_no: &Value| -> Option<(f64, f64)> {
133 let n = page_no.as_u64()? as usize;
134 pages
135 .iter()
136 .find(|(p, _, _)| *p == n)
137 .map(|(_, w, h)| (*w, *h))
138 };
139 let r2 = |v: f64| (v * 100.0).round() / 100.0;
140 let clamp_bbox = |bbox: &mut Value, (w, h): (f64, f64)| {
141 for (key, hi) in [("l", w), ("r", w), ("t", h), ("b", h)] {
142 if let Some(v) = bbox.get(key).and_then(Value::as_f64) {
143 bbox[key] = json!(r2(v.clamp(0.0, hi.max(0.0))));
144 }
145 }
146 };
147 for bucket in [
148 "texts",
149 "pictures",
150 "tables",
151 "key_value_items",
152 "form_items",
153 "field_regions",
154 "field_items",
155 ] {
156 let Some(items) = out.get_mut(bucket).and_then(Value::as_array_mut) else {
157 continue;
158 };
159 for item in items {
160 let mut table_page: Option<Option<(f64, f64)>> = None;
161 if let Some(provs) = item.get_mut("prov").and_then(Value::as_array_mut) {
162 let mut page_nos: Vec<u64> = Vec::new();
163 for prov in provs.iter_mut() {
164 if let Some(n) = prov.get("page_no").and_then(Value::as_u64) {
165 page_nos.push(n);
166 }
167 let Some(sz) = prov.get("page_no").and_then(size) else {
168 continue;
169 };
170 if let Some(bbox) = prov.get_mut("bbox") {
171 clamp_bbox(bbox, sz);
172 }
173 }
174 page_nos.sort_unstable();
175 page_nos.dedup();
176 if let [only] = page_nos[..] {
177 table_page = Some(size(&json!(only)));
178 }
179 }
180 if let (Some(Some(sz)), Some(data)) = (table_page, item.get_mut("data")) {
182 for key in ["table_cells", "grid"] {
183 let Some(rows) = data.get_mut(key).and_then(Value::as_array_mut) else {
184 continue;
185 };
186 for entry in rows.iter_mut() {
187 let cells: Vec<&mut Value> = match entry {
188 Value::Array(row) => row.iter_mut().collect(),
189 other => vec![other],
190 };
191 for cell in cells {
192 if let Some(bbox) = cell.get_mut("bbox").filter(|b| b.is_object()) {
193 clamp_bbox(bbox, sz);
194 }
195 }
196 }
197 }
198 }
199 }
200 }
201}
202
203fn formatting_json(f: &crate::tree::Formatting) -> Value {
205 json!({
206 "bold": f.bold,
207 "italic": f.italic,
208 "underline": f.underline,
209 "strikethrough": f.strikethrough,
210 "script": match f.script {
211 crate::Script::Baseline => "baseline",
212 crate::Script::Sub => "sub",
213 crate::Script::Super => "super",
214 },
215 })
216}
217
218fn track_json(t: &crate::tree::TreeTrack) -> Value {
221 let mut m = serde_json::Map::new();
222 m.insert("kind".into(), json!("track"));
223 m.insert("start_time".into(), json!(t.start_time));
224 m.insert("end_time".into(), json!(t.end_time));
225 if let Some(id) = &t.identifier {
226 m.insert("identifier".into(), json!(id));
227 }
228 if let Some(v) = &t.voice {
229 m.insert("voice".into(), json!(v));
230 }
231 Value::Object(m)
232}
233
234fn image_ref_json(img: &crate::PictureImage) -> Value {
238 let (mimetype, uri) = crate::pixel_digest::docling_data_uri(img);
239 json!({
240 "mimetype": mimetype,
241 "dpi": img.dpi,
242 "size": { "width": img.width as f64, "height": img.height as f64 },
243 "uri": uri,
244 })
245}
246
247pub fn to_json(doc: &DoclingDocument) -> Value {
249 let mut b = Builder::default();
250 let body = match &doc.tree {
254 Some(tree) => {
255 for n in &doc.nodes {
258 if let Node::PageInfo {
259 page_no,
260 width,
261 height,
262 } = n
263 {
264 if *page_no > 0 {
265 b.pages.push((*page_no, *width as f64, *height as f64));
266 }
267 }
268 }
269 let mut tree = tree.clone();
272 tree.wrap_misplaced_list_items();
273 b.write_tree(&tree)
274 }
275 None => b.walk_into(&doc.nodes, "#/body"),
276 };
277 b.link_comments();
278
279 let mut out = json!({
280 "schema_name": "DoclingDocument",
281 "version": SCHEMA_VERSION,
282 "name": doc.name,
283 "origin": {
284 "mimetype": "text/plain",
285 "binary_hash": fnv1a(&doc.name),
286 "filename": doc.name,
287 },
288 "furniture": {
289 "self_ref": "#/furniture",
290 "children": [],
291 "content_layer": "furniture",
292 "name": "_root_",
293 "label": "unspecified",
294 },
295 "body": {
296 "self_ref": "#/body",
297 "children": body,
298 "content_layer": "body",
299 "name": "_root_",
300 "label": "unspecified",
301 },
302 "groups": b.groups,
303 "texts": b.texts,
304 "pictures": b.pictures,
305 "tables": b.tables,
306 "key_value_items": b.key_value_items,
307 "form_items": [],
308 "pages": b.pages.iter().map(|(n, w, h)| {
309 let r2 = |v: f64| (v * 100.0).round() / 100.0;
310 let mut page = serde_json::Map::new();
313 page.insert("size".into(), json!({ "width": r2(*w), "height": r2(*h) }));
314 if let Some(img) = doc.page_images.get(n) {
315 page.insert("image".into(), image_ref_json(img));
316 }
317 page.insert("page_no".into(), json!(n));
318 (n.to_string(), Value::Object(page))
319 }).collect::<serde_json::Map<String, Value>>(),
320 });
321
322 clamp_boxes_to_pages(&mut out, &b.pages);
331
332 if !b.field_regions.is_empty() {
337 if let Some(obj) = out.as_object_mut() {
338 let pages = obj.remove("pages");
339 obj.insert("field_regions".into(), Value::Array(b.field_regions));
340 obj.insert("field_items".into(), Value::Array(b.field_items));
341 if let Some(pages) = pages {
342 obj.insert("pages".into(), pages);
343 }
344 }
345 }
346 out
347}
348
349fn classification_meta(classes: &[crate::PictureClass]) -> Value {
356 json!({
357 "classification": {
358 "predictions": classes.iter().map(|c| json!({
359 "confidence": c.confidence as f64,
360 "created_by": "DocumentPictureClassifier",
361 "class_name": c.class_name,
362 })).collect::<Vec<_>>(),
363 },
364 "annotations": [{
365 "kind": "classification",
366 "provenance": "DocumentPictureClassifier",
367 "predicted_classes": classes.iter().map(|c| json!({
368 "class_name": c.class_name,
369 "confidence": c.confidence as f64,
370 })).collect::<Vec<_>>(),
371 }],
372 })
373}
374
375#[allow(clippy::too_many_arguments)]
386fn cell_value(
387 row_span: usize,
388 col_span: usize,
389 start_row: usize,
390 end_row: usize,
391 start_col: usize,
392 end_col: usize,
393 text: String,
394 column_header: bool,
395 row_header: bool,
396 row_section: bool,
397 bbox: Option<[f32; 4]>,
398) -> Value {
399 let mut m = serde_json::Map::with_capacity(12);
400 m.insert("row_span".into(), row_span.into());
401 m.insert("col_span".into(), col_span.into());
402 m.insert("start_row_offset_idx".into(), start_row.into());
403 m.insert("end_row_offset_idx".into(), end_row.into());
404 m.insert("start_col_offset_idx".into(), start_col.into());
405 m.insert("end_col_offset_idx".into(), end_col.into());
406 m.insert("text".into(), Value::String(text));
407 m.insert("column_header".into(), column_header.into());
408 m.insert("row_header".into(), row_header.into());
409 m.insert("row_section".into(), row_section.into());
410 m.insert("fillable".into(), false.into());
411 if let Some(b) = bbox {
412 m.insert(
413 "bbox".into(),
414 json!({
415 "l": b[0], "t": b[1], "r": b[2], "b": b[3],
416 "coord_origin": "TOPLEFT",
417 }),
418 );
419 }
420 Value::Object(m)
421}
422
423fn table_data(t: &Table) -> Value {
426 table_data_with(t, false)
427}
428
429fn table_data_with(t: &Table, raw: bool) -> Value {
432 let cell_text = |s: &str| {
433 if raw {
434 s.to_string()
435 } else {
436 unescape_text(&crate::markdown::strip_hard_breaks(s))
437 }
438 };
439 let mut num_rows = t.rows.len();
440 let mut num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
441 let ragged = t.rows.iter().any(|r| r.len() != num_cols);
449 if let Some(first_class) = t.cells.as_ref().filter(|c| ragged && !c.is_empty()) {
450 for c in first_class {
451 num_rows = num_rows.max(c.start_row + c.row_span);
452 num_cols = num_cols.max(c.start_col + c.col_span);
453 }
454 }
455 let mut grid = Vec::with_capacity(num_rows);
456 let mut cells = Vec::new();
457 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
461 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
462 for c in first_class {
463 let idx = cells.len();
464 cells.push(cell_value(
465 c.row_span,
466 c.col_span,
467 c.start_row,
468 c.start_row + c.row_span,
469 c.start_col,
470 c.start_col + c.col_span,
471 cell_text(&c.text),
472 c.column_header,
473 c.row_header,
474 c.row_section,
475 c.bbox,
476 ));
477 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
478 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
479 slot[r * num_cols + k] = Some(idx);
480 }
481 }
482 }
483 for r in 0..num_rows {
484 let mut grid_row = Vec::with_capacity(num_cols);
485 for c in 0..num_cols {
486 grid_row.push(match slot[r * num_cols + c] {
487 Some(i) => cells[i].clone(),
488 None => cell_value(
489 1,
490 1,
491 r,
492 r + 1,
493 c,
494 c + 1,
495 String::new(),
496 false,
497 false,
498 false,
499 None,
500 ),
501 });
502 }
503 grid.push(grid_row);
504 }
505 } else {
506 let s = t.structure.as_ref();
507 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
508 grid.and_then(|g| g.get(r))
509 .and_then(|row| row.get(c))
510 .copied()
511 .unwrap_or(false)
512 };
513 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
514 let (mut r0, mut c0) = (r, c);
515 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
516 c0 -= 1;
517 }
518 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
519 r0 -= 1;
520 }
521 (r0, c0)
522 };
523 let anchors: Vec<(usize, usize)> = (0..num_rows)
526 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
527 .map(|(r, c)| anchor_of(r, c))
528 .collect();
529 let mut extent: Vec<(usize, usize)> = (0..num_rows)
530 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
531 .collect();
532 for (i, &(ar, ac)) in anchors.iter().enumerate() {
533 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
534 let e = &mut extent[ar * num_cols + ac];
535 e.0 = e.0.max(r);
536 e.1 = e.1.max(c);
537 }
538 for (r, row) in t.rows.iter().enumerate() {
539 let mut grid_row = Vec::with_capacity(num_cols);
540 for c in 0..num_cols {
541 let (ar, ac) = anchors[r * num_cols + c];
542 if (ar, ac) == (r, c) {
543 let (er, ec) = extent[r * num_cols + c];
544 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
545 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
546 Some(s) => flag(Some(&s.col_header), r, c),
547 None => r == 0,
548 };
549 slot[r * num_cols + c] = Some(cells.len());
550 cells.push(cell_value(
551 er - r + 1,
552 ec - c + 1,
553 r,
554 er + 1,
555 c,
556 ec + 1,
557 text,
558 column_header,
559 flag(s.map(|s| &s.row_header), r, c),
560 false,
561 None,
562 ));
563 }
564 grid_row.push(match slot[ar * num_cols + ac] {
565 Some(i) => cells[i].clone(),
566 None => Value::Null,
567 });
568 }
569 grid.push(grid_row);
570 }
571 }
572 json!({
573 "table_cells": cells,
574 "num_rows": num_rows,
575 "num_cols": num_cols,
576 "orientation": "rot_0",
577 "grid": grid,
578 })
579}
580
581#[derive(Default)]
582struct Builder {
583 texts: Vec<Value>,
584 groups: Vec<Value>,
585 tables: Vec<Value>,
586 pictures: Vec<Value>,
587 field_regions: Vec<Value>,
588 field_items: Vec<Value>,
589 key_value_items: Vec<Value>,
590 pages: Vec<(usize, f64, f64)>,
594 cur_page: usize,
596 cur_w: f64,
597 cur_h: f64,
598 pending_loc: Option<[u16; 4]>,
601 pending_exact: Option<ExactProv>,
604 pending_siblings: Vec<Value>,
608 pending_after: Vec<Value>,
612 pending_body: Vec<Value>,
616 comment_groups: Vec<String>,
620 last_comment_section: Option<(String, String)>,
625 pending_comments: Vec<(String, Vec<usize>)>,
629}
630
631impl Builder {
632 fn take_prov(&mut self, char_len: usize) -> Value {
638 let prov = self.prov_json(char_len, false);
639 self.pending_exact = None;
640 self.pending_loc = None;
641 prov
642 }
643
644 fn prov_json(&self, char_len: usize, span_over_text: bool) -> Value {
649 let r2 = |v: f64| (v * 100.0).round() / 100.0;
650 if let Some(ExactProv {
651 page_no,
652 bbox: [l, t, r, b],
653 bottom_left,
654 charspan,
655 }) = self.pending_exact
656 {
657 let charspan = if span_over_text {
658 [0, char_len]
659 } else {
660 charspan
661 };
662 return json!([{
663 "page_no": page_no,
664 "bbox": {
665 "l": r2(l), "t": r2(t), "r": r2(r), "b": r2(b),
666 "coord_origin": if bottom_left { "BOTTOMLEFT" } else { "TOPLEFT" },
667 },
668 "charspan": charspan,
669 }]);
670 }
671 let Some([x0, y0, x1, y1]) = self.pending_loc else {
672 return json!([]);
673 };
674 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
678 return json!([{
679 "page_no": self.cur_page,
680 "bbox": { "l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT" },
681 "charspan": [0, char_len],
682 }]);
683 }
684 json!([{
685 "page_no": self.cur_page,
686 "bbox": {
687 "l": r2(x0 as f64 * self.cur_w / 512.0),
688 "t": r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
689 "r": r2(x1 as f64 * self.cur_w / 512.0),
690 "b": r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
691 "coord_origin": "BOTTOMLEFT",
692 },
693 "charspan": [0, char_len],
694 }])
695 }
696
697 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
700 if self.pending_loc.is_none() && self.cur_page > 0 {
701 self.pending_loc = loc;
702 }
703 }
704
705 fn link_comments(&mut self) {
713 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
714 .into_iter()
715 .map(|(item, comments)| {
716 let refs = comments
717 .iter()
718 .filter_map(|i| self.comment_groups.get(*i))
719 .map(|r| json!({ "$ref": r }))
720 .collect();
721 (item, refs)
722 })
723 .collect();
724 for (item, comment_refs) in refs {
725 if comment_refs.is_empty() {
726 continue;
727 }
728 let Some(target) = self.item_mut(&item) else {
729 continue;
730 };
731 let Some(obj) = target.as_object_mut() else {
732 continue;
733 };
734 let tail: Vec<(String, Value)> = obj
735 .iter()
736 .skip_while(|(k, _)| k.as_str() != "prov")
737 .skip(1)
738 .map(|(k, v)| (k.clone(), v.clone()))
739 .collect();
740 for (k, _) in &tail {
741 obj.shift_remove(k);
742 }
743 obj.insert("comments".into(), Value::Array(comment_refs));
744 for (k, v) in tail {
745 obj.insert(k, v);
746 }
747 }
748 }
749
750 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
752 let idx = ref_index(self_ref)?;
753 let bucket = if self_ref.starts_with("#/texts/") {
754 &mut self.texts
755 } else if self_ref.starts_with("#/tables/") {
756 &mut self.tables
757 } else if self_ref.starts_with("#/pictures/") {
758 &mut self.pictures
759 } else if self_ref.starts_with("#/groups/") {
760 &mut self.groups
761 } else {
762 return None;
763 };
764 bucket.get_mut(idx)
765 }
766
767 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
771 use crate::tree::TreeKind;
772 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
775 let (mut nt, mut ng, mut ntb, mut np, mut nf, mut nk) = (0, 0, 0, 0, 0, 0);
776 for item in &tree.items {
777 if item.deleted {
778 refs.push(String::new());
779 continue;
780 }
781 let r = match &item.kind {
782 TreeKind::Text { .. } | TreeKind::Code { .. } => {
783 nt += 1;
784 format!("#/texts/{}", nt - 1)
785 }
786 TreeKind::Group { .. } => {
787 ng += 1;
788 format!("#/groups/{}", ng - 1)
789 }
790 TreeKind::Table { .. } => {
791 ntb += 1;
792 format!("#/tables/{}", ntb - 1)
793 }
794 TreeKind::Picture { .. } => {
795 np += 1;
796 format!("#/pictures/{}", np - 1)
797 }
798 TreeKind::FieldRegion { items } => {
799 nt += items
802 .iter()
803 .map(|i| {
804 [&i.marker, &i.key, &i.value]
805 .iter()
806 .filter(|p| p.is_some())
807 .count()
808 })
809 .sum::<usize>();
810 nf += 1;
811 format!("#/field_regions/{}", nf - 1)
812 }
813 TreeKind::KeyValueGraph { .. } => {
814 nk += 1;
815 format!("#/key_value_items/{}", nk - 1)
816 }
817 };
818 refs.push(r);
819 }
820 let ref_of = |id: usize| json!({ "$ref": refs[id] });
821 for (id, item) in tree.items.iter().enumerate() {
822 if item.deleted {
823 continue;
824 }
825 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
826 let children: Vec<Value> = item
830 .children
831 .iter()
832 .filter(|&&c| !tree.items[c].deleted)
833 .map(|&c| ref_of(c))
834 .collect();
835 let layer = item.layer.map_or("body", |l| l.value());
836 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
839 let self_ref = match &item.kind {
840 TreeKind::Text {
841 label,
842 text,
843 orig,
844 formatting,
845 hyperlink,
846 level,
847 list,
848 } => {
849 let mut tail = serde_json::Map::new();
852 if let Some(f) = formatting {
853 tail.insert("formatting".into(), formatting_json(f));
854 }
855 if let Some(h) = hyperlink {
856 tail.insert("hyperlink".into(), json!(h));
857 }
858 if let Some(l) = level {
859 tail.insert("level".into(), json!(l));
860 }
861 if let Some(l) = list {
862 tail.insert("enumerated".into(), json!(l.enumerated));
863 tail.insert("marker".into(), json!(l.marker));
864 }
865 let r = format!("#/texts/{}", self.texts.len());
866 let prov = self.take_prov(text.chars().count());
867 let mut item_json = json!({
868 "self_ref": r,
869 "parent": { "$ref": parent },
870 "children": children,
871 "content_layer": layer,
872 "label": label,
873 "prov": prov,
874 });
875 if !item.comments.is_empty() {
878 item_json["comments"] =
879 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
880 }
881 if let Some(track) = &item.source {
884 item_json["source"] = json!([track_json(track)]);
885 }
886 merge(
887 &mut item_json,
888 json!({
889 "orig": orig.as_deref().unwrap_or(text),
890 "text": text,
891 }),
892 );
893 merge(&mut item_json, Value::Object(tail));
894 self.texts.push(item_json);
895 r
896 }
897 TreeKind::Code {
898 text,
899 orig,
900 language,
901 formatting,
902 hyperlink,
903 } => {
904 let r = format!("#/texts/{}", self.texts.len());
905 let prov = self.take_prov(text.chars().count());
906 let mut item_json = json!({
907 "self_ref": r,
908 "parent": { "$ref": parent },
909 "children": children,
910 "content_layer": layer,
911 "label": "code",
912 "prov": prov,
913 });
914 if !item.comments.is_empty() {
915 item_json["comments"] =
916 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
917 }
918 merge(
919 &mut item_json,
920 json!({
921 "orig": orig.as_deref().unwrap_or(text),
922 "text": text,
923 }),
924 );
925 if let Some(f) = formatting {
926 item_json["formatting"] = formatting_json(f);
927 }
928 if let Some(h) = hyperlink {
929 item_json["hyperlink"] = json!(h);
930 }
931 merge(
932 &mut item_json,
933 json!({
934 "captions": [],
935 "references": [],
936 "footnotes": [],
937 "code_language": code_language(language.as_deref()),
938 }),
939 );
940 self.texts.push(item_json);
941 r
942 }
943 TreeKind::Group { label, name } => {
944 self.pending_exact = None;
945 let r = format!("#/groups/{}", self.groups.len());
946 self.groups.push(json!({
947 "self_ref": r,
948 "parent": { "$ref": parent },
949 "children": children,
950 "content_layer": layer,
951 "name": name,
952 "label": label,
953 }));
954 r
955 }
956 TreeKind::Table {
957 table,
958 rich_cells,
959 captions,
960 } => {
961 let r = self.add_table_with(table, parent, true);
963 let idx = ref_index(&r).expect("table ref");
964 let t = &mut self.tables[idx];
965 t["children"] = Value::Array(children);
966 t["content_layer"] = json!(layer);
967 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
968 for &(row, col, group) in rich_cells {
972 let cell_ref = ref_of(group);
973 let hit = |c: &Value| {
974 c["start_row_offset_idx"] == json!(row)
975 && c["start_col_offset_idx"] == json!(col)
976 };
977 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
978 for c in cells.iter_mut().filter(|c| hit(c)) {
979 c["ref"] = cell_ref.clone();
980 }
981 }
982 }
983 r
984 }
985 TreeKind::Picture {
986 captions,
987 image,
988 classification,
989 confidence,
990 chart,
991 dpi,
992 } => {
993 let mut meta = classification.as_ref().map(|c| {
998 let mut pred = serde_json::Map::new();
999 if let Some(conf) = confidence {
1000 pred.insert("confidence".into(), json!(conf));
1001 }
1002 pred.insert("class_name".into(), json!(c));
1003 json!({ "classification": { "predictions": [pred] } })
1004 });
1005 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
1006 if !t.rows.is_empty() {
1007 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
1008 }
1009 }
1010 let prov = self.take_prov(0);
1011 let r = self.push_picture(
1012 prov,
1013 captions.iter().map(|&c| ref_of(c)).collect(),
1014 children,
1015 image.as_ref(),
1016 meta,
1017 parent,
1018 );
1019 if let Some(idx) = ref_index(&r) {
1020 self.pictures[idx]["content_layer"] = json!(layer);
1021 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
1024 img["dpi"] = json!(dpi);
1025 }
1026 }
1027 r
1028 }
1029 TreeKind::FieldRegion { items } => {
1030 self.pending_exact = None;
1031 let r = self.add_field_region(items, parent);
1032 if let Some(region) = self.field_regions.last_mut() {
1033 region["content_layer"] = json!(layer);
1034 }
1035 r
1036 }
1037 TreeKind::KeyValueGraph { cells, links } => {
1038 self.pending_exact = None;
1039 let r = self.add_key_value_graph(cells, links, parent);
1040 if let Some(item) = self.key_value_items.last_mut() {
1041 item["content_layer"] = json!(layer);
1042 }
1043 r
1044 }
1045 };
1046 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1047 }
1048 tree.body
1049 .iter()
1050 .filter(|&&c| !tree.items[c].deleted)
1051 .map(|&c| ref_of(c))
1052 .collect()
1053 }
1054
1055 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1056 match node {
1057 Node::Heading { level: 1, text } => {
1058 Some(self.add_text("title", text, parent, json!({})))
1059 }
1060 Node::Heading { level, text } => Some(self.add_text(
1061 "section_header",
1062 text,
1063 parent,
1064 json!({ "level": level.saturating_sub(1) }),
1065 )),
1066 Node::Caption { text, href } => {
1067 let extra = match href {
1068 Some(url) => json!({ "hyperlink": url }),
1069 None => json!({}),
1070 };
1071 Some(self.add_text("caption", text, parent, extra))
1072 }
1073 Node::Paragraph { text } => {
1074 let t = text.trim();
1077 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1078 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1079 _ => Some(self.add_text("text", text, parent, json!({}))),
1080 }
1081 }
1082 Node::CheckboxItem { checked, text } => {
1083 let mark = if *checked { "- [x] " } else { "- [ ] " };
1086 Some(self.add_text("text", &format!("{mark}{text}"), parent, json!({})))
1087 }
1088 Node::Code {
1089 language,
1090 text,
1091 orig,
1092 ..
1093 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1094 Node::Formula {
1097 latex,
1098 orig,
1099 location,
1100 } => {
1101 self.adopt_loc(*location);
1102 Some(self.add_formula_item(latex, orig, parent))
1103 }
1104 Node::CommentSection {
1110 name,
1111 text,
1112 refs_note_text,
1113 grouped,
1114 } => {
1115 if !*grouped {
1116 let child =
1120 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1121 self.comment_groups.push(child.clone());
1122 return Some(child);
1123 }
1124 if let Some((_, self_ref)) = self
1125 .last_comment_section
1126 .as_ref()
1127 .filter(|(n, _)| n == name)
1128 .cloned()
1129 {
1130 let child =
1133 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1134 if let Some(children) =
1135 self.groups[group_index(&self_ref)]["children"].as_array_mut()
1136 {
1137 children.push(json!({ "$ref": child }));
1138 }
1139 self.comment_groups.push(if *refs_note_text {
1140 child
1141 } else {
1142 self_ref.clone()
1143 });
1144 return Some(self_ref);
1145 }
1146 let self_ref = format!("#/groups/{}", self.groups.len());
1147 self.groups.push(Value::Null);
1148 let child =
1149 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1150 self.groups[group_index(&self_ref)] = json!({
1151 "self_ref": self_ref,
1152 "parent": { "$ref": parent },
1153 "children": [{ "$ref": child }],
1154 "content_layer": "notes",
1155 "name": name,
1156 "label": "comment_section",
1157 });
1158 self.last_comment_section = Some((name.clone(), self_ref.clone()));
1159 self.comment_groups.push(if *refs_note_text {
1160 child
1161 } else {
1162 self_ref.clone()
1163 });
1164 Some(self_ref)
1165 }
1166 Node::Commented { comments, inner } => {
1169 let item = self.add_node(inner, parent)?;
1170 if !comments.is_empty() {
1171 self.pending_comments.push((item.clone(), comments.clone()));
1172 }
1173 Some(item)
1174 }
1175 Node::Table(t) => Some(self.add_table(t, parent)),
1176 Node::Picture {
1177 caption,
1178 caption_href,
1179 image,
1180 classification,
1181 caption_parent,
1182 } => Some(self.add_picture(
1183 caption.as_deref(),
1184 caption_href.as_deref(),
1185 image.as_ref(),
1186 classification.as_deref().map(classification_meta),
1187 parent,
1188 *caption_parent,
1189 )),
1190 Node::Chart {
1195 kind,
1196 table,
1197 caption,
1198 location,
1199 } => {
1200 self.adopt_loc(*location);
1201 let mut meta = json!({
1202 "classification": { "predictions": [{ "class_name": kind }] },
1203 });
1204 if !table.rows.is_empty() {
1205 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1206 }
1207 let mut captions = Vec::new();
1213 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1214 let prov = self.prov_json(unescape_text(cap).chars().count(), true);
1215 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1216 self.pending_siblings.push(json!({ "$ref": cap_ref }));
1217 captions.push(json!({ "$ref": cap_ref }));
1218 }
1219 let prov = self.take_prov(0);
1220 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1221 }
1222 Node::DoclangOnly(_) => None,
1224 Node::Group {
1225 label,
1226 name,
1227 layer,
1228 children,
1229 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1230 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1231 Node::KeyValueGraph { cells, links } => {
1232 Some(self.add_key_value_graph(cells, links, parent))
1233 }
1234 Node::InlineGroup { md_text, .. } => {
1237 Some(self.add_text("text", md_text, parent, json!({})))
1238 }
1239 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1241 Node::Furniture {
1247 layer: ContentLayer::Notes,
1248 inner,
1249 } => {
1250 let item = self.add_node(inner, parent)?;
1251 self.set_layer(&item, "notes");
1252 Some(item)
1253 }
1254 Node::Furniture { .. } => None,
1255 Node::PageFurniture {
1259 footer,
1260 location,
1261 text,
1262 } => {
1263 if self.cur_page > 0 {
1264 self.pending_loc = Some(*location);
1265 }
1266 let label = if *footer {
1267 "page_footer"
1268 } else {
1269 "page_header"
1270 };
1271 let item = self.add_text(label, text, parent, json!({}));
1272 self.pending_loc = None;
1273 self.set_layer(&item, "furniture");
1274 Some(item)
1275 }
1276 Node::PictureChildren(children) => {
1280 let pic = self.pictures.len().checked_sub(1)?;
1281 let pic_ref = format!("#/pictures/{pic}");
1282 let refs = self.walk_into(children, &pic_ref);
1283 if let Some(list) = self.pictures[pic]["children"].as_array_mut() {
1284 list.extend(refs);
1285 }
1286 None
1287 }
1288 Node::Located { location, inner } => {
1293 if self.cur_page > 0 {
1294 self.pending_loc = Some(*location);
1295 }
1296 let r = self.add_node(inner, parent);
1297 self.pending_loc = None;
1298 r
1299 }
1300 Node::Prov {
1301 page_no,
1302 bbox,
1303 charspan,
1304 inner,
1305 ..
1306 } => {
1307 self.pending_exact = Some(ExactProv {
1308 page_no: *page_no,
1309 bbox: bbox.map(f64::from),
1310 bottom_left: false,
1311 charspan: *charspan,
1312 });
1313 let r = self.add_node(inner, parent);
1314 self.pending_exact = None;
1315 r
1316 }
1317 Node::PageBreak => None,
1319 Node::PageInfo {
1322 page_no,
1323 width,
1324 height,
1325 } => {
1326 self.cur_page = *page_no;
1327 self.cur_w = *width as f64;
1328 self.cur_h = *height as f64;
1329 if *page_no > 0 {
1330 self.pages.push((*page_no, self.cur_w, self.cur_h));
1331 }
1332 None
1333 }
1334 Node::ListItem { .. } => None,
1336 }
1337 }
1338
1339 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1343 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1344 self.field_regions.push(Value::Null);
1345 let region_index = self.field_regions.len() - 1;
1346 let mut item_refs = Vec::new();
1347 for item in items {
1348 item_refs.push(json!({ "$ref": self.add_field_item(item, &self_ref) }));
1349 }
1350 self.field_regions[region_index] = json!({
1351 "self_ref": self_ref,
1352 "parent": { "$ref": parent },
1353 "children": item_refs,
1354 "content_layer": "body",
1355 "label": "field_region",
1356 "prov": [],
1357 });
1358 self_ref
1359 }
1360
1361 fn add_key_value_graph(
1365 &mut self,
1366 cells: &[crate::GraphCell],
1367 links: &[crate::GraphLink],
1368 parent: &str,
1369 ) -> String {
1370 let self_ref = format!("#/key_value_items/{}", self.key_value_items.len());
1371 let cells: Vec<Value> = cells
1372 .iter()
1373 .map(|c| {
1374 json!({
1375 "label": c.label,
1376 "cell_id": c.cell_id,
1377 "text": c.text,
1378 "orig": c.orig,
1379 })
1380 })
1381 .collect();
1382 let links: Vec<Value> = links
1383 .iter()
1384 .map(|l| {
1385 json!({
1386 "label": l.label,
1387 "source_cell_id": l.source_cell_id,
1388 "target_cell_id": l.target_cell_id,
1389 })
1390 })
1391 .collect();
1392 self.key_value_items.push(json!({
1393 "self_ref": self_ref,
1394 "parent": { "$ref": parent },
1395 "children": [],
1396 "content_layer": "body",
1397 "label": "key_value_region",
1398 "prov": [],
1399 "captions": [],
1400 "references": [],
1401 "footnotes": [],
1402 "graph": { "cells": cells, "links": links },
1403 }));
1404 self_ref
1405 }
1406
1407 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1408 let self_ref = format!("#/field_items/{}", self.field_items.len());
1409 self.field_items.push(Value::Null);
1410 let item_index = self.field_items.len() - 1;
1411 let mut child_refs = Vec::new();
1412 for (label, text) in [
1413 ("marker", &item.marker),
1414 ("field_key", &item.key),
1415 ("field_value", &item.value),
1416 ] {
1417 if let Some(text) = text {
1418 let extra = match (label, &item.value_kind) {
1421 ("field_value", Some(kind)) => json!({ "kind": kind }),
1422 _ => json!({}),
1423 };
1424 child_refs.push(json!({ "$ref": self.add_text(label, text, &self_ref, extra) }));
1425 }
1426 }
1427 self.field_items[item_index] = json!({
1428 "self_ref": self_ref,
1429 "parent": { "$ref": parent },
1430 "children": child_refs,
1431 "content_layer": "body",
1432 "label": "field_item",
1433 "prov": [],
1434 });
1435 self_ref
1436 }
1437
1438 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1442 let bucket = match self_ref.split('/').nth(1) {
1443 Some("texts") => &mut self.texts,
1444 Some("tables") => &mut self.tables,
1445 Some("pictures") => &mut self.pictures,
1446 Some("groups") => &mut self.groups,
1447 _ => return,
1448 };
1449 if let Some(item) = self_ref
1450 .rsplit('/')
1451 .next()
1452 .and_then(|i| i.parse::<usize>().ok())
1453 .and_then(|i| bucket.get_mut(i))
1454 {
1455 item["content_layer"] = json!(layer);
1456 }
1457 }
1458
1459 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1460 let prov = self.take_prov(unescape_text(text).chars().count());
1461 self.add_text_with(label, text, parent, extra, prov)
1462 }
1463
1464 fn add_text_with(
1467 &mut self,
1468 label: &str,
1469 text: &str,
1470 parent: &str,
1471 extra: Value,
1472 prov: Value,
1473 ) -> String {
1474 let self_ref = format!("#/texts/{}", self.texts.len());
1475 let raw = unescape_text(text);
1476 let mut item = json!({
1477 "self_ref": self_ref,
1478 "parent": { "$ref": parent },
1479 "children": [],
1480 "content_layer": "body",
1481 "label": label,
1482 "prov": prov,
1483 "orig": raw,
1484 "text": raw,
1485 });
1486 merge(&mut item, extra);
1487 self.texts.push(item);
1488 self_ref
1489 }
1490
1491 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
1494 let self_ref = format!("#/texts/{}", self.texts.len());
1495 let prov = self.take_prov(latex.chars().count());
1496 self.texts.push(json!({
1497 "self_ref": self_ref,
1498 "parent": { "$ref": parent },
1499 "children": [],
1500 "content_layer": "body",
1501 "label": "formula",
1502 "prov": prov,
1503 "orig": latex,
1504 "text": latex,
1505 }));
1506 self_ref
1507 }
1508
1509 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
1513 let self_ref = format!("#/texts/{}", self.texts.len());
1514 let prov = self.take_prov(latex.chars().count());
1515 self.texts.push(json!({
1516 "self_ref": self_ref,
1517 "parent": { "$ref": parent },
1518 "children": [],
1519 "content_layer": "body",
1520 "label": "formula",
1521 "prov": prov,
1522 "orig": orig,
1523 "text": latex,
1524 }));
1525 self_ref
1526 }
1527
1528 fn add_code(
1529 &mut self,
1530 text: &str,
1531 language: Option<&str>,
1532 orig: Option<&str>,
1533 parent: &str,
1534 ) -> String {
1535 let self_ref = format!("#/texts/{}", self.texts.len());
1536 let raw = unescape_text(text);
1537 let prov = self.take_prov(raw.chars().count());
1538 self.texts.push(json!({
1539 "self_ref": self_ref,
1540 "parent": { "$ref": parent },
1541 "children": [],
1542 "content_layer": "body",
1543 "label": "code",
1544 "prov": prov,
1545 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
1548 "text": raw,
1549 "captions": [],
1550 "references": [],
1551 "footnotes": [],
1552 "code_language": code_language(language),
1553 }));
1554 self_ref
1555 }
1556
1557 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
1560 let self_ref = format!("#/groups/{}", self.groups.len());
1561 self.groups.push(Value::Null);
1563 let base = level_of(&items[0]);
1564 let mut children = Vec::new();
1565 let mut i = 0;
1566 while i < items.len() {
1567 if !matches!(items[i], Node::ListItem { .. }) {
1570 i += 1;
1571 continue;
1572 }
1573 let lvl = level_of(&items[i]);
1574 if lvl > base {
1575 i += 1;
1577 continue;
1578 }
1579 let item_ref = self.add_list_item(&items[i], &self_ref);
1580 let mut j = i + 1;
1582 while j < items.len() && level_of(&items[j]) > base {
1583 j += 1;
1584 }
1585 if j > i + 1 {
1586 let mut nested = Vec::new();
1587 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
1588 if let Some(idx) = ref_index(&item_ref) {
1590 self.texts[idx]["children"]
1591 .as_array_mut()
1592 .unwrap()
1593 .extend(nested);
1594 }
1595 }
1596 children.push(json!({ "$ref": item_ref }));
1597 i = j;
1598 }
1599 self.groups[group_index(&self_ref)] = json!({
1600 "self_ref": self_ref,
1601 "parent": { "$ref": parent },
1602 "children": children,
1603 "content_layer": "body",
1604 "name": "list",
1605 "label": "list",
1606 });
1607 self_ref
1608 }
1609
1610 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
1611 let Node::ListItem {
1612 ordered,
1613 number,
1614 text,
1615 location,
1616 ..
1617 } = node
1618 else {
1619 unreachable!()
1620 };
1621 self.adopt_loc(*location);
1622 let self_ref = format!("#/texts/{}", self.texts.len());
1623 let raw = unescape_text(text);
1624 let prov = self.take_prov(raw.chars().count());
1625 let marker = if *ordered {
1626 format!("{number}.")
1627 } else {
1628 "-".to_string()
1629 };
1630 self.texts.push(json!({
1631 "self_ref": self_ref,
1632 "parent": { "$ref": parent },
1633 "children": [],
1634 "content_layer": "body",
1635 "label": "list_item",
1636 "prov": prov,
1637 "orig": raw,
1638 "text": raw,
1639 "enumerated": ordered,
1640 "marker": marker,
1641 }));
1642 self_ref
1643 }
1644
1645 fn add_table(&mut self, t: &Table, parent: &str) -> String {
1646 self.add_table_with(t, parent, false)
1647 }
1648
1649 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
1652 let self_ref = format!("#/tables/{}", self.tables.len());
1653 self.adopt_loc(t.location);
1654 let prov = self.take_prov(0);
1655 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
1659 Some(cap) => self.add_caption(cap, json!({}), &self_ref, parent, t.caption_parent),
1660 None => (Vec::new(), Vec::new()),
1661 };
1662 let data = table_data_with(t, raw);
1663 self.tables.push(json!({
1664 "self_ref": self_ref,
1665 "parent": { "$ref": parent },
1666 "children": children,
1667 "content_layer": "body",
1668 "label": "table",
1669 "prov": prov,
1670 "captions": captions,
1671 "references": [],
1672 "footnotes": [],
1673 "data": data,
1674 "annotations": [],
1675 }));
1676 self_ref
1677 }
1678
1679 fn add_caption(
1685 &mut self,
1686 text: &str,
1687 extra: Value,
1688 self_ref: &str,
1689 parent: &str,
1690 choice: CaptionParent,
1691 ) -> (Vec<Value>, Vec<Value>) {
1692 let cap_parent = match choice {
1697 CaptionParent::Item => self_ref,
1698 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
1699 CaptionParent::Body => "#/body",
1700 };
1701 let cap_ref = json!({ "$ref": self.add_text("caption", text, cap_parent, extra) });
1702 match choice {
1703 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
1704 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
1707 CaptionParent::Body if parent == "#/body" => {
1708 self.pending_siblings.push(cap_ref.clone())
1709 }
1710 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
1711 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
1714 }
1715 (vec![cap_ref], Vec::new())
1716 }
1717
1718 fn add_picture(
1721 &mut self,
1722 caption: Option<&str>,
1723 caption_href: Option<&str>,
1724 image: Option<&crate::PictureImage>,
1725 meta: Option<Value>,
1726 parent: &str,
1727 caption_parent: CaptionParent,
1728 ) -> String {
1729 let self_ref = format!("#/pictures/{}", self.pictures.len());
1730 let prov = self.take_prov(0);
1733 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
1734 Some(cap) => {
1735 let extra = match caption_href {
1739 Some(href) => json!({ "hyperlink": href }),
1740 None => json!({}),
1741 };
1742 self.add_caption(cap, extra, &self_ref, parent, caption_parent)
1743 }
1744 None => (Vec::new(), Vec::new()),
1745 };
1746 self.push_picture(prov, captions, children, image, meta, parent)
1747 }
1748
1749 fn push_picture(
1752 &mut self,
1753 prov: Value,
1754 captions: Vec<Value>,
1755 children: Vec<Value>,
1756 image: Option<&crate::PictureImage>,
1757 meta: Option<Value>,
1758 parent: &str,
1759 ) -> String {
1760 let self_ref = format!("#/pictures/{}", self.pictures.len());
1761 let annotations = meta
1765 .as_ref()
1766 .and_then(|m| m.get("annotations").cloned())
1767 .unwrap_or_else(|| json!([]));
1768 let meta = meta.map(|mut m| {
1769 if let Some(obj) = m.as_object_mut() {
1770 obj.remove("annotations");
1771 }
1772 m
1773 });
1774 let mut item = match meta {
1778 Some(meta) => json!({
1779 "self_ref": self_ref,
1780 "parent": { "$ref": parent },
1781 "children": children,
1782 "content_layer": "body",
1783 "meta": meta,
1784 "label": "picture",
1785 "prov": prov,
1786 "captions": captions,
1787 "references": [],
1788 "footnotes": [],
1789 "annotations": annotations,
1790 }),
1791 None => json!({
1792 "self_ref": self_ref,
1793 "parent": { "$ref": parent },
1794 "children": children,
1795 "content_layer": "body",
1796 "label": "picture",
1797 "prov": prov,
1798 "captions": captions,
1799 "references": [],
1800 "footnotes": [],
1801 "annotations": annotations,
1802 }),
1803 };
1804 if let Some(img) = image {
1809 let image = image_ref_json(img);
1810 if let Some(obj) = item.as_object_mut() {
1811 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
1812 obj.insert("image".into(), image);
1813 obj.insert("annotations".into(), annotations);
1814 }
1815 }
1816 self.pictures.push(item);
1817 self_ref
1818 }
1819
1820 fn add_group(
1821 &mut self,
1822 label: &str,
1823 name: Option<&str>,
1824 layer: Option<ContentLayer>,
1825 nodes: &[Node],
1826 parent: &str,
1827 ) -> String {
1828 let self_ref = format!("#/groups/{}", self.groups.len());
1829 self.groups.push(Value::Null);
1830 let mark = (
1834 self.texts.len(),
1835 self.tables.len(),
1836 self.pictures.len(),
1837 self.groups.len(),
1838 );
1839 let children = self.walk_into(nodes, &self_ref);
1840 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
1841 let content_layer = layer.map_or("body", |l| l.value());
1842 self.groups[group_index(&self_ref)] = json!({
1843 "self_ref": self_ref,
1844 "parent": { "$ref": parent },
1845 "children": children,
1846 "content_layer": content_layer,
1847 "name": name,
1848 "label": label,
1849 });
1850 if layer.is_some() {
1851 let (t, tb, p, g) = mark;
1852 for item in self.texts[t..]
1853 .iter_mut()
1854 .chain(self.tables[tb..].iter_mut())
1855 .chain(self.pictures[p..].iter_mut())
1856 .chain(self.groups[g..].iter_mut())
1857 {
1858 if let Some(obj) = item.as_object_mut() {
1859 obj.insert("content_layer".into(), json!(content_layer));
1860 }
1861 }
1862 }
1863 self_ref
1864 }
1865
1866 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Value> {
1869 let seqs: Option<Vec<usize>> = nodes
1874 .iter()
1875 .map(|n| match n {
1876 Node::Prov { seq: Some(s), .. } => Some(*s),
1877 _ => None,
1878 })
1879 .collect();
1880 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
1881 let mut order: Vec<usize> = (0..nodes.len()).collect();
1882 order.sort_by_key(|&i| seqs[i]);
1883 let mut slots: Vec<Vec<Value>> = vec![Vec::new(); nodes.len()];
1884 for i in order {
1885 if let Some(r) = self.add_node(&nodes[i], parent) {
1886 slots[i].append(&mut self.pending_siblings);
1887 slots[i].push(json!({ "$ref": r }));
1888 slots[i].append(&mut self.pending_after);
1889 }
1890 if parent == "#/body" {
1891 slots[i].append(&mut self.pending_body);
1892 }
1893 }
1894 return slots.into_iter().flatten().collect();
1895 }
1896 let mut children = Vec::new();
1897 let mut i = 0;
1898 while i < nodes.len() {
1899 if matches!(nodes[i], Node::ListItem { .. }) {
1900 let start = i;
1901 i += 1;
1902 loop {
1903 match nodes.get(i) {
1904 Some(Node::ListItem { .. }) => i += 1,
1905 Some(Node::Paragraph { text })
1908 if text.is_empty()
1909 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
1910 {
1911 i += 1
1912 }
1913 _ => break,
1914 }
1915 }
1916 self.add_sibling_lists(&nodes[start..i], parent, &mut children);
1917 } else {
1918 if let Some(r) = self.add_node(&nodes[i], parent) {
1919 children.append(&mut self.pending_siblings);
1920 children.push(json!({ "$ref": r }));
1921 children.append(&mut self.pending_after);
1922 }
1923 i += 1;
1924 }
1925 if parent == "#/body" {
1928 children.append(&mut self.pending_body);
1929 }
1930 }
1931 children
1932 }
1933
1934 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
1939 let base = level_of(&run[0]);
1940 let mut seg = 0;
1941 for k in 0..run.len() {
1942 let Node::ListItem {
1943 first_in_list,
1944 level,
1945 ..
1946 } = &run[k]
1947 else {
1948 continue;
1949 };
1950 if *level != base {
1951 continue; }
1953 if k > seg && *first_in_list {
1954 out.push(json!({ "$ref": self.add_list(&run[seg..k], parent) }));
1955 seg = k;
1956 }
1957 }
1958 out.push(json!({ "$ref": self.add_list(&run[seg..], parent) }));
1959 }
1960}
1961
1962fn level_of(node: &Node) -> u8 {
1963 match node {
1964 Node::ListItem { level, .. } => *level,
1965 _ => 0,
1966 }
1967}
1968
1969fn group_index(self_ref: &str) -> usize {
1970 self_ref.rsplit('/').next().unwrap().parse().unwrap()
1971}
1972
1973fn ref_index(self_ref: &str) -> Option<usize> {
1974 self_ref.rsplit('/').next()?.parse().ok()
1975}
1976
1977fn merge(target: &mut Value, extra: Value) {
1979 if let (Some(t), Some(e)) = (target.as_object_mut(), extra.as_object()) {
1980 for (k, v) in e {
1981 t.insert(k.clone(), v.clone());
1982 }
1983 }
1984}
1985
1986fn unescape_text(s: &str) -> String {
1988 s.replace("<", "<")
1989 .replace(">", ">")
1990 .replace("&", "&")
1991 .replace("\\_", "_")
1992}
1993
1994fn fnv1a(s: &str) -> u64 {
1997 let mut h: u64 = 0xcbf29ce484222325;
1998 for b in s.bytes() {
1999 h ^= b as u64;
2000 h = h.wrapping_mul(0x100000001b3);
2001 }
2002 h
2003}
2004
2005#[cfg(test)]
2006mod tests {
2007 use crate::{
2008 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
2009 };
2010 use serde_json::Value;
2011
2012 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
2013 crate::TableCell {
2014 text: text.into(),
2015 bbox: None,
2016 start_row: row,
2017 start_col: col,
2018 row_span: 1,
2019 col_span,
2020 column_header: row == 0,
2021 row_header: false,
2022 row_section: false,
2023 }
2024 }
2025
2026 #[test]
2032 fn ragged_rows_take_their_grid_from_first_class_cells() {
2033 let cells = vec![
2034 cell_at("h0", 0, 0, 1),
2035 cell_at("h2", 0, 2, 1),
2036 cell_at("wide", 1, 0, 3),
2037 ];
2038 let export = |rows: Vec<Vec<String>>| -> Value {
2039 let mut doc = DoclingDocument::new("t");
2040 doc.push(Node::Table(Table {
2041 rows,
2042 cells: Some(cells.clone()),
2043 ..Table::default()
2044 }));
2045 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2046 v["tables"][0]["data"].clone()
2047 };
2048 let data = export(vec![
2050 vec!["h0".into(), "h2".into()],
2051 vec!["wide".into(), "wide".into(), "wide".into()],
2052 ]);
2053 assert_eq!(data["num_rows"], 2);
2054 assert_eq!(data["num_cols"], 3);
2055 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
2056 let row0: Vec<&str> = data["grid"][0]
2057 .as_array()
2058 .unwrap()
2059 .iter()
2060 .map(|c| c["text"].as_str().unwrap())
2061 .collect();
2062 assert_eq!(row0, ["h0", "", "h2"]);
2063 assert_eq!(data["table_cells"][2]["col_span"], 3);
2064 let data = export(vec![
2067 vec!["h0".into(), "h2".into()],
2068 vec!["wide".into(), "wide".into()],
2069 ]);
2070 assert_eq!(data["num_cols"], 2);
2071 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
2072 }
2073
2074 fn doc_with_image() -> DoclingDocument {
2075 let mut doc = DoclingDocument::new("t");
2076 doc.push(Node::Picture {
2077 caption: Some("Fig 1".into()),
2078 caption_href: None,
2079 image: Some(PictureImage {
2080 dpi: crate::PictureImage::DEFAULT_DPI,
2081 mimetype: "image/png".into(),
2082 width: 4,
2083 height: 2,
2084 data: b"foobar".to_vec(),
2085 }),
2086 classification: None,
2087 caption_parent: Default::default(),
2088 });
2089 doc
2090 }
2091
2092 #[test]
2097 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
2098 let mut doc = DoclingDocument::new("t");
2099 doc.push(Node::Heading {
2100 level: 1,
2101 text: "Slide One".into(),
2102 });
2103 doc.push(Node::Furniture {
2104 layer: ContentLayer::Notes,
2105 inner: Box::new(Node::Located {
2106 location: [0, 0, 0, 0],
2107 inner: Box::new(Node::Paragraph {
2108 text: "Speaker note for slide 1.".into(),
2109 }),
2110 }),
2111 });
2112 doc.push(Node::Furniture {
2113 layer: ContentLayer::Furniture,
2114 inner: Box::new(Node::Paragraph {
2115 text: "page header".into(),
2116 }),
2117 });
2118
2119 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2120 let texts = v["texts"].as_array().unwrap();
2121 assert_eq!(
2122 texts
2123 .iter()
2124 .map(|t| (
2125 t["label"].as_str().unwrap(),
2126 t["content_layer"].as_str().unwrap(),
2127 t["text"].as_str().unwrap()
2128 ))
2129 .collect::<Vec<_>>(),
2130 vec![
2131 ("title", "body", "Slide One"),
2132 ("text", "notes", "Speaker note for slide 1."),
2133 ],
2134 "the note is carried on its own layer; the furniture is not carried"
2135 );
2136 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
2138 }
2139
2140 #[test]
2146 fn continuation_flags_become_spanning_cells() {
2147 let mut doc = DoclingDocument::new("t");
2148 let rows = vec![
2150 vec!["merged".to_string(), "merged".into(), "merged".into()],
2151 vec!["merged".to_string(), "merged".into(), "merged".into()],
2152 vec!["a".to_string(), "b".into(), "c".into()],
2153 ];
2154 doc.push(Node::Table(crate::Table {
2155 rows,
2156 location: None,
2157 structure: Some(crate::TableStructure {
2158 header_row: vec![true, false, false],
2159 col_continuation: vec![
2160 vec![false, true, true],
2161 vec![false, true, true],
2162 vec![false, false, false],
2163 ],
2164 row_continuation: vec![
2165 vec![false, false, false],
2166 vec![true, true, true],
2167 vec![false, false, false],
2168 ],
2169 row_header: Vec::new(),
2170 col_header: Vec::new(),
2171 }),
2172 cell_blocks: None,
2173 cells: None,
2174 caption: None,
2175 caption_parent: Default::default(),
2176 }));
2177 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2178 let data = &v["tables"][0]["data"];
2179 assert_eq!(data["num_rows"], 3);
2180 assert_eq!(data["num_cols"], 3);
2181 let cells = data["table_cells"].as_array().unwrap();
2182 assert_eq!(
2183 cells.len(),
2184 4,
2185 "one cell for the range, three for the plain row"
2186 );
2187 assert_eq!(
2188 cells[0],
2189 serde_json::json!({
2190 "row_span": 2, "col_span": 3,
2191 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2192 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2193 "text": "merged", "column_header": true, "row_header": false,
2194 "row_section": false, "fillable": false,
2195 })
2196 );
2197 assert_eq!(cells[1]["text"], "a");
2198 assert_eq!(cells[1]["row_span"], 1);
2199 assert_eq!(cells[1]["column_header"], false);
2200 let grid = data["grid"].as_array().unwrap();
2202 assert_eq!(grid.len(), 3);
2203 for (r, row) in grid.iter().take(2).enumerate() {
2204 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2205 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2206 }
2207 }
2208 assert_eq!(grid[2][2]["text"], "c");
2209 }
2210
2211 #[test]
2217 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2218 let mut doc = DoclingDocument::new("t");
2219 doc.push(Node::PageInfo {
2220 page_no: 1,
2221 width: 3.0,
2222 height: 4.0,
2223 });
2224 let table = crate::Table {
2225 rows: vec![vec!["a".to_string(), "b".into()]],
2226 ..Default::default()
2227 };
2228 doc.push(Node::Group {
2229 label: "sheet".into(),
2230 name: Some("Data".into()),
2231 layer: None,
2232 children: vec![
2233 Node::Prov {
2238 page_no: 1,
2239 bbox: [0.0, 0.0, 3.0, 4.0],
2240 charspan: [0, 0],
2241 seq: Some(1),
2242 inner: Box::new(Node::Table(table.clone())),
2243 },
2244 Node::Prov {
2245 page_no: 1,
2246 bbox: [0.0, 1.0, 1.0, 1.0],
2247 charspan: [0, 0],
2248 seq: Some(0),
2249 inner: Box::new(Node::Chart {
2250 kind: "bar_chart".into(),
2251 table,
2252 caption: Some("Sales".into()),
2253 location: Some([0, 128, 170, 128]),
2254 }),
2255 },
2256 ],
2257 });
2258 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2259 assert_eq!(
2260 v["pages"],
2261 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2262 );
2263 assert_eq!(
2264 v["tables"][0]["prov"],
2265 serde_json::json!([{
2266 "page_no": 1,
2267 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2268 "charspan": [0, 0],
2269 }])
2270 );
2271 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2272 let sheet = &v["groups"][0];
2275 assert_eq!(
2276 sheet["children"],
2277 serde_json::json!([
2278 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2279 ])
2280 );
2281 let cap = &v["texts"][0];
2282 assert_eq!(cap["label"], "caption");
2283 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2284 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2285 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2286 let pic = &v["pictures"][0];
2287 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2288 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2289 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2290 assert_eq!(
2291 pic["meta"]["classification"]["predictions"][0]["class_name"],
2292 "bar_chart"
2293 );
2294 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2295 }
2296
2297 #[test]
2301 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2302 let mut doc = DoclingDocument::new("t");
2303 doc.push(Node::PageInfo {
2304 page_no: 1,
2305 width: 12192000.0,
2306 height: 6858000.0,
2307 });
2308 doc.push(Node::Furniture {
2309 layer: ContentLayer::Notes,
2310 inner: Box::new(Node::Located {
2311 location: [0, 0, 0, 0],
2312 inner: Box::new(Node::Paragraph {
2313 text: "a note".into(),
2314 }),
2315 }),
2316 });
2317 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2318 let prov = &v["texts"][0]["prov"][0];
2319 assert_eq!(prov["page_no"], 1);
2320 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2321 assert_eq!(
2322 prov["bbox"],
2323 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2324 );
2325 assert_eq!(
2327 v["pages"]["1"]["size"],
2328 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2329 );
2330 }
2331
2332 #[test]
2338 fn page_markers_produce_pages_and_prov() {
2339 let mut doc = DoclingDocument::new("t");
2340 doc.push(Node::PageInfo {
2341 page_no: 1,
2342 width: 512.0,
2343 height: 1024.0,
2344 });
2345 doc.push(Node::Located {
2346 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
2348 text: "hello".into(),
2349 }),
2350 });
2351 doc.push(Node::Table(Table {
2352 rows: vec![vec!["a".into()]],
2353 location: Some([0, 0, 512, 512]),
2354 ..Table::default()
2355 }));
2356 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2357 assert_eq!(v["pages"]["1"]["page_no"], 1);
2358 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
2359 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
2360 let prov = &v["texts"][0]["prov"][0];
2363 assert_eq!(prov["page_no"], 1);
2364 assert_eq!(prov["bbox"]["l"], 128.0);
2365 assert_eq!(prov["bbox"]["t"], 896.0);
2366 assert_eq!(prov["bbox"]["r"], 256.0);
2367 assert_eq!(prov["bbox"]["b"], 768.0);
2368 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
2369 assert_eq!(prov["charspan"][1], 5);
2370 let tprov = &v["tables"][0]["prov"][0];
2372 assert_eq!(tprov["bbox"]["t"], 1024.0);
2373 assert_eq!(tprov["bbox"]["b"], 0.0);
2374 assert_eq!(tprov["charspan"][1], 0);
2375
2376 let mut plain = DoclingDocument::new("t");
2378 plain.push(Node::Located {
2379 location: [1, 2, 3, 4],
2380 inner: Box::new(Node::Paragraph { text: "x".into() }),
2381 });
2382 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2383 assert_eq!(v["pages"], serde_json::json!({}));
2384 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2385 }
2386
2387 #[test]
2390 fn page_furniture_becomes_furniture_layer_text() {
2391 let mut doc = DoclingDocument::new("t");
2392 doc.push(Node::PageInfo {
2393 page_no: 1,
2394 width: 512.0,
2395 height: 512.0,
2396 });
2397 doc.push(Node::PageFurniture {
2398 footer: false,
2399 location: [10, 0, 100, 20],
2400 text: "Chapter 1".into(),
2401 });
2402 doc.push(Node::Paragraph {
2403 text: "body".into(),
2404 });
2405 doc.push(Node::PageFurniture {
2406 footer: true,
2407 location: [400, 490, 500, 512],
2408 text: "1.10.2".into(),
2409 });
2410 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2411 let texts = v["texts"].as_array().unwrap();
2412 assert_eq!(texts.len(), 3);
2413 assert_eq!(texts[0]["label"], "page_header");
2414 assert_eq!(texts[0]["content_layer"], "furniture");
2415 assert_eq!(texts[0]["parent"]["$ref"], "#/body");
2416 assert_eq!(texts[0]["prov"][0]["bbox"]["l"], 10.0);
2417 assert_eq!(texts[1]["content_layer"], "body");
2418 assert_eq!(texts[1]["prov"], serde_json::json!([]));
2419 assert_eq!(texts[2]["label"], "page_footer");
2420 assert_eq!(texts[2]["text"], "1.10.2");
2421 assert_eq!(texts[2]["content_layer"], "furniture");
2422 }
2423
2424 #[test]
2425 fn picture_image_in_markdown_modes_and_json() {
2426 let doc = doc_with_image();
2427 assert!(doc.export_to_markdown().contains("<!-- image -->"));
2429 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
2431 assert!(
2432 md.contains(""),
2433 "got:\n{md}"
2434 );
2435 assert!(files.is_empty());
2436 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
2438 assert!(
2439 md.contains(""),
2440 "got:\n{md}"
2441 );
2442 assert_eq!(
2443 files,
2444 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
2445 );
2446 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2449 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
2450 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
2451 let keys: Vec<&str> = v["pictures"][0]
2452 .as_object()
2453 .unwrap()
2454 .keys()
2455 .map(String::as_str)
2456 .collect();
2457 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
2458 assert_eq!(
2459 v["pictures"][0]["image"]["uri"],
2460 "data:image/png;base64,Zm9vYmFy"
2461 );
2462 }
2463
2464 #[test]
2465 fn exports_docling_schema() {
2466 let mut doc = DoclingDocument::new("t");
2467 doc.push(Node::Heading {
2468 level: 1,
2469 text: "Title".into(),
2470 });
2471 doc.push(Node::Heading {
2472 level: 2,
2473 text: "Sec".into(),
2474 });
2475 doc.push(Node::Paragraph {
2476 text: "Body & more".into(),
2477 }); doc.push(Node::ListItem {
2479 ordered: false,
2480 number: 0,
2481 first_in_list: true,
2482 text: "one".into(),
2483 level: 0,
2484 marker: None,
2485 location: None,
2486 dclx: None,
2487 href: None,
2488 layer: None,
2489 });
2490 doc.push(Node::ListItem {
2491 ordered: false,
2492 number: 0,
2493 first_in_list: false,
2494 text: "two".into(),
2495 level: 0,
2496 marker: None,
2497 location: None,
2498 dclx: None,
2499 href: None,
2500 layer: None,
2501 });
2502 doc.push(Node::Table(Table {
2503 rows: vec![vec!["A".into(), "B".into()]],
2504 location: None,
2505 structure: None,
2506 cell_blocks: None,
2507 cells: None,
2508 caption: None,
2509 caption_parent: Default::default(),
2510 }));
2511
2512 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2513 assert_eq!(v["schema_name"], "DoclingDocument");
2514 assert_eq!(v["version"], "1.10.0");
2515 assert_eq!(v["texts"][0]["label"], "title");
2516 assert_eq!(v["texts"][1]["label"], "section_header");
2517 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
2521 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
2522 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
2523 assert_eq!(v["texts"][3]["marker"], "-");
2524 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
2526 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
2527 }
2528 #[test]
2532 fn a_layered_group_stamps_its_whole_subtree() {
2533 let doc = DoclingDocument {
2534 name: "s".into(),
2535 nodes: vec![
2536 Node::Group {
2537 label: "sheet".into(),
2538 name: Some("Sheet1".into()),
2539 layer: None,
2540 children: vec![Node::Paragraph {
2541 text: "visible".into(),
2542 }],
2543 },
2544 Node::Group {
2545 label: "sheet".into(),
2546 name: Some("Sheet2".into()),
2547 layer: Some(ContentLayer::Invisible),
2548 children: vec![Node::Paragraph {
2549 text: "hidden".into(),
2550 }],
2551 },
2552 ],
2553 ..DoclingDocument::new("s")
2554 };
2555 let v = crate::json::to_json(&doc);
2556 assert_eq!(v["groups"][0]["label"], "sheet");
2557 assert_eq!(v["groups"][0]["name"], "Sheet1");
2558 assert_eq!(v["groups"][0]["content_layer"], "body");
2559 assert_eq!(v["texts"][0]["content_layer"], "body");
2560 assert_eq!(v["groups"][1]["name"], "Sheet2");
2561 assert_eq!(v["groups"][1]["content_layer"], "invisible");
2562 assert_eq!(v["texts"][1]["content_layer"], "invisible");
2563 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
2565 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
2566 }
2567
2568 #[test]
2578 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
2579 use crate::tree::{ItemTree, TreeKind};
2580 let mut t = ItemTree::default();
2581 let text = |txt: &str| TreeKind::Text {
2582 label: "text".into(),
2583 text: txt.into(),
2584 orig: None,
2585 formatting: None,
2586 hyperlink: None,
2587 level: None,
2588 list: None,
2589 };
2590 let a = t.add(None, None, text("a"));
2591 let blank = t.add(None, None, text(""));
2592 let b = t.add(None, None, text("b"));
2593 t.delete(blank);
2594 let group = t.add(
2595 None,
2596 Some(ContentLayer::Notes),
2597 TreeKind::Group {
2598 label: "comment_section".into(),
2599 name: "comment-0".into(),
2600 },
2601 );
2602 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
2603 t.items[a].comments.push(group);
2604 t.add(
2605 None,
2606 None,
2607 TreeKind::Picture {
2608 captions: Vec::new(),
2609 image: None,
2610 classification: Some("bar_chart".into()),
2611 confidence: None,
2612 chart: Some(Table {
2613 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
2614 ..Table::default()
2615 }),
2616 dpi: None,
2617 },
2618 );
2619 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
2620 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
2621 let mut doc = DoclingDocument::new("t");
2622 doc.tree = Some(t);
2623 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2624 let texts = v["texts"].as_array().unwrap();
2625 assert_eq!(texts.len(), 3);
2626 assert_eq!(texts[1]["text"], "b");
2627 assert_eq!(texts[1]["self_ref"], "#/texts/1");
2628 assert_eq!(
2629 v["body"]["children"],
2630 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
2631 );
2632 let keys: Vec<&str> = texts[0]
2633 .as_object()
2634 .unwrap()
2635 .keys()
2636 .map(String::as_str)
2637 .collect();
2638 assert_eq!(
2639 keys,
2640 vec![
2641 "self_ref",
2642 "parent",
2643 "children",
2644 "content_layer",
2645 "label",
2646 "prov",
2647 "comments",
2648 "orig",
2649 "text"
2650 ]
2651 );
2652 assert_eq!(
2653 texts[0]["comments"],
2654 serde_json::json!([{"$ref": "#/groups/0"}])
2655 );
2656 assert!(texts[1].get("comments").is_none());
2657 let meta = &v["pictures"][0]["meta"];
2658 assert_eq!(
2659 meta["classification"]["predictions"][0]["class_name"],
2660 "bar_chart"
2661 );
2662 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
2663 }
2664
2665 #[test]
2671 fn tree_items_carry_exact_provenance_and_dpi() {
2672 use crate::tree::{ItemTree, TreeKind, TreeProv};
2673 let text = |label: &str, t: &str| TreeKind::Text {
2674 label: label.into(),
2675 text: t.into(),
2676 orig: None,
2677 formatting: None,
2678 hyperlink: None,
2679 level: None,
2680 list: None,
2681 };
2682 let mut t = ItemTree::default();
2683 let slide = t.add(
2684 None,
2685 None,
2686 TreeKind::Group {
2687 label: "chapter".into(),
2688 name: "slide-0".into(),
2689 },
2690 );
2691 t.add_with_prov(
2692 Some(slide),
2693 None,
2694 text("paragraph", "héllo"),
2695 TreeProv {
2696 page_no: 1,
2697 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
2698 bottom_left: true,
2699 charspan: [0, 5],
2700 },
2701 );
2702 t.add_with_prov(
2703 Some(slide),
2704 None,
2705 TreeKind::Picture {
2706 captions: Vec::new(),
2707 image: Some(crate::PictureImage {
2708 dpi: crate::PictureImage::DEFAULT_DPI,
2709 mimetype: "image/png".into(),
2710 width: 2,
2711 height: 2,
2712 data: vec![0],
2713 }),
2714 classification: None,
2715 confidence: None,
2716 chart: None,
2717 dpi: Some(300),
2718 },
2719 TreeProv {
2720 page_no: 1,
2721 bbox: [0.0; 4],
2722 bottom_left: false,
2723 charspan: [0, 0],
2724 },
2725 );
2726 t.add(
2727 Some(slide),
2728 Some(ContentLayer::Notes),
2729 text("text", "no geometry"),
2730 );
2731 let mut doc = DoclingDocument::new("t");
2732 doc.push(Node::PageInfo {
2733 page_no: 1,
2734 width: 9144000.0,
2735 height: 6858000.0,
2736 });
2737 doc.tree = Some(t);
2738 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2739 assert_eq!(
2740 v["texts"][0]["prov"],
2741 serde_json::json!([{
2742 "page_no": 1,
2743 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
2744 "charspan": [0, 5],
2745 }])
2746 );
2747 assert_eq!(v["texts"][0]["label"], "paragraph");
2748 assert_eq!(
2749 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
2750 "TOPLEFT"
2751 );
2752 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
2753 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
2754 assert_eq!(v["texts"][1]["content_layer"], "notes");
2755 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
2756 assert_eq!(v["pages"]["1"]["page_no"], 1);
2757 }
2758
2759 #[test]
2764 fn provenance_boxes_are_clamped_to_their_page() {
2765 let mut doc = DoclingDocument::new("t");
2766 doc.push(Node::PageInfo {
2767 page_no: 1,
2768 width: 10.0,
2769 height: 8.0,
2770 });
2771 doc.push(Node::Prov {
2772 page_no: 1,
2773 bbox: [-1.0, 2.0, 12.0, 9.5],
2774 charspan: [0, 1],
2775 seq: None,
2776 inner: Box::new(Node::Paragraph { text: "x".into() }),
2777 });
2778 let mut table = Table {
2779 rows: vec![vec!["a".into()]],
2780 ..Table::default()
2781 };
2782 table.cells = Some(vec![crate::TableCell {
2783 text: "a".into(),
2784 bbox: Some([1.0, 1.0, 11.0, 9.0]),
2785 start_row: 0,
2786 start_col: 0,
2787 row_span: 1,
2788 col_span: 1,
2789 column_header: false,
2790 row_header: false,
2791 row_section: false,
2792 }]);
2793 doc.push(Node::Prov {
2794 page_no: 1,
2795 bbox: [0.0, 0.0, 10.0, 8.0],
2796 charspan: [0, 0],
2797 seq: None,
2798 inner: Box::new(Node::Table(table)),
2799 });
2800 doc.push(Node::Prov {
2801 page_no: 7,
2802 bbox: [-5.0, 0.0, 50.0, 50.0],
2803 charspan: [0, 1],
2804 seq: None,
2805 inner: Box::new(Node::Paragraph { text: "y".into() }),
2806 });
2807 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2808 assert_eq!(
2809 v["texts"][0]["prov"][0]["bbox"],
2810 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
2811 );
2812 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
2813 assert_eq!(
2814 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
2815 (Some(1.0), Some(10.0), Some(8.0))
2816 );
2817 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
2818 assert_eq!(
2819 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
2820 "page 7 is not described"
2821 );
2822 }
2823
2824 #[test]
2831 fn misplaced_list_items_are_wrapped_like_docling_core() {
2832 use crate::tree::{ItemTree, ListMeta, TreeKind};
2833 let mut t = ItemTree::default();
2834 let li = |txt: &str| TreeKind::Text {
2835 label: "list_item".into(),
2836 text: txt.into(),
2837 orig: None,
2838 formatting: None,
2839 hyperlink: None,
2840 level: None,
2841 list: Some(ListMeta {
2842 enumerated: false,
2843 marker: "-".into(),
2844 }),
2845 };
2846 let group = |label: &str, name: &str| TreeKind::Group {
2847 label: label.into(),
2848 name: name.into(),
2849 };
2850 t.add(None, None, li("A"));
2851 t.add(None, None, li("B"));
2852 t.add(
2853 None,
2854 None,
2855 TreeKind::Text {
2856 label: "text".into(),
2857 text: "plain".into(),
2858 orig: None,
2859 formatting: None,
2860 hyperlink: None,
2861 level: None,
2862 list: None,
2863 },
2864 );
2865 let cell = t.add(None, None, group("unspecified", "cell"));
2866 t.add(Some(cell), None, li("C"));
2867 t.add(Some(cell), None, li("D"));
2868 let list = t.add(None, None, group("list", "list"));
2869 t.add(Some(list), None, li("E"));
2870 let doc = DoclingDocument {
2871 tree: Some(t),
2872 ..DoclingDocument::new("t")
2873 };
2874 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2875 let refs = |x: &Value| -> Vec<String> {
2876 x.as_array()
2877 .unwrap()
2878 .iter()
2879 .map(|r| r["$ref"].as_str().unwrap().to_string())
2880 .collect()
2881 };
2882 assert_eq!(
2883 refs(&v["body"]["children"]),
2884 ["#/groups/4", "#/texts/0", "#/groups/0", "#/groups/1"]
2885 );
2886 assert_eq!(
2887 refs(&v["groups"][0]["children"]),
2888 ["#/groups/3", "#/groups/2"]
2889 );
2890 for (g, name, kids) in [
2891 (2, "group", vec!["#/texts/2"]),
2892 (3, "group", vec!["#/texts/3"]),
2893 (4, "group", vec!["#/texts/4", "#/texts/5"]),
2894 (1, "list", vec!["#/texts/1"]),
2895 ] {
2896 assert_eq!(v["groups"][g]["label"], "list");
2897 assert_eq!(v["groups"][g]["name"], name);
2898 assert_eq!(refs(&v["groups"][g]["children"]), kids);
2899 }
2900 let texts: Vec<&str> = v["texts"]
2901 .as_array()
2902 .unwrap()
2903 .iter()
2904 .map(|t| t["text"].as_str().unwrap())
2905 .collect();
2906 assert_eq!(texts, ["plain", "E", "D", "C", "A", "B"]);
2907 assert_eq!(v["texts"][2]["parent"]["$ref"], "#/groups/2");
2908 }
2909
2910 #[test]
2913 fn a_deleted_child_is_not_written_as_an_empty_ref() {
2914 use crate::tree::{ItemTree, TreeKind};
2915 let mut t = ItemTree::default();
2916 let text = |txt: &str| TreeKind::Text {
2917 label: "text".into(),
2918 text: txt.into(),
2919 orig: None,
2920 formatting: None,
2921 hyperlink: None,
2922 level: None,
2923 list: None,
2924 };
2925 let g = t.add(
2926 None,
2927 None,
2928 TreeKind::Group {
2929 label: "unspecified".into(),
2930 name: "g".into(),
2931 },
2932 );
2933 let gone = t.add(Some(g), None, text(""));
2934 t.add(Some(g), None, text("kept"));
2935 t.items[gone].deleted = true; let doc = DoclingDocument {
2937 tree: Some(t),
2938 ..DoclingDocument::new("t")
2939 };
2940 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2941 assert_eq!(
2942 v["groups"][0]["children"],
2943 serde_json::json!([{"$ref": "#/texts/0"}])
2944 );
2945 }
2946
2947 #[test]
2948 fn a_backend_item_tree_is_written_verbatim() {
2949 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
2950 let mut t = ItemTree::default();
2951 let text = |label: &str, txt: &str| TreeKind::Text {
2952 label: label.into(),
2953 text: txt.into(),
2954 orig: None,
2955 formatting: None,
2956 hyperlink: None,
2957 level: None,
2958 list: None,
2959 };
2960 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
2961 let h = t.add(None, None, text("title", "Heading"));
2962 let group = t.add(
2963 Some(h),
2964 None,
2965 TreeKind::Group {
2966 label: "inline".into(),
2967 name: "group".into(),
2968 },
2969 );
2970 t.add(
2971 Some(group),
2972 None,
2973 TreeKind::Text {
2974 label: "text".into(),
2975 text: "bold".into(),
2976 orig: None,
2977 formatting: Some(Formatting {
2978 bold: true,
2979 ..Formatting::default()
2980 }),
2981 hyperlink: Some("https://example.com/".into()),
2982 level: None,
2983 list: None,
2984 },
2985 );
2986 t.add(
2987 Some(group),
2988 None,
2989 TreeKind::Code {
2990 text: "x = 1".into(),
2991 orig: None,
2992 language: Some("python".into()),
2993 formatting: None,
2994 hyperlink: None,
2995 },
2996 );
2997 let sub = t.add(
2998 Some(h),
2999 None,
3000 TreeKind::Text {
3001 label: "section_header".into(),
3002 text: "Sub".into(),
3003 orig: Some("Sub\u{2019}".into()),
3004 formatting: None,
3005 hyperlink: None,
3006 level: Some(1),
3007 list: None,
3008 },
3009 );
3010 let item = t.add(
3011 Some(sub),
3012 None,
3013 TreeKind::Text {
3014 label: "list_item".into(),
3015 text: "item".into(),
3016 orig: None,
3017 formatting: None,
3018 hyperlink: None,
3019 level: None,
3020 list: Some(ListMeta {
3021 enumerated: true,
3022 marker: "3.".into(),
3023 }),
3024 },
3025 );
3026 let _region = t.add(
3027 Some(sub),
3028 None,
3029 TreeKind::FieldRegion {
3030 items: vec![crate::FieldItem {
3031 marker: None,
3032 key: Some("Name".into()),
3033 value: Some("Duck".into()),
3034 value_kind: Some("read_only".into()),
3035 }],
3036 },
3037 );
3038 let table = t.add(
3039 Some(sub),
3040 None,
3041 TreeKind::Table {
3042 table: Table {
3043 rows: vec![vec!["a \n<".into(), "b".into()]],
3044 cells: Some(vec![
3045 crate::TableCell {
3046 text: "a \n<".into(),
3047 bbox: None,
3048 start_row: 0,
3049 start_col: 0,
3050 row_span: 3,
3051 col_span: 1,
3052 column_header: false,
3053 row_header: true,
3054 row_section: false,
3055 },
3056 crate::TableCell {
3057 text: "b".into(),
3058 bbox: None,
3059 start_row: 0,
3060 start_col: 1,
3061 row_span: 1,
3062 col_span: 1,
3063 column_header: false,
3064 row_header: false,
3065 row_section: false,
3066 },
3067 ]),
3068 ..Table::default()
3069 },
3070 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
3072 },
3073 );
3074 let cell_group = t.add(
3075 Some(table),
3076 None,
3077 TreeKind::Group {
3078 label: "unspecified".into(),
3079 name: "rich_cell_group_1_0_0".into(),
3080 },
3081 );
3082 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
3083 *rich_cells = vec![(0, 1, cell_group)];
3084 }
3085 let after = t.add(Some(sub), None, text("text", "after the region"));
3086 let _ = (title, after);
3087 let list = t.add(
3091 Some(sub),
3092 None,
3093 TreeKind::Group {
3094 label: "list".into(),
3095 name: "list".into(),
3096 },
3097 );
3098 t.reparent(item, Some(list));
3099
3100 let doc = DoclingDocument {
3101 tree: Some(t),
3102 ..DoclingDocument::new("t")
3103 };
3104 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3105 let texts: Vec<&str> = v["texts"]
3107 .as_array()
3108 .unwrap()
3109 .iter()
3110 .map(|t| t["text"].as_str().unwrap())
3111 .collect();
3112 assert_eq!(
3113 texts,
3114 [
3115 "Page",
3116 "Heading",
3117 "bold",
3118 "x = 1",
3119 "Sub",
3120 "item",
3121 "Name",
3122 "Duck",
3123 "after the region"
3124 ]
3125 );
3126 assert_eq!(
3127 v["body"]["children"],
3128 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
3129 );
3130 assert_eq!(v["texts"][0]["content_layer"], "furniture");
3131 assert_eq!(
3132 v["texts"][1]["children"],
3133 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
3134 );
3135 let bold = &v["texts"][2];
3136 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
3137 let keys: Vec<&str> = bold
3138 .as_object()
3139 .unwrap()
3140 .keys()
3141 .map(String::as_str)
3142 .collect();
3143 assert_eq!(
3144 keys,
3145 [
3146 "self_ref",
3147 "parent",
3148 "children",
3149 "content_layer",
3150 "label",
3151 "prov",
3152 "orig",
3153 "text",
3154 "formatting",
3155 "hyperlink"
3156 ]
3157 );
3158 assert_eq!(
3159 bold["formatting"],
3160 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
3161 );
3162 let code = &v["texts"][3];
3163 assert_eq!(code["label"], "code");
3164 assert_eq!(code["code_language"], "Python");
3165 let sub = &v["texts"][4];
3166 assert_eq!(sub["orig"], "Sub\u{2019}");
3167 assert_eq!(sub["level"], 1);
3168 let item = &v["texts"][5];
3169 let keys: Vec<&str> = item
3170 .as_object()
3171 .unwrap()
3172 .keys()
3173 .map(String::as_str)
3174 .collect();
3175 assert_eq!(
3176 keys,
3177 [
3178 "self_ref",
3179 "parent",
3180 "children",
3181 "content_layer",
3182 "label",
3183 "prov",
3184 "orig",
3185 "text",
3186 "enumerated",
3187 "marker"
3188 ]
3189 );
3190 assert_eq!(item["marker"], "3.");
3191 assert_eq!(v["texts"][7]["kind"], "read_only");
3192 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
3193 let table = &v["tables"][0];
3194 assert_eq!(
3195 table["children"],
3196 serde_json::json!([{"$ref": "#/groups/1"}])
3197 );
3198 let cells = table["data"]["table_cells"].as_array().unwrap();
3199 assert_eq!(
3200 cells[0]["text"], "a \n<",
3201 "raw cell text is written verbatim"
3202 );
3203 assert_eq!(
3204 cells[0]["end_row_offset_idx"], 3,
3205 "declared spans are not clamped"
3206 );
3207 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
3208 assert!(cells[0].get("ref").is_none());
3209 assert!(
3210 table["data"]["grid"][0][1].get("ref").is_none(),
3211 "the grid shows plain cells"
3212 );
3213 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
3214 }
3215
3216 #[test]
3220 fn a_comment_section_can_be_referenced_by_its_note_text() {
3221 let doc = DoclingDocument {
3222 name: "c".into(),
3223 nodes: vec![
3224 Node::Commented {
3225 comments: vec![0],
3226 inner: Box::new(Node::Paragraph {
3227 text: "annotated".into(),
3228 }),
3229 },
3230 Node::CommentSection {
3231 name: "comment-Sheet1-A1".into(),
3232 text: "[author: A]: note".into(),
3233 refs_note_text: true,
3234 grouped: true,
3235 },
3236 ],
3237 ..DoclingDocument::new("c")
3238 };
3239 let v = crate::json::to_json(&doc);
3240 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
3241 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
3242 }
3243
3244 #[test]
3248 fn comment_sections_link_back_to_their_items() {
3249 let doc = DoclingDocument {
3250 name: "c".into(),
3251 nodes: vec![
3252 Node::Commented {
3253 comments: vec![0],
3254 inner: Box::new(Node::Paragraph {
3255 text: "annotated".into(),
3256 }),
3257 },
3258 Node::Paragraph {
3259 text: "plain".into(),
3260 },
3261 Node::CommentSection {
3262 name: "comment-7".into(),
3263 text: "[time: t]: note".into(),
3264 refs_note_text: false,
3265 grouped: true,
3266 },
3267 ],
3268 ..DoclingDocument::new("c")
3269 };
3270 let v = crate::json::to_json(&doc);
3271 assert_eq!(v["groups"][0]["label"], "comment_section");
3273 assert_eq!(v["groups"][0]["name"], "comment-7");
3274 assert_eq!(v["groups"][0]["content_layer"], "notes");
3275 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
3276 assert_eq!(v["texts"][2]["content_layer"], "notes");
3277 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
3279 assert!(v["texts"][1].get("comments").is_none());
3280 let keys: Vec<&str> = v["texts"][0]
3282 .as_object()
3283 .unwrap()
3284 .keys()
3285 .map(String::as_str)
3286 .collect();
3287 assert_eq!(
3288 &keys[keys.len() - 4..],
3289 &["prov", "comments", "orig", "text"]
3290 );
3291 }
3292
3293 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
3294 Node::Picture {
3295 caption: Some(caption.into()),
3296 caption_href: None,
3297 image: None,
3298 classification: None,
3299 caption_parent,
3300 }
3301 }
3302
3303 fn group(children: Vec<Node>) -> Node {
3304 Node::Group {
3305 label: "section".into(),
3306 name: None,
3307 layer: None,
3308 children,
3309 }
3310 }
3311
3312 fn refs(v: &Value) -> Vec<&str> {
3313 v.as_array()
3314 .unwrap()
3315 .iter()
3316 .map(|r| r["$ref"].as_str().unwrap())
3317 .collect()
3318 }
3319
3320 #[test]
3325 fn a_body_caption_follows_the_enclosing_top_level_item() {
3326 let mut doc = DoclingDocument::new("t");
3327 doc.push(picture("top", CaptionParent::Body));
3328 doc.push(group(vec![
3329 Node::Paragraph { text: "p".into() },
3330 picture("nested", CaptionParent::Body),
3331 ]));
3332 doc.push(Node::Paragraph {
3333 text: "after".into(),
3334 });
3335 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3336 assert_eq!(
3337 refs(&v["body"]["children"]),
3338 [
3339 "#/texts/0",
3340 "#/pictures/0",
3341 "#/groups/0",
3342 "#/texts/2",
3343 "#/texts/3"
3344 ]
3345 );
3346 assert_eq!(
3347 refs(&v["groups"][0]["children"]),
3348 ["#/texts/1", "#/pictures/1"]
3349 );
3350 for (cap, pic) in [(0, 0), (2, 1)] {
3351 assert_eq!(v["texts"][cap]["label"], "caption");
3352 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
3353 assert_eq!(
3354 refs(&v["pictures"][pic]["captions"]),
3355 [format!("#/texts/{cap}")]
3356 );
3357 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
3358 }
3359 }
3360
3361 #[test]
3364 fn an_item_caption_is_the_items_first_child() {
3365 let mut doc = DoclingDocument::new("t");
3366 doc.push(picture("fig", CaptionParent::Item));
3367 doc.push(Node::Table(Table {
3368 rows: vec![vec!["a".into()]],
3369 caption: Some("tab".into()),
3370 caption_parent: CaptionParent::Item,
3371 ..Table::default()
3372 }));
3373 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3374 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
3375 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
3376 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
3377 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3378 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
3379 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
3380 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
3381 }
3382
3383 #[test]
3389 fn picture_children_hang_off_the_picture_after_its_caption() {
3390 let mut doc = DoclingDocument::new("t");
3391 doc.push(picture("fig", CaptionParent::Item));
3392 doc.push(Node::PictureChildren(vec![
3393 Node::Heading {
3394 level: 2,
3395 text: "in-figure title".into(),
3396 },
3397 Node::Paragraph {
3398 text: "axis label".into(),
3399 },
3400 Node::ListItem {
3401 ordered: false,
3402 number: 0,
3403 first_in_list: true,
3404 text: "callout".into(),
3405 level: 0,
3406 marker: None,
3407 location: None,
3408 dclx: None,
3409 href: None,
3410 layer: None,
3411 },
3412 ]));
3413 doc.push(Node::Paragraph {
3414 text: "after".into(),
3415 });
3416 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3417 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/texts/4"]);
3418 assert_eq!(
3419 refs(&v["pictures"][0]["children"]),
3420 ["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"]
3421 );
3422 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3423 assert_eq!(v["texts"][1]["label"], "section_header");
3424 assert_eq!(v["texts"][2]["label"], "text");
3425 for t in 1..=2 {
3426 assert_eq!(v["texts"][t]["parent"]["$ref"], "#/pictures/0");
3427 assert_eq!(v["texts"][t]["content_layer"], "body");
3428 }
3429 assert_eq!(v["groups"][0]["parent"]["$ref"], "#/pictures/0");
3430 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
3431 let md = doc.export_to_markdown();
3432 assert!(
3433 !md.contains("axis label") && !md.contains("callout"),
3434 "{md}"
3435 );
3436 assert!(md.contains("after"), "{md}");
3437 }
3438
3439 #[test]
3443 fn a_container_caption_is_the_items_sibling() {
3444 let mut doc = DoclingDocument::new("t");
3445 doc.push(group(vec![
3446 picture("chart", CaptionParent::Container),
3447 Node::Table(Table {
3448 rows: vec![vec!["a".into()]],
3449 caption: Some("figcaption".into()),
3450 caption_parent: CaptionParent::ContainerAfter,
3451 ..Table::default()
3452 }),
3453 ]));
3454 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3455 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
3456 assert_eq!(
3457 refs(&v["groups"][0]["children"]),
3458 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
3459 );
3460 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
3461 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
3462 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
3463 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
3464 }
3465}