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 {
237 json!({
238 "mimetype": img.mimetype,
239 "dpi": img.dpi,
240 "size": { "width": img.width as f64, "height": img.height as f64 },
241 "uri": img.data_uri(),
242 })
243}
244
245pub fn to_json(doc: &DoclingDocument) -> Value {
247 let mut b = Builder::default();
248 let body = match &doc.tree {
252 Some(tree) => {
253 for n in &doc.nodes {
256 if let Node::PageInfo {
257 page_no,
258 width,
259 height,
260 } = n
261 {
262 if *page_no > 0 {
263 b.pages.push((*page_no, *width as f64, *height as f64));
264 }
265 }
266 }
267 b.write_tree(tree)
268 }
269 None => b.walk_into(&doc.nodes, "#/body"),
270 };
271 b.link_comments();
272
273 let mut out = json!({
274 "schema_name": "DoclingDocument",
275 "version": SCHEMA_VERSION,
276 "name": doc.name,
277 "origin": {
278 "mimetype": "text/plain",
279 "binary_hash": fnv1a(&doc.name),
280 "filename": doc.name,
281 },
282 "furniture": {
283 "self_ref": "#/furniture",
284 "children": [],
285 "content_layer": "furniture",
286 "name": "_root_",
287 "label": "unspecified",
288 },
289 "body": {
290 "self_ref": "#/body",
291 "children": body,
292 "content_layer": "body",
293 "name": "_root_",
294 "label": "unspecified",
295 },
296 "groups": b.groups,
297 "texts": b.texts,
298 "pictures": b.pictures,
299 "tables": b.tables,
300 "key_value_items": b.key_value_items,
301 "form_items": [],
302 "pages": b.pages.iter().map(|(n, w, h)| {
303 let r2 = |v: f64| (v * 100.0).round() / 100.0;
304 let mut page = serde_json::Map::new();
307 page.insert("size".into(), json!({ "width": r2(*w), "height": r2(*h) }));
308 if let Some(img) = doc.page_images.get(n) {
309 page.insert("image".into(), image_ref_json(img));
310 }
311 page.insert("page_no".into(), json!(n));
312 (n.to_string(), Value::Object(page))
313 }).collect::<serde_json::Map<String, Value>>(),
314 });
315
316 clamp_boxes_to_pages(&mut out, &b.pages);
325
326 if !b.field_regions.is_empty() {
331 if let Some(obj) = out.as_object_mut() {
332 let pages = obj.remove("pages");
333 obj.insert("field_regions".into(), Value::Array(b.field_regions));
334 obj.insert("field_items".into(), Value::Array(b.field_items));
335 if let Some(pages) = pages {
336 obj.insert("pages".into(), pages);
337 }
338 }
339 }
340 out
341}
342
343fn classification_meta(classes: &[crate::PictureClass]) -> Value {
350 json!({
351 "classification": {
352 "predictions": classes.iter().map(|c| json!({
353 "confidence": c.confidence as f64,
354 "created_by": "DocumentPictureClassifier",
355 "class_name": c.class_name,
356 })).collect::<Vec<_>>(),
357 },
358 "annotations": [{
359 "kind": "classification",
360 "provenance": "DocumentPictureClassifier",
361 "predicted_classes": classes.iter().map(|c| json!({
362 "class_name": c.class_name,
363 "confidence": c.confidence as f64,
364 })).collect::<Vec<_>>(),
365 }],
366 })
367}
368
369#[allow(clippy::too_many_arguments)]
380fn cell_value(
381 row_span: usize,
382 col_span: usize,
383 start_row: usize,
384 end_row: usize,
385 start_col: usize,
386 end_col: usize,
387 text: String,
388 column_header: bool,
389 row_header: bool,
390 row_section: bool,
391 bbox: Option<[f32; 4]>,
392) -> Value {
393 let mut m = serde_json::Map::with_capacity(12);
394 m.insert("row_span".into(), row_span.into());
395 m.insert("col_span".into(), col_span.into());
396 m.insert("start_row_offset_idx".into(), start_row.into());
397 m.insert("end_row_offset_idx".into(), end_row.into());
398 m.insert("start_col_offset_idx".into(), start_col.into());
399 m.insert("end_col_offset_idx".into(), end_col.into());
400 m.insert("text".into(), Value::String(text));
401 m.insert("column_header".into(), column_header.into());
402 m.insert("row_header".into(), row_header.into());
403 m.insert("row_section".into(), row_section.into());
404 m.insert("fillable".into(), false.into());
405 if let Some(b) = bbox {
406 m.insert(
407 "bbox".into(),
408 json!({
409 "l": b[0], "t": b[1], "r": b[2], "b": b[3],
410 "coord_origin": "TOPLEFT",
411 }),
412 );
413 }
414 Value::Object(m)
415}
416
417fn table_data(t: &Table) -> Value {
420 table_data_with(t, false)
421}
422
423fn table_data_with(t: &Table, raw: bool) -> Value {
426 let cell_text = |s: &str| {
427 if raw {
428 s.to_string()
429 } else {
430 unescape_text(&crate::markdown::strip_hard_breaks(s))
431 }
432 };
433 let mut num_rows = t.rows.len();
434 let mut num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
435 let ragged = t.rows.iter().any(|r| r.len() != num_cols);
443 if let Some(first_class) = t.cells.as_ref().filter(|c| ragged && !c.is_empty()) {
444 for c in first_class {
445 num_rows = num_rows.max(c.start_row + c.row_span);
446 num_cols = num_cols.max(c.start_col + c.col_span);
447 }
448 }
449 let mut grid = Vec::with_capacity(num_rows);
450 let mut cells = Vec::new();
451 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
455 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
456 for c in first_class {
457 let idx = cells.len();
458 cells.push(cell_value(
459 c.row_span,
460 c.col_span,
461 c.start_row,
462 c.start_row + c.row_span,
463 c.start_col,
464 c.start_col + c.col_span,
465 cell_text(&c.text),
466 c.column_header,
467 c.row_header,
468 c.row_section,
469 c.bbox,
470 ));
471 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
472 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
473 slot[r * num_cols + k] = Some(idx);
474 }
475 }
476 }
477 for r in 0..num_rows {
478 let mut grid_row = Vec::with_capacity(num_cols);
479 for c in 0..num_cols {
480 grid_row.push(match slot[r * num_cols + c] {
481 Some(i) => cells[i].clone(),
482 None => cell_value(
483 1,
484 1,
485 r,
486 r + 1,
487 c,
488 c + 1,
489 String::new(),
490 false,
491 false,
492 false,
493 None,
494 ),
495 });
496 }
497 grid.push(grid_row);
498 }
499 } else {
500 let s = t.structure.as_ref();
501 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
502 grid.and_then(|g| g.get(r))
503 .and_then(|row| row.get(c))
504 .copied()
505 .unwrap_or(false)
506 };
507 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
508 let (mut r0, mut c0) = (r, c);
509 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
510 c0 -= 1;
511 }
512 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
513 r0 -= 1;
514 }
515 (r0, c0)
516 };
517 let anchors: Vec<(usize, usize)> = (0..num_rows)
520 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
521 .map(|(r, c)| anchor_of(r, c))
522 .collect();
523 let mut extent: Vec<(usize, usize)> = (0..num_rows)
524 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
525 .collect();
526 for (i, &(ar, ac)) in anchors.iter().enumerate() {
527 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
528 let e = &mut extent[ar * num_cols + ac];
529 e.0 = e.0.max(r);
530 e.1 = e.1.max(c);
531 }
532 for (r, row) in t.rows.iter().enumerate() {
533 let mut grid_row = Vec::with_capacity(num_cols);
534 for c in 0..num_cols {
535 let (ar, ac) = anchors[r * num_cols + c];
536 if (ar, ac) == (r, c) {
537 let (er, ec) = extent[r * num_cols + c];
538 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
539 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
540 Some(s) => flag(Some(&s.col_header), r, c),
541 None => r == 0,
542 };
543 slot[r * num_cols + c] = Some(cells.len());
544 cells.push(cell_value(
545 er - r + 1,
546 ec - c + 1,
547 r,
548 er + 1,
549 c,
550 ec + 1,
551 text,
552 column_header,
553 flag(s.map(|s| &s.row_header), r, c),
554 false,
555 None,
556 ));
557 }
558 grid_row.push(match slot[ar * num_cols + ac] {
559 Some(i) => cells[i].clone(),
560 None => Value::Null,
561 });
562 }
563 grid.push(grid_row);
564 }
565 }
566 json!({
567 "table_cells": cells,
568 "num_rows": num_rows,
569 "num_cols": num_cols,
570 "orientation": "rot_0",
571 "grid": grid,
572 })
573}
574
575#[derive(Default)]
576struct Builder {
577 texts: Vec<Value>,
578 groups: Vec<Value>,
579 tables: Vec<Value>,
580 pictures: Vec<Value>,
581 field_regions: Vec<Value>,
582 field_items: Vec<Value>,
583 key_value_items: Vec<Value>,
584 pages: Vec<(usize, f64, f64)>,
588 cur_page: usize,
590 cur_w: f64,
591 cur_h: f64,
592 pending_loc: Option<[u16; 4]>,
595 pending_exact: Option<ExactProv>,
598 pending_siblings: Vec<Value>,
602 pending_after: Vec<Value>,
606 pending_body: Vec<Value>,
610 comment_groups: Vec<String>,
614 last_comment_section: Option<(String, String)>,
619 pending_comments: Vec<(String, Vec<usize>)>,
623}
624
625impl Builder {
626 fn take_prov(&mut self, char_len: usize) -> Value {
632 let prov = self.prov_json(char_len, false);
633 self.pending_exact = None;
634 self.pending_loc = None;
635 prov
636 }
637
638 fn prov_json(&self, char_len: usize, span_over_text: bool) -> Value {
643 let r2 = |v: f64| (v * 100.0).round() / 100.0;
644 if let Some(ExactProv {
645 page_no,
646 bbox: [l, t, r, b],
647 bottom_left,
648 charspan,
649 }) = self.pending_exact
650 {
651 let charspan = if span_over_text {
652 [0, char_len]
653 } else {
654 charspan
655 };
656 return json!([{
657 "page_no": page_no,
658 "bbox": {
659 "l": r2(l), "t": r2(t), "r": r2(r), "b": r2(b),
660 "coord_origin": if bottom_left { "BOTTOMLEFT" } else { "TOPLEFT" },
661 },
662 "charspan": charspan,
663 }]);
664 }
665 let Some([x0, y0, x1, y1]) = self.pending_loc else {
666 return json!([]);
667 };
668 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
672 return json!([{
673 "page_no": self.cur_page,
674 "bbox": { "l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT" },
675 "charspan": [0, char_len],
676 }]);
677 }
678 json!([{
679 "page_no": self.cur_page,
680 "bbox": {
681 "l": r2(x0 as f64 * self.cur_w / 512.0),
682 "t": r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
683 "r": r2(x1 as f64 * self.cur_w / 512.0),
684 "b": r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
685 "coord_origin": "BOTTOMLEFT",
686 },
687 "charspan": [0, char_len],
688 }])
689 }
690
691 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
694 if self.pending_loc.is_none() && self.cur_page > 0 {
695 self.pending_loc = loc;
696 }
697 }
698
699 fn link_comments(&mut self) {
707 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
708 .into_iter()
709 .map(|(item, comments)| {
710 let refs = comments
711 .iter()
712 .filter_map(|i| self.comment_groups.get(*i))
713 .map(|r| json!({ "$ref": r }))
714 .collect();
715 (item, refs)
716 })
717 .collect();
718 for (item, comment_refs) in refs {
719 if comment_refs.is_empty() {
720 continue;
721 }
722 let Some(target) = self.item_mut(&item) else {
723 continue;
724 };
725 let Some(obj) = target.as_object_mut() else {
726 continue;
727 };
728 let tail: Vec<(String, Value)> = obj
729 .iter()
730 .skip_while(|(k, _)| k.as_str() != "prov")
731 .skip(1)
732 .map(|(k, v)| (k.clone(), v.clone()))
733 .collect();
734 for (k, _) in &tail {
735 obj.shift_remove(k);
736 }
737 obj.insert("comments".into(), Value::Array(comment_refs));
738 for (k, v) in tail {
739 obj.insert(k, v);
740 }
741 }
742 }
743
744 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
746 let idx = ref_index(self_ref)?;
747 let bucket = if self_ref.starts_with("#/texts/") {
748 &mut self.texts
749 } else if self_ref.starts_with("#/tables/") {
750 &mut self.tables
751 } else if self_ref.starts_with("#/pictures/") {
752 &mut self.pictures
753 } else if self_ref.starts_with("#/groups/") {
754 &mut self.groups
755 } else {
756 return None;
757 };
758 bucket.get_mut(idx)
759 }
760
761 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
765 use crate::tree::TreeKind;
766 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
769 let (mut nt, mut ng, mut ntb, mut np, mut nf, mut nk) = (0, 0, 0, 0, 0, 0);
770 for item in &tree.items {
771 if item.deleted {
772 refs.push(String::new());
773 continue;
774 }
775 let r = match &item.kind {
776 TreeKind::Text { .. } | TreeKind::Code { .. } => {
777 nt += 1;
778 format!("#/texts/{}", nt - 1)
779 }
780 TreeKind::Group { .. } => {
781 ng += 1;
782 format!("#/groups/{}", ng - 1)
783 }
784 TreeKind::Table { .. } => {
785 ntb += 1;
786 format!("#/tables/{}", ntb - 1)
787 }
788 TreeKind::Picture { .. } => {
789 np += 1;
790 format!("#/pictures/{}", np - 1)
791 }
792 TreeKind::FieldRegion { items } => {
793 nt += items
796 .iter()
797 .map(|i| {
798 [&i.marker, &i.key, &i.value]
799 .iter()
800 .filter(|p| p.is_some())
801 .count()
802 })
803 .sum::<usize>();
804 nf += 1;
805 format!("#/field_regions/{}", nf - 1)
806 }
807 TreeKind::KeyValueGraph { .. } => {
808 nk += 1;
809 format!("#/key_value_items/{}", nk - 1)
810 }
811 };
812 refs.push(r);
813 }
814 let ref_of = |id: usize| json!({ "$ref": refs[id] });
815 for (id, item) in tree.items.iter().enumerate() {
816 if item.deleted {
817 continue;
818 }
819 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
820 let children: Vec<Value> = item.children.iter().map(|&c| ref_of(c)).collect();
821 let layer = item.layer.map_or("body", |l| l.value());
822 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
825 let self_ref = match &item.kind {
826 TreeKind::Text {
827 label,
828 text,
829 orig,
830 formatting,
831 hyperlink,
832 level,
833 list,
834 } => {
835 let mut tail = serde_json::Map::new();
838 if let Some(f) = formatting {
839 tail.insert("formatting".into(), formatting_json(f));
840 }
841 if let Some(h) = hyperlink {
842 tail.insert("hyperlink".into(), json!(h));
843 }
844 if let Some(l) = level {
845 tail.insert("level".into(), json!(l));
846 }
847 if let Some(l) = list {
848 tail.insert("enumerated".into(), json!(l.enumerated));
849 tail.insert("marker".into(), json!(l.marker));
850 }
851 let r = format!("#/texts/{}", self.texts.len());
852 let prov = self.take_prov(text.chars().count());
853 let mut item_json = json!({
854 "self_ref": r,
855 "parent": { "$ref": parent },
856 "children": children,
857 "content_layer": layer,
858 "label": label,
859 "prov": prov,
860 });
861 if !item.comments.is_empty() {
864 item_json["comments"] =
865 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
866 }
867 if let Some(track) = &item.source {
870 item_json["source"] = json!([track_json(track)]);
871 }
872 merge(
873 &mut item_json,
874 json!({
875 "orig": orig.as_deref().unwrap_or(text),
876 "text": text,
877 }),
878 );
879 merge(&mut item_json, Value::Object(tail));
880 self.texts.push(item_json);
881 r
882 }
883 TreeKind::Code {
884 text,
885 orig,
886 language,
887 formatting,
888 hyperlink,
889 } => {
890 let r = format!("#/texts/{}", self.texts.len());
891 let prov = self.take_prov(text.chars().count());
892 let mut item_json = json!({
893 "self_ref": r,
894 "parent": { "$ref": parent },
895 "children": children,
896 "content_layer": layer,
897 "label": "code",
898 "prov": prov,
899 });
900 if !item.comments.is_empty() {
901 item_json["comments"] =
902 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
903 }
904 merge(
905 &mut item_json,
906 json!({
907 "orig": orig.as_deref().unwrap_or(text),
908 "text": text,
909 }),
910 );
911 if let Some(f) = formatting {
912 item_json["formatting"] = formatting_json(f);
913 }
914 if let Some(h) = hyperlink {
915 item_json["hyperlink"] = json!(h);
916 }
917 merge(
918 &mut item_json,
919 json!({
920 "captions": [],
921 "references": [],
922 "footnotes": [],
923 "code_language": code_language(language.as_deref()),
924 }),
925 );
926 self.texts.push(item_json);
927 r
928 }
929 TreeKind::Group { label, name } => {
930 self.pending_exact = None;
931 let r = format!("#/groups/{}", self.groups.len());
932 self.groups.push(json!({
933 "self_ref": r,
934 "parent": { "$ref": parent },
935 "children": children,
936 "content_layer": layer,
937 "name": name,
938 "label": label,
939 }));
940 r
941 }
942 TreeKind::Table {
943 table,
944 rich_cells,
945 captions,
946 } => {
947 let r = self.add_table_with(table, parent, true);
949 let idx = ref_index(&r).expect("table ref");
950 let t = &mut self.tables[idx];
951 t["children"] = Value::Array(children);
952 t["content_layer"] = json!(layer);
953 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
954 for &(row, col, group) in rich_cells {
958 let cell_ref = ref_of(group);
959 let hit = |c: &Value| {
960 c["start_row_offset_idx"] == json!(row)
961 && c["start_col_offset_idx"] == json!(col)
962 };
963 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
964 for c in cells.iter_mut().filter(|c| hit(c)) {
965 c["ref"] = cell_ref.clone();
966 }
967 }
968 }
969 r
970 }
971 TreeKind::Picture {
972 captions,
973 image,
974 classification,
975 confidence,
976 chart,
977 dpi,
978 } => {
979 let mut meta = classification.as_ref().map(|c| {
984 let mut pred = serde_json::Map::new();
985 if let Some(conf) = confidence {
986 pred.insert("confidence".into(), json!(conf));
987 }
988 pred.insert("class_name".into(), json!(c));
989 json!({ "classification": { "predictions": [pred] } })
990 });
991 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
992 if !t.rows.is_empty() {
993 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
994 }
995 }
996 let prov = self.take_prov(0);
997 let r = self.push_picture(
998 prov,
999 captions.iter().map(|&c| ref_of(c)).collect(),
1000 children,
1001 image.as_ref(),
1002 meta,
1003 parent,
1004 );
1005 if let Some(idx) = ref_index(&r) {
1006 self.pictures[idx]["content_layer"] = json!(layer);
1007 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
1010 img["dpi"] = json!(dpi);
1011 }
1012 }
1013 r
1014 }
1015 TreeKind::FieldRegion { items } => {
1016 self.pending_exact = None;
1017 let r = self.add_field_region(items, parent);
1018 if let Some(region) = self.field_regions.last_mut() {
1019 region["content_layer"] = json!(layer);
1020 }
1021 r
1022 }
1023 TreeKind::KeyValueGraph { cells, links } => {
1024 self.pending_exact = None;
1025 let r = self.add_key_value_graph(cells, links, parent);
1026 if let Some(item) = self.key_value_items.last_mut() {
1027 item["content_layer"] = json!(layer);
1028 }
1029 r
1030 }
1031 };
1032 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1033 }
1034 tree.body.iter().map(|&c| ref_of(c)).collect()
1035 }
1036
1037 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1038 match node {
1039 Node::Heading { level: 1, text } => {
1040 Some(self.add_text("title", text, parent, json!({})))
1041 }
1042 Node::Heading { level, text } => Some(self.add_text(
1043 "section_header",
1044 text,
1045 parent,
1046 json!({ "level": level.saturating_sub(1) }),
1047 )),
1048 Node::Caption { text, href } => {
1049 let extra = match href {
1050 Some(url) => json!({ "hyperlink": url }),
1051 None => json!({}),
1052 };
1053 Some(self.add_text("caption", text, parent, extra))
1054 }
1055 Node::Paragraph { text } => {
1056 let t = text.trim();
1059 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1060 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1061 _ => Some(self.add_text("text", text, parent, json!({}))),
1062 }
1063 }
1064 Node::CheckboxItem { checked, text } => {
1065 let mark = if *checked { "- [x] " } else { "- [ ] " };
1068 Some(self.add_text("text", &format!("{mark}{text}"), parent, json!({})))
1069 }
1070 Node::Code {
1071 language,
1072 text,
1073 orig,
1074 ..
1075 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1076 Node::Formula {
1079 latex,
1080 orig,
1081 location,
1082 } => {
1083 self.adopt_loc(*location);
1084 Some(self.add_formula_item(latex, orig, parent))
1085 }
1086 Node::CommentSection {
1092 name,
1093 text,
1094 refs_note_text,
1095 grouped,
1096 } => {
1097 if !*grouped {
1098 let child =
1102 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1103 self.comment_groups.push(child.clone());
1104 return Some(child);
1105 }
1106 if let Some((_, self_ref)) = self
1107 .last_comment_section
1108 .as_ref()
1109 .filter(|(n, _)| n == name)
1110 .cloned()
1111 {
1112 let child =
1115 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1116 if let Some(children) =
1117 self.groups[group_index(&self_ref)]["children"].as_array_mut()
1118 {
1119 children.push(json!({ "$ref": child }));
1120 }
1121 self.comment_groups.push(if *refs_note_text {
1122 child
1123 } else {
1124 self_ref.clone()
1125 });
1126 return Some(self_ref);
1127 }
1128 let self_ref = format!("#/groups/{}", self.groups.len());
1129 self.groups.push(Value::Null);
1130 let child =
1131 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1132 self.groups[group_index(&self_ref)] = json!({
1133 "self_ref": self_ref,
1134 "parent": { "$ref": parent },
1135 "children": [{ "$ref": child }],
1136 "content_layer": "notes",
1137 "name": name,
1138 "label": "comment_section",
1139 });
1140 self.last_comment_section = Some((name.clone(), self_ref.clone()));
1141 self.comment_groups.push(if *refs_note_text {
1142 child
1143 } else {
1144 self_ref.clone()
1145 });
1146 Some(self_ref)
1147 }
1148 Node::Commented { comments, inner } => {
1151 let item = self.add_node(inner, parent)?;
1152 if !comments.is_empty() {
1153 self.pending_comments.push((item.clone(), comments.clone()));
1154 }
1155 Some(item)
1156 }
1157 Node::Table(t) => Some(self.add_table(t, parent)),
1158 Node::Picture {
1159 caption,
1160 caption_href,
1161 image,
1162 classification,
1163 caption_parent,
1164 } => Some(self.add_picture(
1165 caption.as_deref(),
1166 caption_href.as_deref(),
1167 image.as_ref(),
1168 classification.as_deref().map(classification_meta),
1169 parent,
1170 *caption_parent,
1171 )),
1172 Node::Chart {
1177 kind,
1178 table,
1179 caption,
1180 location,
1181 } => {
1182 self.adopt_loc(*location);
1183 let mut meta = json!({
1184 "classification": { "predictions": [{ "class_name": kind }] },
1185 });
1186 if !table.rows.is_empty() {
1187 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1188 }
1189 let mut captions = Vec::new();
1195 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1196 let prov = self.prov_json(unescape_text(cap).chars().count(), true);
1197 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1198 self.pending_siblings.push(json!({ "$ref": cap_ref }));
1199 captions.push(json!({ "$ref": cap_ref }));
1200 }
1201 let prov = self.take_prov(0);
1202 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1203 }
1204 Node::DoclangOnly(_) => None,
1206 Node::Group {
1207 label,
1208 name,
1209 layer,
1210 children,
1211 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1212 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1213 Node::KeyValueGraph { cells, links } => {
1214 Some(self.add_key_value_graph(cells, links, parent))
1215 }
1216 Node::InlineGroup { md_text, .. } => {
1219 Some(self.add_text("text", md_text, parent, json!({})))
1220 }
1221 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1223 Node::Furniture {
1229 layer: ContentLayer::Notes,
1230 inner,
1231 } => {
1232 let item = self.add_node(inner, parent)?;
1233 self.set_layer(&item, "notes");
1234 Some(item)
1235 }
1236 Node::Furniture { .. } => None,
1237 Node::PageFurniture {
1241 footer,
1242 location,
1243 text,
1244 } => {
1245 if self.cur_page > 0 {
1246 self.pending_loc = Some(*location);
1247 }
1248 let label = if *footer {
1249 "page_footer"
1250 } else {
1251 "page_header"
1252 };
1253 let item = self.add_text(label, text, parent, json!({}));
1254 self.pending_loc = None;
1255 self.set_layer(&item, "furniture");
1256 Some(item)
1257 }
1258 Node::PictureChildren(children) => {
1262 let pic = self.pictures.len().checked_sub(1)?;
1263 let pic_ref = format!("#/pictures/{pic}");
1264 let refs = self.walk_into(children, &pic_ref);
1265 if let Some(list) = self.pictures[pic]["children"].as_array_mut() {
1266 list.extend(refs);
1267 }
1268 None
1269 }
1270 Node::Located { location, inner } => {
1275 if self.cur_page > 0 {
1276 self.pending_loc = Some(*location);
1277 }
1278 let r = self.add_node(inner, parent);
1279 self.pending_loc = None;
1280 r
1281 }
1282 Node::Prov {
1283 page_no,
1284 bbox,
1285 charspan,
1286 inner,
1287 ..
1288 } => {
1289 self.pending_exact = Some(ExactProv {
1290 page_no: *page_no,
1291 bbox: bbox.map(f64::from),
1292 bottom_left: false,
1293 charspan: *charspan,
1294 });
1295 let r = self.add_node(inner, parent);
1296 self.pending_exact = None;
1297 r
1298 }
1299 Node::PageBreak => None,
1301 Node::PageInfo {
1304 page_no,
1305 width,
1306 height,
1307 } => {
1308 self.cur_page = *page_no;
1309 self.cur_w = *width as f64;
1310 self.cur_h = *height as f64;
1311 if *page_no > 0 {
1312 self.pages.push((*page_no, self.cur_w, self.cur_h));
1313 }
1314 None
1315 }
1316 Node::ListItem { .. } => None,
1318 }
1319 }
1320
1321 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1325 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1326 self.field_regions.push(Value::Null);
1327 let region_index = self.field_regions.len() - 1;
1328 let mut item_refs = Vec::new();
1329 for item in items {
1330 item_refs.push(json!({ "$ref": self.add_field_item(item, &self_ref) }));
1331 }
1332 self.field_regions[region_index] = json!({
1333 "self_ref": self_ref,
1334 "parent": { "$ref": parent },
1335 "children": item_refs,
1336 "content_layer": "body",
1337 "label": "field_region",
1338 "prov": [],
1339 });
1340 self_ref
1341 }
1342
1343 fn add_key_value_graph(
1347 &mut self,
1348 cells: &[crate::GraphCell],
1349 links: &[crate::GraphLink],
1350 parent: &str,
1351 ) -> String {
1352 let self_ref = format!("#/key_value_items/{}", self.key_value_items.len());
1353 let cells: Vec<Value> = cells
1354 .iter()
1355 .map(|c| {
1356 json!({
1357 "label": c.label,
1358 "cell_id": c.cell_id,
1359 "text": c.text,
1360 "orig": c.orig,
1361 })
1362 })
1363 .collect();
1364 let links: Vec<Value> = links
1365 .iter()
1366 .map(|l| {
1367 json!({
1368 "label": l.label,
1369 "source_cell_id": l.source_cell_id,
1370 "target_cell_id": l.target_cell_id,
1371 })
1372 })
1373 .collect();
1374 self.key_value_items.push(json!({
1375 "self_ref": self_ref,
1376 "parent": { "$ref": parent },
1377 "children": [],
1378 "content_layer": "body",
1379 "label": "key_value_region",
1380 "prov": [],
1381 "captions": [],
1382 "references": [],
1383 "footnotes": [],
1384 "graph": { "cells": cells, "links": links },
1385 }));
1386 self_ref
1387 }
1388
1389 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1390 let self_ref = format!("#/field_items/{}", self.field_items.len());
1391 self.field_items.push(Value::Null);
1392 let item_index = self.field_items.len() - 1;
1393 let mut child_refs = Vec::new();
1394 for (label, text) in [
1395 ("marker", &item.marker),
1396 ("field_key", &item.key),
1397 ("field_value", &item.value),
1398 ] {
1399 if let Some(text) = text {
1400 let extra = match (label, &item.value_kind) {
1403 ("field_value", Some(kind)) => json!({ "kind": kind }),
1404 _ => json!({}),
1405 };
1406 child_refs.push(json!({ "$ref": self.add_text(label, text, &self_ref, extra) }));
1407 }
1408 }
1409 self.field_items[item_index] = json!({
1410 "self_ref": self_ref,
1411 "parent": { "$ref": parent },
1412 "children": child_refs,
1413 "content_layer": "body",
1414 "label": "field_item",
1415 "prov": [],
1416 });
1417 self_ref
1418 }
1419
1420 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1424 let bucket = match self_ref.split('/').nth(1) {
1425 Some("texts") => &mut self.texts,
1426 Some("tables") => &mut self.tables,
1427 Some("pictures") => &mut self.pictures,
1428 Some("groups") => &mut self.groups,
1429 _ => return,
1430 };
1431 if let Some(item) = self_ref
1432 .rsplit('/')
1433 .next()
1434 .and_then(|i| i.parse::<usize>().ok())
1435 .and_then(|i| bucket.get_mut(i))
1436 {
1437 item["content_layer"] = json!(layer);
1438 }
1439 }
1440
1441 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1442 let prov = self.take_prov(unescape_text(text).chars().count());
1443 self.add_text_with(label, text, parent, extra, prov)
1444 }
1445
1446 fn add_text_with(
1449 &mut self,
1450 label: &str,
1451 text: &str,
1452 parent: &str,
1453 extra: Value,
1454 prov: Value,
1455 ) -> String {
1456 let self_ref = format!("#/texts/{}", self.texts.len());
1457 let raw = unescape_text(text);
1458 let mut item = json!({
1459 "self_ref": self_ref,
1460 "parent": { "$ref": parent },
1461 "children": [],
1462 "content_layer": "body",
1463 "label": label,
1464 "prov": prov,
1465 "orig": raw,
1466 "text": raw,
1467 });
1468 merge(&mut item, extra);
1469 self.texts.push(item);
1470 self_ref
1471 }
1472
1473 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
1476 let self_ref = format!("#/texts/{}", self.texts.len());
1477 let prov = self.take_prov(latex.chars().count());
1478 self.texts.push(json!({
1479 "self_ref": self_ref,
1480 "parent": { "$ref": parent },
1481 "children": [],
1482 "content_layer": "body",
1483 "label": "formula",
1484 "prov": prov,
1485 "orig": latex,
1486 "text": latex,
1487 }));
1488 self_ref
1489 }
1490
1491 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
1495 let self_ref = format!("#/texts/{}", self.texts.len());
1496 let prov = self.take_prov(latex.chars().count());
1497 self.texts.push(json!({
1498 "self_ref": self_ref,
1499 "parent": { "$ref": parent },
1500 "children": [],
1501 "content_layer": "body",
1502 "label": "formula",
1503 "prov": prov,
1504 "orig": orig,
1505 "text": latex,
1506 }));
1507 self_ref
1508 }
1509
1510 fn add_code(
1511 &mut self,
1512 text: &str,
1513 language: Option<&str>,
1514 orig: Option<&str>,
1515 parent: &str,
1516 ) -> String {
1517 let self_ref = format!("#/texts/{}", self.texts.len());
1518 let raw = unescape_text(text);
1519 let prov = self.take_prov(raw.chars().count());
1520 self.texts.push(json!({
1521 "self_ref": self_ref,
1522 "parent": { "$ref": parent },
1523 "children": [],
1524 "content_layer": "body",
1525 "label": "code",
1526 "prov": prov,
1527 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
1530 "text": raw,
1531 "captions": [],
1532 "references": [],
1533 "footnotes": [],
1534 "code_language": code_language(language),
1535 }));
1536 self_ref
1537 }
1538
1539 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
1542 let self_ref = format!("#/groups/{}", self.groups.len());
1543 self.groups.push(Value::Null);
1545 let base = level_of(&items[0]);
1546 let mut children = Vec::new();
1547 let mut i = 0;
1548 while i < items.len() {
1549 if !matches!(items[i], Node::ListItem { .. }) {
1552 i += 1;
1553 continue;
1554 }
1555 let lvl = level_of(&items[i]);
1556 if lvl > base {
1557 i += 1;
1559 continue;
1560 }
1561 let item_ref = self.add_list_item(&items[i], &self_ref);
1562 let mut j = i + 1;
1564 while j < items.len() && level_of(&items[j]) > base {
1565 j += 1;
1566 }
1567 if j > i + 1 {
1568 let mut nested = Vec::new();
1569 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
1570 if let Some(idx) = ref_index(&item_ref) {
1572 self.texts[idx]["children"]
1573 .as_array_mut()
1574 .unwrap()
1575 .extend(nested);
1576 }
1577 }
1578 children.push(json!({ "$ref": item_ref }));
1579 i = j;
1580 }
1581 self.groups[group_index(&self_ref)] = json!({
1582 "self_ref": self_ref,
1583 "parent": { "$ref": parent },
1584 "children": children,
1585 "content_layer": "body",
1586 "name": "list",
1587 "label": "list",
1588 });
1589 self_ref
1590 }
1591
1592 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
1593 let Node::ListItem {
1594 ordered,
1595 number,
1596 text,
1597 location,
1598 ..
1599 } = node
1600 else {
1601 unreachable!()
1602 };
1603 self.adopt_loc(*location);
1604 let self_ref = format!("#/texts/{}", self.texts.len());
1605 let raw = unescape_text(text);
1606 let prov = self.take_prov(raw.chars().count());
1607 let marker = if *ordered {
1608 format!("{number}.")
1609 } else {
1610 "-".to_string()
1611 };
1612 self.texts.push(json!({
1613 "self_ref": self_ref,
1614 "parent": { "$ref": parent },
1615 "children": [],
1616 "content_layer": "body",
1617 "label": "list_item",
1618 "prov": prov,
1619 "orig": raw,
1620 "text": raw,
1621 "enumerated": ordered,
1622 "marker": marker,
1623 }));
1624 self_ref
1625 }
1626
1627 fn add_table(&mut self, t: &Table, parent: &str) -> String {
1628 self.add_table_with(t, parent, false)
1629 }
1630
1631 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
1634 let self_ref = format!("#/tables/{}", self.tables.len());
1635 self.adopt_loc(t.location);
1636 let prov = self.take_prov(0);
1637 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
1641 Some(cap) => self.add_caption(cap, json!({}), &self_ref, parent, t.caption_parent),
1642 None => (Vec::new(), Vec::new()),
1643 };
1644 let data = table_data_with(t, raw);
1645 self.tables.push(json!({
1646 "self_ref": self_ref,
1647 "parent": { "$ref": parent },
1648 "children": children,
1649 "content_layer": "body",
1650 "label": "table",
1651 "prov": prov,
1652 "captions": captions,
1653 "references": [],
1654 "footnotes": [],
1655 "data": data,
1656 "annotations": [],
1657 }));
1658 self_ref
1659 }
1660
1661 fn add_caption(
1667 &mut self,
1668 text: &str,
1669 extra: Value,
1670 self_ref: &str,
1671 parent: &str,
1672 choice: CaptionParent,
1673 ) -> (Vec<Value>, Vec<Value>) {
1674 let cap_parent = match choice {
1679 CaptionParent::Item => self_ref,
1680 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
1681 CaptionParent::Body => "#/body",
1682 };
1683 let cap_ref = json!({ "$ref": self.add_text("caption", text, cap_parent, extra) });
1684 match choice {
1685 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
1686 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
1689 CaptionParent::Body if parent == "#/body" => {
1690 self.pending_siblings.push(cap_ref.clone())
1691 }
1692 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
1693 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
1696 }
1697 (vec![cap_ref], Vec::new())
1698 }
1699
1700 fn add_picture(
1703 &mut self,
1704 caption: Option<&str>,
1705 caption_href: Option<&str>,
1706 image: Option<&crate::PictureImage>,
1707 meta: Option<Value>,
1708 parent: &str,
1709 caption_parent: CaptionParent,
1710 ) -> String {
1711 let self_ref = format!("#/pictures/{}", self.pictures.len());
1712 let prov = self.take_prov(0);
1715 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
1716 Some(cap) => {
1717 let extra = match caption_href {
1721 Some(href) => json!({ "hyperlink": href }),
1722 None => json!({}),
1723 };
1724 self.add_caption(cap, extra, &self_ref, parent, caption_parent)
1725 }
1726 None => (Vec::new(), Vec::new()),
1727 };
1728 self.push_picture(prov, captions, children, image, meta, parent)
1729 }
1730
1731 fn push_picture(
1734 &mut self,
1735 prov: Value,
1736 captions: Vec<Value>,
1737 children: Vec<Value>,
1738 image: Option<&crate::PictureImage>,
1739 meta: Option<Value>,
1740 parent: &str,
1741 ) -> String {
1742 let self_ref = format!("#/pictures/{}", self.pictures.len());
1743 let annotations = meta
1747 .as_ref()
1748 .and_then(|m| m.get("annotations").cloned())
1749 .unwrap_or_else(|| json!([]));
1750 let meta = meta.map(|mut m| {
1751 if let Some(obj) = m.as_object_mut() {
1752 obj.remove("annotations");
1753 }
1754 m
1755 });
1756 let mut item = match meta {
1760 Some(meta) => json!({
1761 "self_ref": self_ref,
1762 "parent": { "$ref": parent },
1763 "children": children,
1764 "content_layer": "body",
1765 "meta": meta,
1766 "label": "picture",
1767 "prov": prov,
1768 "captions": captions,
1769 "references": [],
1770 "footnotes": [],
1771 "annotations": annotations,
1772 }),
1773 None => json!({
1774 "self_ref": self_ref,
1775 "parent": { "$ref": parent },
1776 "children": children,
1777 "content_layer": "body",
1778 "label": "picture",
1779 "prov": prov,
1780 "captions": captions,
1781 "references": [],
1782 "footnotes": [],
1783 "annotations": annotations,
1784 }),
1785 };
1786 if let Some(img) = image {
1791 let image = image_ref_json(img);
1792 if let Some(obj) = item.as_object_mut() {
1793 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
1794 obj.insert("image".into(), image);
1795 obj.insert("annotations".into(), annotations);
1796 }
1797 }
1798 self.pictures.push(item);
1799 self_ref
1800 }
1801
1802 fn add_group(
1803 &mut self,
1804 label: &str,
1805 name: Option<&str>,
1806 layer: Option<ContentLayer>,
1807 nodes: &[Node],
1808 parent: &str,
1809 ) -> String {
1810 let self_ref = format!("#/groups/{}", self.groups.len());
1811 self.groups.push(Value::Null);
1812 let mark = (
1816 self.texts.len(),
1817 self.tables.len(),
1818 self.pictures.len(),
1819 self.groups.len(),
1820 );
1821 let children = self.walk_into(nodes, &self_ref);
1822 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
1823 let content_layer = layer.map_or("body", |l| l.value());
1824 self.groups[group_index(&self_ref)] = json!({
1825 "self_ref": self_ref,
1826 "parent": { "$ref": parent },
1827 "children": children,
1828 "content_layer": content_layer,
1829 "name": name,
1830 "label": label,
1831 });
1832 if layer.is_some() {
1833 let (t, tb, p, g) = mark;
1834 for item in self.texts[t..]
1835 .iter_mut()
1836 .chain(self.tables[tb..].iter_mut())
1837 .chain(self.pictures[p..].iter_mut())
1838 .chain(self.groups[g..].iter_mut())
1839 {
1840 if let Some(obj) = item.as_object_mut() {
1841 obj.insert("content_layer".into(), json!(content_layer));
1842 }
1843 }
1844 }
1845 self_ref
1846 }
1847
1848 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Value> {
1851 let seqs: Option<Vec<usize>> = nodes
1856 .iter()
1857 .map(|n| match n {
1858 Node::Prov { seq: Some(s), .. } => Some(*s),
1859 _ => None,
1860 })
1861 .collect();
1862 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
1863 let mut order: Vec<usize> = (0..nodes.len()).collect();
1864 order.sort_by_key(|&i| seqs[i]);
1865 let mut slots: Vec<Vec<Value>> = vec![Vec::new(); nodes.len()];
1866 for i in order {
1867 if let Some(r) = self.add_node(&nodes[i], parent) {
1868 slots[i].append(&mut self.pending_siblings);
1869 slots[i].push(json!({ "$ref": r }));
1870 slots[i].append(&mut self.pending_after);
1871 }
1872 if parent == "#/body" {
1873 slots[i].append(&mut self.pending_body);
1874 }
1875 }
1876 return slots.into_iter().flatten().collect();
1877 }
1878 let mut children = Vec::new();
1879 let mut i = 0;
1880 while i < nodes.len() {
1881 if matches!(nodes[i], Node::ListItem { .. }) {
1882 let start = i;
1883 i += 1;
1884 loop {
1885 match nodes.get(i) {
1886 Some(Node::ListItem { .. }) => i += 1,
1887 Some(Node::Paragraph { text })
1890 if text.is_empty()
1891 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
1892 {
1893 i += 1
1894 }
1895 _ => break,
1896 }
1897 }
1898 self.add_sibling_lists(&nodes[start..i], parent, &mut children);
1899 } else {
1900 if let Some(r) = self.add_node(&nodes[i], parent) {
1901 children.append(&mut self.pending_siblings);
1902 children.push(json!({ "$ref": r }));
1903 children.append(&mut self.pending_after);
1904 }
1905 i += 1;
1906 }
1907 if parent == "#/body" {
1910 children.append(&mut self.pending_body);
1911 }
1912 }
1913 children
1914 }
1915
1916 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
1921 let base = level_of(&run[0]);
1922 let mut seg = 0;
1923 for k in 0..run.len() {
1924 let Node::ListItem {
1925 first_in_list,
1926 level,
1927 ..
1928 } = &run[k]
1929 else {
1930 continue;
1931 };
1932 if *level != base {
1933 continue; }
1935 if k > seg && *first_in_list {
1936 out.push(json!({ "$ref": self.add_list(&run[seg..k], parent) }));
1937 seg = k;
1938 }
1939 }
1940 out.push(json!({ "$ref": self.add_list(&run[seg..], parent) }));
1941 }
1942}
1943
1944fn level_of(node: &Node) -> u8 {
1945 match node {
1946 Node::ListItem { level, .. } => *level,
1947 _ => 0,
1948 }
1949}
1950
1951fn group_index(self_ref: &str) -> usize {
1952 self_ref.rsplit('/').next().unwrap().parse().unwrap()
1953}
1954
1955fn ref_index(self_ref: &str) -> Option<usize> {
1956 self_ref.rsplit('/').next()?.parse().ok()
1957}
1958
1959fn merge(target: &mut Value, extra: Value) {
1961 if let (Some(t), Some(e)) = (target.as_object_mut(), extra.as_object()) {
1962 for (k, v) in e {
1963 t.insert(k.clone(), v.clone());
1964 }
1965 }
1966}
1967
1968fn unescape_text(s: &str) -> String {
1970 s.replace("<", "<")
1971 .replace(">", ">")
1972 .replace("&", "&")
1973 .replace("\\_", "_")
1974}
1975
1976fn fnv1a(s: &str) -> u64 {
1979 let mut h: u64 = 0xcbf29ce484222325;
1980 for b in s.bytes() {
1981 h ^= b as u64;
1982 h = h.wrapping_mul(0x100000001b3);
1983 }
1984 h
1985}
1986
1987#[cfg(test)]
1988mod tests {
1989 use crate::{
1990 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
1991 };
1992 use serde_json::Value;
1993
1994 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
1995 crate::TableCell {
1996 text: text.into(),
1997 bbox: None,
1998 start_row: row,
1999 start_col: col,
2000 row_span: 1,
2001 col_span,
2002 column_header: row == 0,
2003 row_header: false,
2004 row_section: false,
2005 }
2006 }
2007
2008 #[test]
2014 fn ragged_rows_take_their_grid_from_first_class_cells() {
2015 let cells = vec![
2016 cell_at("h0", 0, 0, 1),
2017 cell_at("h2", 0, 2, 1),
2018 cell_at("wide", 1, 0, 3),
2019 ];
2020 let export = |rows: Vec<Vec<String>>| -> Value {
2021 let mut doc = DoclingDocument::new("t");
2022 doc.push(Node::Table(Table {
2023 rows,
2024 cells: Some(cells.clone()),
2025 ..Table::default()
2026 }));
2027 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2028 v["tables"][0]["data"].clone()
2029 };
2030 let data = export(vec![
2032 vec!["h0".into(), "h2".into()],
2033 vec!["wide".into(), "wide".into(), "wide".into()],
2034 ]);
2035 assert_eq!(data["num_rows"], 2);
2036 assert_eq!(data["num_cols"], 3);
2037 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
2038 let row0: Vec<&str> = data["grid"][0]
2039 .as_array()
2040 .unwrap()
2041 .iter()
2042 .map(|c| c["text"].as_str().unwrap())
2043 .collect();
2044 assert_eq!(row0, ["h0", "", "h2"]);
2045 assert_eq!(data["table_cells"][2]["col_span"], 3);
2046 let data = export(vec![
2049 vec!["h0".into(), "h2".into()],
2050 vec!["wide".into(), "wide".into()],
2051 ]);
2052 assert_eq!(data["num_cols"], 2);
2053 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
2054 }
2055
2056 fn doc_with_image() -> DoclingDocument {
2057 let mut doc = DoclingDocument::new("t");
2058 doc.push(Node::Picture {
2059 caption: Some("Fig 1".into()),
2060 caption_href: None,
2061 image: Some(PictureImage {
2062 dpi: crate::PictureImage::DEFAULT_DPI,
2063 mimetype: "image/png".into(),
2064 width: 4,
2065 height: 2,
2066 data: b"foobar".to_vec(),
2067 }),
2068 classification: None,
2069 caption_parent: Default::default(),
2070 });
2071 doc
2072 }
2073
2074 #[test]
2079 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
2080 let mut doc = DoclingDocument::new("t");
2081 doc.push(Node::Heading {
2082 level: 1,
2083 text: "Slide One".into(),
2084 });
2085 doc.push(Node::Furniture {
2086 layer: ContentLayer::Notes,
2087 inner: Box::new(Node::Located {
2088 location: [0, 0, 0, 0],
2089 inner: Box::new(Node::Paragraph {
2090 text: "Speaker note for slide 1.".into(),
2091 }),
2092 }),
2093 });
2094 doc.push(Node::Furniture {
2095 layer: ContentLayer::Furniture,
2096 inner: Box::new(Node::Paragraph {
2097 text: "page header".into(),
2098 }),
2099 });
2100
2101 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2102 let texts = v["texts"].as_array().unwrap();
2103 assert_eq!(
2104 texts
2105 .iter()
2106 .map(|t| (
2107 t["label"].as_str().unwrap(),
2108 t["content_layer"].as_str().unwrap(),
2109 t["text"].as_str().unwrap()
2110 ))
2111 .collect::<Vec<_>>(),
2112 vec![
2113 ("title", "body", "Slide One"),
2114 ("text", "notes", "Speaker note for slide 1."),
2115 ],
2116 "the note is carried on its own layer; the furniture is not carried"
2117 );
2118 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
2120 }
2121
2122 #[test]
2128 fn continuation_flags_become_spanning_cells() {
2129 let mut doc = DoclingDocument::new("t");
2130 let rows = vec![
2132 vec!["merged".to_string(), "merged".into(), "merged".into()],
2133 vec!["merged".to_string(), "merged".into(), "merged".into()],
2134 vec!["a".to_string(), "b".into(), "c".into()],
2135 ];
2136 doc.push(Node::Table(crate::Table {
2137 rows,
2138 location: None,
2139 structure: Some(crate::TableStructure {
2140 header_row: vec![true, false, false],
2141 col_continuation: vec![
2142 vec![false, true, true],
2143 vec![false, true, true],
2144 vec![false, false, false],
2145 ],
2146 row_continuation: vec![
2147 vec![false, false, false],
2148 vec![true, true, true],
2149 vec![false, false, false],
2150 ],
2151 row_header: Vec::new(),
2152 col_header: Vec::new(),
2153 }),
2154 cell_blocks: None,
2155 cells: None,
2156 caption: None,
2157 caption_parent: Default::default(),
2158 }));
2159 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2160 let data = &v["tables"][0]["data"];
2161 assert_eq!(data["num_rows"], 3);
2162 assert_eq!(data["num_cols"], 3);
2163 let cells = data["table_cells"].as_array().unwrap();
2164 assert_eq!(
2165 cells.len(),
2166 4,
2167 "one cell for the range, three for the plain row"
2168 );
2169 assert_eq!(
2170 cells[0],
2171 serde_json::json!({
2172 "row_span": 2, "col_span": 3,
2173 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2174 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2175 "text": "merged", "column_header": true, "row_header": false,
2176 "row_section": false, "fillable": false,
2177 })
2178 );
2179 assert_eq!(cells[1]["text"], "a");
2180 assert_eq!(cells[1]["row_span"], 1);
2181 assert_eq!(cells[1]["column_header"], false);
2182 let grid = data["grid"].as_array().unwrap();
2184 assert_eq!(grid.len(), 3);
2185 for (r, row) in grid.iter().take(2).enumerate() {
2186 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2187 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2188 }
2189 }
2190 assert_eq!(grid[2][2]["text"], "c");
2191 }
2192
2193 #[test]
2199 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2200 let mut doc = DoclingDocument::new("t");
2201 doc.push(Node::PageInfo {
2202 page_no: 1,
2203 width: 3.0,
2204 height: 4.0,
2205 });
2206 let table = crate::Table {
2207 rows: vec![vec!["a".to_string(), "b".into()]],
2208 ..Default::default()
2209 };
2210 doc.push(Node::Group {
2211 label: "sheet".into(),
2212 name: Some("Data".into()),
2213 layer: None,
2214 children: vec![
2215 Node::Prov {
2220 page_no: 1,
2221 bbox: [0.0, 0.0, 3.0, 4.0],
2222 charspan: [0, 0],
2223 seq: Some(1),
2224 inner: Box::new(Node::Table(table.clone())),
2225 },
2226 Node::Prov {
2227 page_no: 1,
2228 bbox: [0.0, 1.0, 1.0, 1.0],
2229 charspan: [0, 0],
2230 seq: Some(0),
2231 inner: Box::new(Node::Chart {
2232 kind: "bar_chart".into(),
2233 table,
2234 caption: Some("Sales".into()),
2235 location: Some([0, 128, 170, 128]),
2236 }),
2237 },
2238 ],
2239 });
2240 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2241 assert_eq!(
2242 v["pages"],
2243 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2244 );
2245 assert_eq!(
2246 v["tables"][0]["prov"],
2247 serde_json::json!([{
2248 "page_no": 1,
2249 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2250 "charspan": [0, 0],
2251 }])
2252 );
2253 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2254 let sheet = &v["groups"][0];
2257 assert_eq!(
2258 sheet["children"],
2259 serde_json::json!([
2260 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2261 ])
2262 );
2263 let cap = &v["texts"][0];
2264 assert_eq!(cap["label"], "caption");
2265 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2266 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2267 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2268 let pic = &v["pictures"][0];
2269 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2270 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2271 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2272 assert_eq!(
2273 pic["meta"]["classification"]["predictions"][0]["class_name"],
2274 "bar_chart"
2275 );
2276 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2277 }
2278
2279 #[test]
2283 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2284 let mut doc = DoclingDocument::new("t");
2285 doc.push(Node::PageInfo {
2286 page_no: 1,
2287 width: 12192000.0,
2288 height: 6858000.0,
2289 });
2290 doc.push(Node::Furniture {
2291 layer: ContentLayer::Notes,
2292 inner: Box::new(Node::Located {
2293 location: [0, 0, 0, 0],
2294 inner: Box::new(Node::Paragraph {
2295 text: "a note".into(),
2296 }),
2297 }),
2298 });
2299 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2300 let prov = &v["texts"][0]["prov"][0];
2301 assert_eq!(prov["page_no"], 1);
2302 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2303 assert_eq!(
2304 prov["bbox"],
2305 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2306 );
2307 assert_eq!(
2309 v["pages"]["1"]["size"],
2310 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2311 );
2312 }
2313
2314 #[test]
2320 fn page_markers_produce_pages_and_prov() {
2321 let mut doc = DoclingDocument::new("t");
2322 doc.push(Node::PageInfo {
2323 page_no: 1,
2324 width: 512.0,
2325 height: 1024.0,
2326 });
2327 doc.push(Node::Located {
2328 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
2330 text: "hello".into(),
2331 }),
2332 });
2333 doc.push(Node::Table(Table {
2334 rows: vec![vec!["a".into()]],
2335 location: Some([0, 0, 512, 512]),
2336 ..Table::default()
2337 }));
2338 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2339 assert_eq!(v["pages"]["1"]["page_no"], 1);
2340 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
2341 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
2342 let prov = &v["texts"][0]["prov"][0];
2345 assert_eq!(prov["page_no"], 1);
2346 assert_eq!(prov["bbox"]["l"], 128.0);
2347 assert_eq!(prov["bbox"]["t"], 896.0);
2348 assert_eq!(prov["bbox"]["r"], 256.0);
2349 assert_eq!(prov["bbox"]["b"], 768.0);
2350 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
2351 assert_eq!(prov["charspan"][1], 5);
2352 let tprov = &v["tables"][0]["prov"][0];
2354 assert_eq!(tprov["bbox"]["t"], 1024.0);
2355 assert_eq!(tprov["bbox"]["b"], 0.0);
2356 assert_eq!(tprov["charspan"][1], 0);
2357
2358 let mut plain = DoclingDocument::new("t");
2360 plain.push(Node::Located {
2361 location: [1, 2, 3, 4],
2362 inner: Box::new(Node::Paragraph { text: "x".into() }),
2363 });
2364 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2365 assert_eq!(v["pages"], serde_json::json!({}));
2366 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2367 }
2368
2369 #[test]
2372 fn page_furniture_becomes_furniture_layer_text() {
2373 let mut doc = DoclingDocument::new("t");
2374 doc.push(Node::PageInfo {
2375 page_no: 1,
2376 width: 512.0,
2377 height: 512.0,
2378 });
2379 doc.push(Node::PageFurniture {
2380 footer: false,
2381 location: [10, 0, 100, 20],
2382 text: "Chapter 1".into(),
2383 });
2384 doc.push(Node::Paragraph {
2385 text: "body".into(),
2386 });
2387 doc.push(Node::PageFurniture {
2388 footer: true,
2389 location: [400, 490, 500, 512],
2390 text: "1.10.2".into(),
2391 });
2392 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2393 let texts = v["texts"].as_array().unwrap();
2394 assert_eq!(texts.len(), 3);
2395 assert_eq!(texts[0]["label"], "page_header");
2396 assert_eq!(texts[0]["content_layer"], "furniture");
2397 assert_eq!(texts[0]["parent"]["$ref"], "#/body");
2398 assert_eq!(texts[0]["prov"][0]["bbox"]["l"], 10.0);
2399 assert_eq!(texts[1]["content_layer"], "body");
2400 assert_eq!(texts[1]["prov"], serde_json::json!([]));
2401 assert_eq!(texts[2]["label"], "page_footer");
2402 assert_eq!(texts[2]["text"], "1.10.2");
2403 assert_eq!(texts[2]["content_layer"], "furniture");
2404 }
2405
2406 #[test]
2407 fn picture_image_in_markdown_modes_and_json() {
2408 let doc = doc_with_image();
2409 assert!(doc.export_to_markdown().contains("<!-- image -->"));
2411 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
2413 assert!(
2414 md.contains(""),
2415 "got:\n{md}"
2416 );
2417 assert!(files.is_empty());
2418 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
2420 assert!(
2421 md.contains(""),
2422 "got:\n{md}"
2423 );
2424 assert_eq!(
2425 files,
2426 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
2427 );
2428 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2431 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
2432 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
2433 let keys: Vec<&str> = v["pictures"][0]
2434 .as_object()
2435 .unwrap()
2436 .keys()
2437 .map(String::as_str)
2438 .collect();
2439 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
2440 assert_eq!(
2441 v["pictures"][0]["image"]["uri"],
2442 "data:image/png;base64,Zm9vYmFy"
2443 );
2444 }
2445
2446 #[test]
2447 fn exports_docling_schema() {
2448 let mut doc = DoclingDocument::new("t");
2449 doc.push(Node::Heading {
2450 level: 1,
2451 text: "Title".into(),
2452 });
2453 doc.push(Node::Heading {
2454 level: 2,
2455 text: "Sec".into(),
2456 });
2457 doc.push(Node::Paragraph {
2458 text: "Body & more".into(),
2459 }); doc.push(Node::ListItem {
2461 ordered: false,
2462 number: 0,
2463 first_in_list: true,
2464 text: "one".into(),
2465 level: 0,
2466 marker: None,
2467 location: None,
2468 dclx: None,
2469 href: None,
2470 layer: None,
2471 });
2472 doc.push(Node::ListItem {
2473 ordered: false,
2474 number: 0,
2475 first_in_list: false,
2476 text: "two".into(),
2477 level: 0,
2478 marker: None,
2479 location: None,
2480 dclx: None,
2481 href: None,
2482 layer: None,
2483 });
2484 doc.push(Node::Table(Table {
2485 rows: vec![vec!["A".into(), "B".into()]],
2486 location: None,
2487 structure: None,
2488 cell_blocks: None,
2489 cells: None,
2490 caption: None,
2491 caption_parent: Default::default(),
2492 }));
2493
2494 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2495 assert_eq!(v["schema_name"], "DoclingDocument");
2496 assert_eq!(v["version"], "1.10.0");
2497 assert_eq!(v["texts"][0]["label"], "title");
2498 assert_eq!(v["texts"][1]["label"], "section_header");
2499 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
2503 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
2504 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
2505 assert_eq!(v["texts"][3]["marker"], "-");
2506 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
2508 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
2509 }
2510 #[test]
2514 fn a_layered_group_stamps_its_whole_subtree() {
2515 let doc = DoclingDocument {
2516 name: "s".into(),
2517 nodes: vec![
2518 Node::Group {
2519 label: "sheet".into(),
2520 name: Some("Sheet1".into()),
2521 layer: None,
2522 children: vec![Node::Paragraph {
2523 text: "visible".into(),
2524 }],
2525 },
2526 Node::Group {
2527 label: "sheet".into(),
2528 name: Some("Sheet2".into()),
2529 layer: Some(ContentLayer::Invisible),
2530 children: vec![Node::Paragraph {
2531 text: "hidden".into(),
2532 }],
2533 },
2534 ],
2535 ..DoclingDocument::new("s")
2536 };
2537 let v = crate::json::to_json(&doc);
2538 assert_eq!(v["groups"][0]["label"], "sheet");
2539 assert_eq!(v["groups"][0]["name"], "Sheet1");
2540 assert_eq!(v["groups"][0]["content_layer"], "body");
2541 assert_eq!(v["texts"][0]["content_layer"], "body");
2542 assert_eq!(v["groups"][1]["name"], "Sheet2");
2543 assert_eq!(v["groups"][1]["content_layer"], "invisible");
2544 assert_eq!(v["texts"][1]["content_layer"], "invisible");
2545 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
2547 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
2548 }
2549
2550 #[test]
2560 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
2561 use crate::tree::{ItemTree, TreeKind};
2562 let mut t = ItemTree::default();
2563 let text = |txt: &str| TreeKind::Text {
2564 label: "text".into(),
2565 text: txt.into(),
2566 orig: None,
2567 formatting: None,
2568 hyperlink: None,
2569 level: None,
2570 list: None,
2571 };
2572 let a = t.add(None, None, text("a"));
2573 let blank = t.add(None, None, text(""));
2574 let b = t.add(None, None, text("b"));
2575 t.delete(blank);
2576 let group = t.add(
2577 None,
2578 Some(ContentLayer::Notes),
2579 TreeKind::Group {
2580 label: "comment_section".into(),
2581 name: "comment-0".into(),
2582 },
2583 );
2584 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
2585 t.items[a].comments.push(group);
2586 t.add(
2587 None,
2588 None,
2589 TreeKind::Picture {
2590 captions: Vec::new(),
2591 image: None,
2592 classification: Some("bar_chart".into()),
2593 confidence: None,
2594 chart: Some(Table {
2595 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
2596 ..Table::default()
2597 }),
2598 dpi: None,
2599 },
2600 );
2601 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
2602 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
2603 let mut doc = DoclingDocument::new("t");
2604 doc.tree = Some(t);
2605 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2606 let texts = v["texts"].as_array().unwrap();
2607 assert_eq!(texts.len(), 3);
2608 assert_eq!(texts[1]["text"], "b");
2609 assert_eq!(texts[1]["self_ref"], "#/texts/1");
2610 assert_eq!(
2611 v["body"]["children"],
2612 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
2613 );
2614 let keys: Vec<&str> = texts[0]
2615 .as_object()
2616 .unwrap()
2617 .keys()
2618 .map(String::as_str)
2619 .collect();
2620 assert_eq!(
2621 keys,
2622 vec![
2623 "self_ref",
2624 "parent",
2625 "children",
2626 "content_layer",
2627 "label",
2628 "prov",
2629 "comments",
2630 "orig",
2631 "text"
2632 ]
2633 );
2634 assert_eq!(
2635 texts[0]["comments"],
2636 serde_json::json!([{"$ref": "#/groups/0"}])
2637 );
2638 assert!(texts[1].get("comments").is_none());
2639 let meta = &v["pictures"][0]["meta"];
2640 assert_eq!(
2641 meta["classification"]["predictions"][0]["class_name"],
2642 "bar_chart"
2643 );
2644 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
2645 }
2646
2647 #[test]
2653 fn tree_items_carry_exact_provenance_and_dpi() {
2654 use crate::tree::{ItemTree, TreeKind, TreeProv};
2655 let text = |label: &str, t: &str| TreeKind::Text {
2656 label: label.into(),
2657 text: t.into(),
2658 orig: None,
2659 formatting: None,
2660 hyperlink: None,
2661 level: None,
2662 list: None,
2663 };
2664 let mut t = ItemTree::default();
2665 let slide = t.add(
2666 None,
2667 None,
2668 TreeKind::Group {
2669 label: "chapter".into(),
2670 name: "slide-0".into(),
2671 },
2672 );
2673 t.add_with_prov(
2674 Some(slide),
2675 None,
2676 text("paragraph", "héllo"),
2677 TreeProv {
2678 page_no: 1,
2679 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
2680 bottom_left: true,
2681 charspan: [0, 5],
2682 },
2683 );
2684 t.add_with_prov(
2685 Some(slide),
2686 None,
2687 TreeKind::Picture {
2688 captions: Vec::new(),
2689 image: Some(crate::PictureImage {
2690 dpi: crate::PictureImage::DEFAULT_DPI,
2691 mimetype: "image/png".into(),
2692 width: 2,
2693 height: 2,
2694 data: vec![0],
2695 }),
2696 classification: None,
2697 confidence: None,
2698 chart: None,
2699 dpi: Some(300),
2700 },
2701 TreeProv {
2702 page_no: 1,
2703 bbox: [0.0; 4],
2704 bottom_left: false,
2705 charspan: [0, 0],
2706 },
2707 );
2708 t.add(
2709 Some(slide),
2710 Some(ContentLayer::Notes),
2711 text("text", "no geometry"),
2712 );
2713 let mut doc = DoclingDocument::new("t");
2714 doc.push(Node::PageInfo {
2715 page_no: 1,
2716 width: 9144000.0,
2717 height: 6858000.0,
2718 });
2719 doc.tree = Some(t);
2720 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2721 assert_eq!(
2722 v["texts"][0]["prov"],
2723 serde_json::json!([{
2724 "page_no": 1,
2725 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
2726 "charspan": [0, 5],
2727 }])
2728 );
2729 assert_eq!(v["texts"][0]["label"], "paragraph");
2730 assert_eq!(
2731 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
2732 "TOPLEFT"
2733 );
2734 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
2735 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
2736 assert_eq!(v["texts"][1]["content_layer"], "notes");
2737 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
2738 assert_eq!(v["pages"]["1"]["page_no"], 1);
2739 }
2740
2741 #[test]
2746 fn provenance_boxes_are_clamped_to_their_page() {
2747 let mut doc = DoclingDocument::new("t");
2748 doc.push(Node::PageInfo {
2749 page_no: 1,
2750 width: 10.0,
2751 height: 8.0,
2752 });
2753 doc.push(Node::Prov {
2754 page_no: 1,
2755 bbox: [-1.0, 2.0, 12.0, 9.5],
2756 charspan: [0, 1],
2757 seq: None,
2758 inner: Box::new(Node::Paragraph { text: "x".into() }),
2759 });
2760 let mut table = Table {
2761 rows: vec![vec!["a".into()]],
2762 ..Table::default()
2763 };
2764 table.cells = Some(vec![crate::TableCell {
2765 text: "a".into(),
2766 bbox: Some([1.0, 1.0, 11.0, 9.0]),
2767 start_row: 0,
2768 start_col: 0,
2769 row_span: 1,
2770 col_span: 1,
2771 column_header: false,
2772 row_header: false,
2773 row_section: false,
2774 }]);
2775 doc.push(Node::Prov {
2776 page_no: 1,
2777 bbox: [0.0, 0.0, 10.0, 8.0],
2778 charspan: [0, 0],
2779 seq: None,
2780 inner: Box::new(Node::Table(table)),
2781 });
2782 doc.push(Node::Prov {
2783 page_no: 7,
2784 bbox: [-5.0, 0.0, 50.0, 50.0],
2785 charspan: [0, 1],
2786 seq: None,
2787 inner: Box::new(Node::Paragraph { text: "y".into() }),
2788 });
2789 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2790 assert_eq!(
2791 v["texts"][0]["prov"][0]["bbox"],
2792 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
2793 );
2794 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
2795 assert_eq!(
2796 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
2797 (Some(1.0), Some(10.0), Some(8.0))
2798 );
2799 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
2800 assert_eq!(
2801 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
2802 "page 7 is not described"
2803 );
2804 }
2805
2806 #[test]
2807 fn a_backend_item_tree_is_written_verbatim() {
2808 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
2809 let mut t = ItemTree::default();
2810 let text = |label: &str, txt: &str| TreeKind::Text {
2811 label: label.into(),
2812 text: txt.into(),
2813 orig: None,
2814 formatting: None,
2815 hyperlink: None,
2816 level: None,
2817 list: None,
2818 };
2819 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
2820 let h = t.add(None, None, text("title", "Heading"));
2821 let group = t.add(
2822 Some(h),
2823 None,
2824 TreeKind::Group {
2825 label: "inline".into(),
2826 name: "group".into(),
2827 },
2828 );
2829 t.add(
2830 Some(group),
2831 None,
2832 TreeKind::Text {
2833 label: "text".into(),
2834 text: "bold".into(),
2835 orig: None,
2836 formatting: Some(Formatting {
2837 bold: true,
2838 ..Formatting::default()
2839 }),
2840 hyperlink: Some("https://example.com/".into()),
2841 level: None,
2842 list: None,
2843 },
2844 );
2845 t.add(
2846 Some(group),
2847 None,
2848 TreeKind::Code {
2849 text: "x = 1".into(),
2850 orig: None,
2851 language: Some("python".into()),
2852 formatting: None,
2853 hyperlink: None,
2854 },
2855 );
2856 let sub = t.add(
2857 Some(h),
2858 None,
2859 TreeKind::Text {
2860 label: "section_header".into(),
2861 text: "Sub".into(),
2862 orig: Some("Sub\u{2019}".into()),
2863 formatting: None,
2864 hyperlink: None,
2865 level: Some(1),
2866 list: None,
2867 },
2868 );
2869 t.add(
2870 Some(sub),
2871 None,
2872 TreeKind::Text {
2873 label: "list_item".into(),
2874 text: "item".into(),
2875 orig: None,
2876 formatting: None,
2877 hyperlink: None,
2878 level: None,
2879 list: Some(ListMeta {
2880 enumerated: true,
2881 marker: "3.".into(),
2882 }),
2883 },
2884 );
2885 let _region = t.add(
2886 Some(sub),
2887 None,
2888 TreeKind::FieldRegion {
2889 items: vec![crate::FieldItem {
2890 marker: None,
2891 key: Some("Name".into()),
2892 value: Some("Duck".into()),
2893 value_kind: Some("read_only".into()),
2894 }],
2895 },
2896 );
2897 let table = t.add(
2898 Some(sub),
2899 None,
2900 TreeKind::Table {
2901 table: Table {
2902 rows: vec![vec!["a \n<".into(), "b".into()]],
2903 cells: Some(vec![
2904 crate::TableCell {
2905 text: "a \n<".into(),
2906 bbox: None,
2907 start_row: 0,
2908 start_col: 0,
2909 row_span: 3,
2910 col_span: 1,
2911 column_header: false,
2912 row_header: true,
2913 row_section: false,
2914 },
2915 crate::TableCell {
2916 text: "b".into(),
2917 bbox: None,
2918 start_row: 0,
2919 start_col: 1,
2920 row_span: 1,
2921 col_span: 1,
2922 column_header: false,
2923 row_header: false,
2924 row_section: false,
2925 },
2926 ]),
2927 ..Table::default()
2928 },
2929 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
2931 },
2932 );
2933 let cell_group = t.add(
2934 Some(table),
2935 None,
2936 TreeKind::Group {
2937 label: "unspecified".into(),
2938 name: "rich_cell_group_1_0_0".into(),
2939 },
2940 );
2941 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
2942 *rich_cells = vec![(0, 1, cell_group)];
2943 }
2944 let after = t.add(Some(sub), None, text("text", "after the region"));
2945 let _ = (title, after);
2946
2947 let doc = DoclingDocument {
2948 tree: Some(t),
2949 ..DoclingDocument::new("t")
2950 };
2951 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2952 let texts: Vec<&str> = v["texts"]
2954 .as_array()
2955 .unwrap()
2956 .iter()
2957 .map(|t| t["text"].as_str().unwrap())
2958 .collect();
2959 assert_eq!(
2960 texts,
2961 [
2962 "Page",
2963 "Heading",
2964 "bold",
2965 "x = 1",
2966 "Sub",
2967 "item",
2968 "Name",
2969 "Duck",
2970 "after the region"
2971 ]
2972 );
2973 assert_eq!(
2974 v["body"]["children"],
2975 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
2976 );
2977 assert_eq!(v["texts"][0]["content_layer"], "furniture");
2978 assert_eq!(
2979 v["texts"][1]["children"],
2980 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
2981 );
2982 let bold = &v["texts"][2];
2983 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
2984 let keys: Vec<&str> = bold
2985 .as_object()
2986 .unwrap()
2987 .keys()
2988 .map(String::as_str)
2989 .collect();
2990 assert_eq!(
2991 keys,
2992 [
2993 "self_ref",
2994 "parent",
2995 "children",
2996 "content_layer",
2997 "label",
2998 "prov",
2999 "orig",
3000 "text",
3001 "formatting",
3002 "hyperlink"
3003 ]
3004 );
3005 assert_eq!(
3006 bold["formatting"],
3007 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
3008 );
3009 let code = &v["texts"][3];
3010 assert_eq!(code["label"], "code");
3011 assert_eq!(code["code_language"], "Python");
3012 let sub = &v["texts"][4];
3013 assert_eq!(sub["orig"], "Sub\u{2019}");
3014 assert_eq!(sub["level"], 1);
3015 let item = &v["texts"][5];
3016 let keys: Vec<&str> = item
3017 .as_object()
3018 .unwrap()
3019 .keys()
3020 .map(String::as_str)
3021 .collect();
3022 assert_eq!(
3023 keys,
3024 [
3025 "self_ref",
3026 "parent",
3027 "children",
3028 "content_layer",
3029 "label",
3030 "prov",
3031 "orig",
3032 "text",
3033 "enumerated",
3034 "marker"
3035 ]
3036 );
3037 assert_eq!(item["marker"], "3.");
3038 assert_eq!(v["texts"][7]["kind"], "read_only");
3039 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
3040 let table = &v["tables"][0];
3041 assert_eq!(
3042 table["children"],
3043 serde_json::json!([{"$ref": "#/groups/1"}])
3044 );
3045 let cells = table["data"]["table_cells"].as_array().unwrap();
3046 assert_eq!(
3047 cells[0]["text"], "a \n<",
3048 "raw cell text is written verbatim"
3049 );
3050 assert_eq!(
3051 cells[0]["end_row_offset_idx"], 3,
3052 "declared spans are not clamped"
3053 );
3054 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
3055 assert!(cells[0].get("ref").is_none());
3056 assert!(
3057 table["data"]["grid"][0][1].get("ref").is_none(),
3058 "the grid shows plain cells"
3059 );
3060 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
3061 }
3062
3063 #[test]
3067 fn a_comment_section_can_be_referenced_by_its_note_text() {
3068 let doc = DoclingDocument {
3069 name: "c".into(),
3070 nodes: vec![
3071 Node::Commented {
3072 comments: vec![0],
3073 inner: Box::new(Node::Paragraph {
3074 text: "annotated".into(),
3075 }),
3076 },
3077 Node::CommentSection {
3078 name: "comment-Sheet1-A1".into(),
3079 text: "[author: A]: note".into(),
3080 refs_note_text: true,
3081 grouped: true,
3082 },
3083 ],
3084 ..DoclingDocument::new("c")
3085 };
3086 let v = crate::json::to_json(&doc);
3087 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
3088 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
3089 }
3090
3091 #[test]
3095 fn comment_sections_link_back_to_their_items() {
3096 let doc = DoclingDocument {
3097 name: "c".into(),
3098 nodes: vec![
3099 Node::Commented {
3100 comments: vec![0],
3101 inner: Box::new(Node::Paragraph {
3102 text: "annotated".into(),
3103 }),
3104 },
3105 Node::Paragraph {
3106 text: "plain".into(),
3107 },
3108 Node::CommentSection {
3109 name: "comment-7".into(),
3110 text: "[time: t]: note".into(),
3111 refs_note_text: false,
3112 grouped: true,
3113 },
3114 ],
3115 ..DoclingDocument::new("c")
3116 };
3117 let v = crate::json::to_json(&doc);
3118 assert_eq!(v["groups"][0]["label"], "comment_section");
3120 assert_eq!(v["groups"][0]["name"], "comment-7");
3121 assert_eq!(v["groups"][0]["content_layer"], "notes");
3122 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
3123 assert_eq!(v["texts"][2]["content_layer"], "notes");
3124 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
3126 assert!(v["texts"][1].get("comments").is_none());
3127 let keys: Vec<&str> = v["texts"][0]
3129 .as_object()
3130 .unwrap()
3131 .keys()
3132 .map(String::as_str)
3133 .collect();
3134 assert_eq!(
3135 &keys[keys.len() - 4..],
3136 &["prov", "comments", "orig", "text"]
3137 );
3138 }
3139
3140 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
3141 Node::Picture {
3142 caption: Some(caption.into()),
3143 caption_href: None,
3144 image: None,
3145 classification: None,
3146 caption_parent,
3147 }
3148 }
3149
3150 fn group(children: Vec<Node>) -> Node {
3151 Node::Group {
3152 label: "section".into(),
3153 name: None,
3154 layer: None,
3155 children,
3156 }
3157 }
3158
3159 fn refs(v: &Value) -> Vec<&str> {
3160 v.as_array()
3161 .unwrap()
3162 .iter()
3163 .map(|r| r["$ref"].as_str().unwrap())
3164 .collect()
3165 }
3166
3167 #[test]
3172 fn a_body_caption_follows_the_enclosing_top_level_item() {
3173 let mut doc = DoclingDocument::new("t");
3174 doc.push(picture("top", CaptionParent::Body));
3175 doc.push(group(vec![
3176 Node::Paragraph { text: "p".into() },
3177 picture("nested", CaptionParent::Body),
3178 ]));
3179 doc.push(Node::Paragraph {
3180 text: "after".into(),
3181 });
3182 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3183 assert_eq!(
3184 refs(&v["body"]["children"]),
3185 [
3186 "#/texts/0",
3187 "#/pictures/0",
3188 "#/groups/0",
3189 "#/texts/2",
3190 "#/texts/3"
3191 ]
3192 );
3193 assert_eq!(
3194 refs(&v["groups"][0]["children"]),
3195 ["#/texts/1", "#/pictures/1"]
3196 );
3197 for (cap, pic) in [(0, 0), (2, 1)] {
3198 assert_eq!(v["texts"][cap]["label"], "caption");
3199 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
3200 assert_eq!(
3201 refs(&v["pictures"][pic]["captions"]),
3202 [format!("#/texts/{cap}")]
3203 );
3204 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
3205 }
3206 }
3207
3208 #[test]
3211 fn an_item_caption_is_the_items_first_child() {
3212 let mut doc = DoclingDocument::new("t");
3213 doc.push(picture("fig", CaptionParent::Item));
3214 doc.push(Node::Table(Table {
3215 rows: vec![vec!["a".into()]],
3216 caption: Some("tab".into()),
3217 caption_parent: CaptionParent::Item,
3218 ..Table::default()
3219 }));
3220 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3221 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
3222 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
3223 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
3224 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3225 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
3226 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
3227 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
3228 }
3229
3230 #[test]
3236 fn picture_children_hang_off_the_picture_after_its_caption() {
3237 let mut doc = DoclingDocument::new("t");
3238 doc.push(picture("fig", CaptionParent::Item));
3239 doc.push(Node::PictureChildren(vec![
3240 Node::Heading {
3241 level: 2,
3242 text: "in-figure title".into(),
3243 },
3244 Node::Paragraph {
3245 text: "axis label".into(),
3246 },
3247 Node::ListItem {
3248 ordered: false,
3249 number: 0,
3250 first_in_list: true,
3251 text: "callout".into(),
3252 level: 0,
3253 marker: None,
3254 location: None,
3255 dclx: None,
3256 href: None,
3257 layer: None,
3258 },
3259 ]));
3260 doc.push(Node::Paragraph {
3261 text: "after".into(),
3262 });
3263 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3264 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/texts/4"]);
3265 assert_eq!(
3266 refs(&v["pictures"][0]["children"]),
3267 ["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"]
3268 );
3269 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3270 assert_eq!(v["texts"][1]["label"], "section_header");
3271 assert_eq!(v["texts"][2]["label"], "text");
3272 for t in 1..=2 {
3273 assert_eq!(v["texts"][t]["parent"]["$ref"], "#/pictures/0");
3274 assert_eq!(v["texts"][t]["content_layer"], "body");
3275 }
3276 assert_eq!(v["groups"][0]["parent"]["$ref"], "#/pictures/0");
3277 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
3278 let md = doc.export_to_markdown();
3279 assert!(
3280 !md.contains("axis label") && !md.contains("callout"),
3281 "{md}"
3282 );
3283 assert!(md.contains("after"), "{md}");
3284 }
3285
3286 #[test]
3290 fn a_container_caption_is_the_items_sibling() {
3291 let mut doc = DoclingDocument::new("t");
3292 doc.push(group(vec![
3293 picture("chart", CaptionParent::Container),
3294 Node::Table(Table {
3295 rows: vec![vec!["a".into()]],
3296 caption: Some("figcaption".into()),
3297 caption_parent: CaptionParent::ContainerAfter,
3298 ..Table::default()
3299 }),
3300 ]));
3301 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3302 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
3303 assert_eq!(
3304 refs(&v["groups"][0]["children"]),
3305 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
3306 );
3307 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
3308 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
3309 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
3310 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
3311 }
3312}