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 build_json(doc, false)
250}
251
252pub(crate) fn to_json_with_notes(doc: &DoclingDocument) -> Value {
257 build_json(doc, true)
258}
259
260fn build_json(doc: &DoclingDocument, note_keys: bool) -> Value {
261 let mut b = Builder {
262 note_keys,
263 ..Builder::default()
264 };
265 let body = match &doc.tree {
269 Some(tree) => {
270 for n in &doc.nodes {
273 if let Node::PageInfo {
274 page_no,
275 width,
276 height,
277 } = n
278 {
279 if *page_no > 0 {
280 b.pages.push((*page_no, *width as f64, *height as f64));
281 }
282 }
283 }
284 let mut tree = tree.clone();
287 tree.wrap_misplaced_list_items();
288 b.write_tree(&tree)
289 }
290 None => b.walk_into(&doc.nodes, "#/body"),
291 };
292 b.link_comments();
293
294 let mut out = json!({
295 "schema_name": "DoclingDocument",
296 "version": SCHEMA_VERSION,
297 "name": doc.name,
298 "origin": {
299 "mimetype": "text/plain",
300 "binary_hash": fnv1a(&doc.name),
301 "filename": doc.name,
302 },
303 "furniture": {
304 "self_ref": "#/furniture",
305 "children": [],
306 "content_layer": "furniture",
307 "name": "_root_",
308 "label": "unspecified",
309 },
310 "body": {
311 "self_ref": "#/body",
312 "children": body,
313 "content_layer": "body",
314 "name": "_root_",
315 "label": "unspecified",
316 },
317 "groups": b.groups,
318 "texts": b.texts,
319 "pictures": b.pictures,
320 "tables": b.tables,
321 "key_value_items": b.key_value_items,
322 "form_items": [],
323 "pages": b.pages.iter().map(|(n, w, h)| {
324 let r2 = |v: f64| (v * 100.0).round() / 100.0;
325 let mut page = serde_json::Map::new();
328 page.insert("size".into(), json!({ "width": r2(*w), "height": r2(*h) }));
329 if let Some(img) = doc.page_images.get(n) {
330 page.insert("image".into(), image_ref_json(img));
331 }
332 page.insert("page_no".into(), json!(n));
333 (n.to_string(), Value::Object(page))
334 }).collect::<serde_json::Map<String, Value>>(),
335 });
336
337 clamp_boxes_to_pages(&mut out, &b.pages);
346
347 if !b.field_regions.is_empty() {
352 if let Some(obj) = out.as_object_mut() {
353 let pages = obj.remove("pages");
354 obj.insert("field_regions".into(), Value::Array(b.field_regions));
355 obj.insert("field_items".into(), Value::Array(b.field_items));
356 if let Some(pages) = pages {
357 obj.insert("pages".into(), pages);
358 }
359 }
360 }
361 out
362}
363
364fn classification_meta(classes: &[crate::PictureClass]) -> Value {
371 json!({
372 "classification": {
373 "predictions": classes.iter().map(|c| json!({
374 "confidence": c.confidence as f64,
375 "created_by": "DocumentPictureClassifier",
376 "class_name": c.class_name,
377 })).collect::<Vec<_>>(),
378 },
379 "annotations": [{
380 "kind": "classification",
381 "provenance": "DocumentPictureClassifier",
382 "predicted_classes": classes.iter().map(|c| json!({
383 "class_name": c.class_name,
384 "confidence": c.confidence as f64,
385 })).collect::<Vec<_>>(),
386 }],
387 })
388}
389
390#[allow(clippy::too_many_arguments)]
401fn cell_value(
402 row_span: usize,
403 col_span: usize,
404 start_row: usize,
405 end_row: usize,
406 start_col: usize,
407 end_col: usize,
408 text: String,
409 column_header: bool,
410 row_header: bool,
411 row_section: bool,
412 bbox: Option<[f32; 4]>,
413) -> Value {
414 let mut m = serde_json::Map::with_capacity(12);
415 m.insert("row_span".into(), row_span.into());
416 m.insert("col_span".into(), col_span.into());
417 m.insert("start_row_offset_idx".into(), start_row.into());
418 m.insert("end_row_offset_idx".into(), end_row.into());
419 m.insert("start_col_offset_idx".into(), start_col.into());
420 m.insert("end_col_offset_idx".into(), end_col.into());
421 m.insert("text".into(), Value::String(text));
422 m.insert("column_header".into(), column_header.into());
423 m.insert("row_header".into(), row_header.into());
424 m.insert("row_section".into(), row_section.into());
425 m.insert("fillable".into(), false.into());
426 if let Some(b) = bbox {
427 m.insert(
428 "bbox".into(),
429 json!({
430 "l": b[0], "t": b[1], "r": b[2], "b": b[3],
431 "coord_origin": "TOPLEFT",
432 }),
433 );
434 }
435 Value::Object(m)
436}
437
438fn table_data(t: &Table) -> Value {
441 table_data_with(t, false)
442}
443
444fn table_data_with(t: &Table, raw: bool) -> Value {
447 let cell_text = |s: &str| {
448 if raw {
449 s.to_string()
450 } else {
451 unescape_text(&crate::markdown::strip_hard_breaks(s))
452 }
453 };
454 let mut num_rows = t.rows.len();
455 let mut num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
456 let ragged = t.rows.iter().any(|r| r.len() != num_cols);
464 if let Some(first_class) = t.cells.as_ref().filter(|c| ragged && !c.is_empty()) {
465 for c in first_class {
466 num_rows = num_rows.max(c.start_row + c.row_span);
467 num_cols = num_cols.max(c.start_col + c.col_span);
468 }
469 }
470 let mut grid = Vec::with_capacity(num_rows);
471 let mut cells = Vec::new();
472 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
476 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
477 for c in first_class {
478 let idx = cells.len();
479 cells.push(cell_value(
480 c.row_span,
481 c.col_span,
482 c.start_row,
483 c.start_row + c.row_span,
484 c.start_col,
485 c.start_col + c.col_span,
486 cell_text(&c.text),
487 c.column_header,
488 c.row_header,
489 c.row_section,
490 c.bbox,
491 ));
492 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
493 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
494 slot[r * num_cols + k] = Some(idx);
495 }
496 }
497 }
498 for r in 0..num_rows {
499 let mut grid_row = Vec::with_capacity(num_cols);
500 for c in 0..num_cols {
501 grid_row.push(match slot[r * num_cols + c] {
502 Some(i) => cells[i].clone(),
503 None => cell_value(
504 1,
505 1,
506 r,
507 r + 1,
508 c,
509 c + 1,
510 String::new(),
511 false,
512 false,
513 false,
514 None,
515 ),
516 });
517 }
518 grid.push(grid_row);
519 }
520 } else {
521 let s = t.structure.as_ref();
522 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
523 grid.and_then(|g| g.get(r))
524 .and_then(|row| row.get(c))
525 .copied()
526 .unwrap_or(false)
527 };
528 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
529 let (mut r0, mut c0) = (r, c);
530 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
531 c0 -= 1;
532 }
533 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
534 r0 -= 1;
535 }
536 (r0, c0)
537 };
538 let anchors: Vec<(usize, usize)> = (0..num_rows)
541 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
542 .map(|(r, c)| anchor_of(r, c))
543 .collect();
544 let mut extent: Vec<(usize, usize)> = (0..num_rows)
545 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
546 .collect();
547 for (i, &(ar, ac)) in anchors.iter().enumerate() {
548 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
549 let e = &mut extent[ar * num_cols + ac];
550 e.0 = e.0.max(r);
551 e.1 = e.1.max(c);
552 }
553 for (r, row) in t.rows.iter().enumerate() {
554 let mut grid_row = Vec::with_capacity(num_cols);
555 for c in 0..num_cols {
556 let (ar, ac) = anchors[r * num_cols + c];
557 if (ar, ac) == (r, c) {
558 let (er, ec) = extent[r * num_cols + c];
559 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
560 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
561 Some(s) => flag(Some(&s.col_header), r, c),
562 None => r == 0,
563 };
564 slot[r * num_cols + c] = Some(cells.len());
565 cells.push(cell_value(
566 er - r + 1,
567 ec - c + 1,
568 r,
569 er + 1,
570 c,
571 ec + 1,
572 text,
573 column_header,
574 flag(s.map(|s| &s.row_header), r, c),
575 false,
576 None,
577 ));
578 }
579 grid_row.push(match slot[ar * num_cols + ac] {
580 Some(i) => cells[i].clone(),
581 None => Value::Null,
582 });
583 }
584 grid.push(grid_row);
585 }
586 }
587 json!({
588 "table_cells": cells,
589 "num_rows": num_rows,
590 "num_cols": num_cols,
591 "orientation": "rot_0",
592 "grid": grid,
593 })
594}
595
596#[derive(Default)]
597struct Builder {
598 note_keys: bool,
602 texts: Vec<Value>,
603 groups: Vec<Value>,
604 tables: Vec<Value>,
605 pictures: Vec<Value>,
606 field_regions: Vec<Value>,
607 field_items: Vec<Value>,
608 key_value_items: Vec<Value>,
609 pages: Vec<(usize, f64, f64)>,
613 cur_page: usize,
615 cur_w: f64,
616 cur_h: f64,
617 pending_loc: Option<[u16; 4]>,
620 pending_exact: Option<ExactProv>,
623 pending_siblings: Vec<Value>,
627 pending_after: Vec<Value>,
631 pending_body: Vec<Value>,
635 comment_groups: Vec<String>,
639 last_comment_section: Option<(String, String)>,
644 pending_comments: Vec<(String, Vec<usize>)>,
648}
649
650impl Builder {
651 fn take_prov(&mut self, char_len: usize) -> Value {
657 let prov = self.prov_json(char_len, false);
658 self.pending_exact = None;
659 self.pending_loc = None;
660 prov
661 }
662
663 fn prov_json(&self, char_len: usize, span_over_text: bool) -> Value {
668 let r2 = |v: f64| (v * 100.0).round() / 100.0;
669 if let Some(ExactProv {
670 page_no,
671 bbox: [l, t, r, b],
672 bottom_left,
673 charspan,
674 }) = self.pending_exact
675 {
676 let charspan = if span_over_text {
677 [0, char_len]
678 } else {
679 charspan
680 };
681 return json!([{
682 "page_no": page_no,
683 "bbox": {
684 "l": r2(l), "t": r2(t), "r": r2(r), "b": r2(b),
685 "coord_origin": if bottom_left { "BOTTOMLEFT" } else { "TOPLEFT" },
686 },
687 "charspan": charspan,
688 }]);
689 }
690 let Some([x0, y0, x1, y1]) = self.pending_loc else {
691 return json!([]);
692 };
693 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
697 return json!([{
698 "page_no": self.cur_page,
699 "bbox": { "l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT" },
700 "charspan": [0, char_len],
701 }]);
702 }
703 json!([{
704 "page_no": self.cur_page,
705 "bbox": {
706 "l": r2(x0 as f64 * self.cur_w / 512.0),
707 "t": r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
708 "r": r2(x1 as f64 * self.cur_w / 512.0),
709 "b": r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
710 "coord_origin": "BOTTOMLEFT",
711 },
712 "charspan": [0, char_len],
713 }])
714 }
715
716 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
719 if self.pending_loc.is_none() && self.cur_page > 0 {
720 self.pending_loc = loc;
721 }
722 }
723
724 fn link_comments(&mut self) {
732 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
733 .into_iter()
734 .map(|(item, comments)| {
735 let refs = comments
736 .iter()
737 .filter_map(|i| self.comment_groups.get(*i))
738 .map(|r| json!({ "$ref": r }))
739 .collect();
740 (item, refs)
741 })
742 .collect();
743 for (item, comment_refs) in refs {
744 if comment_refs.is_empty() {
745 continue;
746 }
747 let Some(target) = self.item_mut(&item) else {
748 continue;
749 };
750 let Some(obj) = target.as_object_mut() else {
751 continue;
752 };
753 let tail: Vec<(String, Value)> = obj
754 .iter()
755 .skip_while(|(k, _)| k.as_str() != "prov")
756 .skip(1)
757 .map(|(k, v)| (k.clone(), v.clone()))
758 .collect();
759 for (k, _) in &tail {
760 obj.shift_remove(k);
761 }
762 obj.insert("comments".into(), Value::Array(comment_refs));
763 for (k, v) in tail {
764 obj.insert(k, v);
765 }
766 }
767 }
768
769 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
771 let idx = ref_index(self_ref)?;
772 let bucket = if self_ref.starts_with("#/texts/") {
773 &mut self.texts
774 } else if self_ref.starts_with("#/tables/") {
775 &mut self.tables
776 } else if self_ref.starts_with("#/pictures/") {
777 &mut self.pictures
778 } else if self_ref.starts_with("#/groups/") {
779 &mut self.groups
780 } else {
781 return None;
782 };
783 bucket.get_mut(idx)
784 }
785
786 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
790 use crate::tree::TreeKind;
791 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
794 let (mut nt, mut ng, mut ntb, mut np, mut nf, mut nk) = (0, 0, 0, 0, 0, 0);
795 for item in &tree.items {
796 if item.deleted {
797 refs.push(String::new());
798 continue;
799 }
800 let r = match &item.kind {
801 TreeKind::Text { .. } | TreeKind::Code { .. } => {
802 nt += 1;
803 format!("#/texts/{}", nt - 1)
804 }
805 TreeKind::Group { .. } => {
806 ng += 1;
807 format!("#/groups/{}", ng - 1)
808 }
809 TreeKind::Table { .. } => {
810 ntb += 1;
811 format!("#/tables/{}", ntb - 1)
812 }
813 TreeKind::Picture { .. } => {
814 np += 1;
815 format!("#/pictures/{}", np - 1)
816 }
817 TreeKind::FieldRegion { items } => {
818 nt += items
821 .iter()
822 .map(|i| {
823 [&i.marker, &i.key, &i.value]
824 .iter()
825 .filter(|p| p.is_some())
826 .count()
827 })
828 .sum::<usize>();
829 nf += 1;
830 format!("#/field_regions/{}", nf - 1)
831 }
832 TreeKind::KeyValueGraph { .. } => {
833 nk += 1;
834 format!("#/key_value_items/{}", nk - 1)
835 }
836 };
837 refs.push(r);
838 }
839 let ref_of = |id: usize| json!({ "$ref": refs[id] });
840 for (id, item) in tree.items.iter().enumerate() {
841 if item.deleted {
842 continue;
843 }
844 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
845 let children: Vec<Value> = item
849 .children
850 .iter()
851 .filter(|&&c| !tree.items[c].deleted)
852 .map(|&c| ref_of(c))
853 .collect();
854 let layer = item.layer.map_or("body", |l| l.value());
855 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
858 let self_ref = match &item.kind {
859 TreeKind::Text {
860 label,
861 text,
862 orig,
863 formatting,
864 hyperlink,
865 level,
866 list,
867 } => {
868 let mut tail = serde_json::Map::new();
871 if let Some(f) = formatting {
872 tail.insert("formatting".into(), formatting_json(f));
873 }
874 if let Some(h) = hyperlink {
875 tail.insert("hyperlink".into(), json!(h));
876 }
877 if let Some(l) = level {
878 tail.insert("level".into(), json!(l));
879 }
880 if let Some(l) = list {
881 tail.insert("enumerated".into(), json!(l.enumerated));
882 tail.insert("marker".into(), json!(l.marker));
883 }
884 let r = format!("#/texts/{}", self.texts.len());
885 let prov = self.take_prov(text.chars().count());
886 let mut item_json = json!({
887 "self_ref": r,
888 "parent": { "$ref": parent },
889 "children": children,
890 "content_layer": layer,
891 "label": label,
892 "prov": prov,
893 });
894 if !item.comments.is_empty() {
897 item_json["comments"] =
898 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
899 }
900 if let Some(track) = &item.source {
903 item_json["source"] = json!([track_json(track)]);
904 }
905 merge(
906 &mut item_json,
907 json!({
908 "orig": orig.as_deref().unwrap_or(text),
909 "text": text,
910 }),
911 );
912 merge(&mut item_json, Value::Object(tail));
913 if self.note_keys {
914 if !item.notes.is_empty() {
915 item_json["_notes"] = Value::Array(
916 item.notes
917 .iter()
918 .map(|n| json!([n.offset, n.text]))
919 .collect(),
920 );
921 }
922 if item.note_body {
923 item_json["_note_body"] = json!(true);
924 }
925 }
926 self.texts.push(item_json);
927 r
928 }
929 TreeKind::Code {
930 text,
931 orig,
932 language,
933 formatting,
934 hyperlink,
935 } => {
936 let r = format!("#/texts/{}", self.texts.len());
937 let prov = self.take_prov(text.chars().count());
938 let mut item_json = json!({
939 "self_ref": r,
940 "parent": { "$ref": parent },
941 "children": children,
942 "content_layer": layer,
943 "label": "code",
944 "prov": prov,
945 });
946 if !item.comments.is_empty() {
947 item_json["comments"] =
948 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
949 }
950 merge(
951 &mut item_json,
952 json!({
953 "orig": orig.as_deref().unwrap_or(text),
954 "text": text,
955 }),
956 );
957 if let Some(f) = formatting {
958 item_json["formatting"] = formatting_json(f);
959 }
960 if let Some(h) = hyperlink {
961 item_json["hyperlink"] = json!(h);
962 }
963 merge(
964 &mut item_json,
965 json!({
966 "captions": [],
967 "references": [],
968 "footnotes": [],
969 "code_language": code_language(language.as_deref()),
970 }),
971 );
972 self.texts.push(item_json);
973 r
974 }
975 TreeKind::Group { label, name } => {
976 self.pending_exact = None;
977 let r = format!("#/groups/{}", self.groups.len());
978 self.groups.push(json!({
979 "self_ref": r,
980 "parent": { "$ref": parent },
981 "children": children,
982 "content_layer": layer,
983 "name": name,
984 "label": label,
985 }));
986 r
987 }
988 TreeKind::Table {
989 table,
990 rich_cells,
991 captions,
992 } => {
993 let r = self.add_table_with(table, parent, true);
995 let idx = ref_index(&r).expect("table ref");
996 let t = &mut self.tables[idx];
997 t["children"] = Value::Array(children);
998 t["content_layer"] = json!(layer);
999 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
1000 for &(row, col, group) in rich_cells {
1004 let cell_ref = ref_of(group);
1005 let hit = |c: &Value| {
1006 c["start_row_offset_idx"] == json!(row)
1007 && c["start_col_offset_idx"] == json!(col)
1008 };
1009 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
1010 for c in cells.iter_mut().filter(|c| hit(c)) {
1011 c["ref"] = cell_ref.clone();
1012 }
1013 }
1014 }
1015 r
1016 }
1017 TreeKind::Picture {
1018 captions,
1019 image,
1020 classification,
1021 confidence,
1022 chart,
1023 dpi,
1024 } => {
1025 let mut meta = classification.as_ref().map(|c| {
1030 let mut pred = serde_json::Map::new();
1031 if let Some(conf) = confidence {
1032 pred.insert("confidence".into(), json!(conf));
1033 }
1034 pred.insert("class_name".into(), json!(c));
1035 json!({ "classification": { "predictions": [pred] } })
1036 });
1037 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
1038 if !t.rows.is_empty() {
1039 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
1040 }
1041 }
1042 let prov = self.take_prov(0);
1043 let r = self.push_picture(
1044 prov,
1045 captions.iter().map(|&c| ref_of(c)).collect(),
1046 children,
1047 image.as_ref(),
1048 meta,
1049 parent,
1050 );
1051 if let Some(idx) = ref_index(&r) {
1052 self.pictures[idx]["content_layer"] = json!(layer);
1053 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
1056 img["dpi"] = json!(dpi);
1057 }
1058 }
1059 r
1060 }
1061 TreeKind::FieldRegion { items } => {
1062 self.pending_exact = None;
1063 let r = self.add_field_region(items, parent);
1064 if let Some(region) = self.field_regions.last_mut() {
1065 region["content_layer"] = json!(layer);
1066 }
1067 r
1068 }
1069 TreeKind::KeyValueGraph { cells, links } => {
1070 self.pending_exact = None;
1071 let r = self.add_key_value_graph(cells, links, parent);
1072 if let Some(item) = self.key_value_items.last_mut() {
1073 item["content_layer"] = json!(layer);
1074 }
1075 r
1076 }
1077 };
1078 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1079 }
1080 tree.body
1081 .iter()
1082 .filter(|&&c| !tree.items[c].deleted)
1083 .map(|&c| ref_of(c))
1084 .collect()
1085 }
1086
1087 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1088 match node {
1089 Node::Heading { level: 1, text } => {
1090 Some(self.add_text("title", text, parent, json!({})))
1091 }
1092 Node::Heading { level, text } => Some(self.add_text(
1093 "section_header",
1094 text,
1095 parent,
1096 json!({ "level": level.saturating_sub(1) }),
1097 )),
1098 Node::Caption { text, href } => {
1099 let extra = match href {
1100 Some(url) => json!({ "hyperlink": url }),
1101 None => json!({}),
1102 };
1103 Some(self.add_text("caption", text, parent, extra))
1104 }
1105 Node::Paragraph { text } => {
1106 let t = text.trim();
1109 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1110 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1111 _ => Some(self.add_text("text", text, parent, json!({}))),
1112 }
1113 }
1114 Node::CheckboxItem { checked, text } => {
1115 let mark = if *checked { "- [x] " } else { "- [ ] " };
1118 Some(self.add_text("text", &format!("{mark}{text}"), parent, json!({})))
1119 }
1120 Node::Code {
1121 language,
1122 text,
1123 orig,
1124 ..
1125 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1126 Node::Formula {
1129 latex,
1130 orig,
1131 location,
1132 } => {
1133 self.adopt_loc(*location);
1134 Some(self.add_formula_item(latex, orig, parent))
1135 }
1136 Node::CommentSection {
1142 name,
1143 text,
1144 refs_note_text,
1145 grouped,
1146 } => {
1147 if !*grouped {
1148 let child =
1152 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1153 self.comment_groups.push(child.clone());
1154 return Some(child);
1155 }
1156 if let Some((_, self_ref)) = self
1157 .last_comment_section
1158 .as_ref()
1159 .filter(|(n, _)| n == name)
1160 .cloned()
1161 {
1162 let child =
1165 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1166 if let Some(children) =
1167 self.groups[group_index(&self_ref)]["children"].as_array_mut()
1168 {
1169 children.push(json!({ "$ref": child }));
1170 }
1171 self.comment_groups.push(if *refs_note_text {
1172 child
1173 } else {
1174 self_ref.clone()
1175 });
1176 return Some(self_ref);
1177 }
1178 let self_ref = format!("#/groups/{}", self.groups.len());
1179 self.groups.push(Value::Null);
1180 let child =
1181 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1182 self.groups[group_index(&self_ref)] = json!({
1183 "self_ref": self_ref,
1184 "parent": { "$ref": parent },
1185 "children": [{ "$ref": child }],
1186 "content_layer": "notes",
1187 "name": name,
1188 "label": "comment_section",
1189 });
1190 self.last_comment_section = Some((name.clone(), self_ref.clone()));
1191 self.comment_groups.push(if *refs_note_text {
1192 child
1193 } else {
1194 self_ref.clone()
1195 });
1196 Some(self_ref)
1197 }
1198 Node::Commented { comments, inner } => {
1201 let item = self.add_node(inner, parent)?;
1202 if !comments.is_empty() {
1203 self.pending_comments.push((item.clone(), comments.clone()));
1204 }
1205 Some(item)
1206 }
1207 Node::Table(t) => Some(self.add_table(t, parent)),
1208 Node::Picture {
1209 caption,
1210 caption_href,
1211 image,
1212 classification,
1213 caption_parent,
1214 } => Some(self.add_picture(
1215 caption.as_deref(),
1216 caption_href.as_deref(),
1217 image.as_ref(),
1218 classification.as_deref().map(classification_meta),
1219 parent,
1220 *caption_parent,
1221 )),
1222 Node::Chart {
1227 kind,
1228 table,
1229 caption,
1230 location,
1231 } => {
1232 self.adopt_loc(*location);
1233 let mut meta = json!({
1234 "classification": { "predictions": [{ "class_name": kind }] },
1235 });
1236 if !table.rows.is_empty() {
1237 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1238 }
1239 let mut captions = Vec::new();
1245 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1246 let prov = self.prov_json(unescape_text(cap).chars().count(), true);
1247 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1248 self.pending_siblings.push(json!({ "$ref": cap_ref }));
1249 captions.push(json!({ "$ref": cap_ref }));
1250 }
1251 let prov = self.take_prov(0);
1252 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1253 }
1254 Node::DoclangOnly(_) => None,
1256 Node::Group {
1257 label,
1258 name,
1259 layer,
1260 children,
1261 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1262 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1263 Node::KeyValueGraph { cells, links } => {
1264 Some(self.add_key_value_graph(cells, links, parent))
1265 }
1266 Node::InlineGroup { md_text, .. } => {
1269 Some(self.add_text("text", md_text, parent, json!({})))
1270 }
1271 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1273 Node::Furniture {
1279 layer: ContentLayer::Notes,
1280 inner,
1281 } => {
1282 let item = self.add_node(inner, parent)?;
1283 self.set_layer(&item, "notes");
1284 Some(item)
1285 }
1286 Node::Furniture { .. } => None,
1287 Node::PageFurniture {
1291 footer,
1292 location,
1293 text,
1294 } => {
1295 if self.cur_page > 0 {
1296 self.pending_loc = Some(*location);
1297 }
1298 let label = if *footer {
1299 "page_footer"
1300 } else {
1301 "page_header"
1302 };
1303 let item = self.add_text(label, text, parent, json!({}));
1304 self.pending_loc = None;
1305 self.set_layer(&item, "furniture");
1306 Some(item)
1307 }
1308 Node::FurnitureText { label, text } => {
1311 let item = self.add_text(label, text, parent, json!({}));
1312 self.set_layer(&item, "furniture");
1313 Some(item)
1314 }
1315 Node::PictureChildren(children) => {
1319 let pic = self.pictures.len().checked_sub(1)?;
1320 let pic_ref = format!("#/pictures/{pic}");
1321 let refs = self.walk_into(children, &pic_ref);
1322 if let Some(list) = self.pictures[pic]["children"].as_array_mut() {
1323 list.extend(refs);
1324 }
1325 None
1326 }
1327 Node::Located { location, inner } => {
1332 if self.cur_page > 0 {
1333 self.pending_loc = Some(*location);
1334 }
1335 let r = self.add_node(inner, parent);
1336 self.pending_loc = None;
1337 r
1338 }
1339 Node::Prov {
1340 page_no,
1341 bbox,
1342 charspan,
1343 inner,
1344 ..
1345 } => {
1346 self.pending_exact = Some(ExactProv {
1347 page_no: *page_no,
1348 bbox: bbox.map(f64::from),
1349 bottom_left: false,
1350 charspan: *charspan,
1351 });
1352 let r = self.add_node(inner, parent);
1353 self.pending_exact = None;
1354 r
1355 }
1356 Node::PageBreak => None,
1358 Node::PageInfo {
1361 page_no,
1362 width,
1363 height,
1364 } => {
1365 self.cur_page = *page_no;
1366 self.cur_w = *width as f64;
1367 self.cur_h = *height as f64;
1368 if *page_no > 0 {
1369 self.pages.push((*page_no, self.cur_w, self.cur_h));
1370 }
1371 None
1372 }
1373 Node::ListItem { .. } => None,
1375 }
1376 }
1377
1378 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1382 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1383 self.field_regions.push(Value::Null);
1384 let region_index = self.field_regions.len() - 1;
1385 let mut item_refs = Vec::new();
1386 for item in items {
1387 item_refs.push(json!({ "$ref": self.add_field_item(item, &self_ref) }));
1388 }
1389 self.field_regions[region_index] = json!({
1390 "self_ref": self_ref,
1391 "parent": { "$ref": parent },
1392 "children": item_refs,
1393 "content_layer": "body",
1394 "label": "field_region",
1395 "prov": [],
1396 });
1397 self_ref
1398 }
1399
1400 fn add_key_value_graph(
1404 &mut self,
1405 cells: &[crate::GraphCell],
1406 links: &[crate::GraphLink],
1407 parent: &str,
1408 ) -> String {
1409 let self_ref = format!("#/key_value_items/{}", self.key_value_items.len());
1410 let cells: Vec<Value> = cells
1411 .iter()
1412 .map(|c| {
1413 json!({
1414 "label": c.label,
1415 "cell_id": c.cell_id,
1416 "text": c.text,
1417 "orig": c.orig,
1418 })
1419 })
1420 .collect();
1421 let links: Vec<Value> = links
1422 .iter()
1423 .map(|l| {
1424 json!({
1425 "label": l.label,
1426 "source_cell_id": l.source_cell_id,
1427 "target_cell_id": l.target_cell_id,
1428 })
1429 })
1430 .collect();
1431 self.key_value_items.push(json!({
1432 "self_ref": self_ref,
1433 "parent": { "$ref": parent },
1434 "children": [],
1435 "content_layer": "body",
1436 "label": "key_value_region",
1437 "prov": [],
1438 "captions": [],
1439 "references": [],
1440 "footnotes": [],
1441 "graph": { "cells": cells, "links": links },
1442 }));
1443 self_ref
1444 }
1445
1446 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1447 let self_ref = format!("#/field_items/{}", self.field_items.len());
1448 self.field_items.push(Value::Null);
1449 let item_index = self.field_items.len() - 1;
1450 let mut child_refs = Vec::new();
1451 for (label, text) in [
1452 ("marker", &item.marker),
1453 ("field_key", &item.key),
1454 ("field_value", &item.value),
1455 ] {
1456 if let Some(text) = text {
1457 let extra = match (label, &item.value_kind) {
1460 ("field_value", Some(kind)) => json!({ "kind": kind }),
1461 _ => json!({}),
1462 };
1463 child_refs.push(json!({ "$ref": self.add_text(label, text, &self_ref, extra) }));
1464 }
1465 }
1466 self.field_items[item_index] = json!({
1467 "self_ref": self_ref,
1468 "parent": { "$ref": parent },
1469 "children": child_refs,
1470 "content_layer": "body",
1471 "label": "field_item",
1472 "prov": [],
1473 });
1474 self_ref
1475 }
1476
1477 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1481 let bucket = match self_ref.split('/').nth(1) {
1482 Some("texts") => &mut self.texts,
1483 Some("tables") => &mut self.tables,
1484 Some("pictures") => &mut self.pictures,
1485 Some("groups") => &mut self.groups,
1486 _ => return,
1487 };
1488 if let Some(item) = self_ref
1489 .rsplit('/')
1490 .next()
1491 .and_then(|i| i.parse::<usize>().ok())
1492 .and_then(|i| bucket.get_mut(i))
1493 {
1494 item["content_layer"] = json!(layer);
1495 }
1496 }
1497
1498 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1499 let prov = self.take_prov(unescape_text(text).chars().count());
1500 self.add_text_with(label, text, parent, extra, prov)
1501 }
1502
1503 fn add_text_with(
1506 &mut self,
1507 label: &str,
1508 text: &str,
1509 parent: &str,
1510 extra: Value,
1511 prov: Value,
1512 ) -> String {
1513 let self_ref = format!("#/texts/{}", self.texts.len());
1514 let raw = unescape_text(text);
1515 let mut item = json!({
1516 "self_ref": self_ref,
1517 "parent": { "$ref": parent },
1518 "children": [],
1519 "content_layer": "body",
1520 "label": label,
1521 "prov": prov,
1522 "orig": raw,
1523 "text": raw,
1524 });
1525 merge(&mut item, extra);
1526 self.texts.push(item);
1527 self_ref
1528 }
1529
1530 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
1533 let self_ref = format!("#/texts/{}", self.texts.len());
1534 let prov = self.take_prov(latex.chars().count());
1535 self.texts.push(json!({
1536 "self_ref": self_ref,
1537 "parent": { "$ref": parent },
1538 "children": [],
1539 "content_layer": "body",
1540 "label": "formula",
1541 "prov": prov,
1542 "orig": latex,
1543 "text": latex,
1544 }));
1545 self_ref
1546 }
1547
1548 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
1552 let self_ref = format!("#/texts/{}", self.texts.len());
1553 let prov = self.take_prov(latex.chars().count());
1554 self.texts.push(json!({
1555 "self_ref": self_ref,
1556 "parent": { "$ref": parent },
1557 "children": [],
1558 "content_layer": "body",
1559 "label": "formula",
1560 "prov": prov,
1561 "orig": orig,
1562 "text": latex,
1563 }));
1564 self_ref
1565 }
1566
1567 fn add_code(
1568 &mut self,
1569 text: &str,
1570 language: Option<&str>,
1571 orig: Option<&str>,
1572 parent: &str,
1573 ) -> String {
1574 let self_ref = format!("#/texts/{}", self.texts.len());
1575 let raw = unescape_text(text);
1576 let prov = self.take_prov(raw.chars().count());
1577 self.texts.push(json!({
1578 "self_ref": self_ref,
1579 "parent": { "$ref": parent },
1580 "children": [],
1581 "content_layer": "body",
1582 "label": "code",
1583 "prov": prov,
1584 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
1587 "text": raw,
1588 "captions": [],
1589 "references": [],
1590 "footnotes": [],
1591 "code_language": code_language(language),
1592 }));
1593 self_ref
1594 }
1595
1596 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
1599 let self_ref = format!("#/groups/{}", self.groups.len());
1600 self.groups.push(Value::Null);
1602 let base = level_of(&items[0]);
1603 let mut children = Vec::new();
1604 let mut i = 0;
1605 while i < items.len() {
1606 if !matches!(items[i], Node::ListItem { .. }) {
1609 i += 1;
1610 continue;
1611 }
1612 let lvl = level_of(&items[i]);
1613 if lvl > base {
1614 i += 1;
1616 continue;
1617 }
1618 let item_ref = self.add_list_item(&items[i], &self_ref);
1619 let mut j = i + 1;
1621 while j < items.len() && level_of(&items[j]) > base {
1622 j += 1;
1623 }
1624 if j > i + 1 {
1625 let mut nested = Vec::new();
1626 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
1627 if let Some(idx) = ref_index(&item_ref) {
1629 self.texts[idx]["children"]
1630 .as_array_mut()
1631 .unwrap()
1632 .extend(nested);
1633 }
1634 }
1635 children.push(json!({ "$ref": item_ref }));
1636 i = j;
1637 }
1638 self.groups[group_index(&self_ref)] = json!({
1639 "self_ref": self_ref,
1640 "parent": { "$ref": parent },
1641 "children": children,
1642 "content_layer": "body",
1643 "name": "list",
1644 "label": "list",
1645 });
1646 self_ref
1647 }
1648
1649 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
1650 let Node::ListItem {
1651 ordered,
1652 number,
1653 text,
1654 location,
1655 ..
1656 } = node
1657 else {
1658 unreachable!()
1659 };
1660 self.adopt_loc(*location);
1661 let self_ref = format!("#/texts/{}", self.texts.len());
1662 let raw = unescape_text(text);
1663 let prov = self.take_prov(raw.chars().count());
1664 let marker = if *ordered {
1665 format!("{number}.")
1666 } else {
1667 "-".to_string()
1668 };
1669 self.texts.push(json!({
1670 "self_ref": self_ref,
1671 "parent": { "$ref": parent },
1672 "children": [],
1673 "content_layer": "body",
1674 "label": "list_item",
1675 "prov": prov,
1676 "orig": raw,
1677 "text": raw,
1678 "enumerated": ordered,
1679 "marker": marker,
1680 }));
1681 self_ref
1682 }
1683
1684 fn add_table(&mut self, t: &Table, parent: &str) -> String {
1685 self.add_table_with(t, parent, false)
1686 }
1687
1688 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
1691 let self_ref = format!("#/tables/{}", self.tables.len());
1692 self.adopt_loc(t.location);
1693 let prov = self.take_prov(0);
1694 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
1698 Some(cap) => self.add_caption(cap, json!({}), &self_ref, parent, t.caption_parent),
1699 None => (Vec::new(), Vec::new()),
1700 };
1701 let data = table_data_with(t, raw);
1702 self.tables.push(json!({
1703 "self_ref": self_ref,
1704 "parent": { "$ref": parent },
1705 "children": children,
1706 "content_layer": "body",
1707 "label": "table",
1708 "prov": prov,
1709 "captions": captions,
1710 "references": [],
1711 "footnotes": [],
1712 "data": data,
1713 "annotations": [],
1714 }));
1715 self_ref
1716 }
1717
1718 fn add_caption(
1724 &mut self,
1725 text: &str,
1726 extra: Value,
1727 self_ref: &str,
1728 parent: &str,
1729 choice: CaptionParent,
1730 ) -> (Vec<Value>, Vec<Value>) {
1731 let cap_parent = match choice {
1736 CaptionParent::Item => self_ref,
1737 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
1738 CaptionParent::Body => "#/body",
1739 };
1740 let cap_ref = json!({ "$ref": self.add_text("caption", text, cap_parent, extra) });
1741 match choice {
1742 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
1743 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
1746 CaptionParent::Body if parent == "#/body" => {
1747 self.pending_siblings.push(cap_ref.clone())
1748 }
1749 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
1750 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
1753 }
1754 (vec![cap_ref], Vec::new())
1755 }
1756
1757 fn add_picture(
1760 &mut self,
1761 caption: Option<&str>,
1762 caption_href: Option<&str>,
1763 image: Option<&crate::PictureImage>,
1764 meta: Option<Value>,
1765 parent: &str,
1766 caption_parent: CaptionParent,
1767 ) -> String {
1768 let self_ref = format!("#/pictures/{}", self.pictures.len());
1769 let prov = self.take_prov(0);
1772 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
1773 Some(cap) => {
1774 let extra = match caption_href {
1778 Some(href) => json!({ "hyperlink": href }),
1779 None => json!({}),
1780 };
1781 self.add_caption(cap, extra, &self_ref, parent, caption_parent)
1782 }
1783 None => (Vec::new(), Vec::new()),
1784 };
1785 self.push_picture(prov, captions, children, image, meta, parent)
1786 }
1787
1788 fn push_picture(
1791 &mut self,
1792 prov: Value,
1793 captions: Vec<Value>,
1794 children: Vec<Value>,
1795 image: Option<&crate::PictureImage>,
1796 meta: Option<Value>,
1797 parent: &str,
1798 ) -> String {
1799 let self_ref = format!("#/pictures/{}", self.pictures.len());
1800 let annotations = meta
1804 .as_ref()
1805 .and_then(|m| m.get("annotations").cloned())
1806 .unwrap_or_else(|| json!([]));
1807 let meta = meta.map(|mut m| {
1808 if let Some(obj) = m.as_object_mut() {
1809 obj.remove("annotations");
1810 }
1811 m
1812 });
1813 let mut item = match meta {
1817 Some(meta) => json!({
1818 "self_ref": self_ref,
1819 "parent": { "$ref": parent },
1820 "children": children,
1821 "content_layer": "body",
1822 "meta": meta,
1823 "label": "picture",
1824 "prov": prov,
1825 "captions": captions,
1826 "references": [],
1827 "footnotes": [],
1828 "annotations": annotations,
1829 }),
1830 None => json!({
1831 "self_ref": self_ref,
1832 "parent": { "$ref": parent },
1833 "children": children,
1834 "content_layer": "body",
1835 "label": "picture",
1836 "prov": prov,
1837 "captions": captions,
1838 "references": [],
1839 "footnotes": [],
1840 "annotations": annotations,
1841 }),
1842 };
1843 if let Some(img) = image {
1848 let image = image_ref_json(img);
1849 if let Some(obj) = item.as_object_mut() {
1850 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
1851 obj.insert("image".into(), image);
1852 obj.insert("annotations".into(), annotations);
1853 }
1854 }
1855 self.pictures.push(item);
1856 self_ref
1857 }
1858
1859 fn add_group(
1860 &mut self,
1861 label: &str,
1862 name: Option<&str>,
1863 layer: Option<ContentLayer>,
1864 nodes: &[Node],
1865 parent: &str,
1866 ) -> String {
1867 let self_ref = format!("#/groups/{}", self.groups.len());
1868 self.groups.push(Value::Null);
1869 let mark = (
1873 self.texts.len(),
1874 self.tables.len(),
1875 self.pictures.len(),
1876 self.groups.len(),
1877 );
1878 let children = self.walk_into(nodes, &self_ref);
1879 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
1880 let content_layer = layer.map_or("body", |l| l.value());
1881 self.groups[group_index(&self_ref)] = json!({
1882 "self_ref": self_ref,
1883 "parent": { "$ref": parent },
1884 "children": children,
1885 "content_layer": content_layer,
1886 "name": name,
1887 "label": label,
1888 });
1889 if layer.is_some() {
1890 let (t, tb, p, g) = mark;
1891 for item in self.texts[t..]
1892 .iter_mut()
1893 .chain(self.tables[tb..].iter_mut())
1894 .chain(self.pictures[p..].iter_mut())
1895 .chain(self.groups[g..].iter_mut())
1896 {
1897 if let Some(obj) = item.as_object_mut() {
1898 obj.insert("content_layer".into(), json!(content_layer));
1899 }
1900 }
1901 }
1902 self_ref
1903 }
1904
1905 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Value> {
1908 let seqs: Option<Vec<usize>> = nodes
1913 .iter()
1914 .map(|n| match n {
1915 Node::Prov { seq: Some(s), .. } => Some(*s),
1916 _ => None,
1917 })
1918 .collect();
1919 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
1920 let mut order: Vec<usize> = (0..nodes.len()).collect();
1921 order.sort_by_key(|&i| seqs[i]);
1922 let mut slots: Vec<Vec<Value>> = vec![Vec::new(); nodes.len()];
1923 for i in order {
1924 if let Some(r) = self.add_node(&nodes[i], parent) {
1925 slots[i].append(&mut self.pending_siblings);
1926 slots[i].push(json!({ "$ref": r }));
1927 slots[i].append(&mut self.pending_after);
1928 }
1929 if parent == "#/body" {
1930 slots[i].append(&mut self.pending_body);
1931 }
1932 }
1933 return slots.into_iter().flatten().collect();
1934 }
1935 let mut children = Vec::new();
1936 let mut i = 0;
1937 while i < nodes.len() {
1938 if matches!(nodes[i], Node::ListItem { .. }) {
1939 let start = i;
1940 i += 1;
1941 loop {
1942 match nodes.get(i) {
1943 Some(Node::ListItem { .. }) => i += 1,
1944 Some(Node::Paragraph { text })
1947 if text.is_empty()
1948 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
1949 {
1950 i += 1
1951 }
1952 _ => break,
1953 }
1954 }
1955 self.add_sibling_lists(&nodes[start..i], parent, &mut children);
1956 } else {
1957 if let Some(r) = self.add_node(&nodes[i], parent) {
1958 children.append(&mut self.pending_siblings);
1959 children.push(json!({ "$ref": r }));
1960 children.append(&mut self.pending_after);
1961 }
1962 i += 1;
1963 }
1964 if parent == "#/body" {
1967 children.append(&mut self.pending_body);
1968 }
1969 }
1970 children
1971 }
1972
1973 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
1978 let base = level_of(&run[0]);
1979 let mut seg = 0;
1980 for k in 0..run.len() {
1981 let Node::ListItem {
1982 first_in_list,
1983 level,
1984 ..
1985 } = &run[k]
1986 else {
1987 continue;
1988 };
1989 if *level != base {
1990 continue; }
1992 if k > seg && *first_in_list {
1993 out.push(json!({ "$ref": self.add_list(&run[seg..k], parent) }));
1994 seg = k;
1995 }
1996 }
1997 out.push(json!({ "$ref": self.add_list(&run[seg..], parent) }));
1998 }
1999}
2000
2001fn level_of(node: &Node) -> u8 {
2002 match node {
2003 Node::ListItem { level, .. } => *level,
2004 _ => 0,
2005 }
2006}
2007
2008fn group_index(self_ref: &str) -> usize {
2009 self_ref.rsplit('/').next().unwrap().parse().unwrap()
2010}
2011
2012fn ref_index(self_ref: &str) -> Option<usize> {
2013 self_ref.rsplit('/').next()?.parse().ok()
2014}
2015
2016fn merge(target: &mut Value, extra: Value) {
2018 if let (Some(t), Some(e)) = (target.as_object_mut(), extra.as_object()) {
2019 for (k, v) in e {
2020 t.insert(k.clone(), v.clone());
2021 }
2022 }
2023}
2024
2025fn unescape_text(s: &str) -> String {
2027 s.replace("<", "<")
2028 .replace(">", ">")
2029 .replace("&", "&")
2030 .replace("\\_", "_")
2031}
2032
2033fn fnv1a(s: &str) -> u64 {
2036 let mut h: u64 = 0xcbf29ce484222325;
2037 for b in s.bytes() {
2038 h ^= b as u64;
2039 h = h.wrapping_mul(0x100000001b3);
2040 }
2041 h
2042}
2043
2044#[cfg(test)]
2045mod tests {
2046 use crate::{
2047 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
2048 };
2049 use serde_json::Value;
2050
2051 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
2052 crate::TableCell {
2053 text: text.into(),
2054 bbox: None,
2055 start_row: row,
2056 start_col: col,
2057 row_span: 1,
2058 col_span,
2059 column_header: row == 0,
2060 row_header: false,
2061 row_section: false,
2062 }
2063 }
2064
2065 #[test]
2071 fn ragged_rows_take_their_grid_from_first_class_cells() {
2072 let cells = vec![
2073 cell_at("h0", 0, 0, 1),
2074 cell_at("h2", 0, 2, 1),
2075 cell_at("wide", 1, 0, 3),
2076 ];
2077 let export = |rows: Vec<Vec<String>>| -> Value {
2078 let mut doc = DoclingDocument::new("t");
2079 doc.push(Node::Table(Table {
2080 rows,
2081 cells: Some(cells.clone()),
2082 ..Table::default()
2083 }));
2084 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2085 v["tables"][0]["data"].clone()
2086 };
2087 let data = export(vec![
2089 vec!["h0".into(), "h2".into()],
2090 vec!["wide".into(), "wide".into(), "wide".into()],
2091 ]);
2092 assert_eq!(data["num_rows"], 2);
2093 assert_eq!(data["num_cols"], 3);
2094 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
2095 let row0: Vec<&str> = data["grid"][0]
2096 .as_array()
2097 .unwrap()
2098 .iter()
2099 .map(|c| c["text"].as_str().unwrap())
2100 .collect();
2101 assert_eq!(row0, ["h0", "", "h2"]);
2102 assert_eq!(data["table_cells"][2]["col_span"], 3);
2103 let data = export(vec![
2106 vec!["h0".into(), "h2".into()],
2107 vec!["wide".into(), "wide".into()],
2108 ]);
2109 assert_eq!(data["num_cols"], 2);
2110 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
2111 }
2112
2113 fn doc_with_image() -> DoclingDocument {
2114 let mut doc = DoclingDocument::new("t");
2115 doc.push(Node::Picture {
2116 caption: Some("Fig 1".into()),
2117 caption_href: None,
2118 image: Some(PictureImage {
2119 dpi: crate::PictureImage::DEFAULT_DPI,
2120 mimetype: "image/png".into(),
2121 width: 4,
2122 height: 2,
2123 data: b"foobar".to_vec(),
2124 }),
2125 classification: None,
2126 caption_parent: Default::default(),
2127 });
2128 doc
2129 }
2130
2131 #[test]
2136 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
2137 let mut doc = DoclingDocument::new("t");
2138 doc.push(Node::Heading {
2139 level: 1,
2140 text: "Slide One".into(),
2141 });
2142 doc.push(Node::Furniture {
2143 layer: ContentLayer::Notes,
2144 inner: Box::new(Node::Located {
2145 location: [0, 0, 0, 0],
2146 inner: Box::new(Node::Paragraph {
2147 text: "Speaker note for slide 1.".into(),
2148 }),
2149 }),
2150 });
2151 doc.push(Node::Furniture {
2152 layer: ContentLayer::Furniture,
2153 inner: Box::new(Node::Paragraph {
2154 text: "page header".into(),
2155 }),
2156 });
2157
2158 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2159 let texts = v["texts"].as_array().unwrap();
2160 assert_eq!(
2161 texts
2162 .iter()
2163 .map(|t| (
2164 t["label"].as_str().unwrap(),
2165 t["content_layer"].as_str().unwrap(),
2166 t["text"].as_str().unwrap()
2167 ))
2168 .collect::<Vec<_>>(),
2169 vec![
2170 ("title", "body", "Slide One"),
2171 ("text", "notes", "Speaker note for slide 1."),
2172 ],
2173 "the note is carried on its own layer; the furniture is not carried"
2174 );
2175 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
2177 }
2178
2179 #[test]
2185 fn continuation_flags_become_spanning_cells() {
2186 let mut doc = DoclingDocument::new("t");
2187 let rows = vec![
2189 vec!["merged".to_string(), "merged".into(), "merged".into()],
2190 vec!["merged".to_string(), "merged".into(), "merged".into()],
2191 vec!["a".to_string(), "b".into(), "c".into()],
2192 ];
2193 doc.push(Node::Table(crate::Table {
2194 rows,
2195 location: None,
2196 structure: Some(crate::TableStructure {
2197 header_row: vec![true, false, false],
2198 col_continuation: vec![
2199 vec![false, true, true],
2200 vec![false, true, true],
2201 vec![false, false, false],
2202 ],
2203 row_continuation: vec![
2204 vec![false, false, false],
2205 vec![true, true, true],
2206 vec![false, false, false],
2207 ],
2208 row_header: Vec::new(),
2209 col_header: Vec::new(),
2210 }),
2211 cell_blocks: None,
2212 cells: None,
2213 caption: None,
2214 caption_parent: Default::default(),
2215 }));
2216 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2217 let data = &v["tables"][0]["data"];
2218 assert_eq!(data["num_rows"], 3);
2219 assert_eq!(data["num_cols"], 3);
2220 let cells = data["table_cells"].as_array().unwrap();
2221 assert_eq!(
2222 cells.len(),
2223 4,
2224 "one cell for the range, three for the plain row"
2225 );
2226 assert_eq!(
2227 cells[0],
2228 serde_json::json!({
2229 "row_span": 2, "col_span": 3,
2230 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2231 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2232 "text": "merged", "column_header": true, "row_header": false,
2233 "row_section": false, "fillable": false,
2234 })
2235 );
2236 assert_eq!(cells[1]["text"], "a");
2237 assert_eq!(cells[1]["row_span"], 1);
2238 assert_eq!(cells[1]["column_header"], false);
2239 let grid = data["grid"].as_array().unwrap();
2241 assert_eq!(grid.len(), 3);
2242 for (r, row) in grid.iter().take(2).enumerate() {
2243 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2244 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2245 }
2246 }
2247 assert_eq!(grid[2][2]["text"], "c");
2248 }
2249
2250 #[test]
2256 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2257 let mut doc = DoclingDocument::new("t");
2258 doc.push(Node::PageInfo {
2259 page_no: 1,
2260 width: 3.0,
2261 height: 4.0,
2262 });
2263 let table = crate::Table {
2264 rows: vec![vec!["a".to_string(), "b".into()]],
2265 ..Default::default()
2266 };
2267 doc.push(Node::Group {
2268 label: "sheet".into(),
2269 name: Some("Data".into()),
2270 layer: None,
2271 children: vec![
2272 Node::Prov {
2277 page_no: 1,
2278 bbox: [0.0, 0.0, 3.0, 4.0],
2279 charspan: [0, 0],
2280 seq: Some(1),
2281 inner: Box::new(Node::Table(table.clone())),
2282 },
2283 Node::Prov {
2284 page_no: 1,
2285 bbox: [0.0, 1.0, 1.0, 1.0],
2286 charspan: [0, 0],
2287 seq: Some(0),
2288 inner: Box::new(Node::Chart {
2289 kind: "bar_chart".into(),
2290 table,
2291 caption: Some("Sales".into()),
2292 location: Some([0, 128, 170, 128]),
2293 }),
2294 },
2295 ],
2296 });
2297 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2298 assert_eq!(
2299 v["pages"],
2300 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2301 );
2302 assert_eq!(
2303 v["tables"][0]["prov"],
2304 serde_json::json!([{
2305 "page_no": 1,
2306 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2307 "charspan": [0, 0],
2308 }])
2309 );
2310 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2311 let sheet = &v["groups"][0];
2314 assert_eq!(
2315 sheet["children"],
2316 serde_json::json!([
2317 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2318 ])
2319 );
2320 let cap = &v["texts"][0];
2321 assert_eq!(cap["label"], "caption");
2322 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2323 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2324 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2325 let pic = &v["pictures"][0];
2326 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2327 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2328 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2329 assert_eq!(
2330 pic["meta"]["classification"]["predictions"][0]["class_name"],
2331 "bar_chart"
2332 );
2333 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2334 }
2335
2336 #[test]
2340 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2341 let mut doc = DoclingDocument::new("t");
2342 doc.push(Node::PageInfo {
2343 page_no: 1,
2344 width: 12192000.0,
2345 height: 6858000.0,
2346 });
2347 doc.push(Node::Furniture {
2348 layer: ContentLayer::Notes,
2349 inner: Box::new(Node::Located {
2350 location: [0, 0, 0, 0],
2351 inner: Box::new(Node::Paragraph {
2352 text: "a note".into(),
2353 }),
2354 }),
2355 });
2356 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2357 let prov = &v["texts"][0]["prov"][0];
2358 assert_eq!(prov["page_no"], 1);
2359 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2360 assert_eq!(
2361 prov["bbox"],
2362 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2363 );
2364 assert_eq!(
2366 v["pages"]["1"]["size"],
2367 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2368 );
2369 }
2370
2371 #[test]
2377 fn page_markers_produce_pages_and_prov() {
2378 let mut doc = DoclingDocument::new("t");
2379 doc.push(Node::PageInfo {
2380 page_no: 1,
2381 width: 512.0,
2382 height: 1024.0,
2383 });
2384 doc.push(Node::Located {
2385 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
2387 text: "hello".into(),
2388 }),
2389 });
2390 doc.push(Node::Table(Table {
2391 rows: vec![vec!["a".into()]],
2392 location: Some([0, 0, 512, 512]),
2393 ..Table::default()
2394 }));
2395 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2396 assert_eq!(v["pages"]["1"]["page_no"], 1);
2397 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
2398 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
2399 let prov = &v["texts"][0]["prov"][0];
2402 assert_eq!(prov["page_no"], 1);
2403 assert_eq!(prov["bbox"]["l"], 128.0);
2404 assert_eq!(prov["bbox"]["t"], 896.0);
2405 assert_eq!(prov["bbox"]["r"], 256.0);
2406 assert_eq!(prov["bbox"]["b"], 768.0);
2407 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
2408 assert_eq!(prov["charspan"][1], 5);
2409 let tprov = &v["tables"][0]["prov"][0];
2411 assert_eq!(tprov["bbox"]["t"], 1024.0);
2412 assert_eq!(tprov["bbox"]["b"], 0.0);
2413 assert_eq!(tprov["charspan"][1], 0);
2414
2415 let mut plain = DoclingDocument::new("t");
2417 plain.push(Node::Located {
2418 location: [1, 2, 3, 4],
2419 inner: Box::new(Node::Paragraph { text: "x".into() }),
2420 });
2421 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2422 assert_eq!(v["pages"], serde_json::json!({}));
2423 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2424 }
2425
2426 #[test]
2429 fn page_furniture_becomes_furniture_layer_text() {
2430 let mut doc = DoclingDocument::new("t");
2431 doc.push(Node::PageInfo {
2432 page_no: 1,
2433 width: 512.0,
2434 height: 512.0,
2435 });
2436 doc.push(Node::PageFurniture {
2437 footer: false,
2438 location: [10, 0, 100, 20],
2439 text: "Chapter 1".into(),
2440 });
2441 doc.push(Node::Paragraph {
2442 text: "body".into(),
2443 });
2444 doc.push(Node::PageFurniture {
2445 footer: true,
2446 location: [400, 490, 500, 512],
2447 text: "1.10.2".into(),
2448 });
2449 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2450 let texts = v["texts"].as_array().unwrap();
2451 assert_eq!(texts.len(), 3);
2452 assert_eq!(texts[0]["label"], "page_header");
2453 assert_eq!(texts[0]["content_layer"], "furniture");
2454 assert_eq!(texts[0]["parent"]["$ref"], "#/body");
2455 assert_eq!(texts[0]["prov"][0]["bbox"]["l"], 10.0);
2456 assert_eq!(texts[1]["content_layer"], "body");
2457 assert_eq!(texts[1]["prov"], serde_json::json!([]));
2458 assert_eq!(texts[2]["label"], "page_footer");
2459 assert_eq!(texts[2]["text"], "1.10.2");
2460 assert_eq!(texts[2]["content_layer"], "furniture");
2461 }
2462
2463 #[test]
2464 fn picture_image_in_markdown_modes_and_json() {
2465 let doc = doc_with_image();
2466 assert!(doc.export_to_markdown().contains("<!-- image -->"));
2468 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
2470 assert!(
2471 md.contains(""),
2472 "got:\n{md}"
2473 );
2474 assert!(files.is_empty());
2475 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
2477 assert!(
2478 md.contains(""),
2479 "got:\n{md}"
2480 );
2481 assert_eq!(
2482 files,
2483 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
2484 );
2485 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2488 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
2489 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
2490 let keys: Vec<&str> = v["pictures"][0]
2491 .as_object()
2492 .unwrap()
2493 .keys()
2494 .map(String::as_str)
2495 .collect();
2496 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
2497 assert_eq!(
2498 v["pictures"][0]["image"]["uri"],
2499 "data:image/png;base64,Zm9vYmFy"
2500 );
2501 }
2502
2503 #[test]
2504 fn exports_docling_schema() {
2505 let mut doc = DoclingDocument::new("t");
2506 doc.push(Node::Heading {
2507 level: 1,
2508 text: "Title".into(),
2509 });
2510 doc.push(Node::Heading {
2511 level: 2,
2512 text: "Sec".into(),
2513 });
2514 doc.push(Node::Paragraph {
2515 text: "Body & more".into(),
2516 }); doc.push(Node::ListItem {
2518 ordered: false,
2519 number: 0,
2520 first_in_list: true,
2521 text: "one".into(),
2522 level: 0,
2523 marker: None,
2524 location: None,
2525 dclx: None,
2526 href: None,
2527 layer: None,
2528 });
2529 doc.push(Node::ListItem {
2530 ordered: false,
2531 number: 0,
2532 first_in_list: false,
2533 text: "two".into(),
2534 level: 0,
2535 marker: None,
2536 location: None,
2537 dclx: None,
2538 href: None,
2539 layer: None,
2540 });
2541 doc.push(Node::Table(Table {
2542 rows: vec![vec!["A".into(), "B".into()]],
2543 location: None,
2544 structure: None,
2545 cell_blocks: None,
2546 cells: None,
2547 caption: None,
2548 caption_parent: Default::default(),
2549 }));
2550
2551 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2552 assert_eq!(v["schema_name"], "DoclingDocument");
2553 assert_eq!(v["version"], "1.10.0");
2554 assert_eq!(v["texts"][0]["label"], "title");
2555 assert_eq!(v["texts"][1]["label"], "section_header");
2556 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
2560 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
2561 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
2562 assert_eq!(v["texts"][3]["marker"], "-");
2563 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
2565 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
2566 }
2567 #[test]
2571 fn a_layered_group_stamps_its_whole_subtree() {
2572 let doc = DoclingDocument {
2573 name: "s".into(),
2574 nodes: vec![
2575 Node::Group {
2576 label: "sheet".into(),
2577 name: Some("Sheet1".into()),
2578 layer: None,
2579 children: vec![Node::Paragraph {
2580 text: "visible".into(),
2581 }],
2582 },
2583 Node::Group {
2584 label: "sheet".into(),
2585 name: Some("Sheet2".into()),
2586 layer: Some(ContentLayer::Invisible),
2587 children: vec![Node::Paragraph {
2588 text: "hidden".into(),
2589 }],
2590 },
2591 ],
2592 ..DoclingDocument::new("s")
2593 };
2594 let v = crate::json::to_json(&doc);
2595 assert_eq!(v["groups"][0]["label"], "sheet");
2596 assert_eq!(v["groups"][0]["name"], "Sheet1");
2597 assert_eq!(v["groups"][0]["content_layer"], "body");
2598 assert_eq!(v["texts"][0]["content_layer"], "body");
2599 assert_eq!(v["groups"][1]["name"], "Sheet2");
2600 assert_eq!(v["groups"][1]["content_layer"], "invisible");
2601 assert_eq!(v["texts"][1]["content_layer"], "invisible");
2602 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
2604 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
2605 }
2606
2607 #[test]
2617 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
2618 use crate::tree::{ItemTree, TreeKind};
2619 let mut t = ItemTree::default();
2620 let text = |txt: &str| TreeKind::Text {
2621 label: "text".into(),
2622 text: txt.into(),
2623 orig: None,
2624 formatting: None,
2625 hyperlink: None,
2626 level: None,
2627 list: None,
2628 };
2629 let a = t.add(None, None, text("a"));
2630 let blank = t.add(None, None, text(""));
2631 let b = t.add(None, None, text("b"));
2632 t.delete(blank);
2633 let group = t.add(
2634 None,
2635 Some(ContentLayer::Notes),
2636 TreeKind::Group {
2637 label: "comment_section".into(),
2638 name: "comment-0".into(),
2639 },
2640 );
2641 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
2642 t.items[a].comments.push(group);
2643 t.add(
2644 None,
2645 None,
2646 TreeKind::Picture {
2647 captions: Vec::new(),
2648 image: None,
2649 classification: Some("bar_chart".into()),
2650 confidence: None,
2651 chart: Some(Table {
2652 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
2653 ..Table::default()
2654 }),
2655 dpi: None,
2656 },
2657 );
2658 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
2659 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
2660 let mut doc = DoclingDocument::new("t");
2661 doc.tree = Some(t);
2662 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2663 let texts = v["texts"].as_array().unwrap();
2664 assert_eq!(texts.len(), 3);
2665 assert_eq!(texts[1]["text"], "b");
2666 assert_eq!(texts[1]["self_ref"], "#/texts/1");
2667 assert_eq!(
2668 v["body"]["children"],
2669 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
2670 );
2671 let keys: Vec<&str> = texts[0]
2672 .as_object()
2673 .unwrap()
2674 .keys()
2675 .map(String::as_str)
2676 .collect();
2677 assert_eq!(
2678 keys,
2679 vec![
2680 "self_ref",
2681 "parent",
2682 "children",
2683 "content_layer",
2684 "label",
2685 "prov",
2686 "comments",
2687 "orig",
2688 "text"
2689 ]
2690 );
2691 assert_eq!(
2692 texts[0]["comments"],
2693 serde_json::json!([{"$ref": "#/groups/0"}])
2694 );
2695 assert!(texts[1].get("comments").is_none());
2696 let meta = &v["pictures"][0]["meta"];
2697 assert_eq!(
2698 meta["classification"]["predictions"][0]["class_name"],
2699 "bar_chart"
2700 );
2701 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
2702 }
2703
2704 #[test]
2710 fn tree_items_carry_exact_provenance_and_dpi() {
2711 use crate::tree::{ItemTree, TreeKind, TreeProv};
2712 let text = |label: &str, t: &str| TreeKind::Text {
2713 label: label.into(),
2714 text: t.into(),
2715 orig: None,
2716 formatting: None,
2717 hyperlink: None,
2718 level: None,
2719 list: None,
2720 };
2721 let mut t = ItemTree::default();
2722 let slide = t.add(
2723 None,
2724 None,
2725 TreeKind::Group {
2726 label: "chapter".into(),
2727 name: "slide-0".into(),
2728 },
2729 );
2730 t.add_with_prov(
2731 Some(slide),
2732 None,
2733 text("paragraph", "héllo"),
2734 TreeProv {
2735 page_no: 1,
2736 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
2737 bottom_left: true,
2738 charspan: [0, 5],
2739 },
2740 );
2741 t.add_with_prov(
2742 Some(slide),
2743 None,
2744 TreeKind::Picture {
2745 captions: Vec::new(),
2746 image: Some(crate::PictureImage {
2747 dpi: crate::PictureImage::DEFAULT_DPI,
2748 mimetype: "image/png".into(),
2749 width: 2,
2750 height: 2,
2751 data: vec![0],
2752 }),
2753 classification: None,
2754 confidence: None,
2755 chart: None,
2756 dpi: Some(300),
2757 },
2758 TreeProv {
2759 page_no: 1,
2760 bbox: [0.0; 4],
2761 bottom_left: false,
2762 charspan: [0, 0],
2763 },
2764 );
2765 t.add(
2766 Some(slide),
2767 Some(ContentLayer::Notes),
2768 text("text", "no geometry"),
2769 );
2770 let mut doc = DoclingDocument::new("t");
2771 doc.push(Node::PageInfo {
2772 page_no: 1,
2773 width: 9144000.0,
2774 height: 6858000.0,
2775 });
2776 doc.tree = Some(t);
2777 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2778 assert_eq!(
2779 v["texts"][0]["prov"],
2780 serde_json::json!([{
2781 "page_no": 1,
2782 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
2783 "charspan": [0, 5],
2784 }])
2785 );
2786 assert_eq!(v["texts"][0]["label"], "paragraph");
2787 assert_eq!(
2788 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
2789 "TOPLEFT"
2790 );
2791 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
2792 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
2793 assert_eq!(v["texts"][1]["content_layer"], "notes");
2794 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
2795 assert_eq!(v["pages"]["1"]["page_no"], 1);
2796 }
2797
2798 #[test]
2803 fn provenance_boxes_are_clamped_to_their_page() {
2804 let mut doc = DoclingDocument::new("t");
2805 doc.push(Node::PageInfo {
2806 page_no: 1,
2807 width: 10.0,
2808 height: 8.0,
2809 });
2810 doc.push(Node::Prov {
2811 page_no: 1,
2812 bbox: [-1.0, 2.0, 12.0, 9.5],
2813 charspan: [0, 1],
2814 seq: None,
2815 inner: Box::new(Node::Paragraph { text: "x".into() }),
2816 });
2817 let mut table = Table {
2818 rows: vec![vec!["a".into()]],
2819 ..Table::default()
2820 };
2821 table.cells = Some(vec![crate::TableCell {
2822 text: "a".into(),
2823 bbox: Some([1.0, 1.0, 11.0, 9.0]),
2824 start_row: 0,
2825 start_col: 0,
2826 row_span: 1,
2827 col_span: 1,
2828 column_header: false,
2829 row_header: false,
2830 row_section: false,
2831 }]);
2832 doc.push(Node::Prov {
2833 page_no: 1,
2834 bbox: [0.0, 0.0, 10.0, 8.0],
2835 charspan: [0, 0],
2836 seq: None,
2837 inner: Box::new(Node::Table(table)),
2838 });
2839 doc.push(Node::Prov {
2840 page_no: 7,
2841 bbox: [-5.0, 0.0, 50.0, 50.0],
2842 charspan: [0, 1],
2843 seq: None,
2844 inner: Box::new(Node::Paragraph { text: "y".into() }),
2845 });
2846 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2847 assert_eq!(
2848 v["texts"][0]["prov"][0]["bbox"],
2849 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
2850 );
2851 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
2852 assert_eq!(
2853 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
2854 (Some(1.0), Some(10.0), Some(8.0))
2855 );
2856 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
2857 assert_eq!(
2858 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
2859 "page 7 is not described"
2860 );
2861 }
2862
2863 #[test]
2870 fn misplaced_list_items_are_wrapped_like_docling_core() {
2871 use crate::tree::{ItemTree, ListMeta, TreeKind};
2872 let mut t = ItemTree::default();
2873 let li = |txt: &str| TreeKind::Text {
2874 label: "list_item".into(),
2875 text: txt.into(),
2876 orig: None,
2877 formatting: None,
2878 hyperlink: None,
2879 level: None,
2880 list: Some(ListMeta {
2881 enumerated: false,
2882 marker: "-".into(),
2883 }),
2884 };
2885 let group = |label: &str, name: &str| TreeKind::Group {
2886 label: label.into(),
2887 name: name.into(),
2888 };
2889 t.add(None, None, li("A"));
2890 t.add(None, None, li("B"));
2891 t.add(
2892 None,
2893 None,
2894 TreeKind::Text {
2895 label: "text".into(),
2896 text: "plain".into(),
2897 orig: None,
2898 formatting: None,
2899 hyperlink: None,
2900 level: None,
2901 list: None,
2902 },
2903 );
2904 let cell = t.add(None, None, group("unspecified", "cell"));
2905 t.add(Some(cell), None, li("C"));
2906 t.add(Some(cell), None, li("D"));
2907 let list = t.add(None, None, group("list", "list"));
2908 t.add(Some(list), None, li("E"));
2909 let doc = DoclingDocument {
2910 tree: Some(t),
2911 ..DoclingDocument::new("t")
2912 };
2913 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2914 let refs = |x: &Value| -> Vec<String> {
2915 x.as_array()
2916 .unwrap()
2917 .iter()
2918 .map(|r| r["$ref"].as_str().unwrap().to_string())
2919 .collect()
2920 };
2921 assert_eq!(
2922 refs(&v["body"]["children"]),
2923 ["#/groups/4", "#/texts/0", "#/groups/0", "#/groups/1"]
2924 );
2925 assert_eq!(
2926 refs(&v["groups"][0]["children"]),
2927 ["#/groups/3", "#/groups/2"]
2928 );
2929 for (g, name, kids) in [
2930 (2, "group", vec!["#/texts/2"]),
2931 (3, "group", vec!["#/texts/3"]),
2932 (4, "group", vec!["#/texts/4", "#/texts/5"]),
2933 (1, "list", vec!["#/texts/1"]),
2934 ] {
2935 assert_eq!(v["groups"][g]["label"], "list");
2936 assert_eq!(v["groups"][g]["name"], name);
2937 assert_eq!(refs(&v["groups"][g]["children"]), kids);
2938 }
2939 let texts: Vec<&str> = v["texts"]
2940 .as_array()
2941 .unwrap()
2942 .iter()
2943 .map(|t| t["text"].as_str().unwrap())
2944 .collect();
2945 assert_eq!(texts, ["plain", "E", "D", "C", "A", "B"]);
2946 assert_eq!(v["texts"][2]["parent"]["$ref"], "#/groups/2");
2947 }
2948
2949 #[test]
2952 fn a_deleted_child_is_not_written_as_an_empty_ref() {
2953 use crate::tree::{ItemTree, TreeKind};
2954 let mut t = ItemTree::default();
2955 let text = |txt: &str| TreeKind::Text {
2956 label: "text".into(),
2957 text: txt.into(),
2958 orig: None,
2959 formatting: None,
2960 hyperlink: None,
2961 level: None,
2962 list: None,
2963 };
2964 let g = t.add(
2965 None,
2966 None,
2967 TreeKind::Group {
2968 label: "unspecified".into(),
2969 name: "g".into(),
2970 },
2971 );
2972 let gone = t.add(Some(g), None, text(""));
2973 t.add(Some(g), None, text("kept"));
2974 t.items[gone].deleted = true; let doc = DoclingDocument {
2976 tree: Some(t),
2977 ..DoclingDocument::new("t")
2978 };
2979 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2980 assert_eq!(
2981 v["groups"][0]["children"],
2982 serde_json::json!([{"$ref": "#/texts/0"}])
2983 );
2984 }
2985
2986 #[test]
2987 fn a_backend_item_tree_is_written_verbatim() {
2988 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
2989 let mut t = ItemTree::default();
2990 let text = |label: &str, txt: &str| TreeKind::Text {
2991 label: label.into(),
2992 text: txt.into(),
2993 orig: None,
2994 formatting: None,
2995 hyperlink: None,
2996 level: None,
2997 list: None,
2998 };
2999 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
3000 let h = t.add(None, None, text("title", "Heading"));
3001 let group = t.add(
3002 Some(h),
3003 None,
3004 TreeKind::Group {
3005 label: "inline".into(),
3006 name: "group".into(),
3007 },
3008 );
3009 t.add(
3010 Some(group),
3011 None,
3012 TreeKind::Text {
3013 label: "text".into(),
3014 text: "bold".into(),
3015 orig: None,
3016 formatting: Some(Formatting {
3017 bold: true,
3018 ..Formatting::default()
3019 }),
3020 hyperlink: Some("https://example.com/".into()),
3021 level: None,
3022 list: None,
3023 },
3024 );
3025 t.add(
3026 Some(group),
3027 None,
3028 TreeKind::Code {
3029 text: "x = 1".into(),
3030 orig: None,
3031 language: Some("python".into()),
3032 formatting: None,
3033 hyperlink: None,
3034 },
3035 );
3036 let sub = t.add(
3037 Some(h),
3038 None,
3039 TreeKind::Text {
3040 label: "section_header".into(),
3041 text: "Sub".into(),
3042 orig: Some("Sub\u{2019}".into()),
3043 formatting: None,
3044 hyperlink: None,
3045 level: Some(1),
3046 list: None,
3047 },
3048 );
3049 let item = t.add(
3050 Some(sub),
3051 None,
3052 TreeKind::Text {
3053 label: "list_item".into(),
3054 text: "item".into(),
3055 orig: None,
3056 formatting: None,
3057 hyperlink: None,
3058 level: None,
3059 list: Some(ListMeta {
3060 enumerated: true,
3061 marker: "3.".into(),
3062 }),
3063 },
3064 );
3065 let _region = t.add(
3066 Some(sub),
3067 None,
3068 TreeKind::FieldRegion {
3069 items: vec![crate::FieldItem {
3070 marker: None,
3071 key: Some("Name".into()),
3072 value: Some("Duck".into()),
3073 value_kind: Some("read_only".into()),
3074 }],
3075 },
3076 );
3077 let table = t.add(
3078 Some(sub),
3079 None,
3080 TreeKind::Table {
3081 table: Table {
3082 rows: vec![vec!["a \n<".into(), "b".into()]],
3083 cells: Some(vec![
3084 crate::TableCell {
3085 text: "a \n<".into(),
3086 bbox: None,
3087 start_row: 0,
3088 start_col: 0,
3089 row_span: 3,
3090 col_span: 1,
3091 column_header: false,
3092 row_header: true,
3093 row_section: false,
3094 },
3095 crate::TableCell {
3096 text: "b".into(),
3097 bbox: None,
3098 start_row: 0,
3099 start_col: 1,
3100 row_span: 1,
3101 col_span: 1,
3102 column_header: false,
3103 row_header: false,
3104 row_section: false,
3105 },
3106 ]),
3107 ..Table::default()
3108 },
3109 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
3111 },
3112 );
3113 let cell_group = t.add(
3114 Some(table),
3115 None,
3116 TreeKind::Group {
3117 label: "unspecified".into(),
3118 name: "rich_cell_group_1_0_0".into(),
3119 },
3120 );
3121 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
3122 *rich_cells = vec![(0, 1, cell_group)];
3123 }
3124 let after = t.add(Some(sub), None, text("text", "after the region"));
3125 let _ = (title, after);
3126 let list = t.add(
3130 Some(sub),
3131 None,
3132 TreeKind::Group {
3133 label: "list".into(),
3134 name: "list".into(),
3135 },
3136 );
3137 t.reparent(item, Some(list));
3138
3139 let doc = DoclingDocument {
3140 tree: Some(t),
3141 ..DoclingDocument::new("t")
3142 };
3143 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3144 let texts: Vec<&str> = v["texts"]
3146 .as_array()
3147 .unwrap()
3148 .iter()
3149 .map(|t| t["text"].as_str().unwrap())
3150 .collect();
3151 assert_eq!(
3152 texts,
3153 [
3154 "Page",
3155 "Heading",
3156 "bold",
3157 "x = 1",
3158 "Sub",
3159 "item",
3160 "Name",
3161 "Duck",
3162 "after the region"
3163 ]
3164 );
3165 assert_eq!(
3166 v["body"]["children"],
3167 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
3168 );
3169 assert_eq!(v["texts"][0]["content_layer"], "furniture");
3170 assert_eq!(
3171 v["texts"][1]["children"],
3172 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
3173 );
3174 let bold = &v["texts"][2];
3175 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
3176 let keys: Vec<&str> = bold
3177 .as_object()
3178 .unwrap()
3179 .keys()
3180 .map(String::as_str)
3181 .collect();
3182 assert_eq!(
3183 keys,
3184 [
3185 "self_ref",
3186 "parent",
3187 "children",
3188 "content_layer",
3189 "label",
3190 "prov",
3191 "orig",
3192 "text",
3193 "formatting",
3194 "hyperlink"
3195 ]
3196 );
3197 assert_eq!(
3198 bold["formatting"],
3199 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
3200 );
3201 let code = &v["texts"][3];
3202 assert_eq!(code["label"], "code");
3203 assert_eq!(code["code_language"], "Python");
3204 let sub = &v["texts"][4];
3205 assert_eq!(sub["orig"], "Sub\u{2019}");
3206 assert_eq!(sub["level"], 1);
3207 let item = &v["texts"][5];
3208 let keys: Vec<&str> = item
3209 .as_object()
3210 .unwrap()
3211 .keys()
3212 .map(String::as_str)
3213 .collect();
3214 assert_eq!(
3215 keys,
3216 [
3217 "self_ref",
3218 "parent",
3219 "children",
3220 "content_layer",
3221 "label",
3222 "prov",
3223 "orig",
3224 "text",
3225 "enumerated",
3226 "marker"
3227 ]
3228 );
3229 assert_eq!(item["marker"], "3.");
3230 assert_eq!(v["texts"][7]["kind"], "read_only");
3231 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
3232 let table = &v["tables"][0];
3233 assert_eq!(
3234 table["children"],
3235 serde_json::json!([{"$ref": "#/groups/1"}])
3236 );
3237 let cells = table["data"]["table_cells"].as_array().unwrap();
3238 assert_eq!(
3239 cells[0]["text"], "a \n<",
3240 "raw cell text is written verbatim"
3241 );
3242 assert_eq!(
3243 cells[0]["end_row_offset_idx"], 3,
3244 "declared spans are not clamped"
3245 );
3246 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
3247 assert!(cells[0].get("ref").is_none());
3248 assert!(
3249 table["data"]["grid"][0][1].get("ref").is_none(),
3250 "the grid shows plain cells"
3251 );
3252 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
3253 }
3254
3255 #[test]
3259 fn a_comment_section_can_be_referenced_by_its_note_text() {
3260 let doc = DoclingDocument {
3261 name: "c".into(),
3262 nodes: vec![
3263 Node::Commented {
3264 comments: vec![0],
3265 inner: Box::new(Node::Paragraph {
3266 text: "annotated".into(),
3267 }),
3268 },
3269 Node::CommentSection {
3270 name: "comment-Sheet1-A1".into(),
3271 text: "[author: A]: note".into(),
3272 refs_note_text: true,
3273 grouped: true,
3274 },
3275 ],
3276 ..DoclingDocument::new("c")
3277 };
3278 let v = crate::json::to_json(&doc);
3279 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
3280 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
3281 }
3282
3283 #[test]
3287 fn comment_sections_link_back_to_their_items() {
3288 let doc = DoclingDocument {
3289 name: "c".into(),
3290 nodes: vec![
3291 Node::Commented {
3292 comments: vec![0],
3293 inner: Box::new(Node::Paragraph {
3294 text: "annotated".into(),
3295 }),
3296 },
3297 Node::Paragraph {
3298 text: "plain".into(),
3299 },
3300 Node::CommentSection {
3301 name: "comment-7".into(),
3302 text: "[time: t]: note".into(),
3303 refs_note_text: false,
3304 grouped: true,
3305 },
3306 ],
3307 ..DoclingDocument::new("c")
3308 };
3309 let v = crate::json::to_json(&doc);
3310 assert_eq!(v["groups"][0]["label"], "comment_section");
3312 assert_eq!(v["groups"][0]["name"], "comment-7");
3313 assert_eq!(v["groups"][0]["content_layer"], "notes");
3314 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
3315 assert_eq!(v["texts"][2]["content_layer"], "notes");
3316 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
3318 assert!(v["texts"][1].get("comments").is_none());
3319 let keys: Vec<&str> = v["texts"][0]
3321 .as_object()
3322 .unwrap()
3323 .keys()
3324 .map(String::as_str)
3325 .collect();
3326 assert_eq!(
3327 &keys[keys.len() - 4..],
3328 &["prov", "comments", "orig", "text"]
3329 );
3330 }
3331
3332 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
3333 Node::Picture {
3334 caption: Some(caption.into()),
3335 caption_href: None,
3336 image: None,
3337 classification: None,
3338 caption_parent,
3339 }
3340 }
3341
3342 fn group(children: Vec<Node>) -> Node {
3343 Node::Group {
3344 label: "section".into(),
3345 name: None,
3346 layer: None,
3347 children,
3348 }
3349 }
3350
3351 fn refs(v: &Value) -> Vec<&str> {
3352 v.as_array()
3353 .unwrap()
3354 .iter()
3355 .map(|r| r["$ref"].as_str().unwrap())
3356 .collect()
3357 }
3358
3359 #[test]
3364 fn a_body_caption_follows_the_enclosing_top_level_item() {
3365 let mut doc = DoclingDocument::new("t");
3366 doc.push(picture("top", CaptionParent::Body));
3367 doc.push(group(vec![
3368 Node::Paragraph { text: "p".into() },
3369 picture("nested", CaptionParent::Body),
3370 ]));
3371 doc.push(Node::Paragraph {
3372 text: "after".into(),
3373 });
3374 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3375 assert_eq!(
3376 refs(&v["body"]["children"]),
3377 [
3378 "#/texts/0",
3379 "#/pictures/0",
3380 "#/groups/0",
3381 "#/texts/2",
3382 "#/texts/3"
3383 ]
3384 );
3385 assert_eq!(
3386 refs(&v["groups"][0]["children"]),
3387 ["#/texts/1", "#/pictures/1"]
3388 );
3389 for (cap, pic) in [(0, 0), (2, 1)] {
3390 assert_eq!(v["texts"][cap]["label"], "caption");
3391 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
3392 assert_eq!(
3393 refs(&v["pictures"][pic]["captions"]),
3394 [format!("#/texts/{cap}")]
3395 );
3396 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
3397 }
3398 }
3399
3400 #[test]
3403 fn an_item_caption_is_the_items_first_child() {
3404 let mut doc = DoclingDocument::new("t");
3405 doc.push(picture("fig", CaptionParent::Item));
3406 doc.push(Node::Table(Table {
3407 rows: vec![vec!["a".into()]],
3408 caption: Some("tab".into()),
3409 caption_parent: CaptionParent::Item,
3410 ..Table::default()
3411 }));
3412 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3413 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
3414 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
3415 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
3416 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3417 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
3418 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
3419 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
3420 }
3421
3422 #[test]
3428 fn picture_children_hang_off_the_picture_after_its_caption() {
3429 let mut doc = DoclingDocument::new("t");
3430 doc.push(picture("fig", CaptionParent::Item));
3431 doc.push(Node::PictureChildren(vec![
3432 Node::Heading {
3433 level: 2,
3434 text: "in-figure title".into(),
3435 },
3436 Node::Paragraph {
3437 text: "axis label".into(),
3438 },
3439 Node::ListItem {
3440 ordered: false,
3441 number: 0,
3442 first_in_list: true,
3443 text: "callout".into(),
3444 level: 0,
3445 marker: None,
3446 location: None,
3447 dclx: None,
3448 href: None,
3449 layer: None,
3450 },
3451 ]));
3452 doc.push(Node::Paragraph {
3453 text: "after".into(),
3454 });
3455 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3456 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/texts/4"]);
3457 assert_eq!(
3458 refs(&v["pictures"][0]["children"]),
3459 ["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"]
3460 );
3461 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3462 assert_eq!(v["texts"][1]["label"], "section_header");
3463 assert_eq!(v["texts"][2]["label"], "text");
3464 for t in 1..=2 {
3465 assert_eq!(v["texts"][t]["parent"]["$ref"], "#/pictures/0");
3466 assert_eq!(v["texts"][t]["content_layer"], "body");
3467 }
3468 assert_eq!(v["groups"][0]["parent"]["$ref"], "#/pictures/0");
3469 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
3470 let md = doc.export_to_markdown();
3471 assert!(
3472 !md.contains("axis label") && !md.contains("callout"),
3473 "{md}"
3474 );
3475 assert!(md.contains("after"), "{md}");
3476 }
3477
3478 #[test]
3482 fn a_container_caption_is_the_items_sibling() {
3483 let mut doc = DoclingDocument::new("t");
3484 doc.push(group(vec![
3485 picture("chart", CaptionParent::Container),
3486 Node::Table(Table {
3487 rows: vec![vec!["a".into()]],
3488 caption: Some("figcaption".into()),
3489 caption_parent: CaptionParent::ContainerAfter,
3490 ..Table::default()
3491 }),
3492 ]));
3493 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3494 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
3495 assert_eq!(
3496 refs(&v["groups"][0]["children"]),
3497 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
3498 );
3499 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
3500 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
3501 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
3502 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
3503 }
3504}