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