1use serde_json::{json, Value};
12
13use crate::document::{CaptionParent, ContentLayer, DoclingDocument, Node, Table};
14
15const SCHEMA_VERSION: &str = "1.10.0";
16
17const CODE_LANGUAGES: &[&str] = &[
20 "Ada",
21 "Awk",
22 "Bash",
23 "bc",
24 "C",
25 "C#",
26 "C++",
27 "CMake",
28 "COBOL",
29 "CSS",
30 "Ceylon",
31 "Clojure",
32 "Crystal",
33 "Cuda",
34 "Cython",
35 "D",
36 "Dart",
37 "dc",
38 "Dockerfile",
39 "DocLang",
40 "Elixir",
41 "Erlang",
42 "FORTRAN",
43 "Forth",
44 "Go",
45 "HTML",
46 "Haskell",
47 "Haxe",
48 "Java",
49 "JavaScript",
50 "JSON",
51 "Julia",
52 "Kotlin",
53 "Latex",
54 "Lisp",
55 "Lua",
56 "Matlab",
57 "MoonScript",
58 "Nim",
59 "OCaml",
60 "ObjectiveC",
61 "Octave",
62 "PHP",
63 "Pascal",
64 "Perl",
65 "Prolog",
66 "Python",
67 "Racket",
68 "Ruby",
69 "Rust",
70 "SML",
71 "SQL",
72 "Scala",
73 "Scheme",
74 "Swift",
75 "Tikz",
76 "TypeScript",
77 "VisualBasic",
78 "XML",
79 "YAML",
80];
81
82pub fn code_language_label(lang: &str) -> &'static str {
88 code_language(Some(lang))
89}
90
91pub(crate) fn code_language(lang: Option<&str>) -> &'static str {
92 match lang {
93 Some(l) => CODE_LANGUAGES
94 .iter()
95 .find(|c| c.eq_ignore_ascii_case(l))
96 .copied()
97 .unwrap_or("unknown"),
98 None => "unknown",
99 }
100}
101
102#[derive(Clone, Copy)]
106struct ExactProv {
107 page_no: usize,
108 bbox: [f64; 4],
109 bottom_left: bool,
110 charspan: [usize; 2],
111}
112
113impl From<&crate::tree::TreeProv> for ExactProv {
114 fn from(p: &crate::tree::TreeProv) -> Self {
115 ExactProv {
116 page_no: p.page_no,
117 bbox: p.bbox,
118 bottom_left: p.bottom_left,
119 charspan: p.charspan,
120 }
121 }
122}
123
124fn clamp_boxes_to_pages(out: &mut Value, pages: &[(usize, f64, f64)]) {
129 if pages.is_empty() {
130 return;
131 }
132 let size = |page_no: &Value| -> Option<(f64, f64)> {
133 let n = page_no.as_u64()? as usize;
134 pages
135 .iter()
136 .find(|(p, _, _)| *p == n)
137 .map(|(_, w, h)| (*w, *h))
138 };
139 let r2 = |v: f64| (v * 100.0).round() / 100.0;
140 let clamp_bbox = |bbox: &mut Value, (w, h): (f64, f64)| {
141 for (key, hi) in [("l", w), ("r", w), ("t", h), ("b", h)] {
142 if let Some(v) = bbox.get(key).and_then(Value::as_f64) {
143 bbox[key] = json!(r2(v.clamp(0.0, hi.max(0.0))));
144 }
145 }
146 };
147 for bucket in [
148 "texts",
149 "pictures",
150 "tables",
151 "key_value_items",
152 "form_items",
153 "field_regions",
154 "field_items",
155 ] {
156 let Some(items) = out.get_mut(bucket).and_then(Value::as_array_mut) else {
157 continue;
158 };
159 for item in items {
160 let mut table_page: Option<Option<(f64, f64)>> = None;
161 if let Some(provs) = item.get_mut("prov").and_then(Value::as_array_mut) {
162 let mut page_nos: Vec<u64> = Vec::new();
163 for prov in provs.iter_mut() {
164 if let Some(n) = prov.get("page_no").and_then(Value::as_u64) {
165 page_nos.push(n);
166 }
167 let Some(sz) = prov.get("page_no").and_then(size) else {
168 continue;
169 };
170 if let Some(bbox) = prov.get_mut("bbox") {
171 clamp_bbox(bbox, sz);
172 }
173 }
174 page_nos.sort_unstable();
175 page_nos.dedup();
176 if let [only] = page_nos[..] {
177 table_page = Some(size(&json!(only)));
178 }
179 }
180 if let (Some(Some(sz)), Some(data)) = (table_page, item.get_mut("data")) {
182 for key in ["table_cells", "grid"] {
183 let Some(rows) = data.get_mut(key).and_then(Value::as_array_mut) else {
184 continue;
185 };
186 for entry in rows.iter_mut() {
187 let cells: Vec<&mut Value> = match entry {
188 Value::Array(row) => row.iter_mut().collect(),
189 other => vec![other],
190 };
191 for cell in cells {
192 if let Some(bbox) = cell.get_mut("bbox").filter(|b| b.is_object()) {
193 clamp_bbox(bbox, sz);
194 }
195 }
196 }
197 }
198 }
199 }
200 }
201}
202
203fn formatting_json(f: &crate::tree::Formatting) -> Value {
205 json!({
206 "bold": f.bold,
207 "italic": f.italic,
208 "underline": f.underline,
209 "strikethrough": f.strikethrough,
210 "script": match f.script {
211 crate::Script::Baseline => "baseline",
212 crate::Script::Sub => "sub",
213 crate::Script::Super => "super",
214 },
215 })
216}
217
218fn track_json(t: &crate::tree::TreeTrack) -> Value {
221 let mut m = serde_json::Map::new();
222 m.insert("kind".into(), json!("track"));
223 m.insert("start_time".into(), json!(t.start_time));
224 m.insert("end_time".into(), json!(t.end_time));
225 if let Some(id) = &t.identifier {
226 m.insert("identifier".into(), json!(id));
227 }
228 if let Some(v) = &t.voice {
229 m.insert("voice".into(), json!(v));
230 }
231 Value::Object(m)
232}
233
234fn image_ref_json(img: &crate::PictureImage) -> Value {
238 let (mimetype, uri) = crate::pixel_digest::docling_data_uri(img);
239 json!({
240 "mimetype": mimetype,
241 "dpi": img.dpi,
242 "size": { "width": img.width as f64, "height": img.height as f64 },
243 "uri": uri,
244 })
245}
246
247pub fn to_json(doc: &DoclingDocument) -> Value {
249 build_json(doc, false)
250}
251
252pub(crate) fn to_json_with_notes(doc: &DoclingDocument) -> Value {
257 build_json(doc, true)
258}
259
260fn build_json(doc: &DoclingDocument, note_keys: bool) -> Value {
261 let mut b = Builder {
262 note_keys,
263 ..Builder::default()
264 };
265 let body = match &doc.tree {
269 Some(tree) => {
270 for n in &doc.nodes {
273 if let Node::PageInfo {
274 page_no,
275 width,
276 height,
277 } = n
278 {
279 if *page_no > 0 {
280 b.pages.push((*page_no, *width as f64, *height as f64));
281 }
282 }
283 }
284 let mut tree = tree.clone();
288 tree.wrap_misplaced_list_items();
289 b.write_tree(&tree)
290 }
291 None => b.walk_into(&doc.nodes, "#/body"),
292 };
293 b.link_comments();
294
295 let mut out = json!({
296 "schema_name": "DoclingDocument",
297 "version": SCHEMA_VERSION,
298 "name": doc.name,
299 "origin": {
300 "mimetype": "text/plain",
301 "binary_hash": fnv1a(&doc.name),
302 "filename": doc.name,
303 },
304 "furniture": {
305 "self_ref": "#/furniture",
306 "children": [],
307 "content_layer": "furniture",
308 "name": "_root_",
309 "label": "unspecified",
310 },
311 "body": {
312 "self_ref": "#/body",
313 "children": body,
314 "content_layer": "body",
315 "name": "_root_",
316 "label": "unspecified",
317 },
318 "groups": b.groups,
319 "texts": b.texts,
320 "pictures": b.pictures,
321 "tables": b.tables,
322 "key_value_items": b.key_value_items,
323 "form_items": [],
324 "pages": b.pages.iter().map(|(n, w, h)| {
325 let r2 = |v: f64| (v * 100.0).round() / 100.0;
326 let mut page = serde_json::Map::new();
329 page.insert("size".into(), json!({ "width": r2(*w), "height": r2(*h) }));
330 if let Some(img) = doc.page_images.get(n) {
331 page.insert("image".into(), image_ref_json(img));
332 }
333 page.insert("page_no".into(), json!(n));
334 (n.to_string(), Value::Object(page))
335 }).collect::<serde_json::Map<String, Value>>(),
336 });
337
338 clamp_boxes_to_pages(&mut out, &b.pages);
347
348 if !b.field_regions.is_empty() {
353 if let Some(obj) = out.as_object_mut() {
354 let pages = obj.remove("pages");
355 obj.insert("field_regions".into(), Value::Array(b.field_regions));
356 obj.insert("field_items".into(), Value::Array(b.field_items));
357 if let Some(pages) = pages {
358 obj.insert("pages".into(), pages);
359 }
360 }
361 }
362 out
363}
364
365fn classification_meta(classes: &[crate::PictureClass]) -> Value {
372 json!({
373 "classification": {
374 "predictions": classes.iter().map(|c| json!({
375 "confidence": c.confidence as f64,
376 "created_by": "DocumentPictureClassifier",
377 "class_name": c.class_name,
378 })).collect::<Vec<_>>(),
379 },
380 "annotations": [{
381 "kind": "classification",
382 "provenance": "DocumentPictureClassifier",
383 "predicted_classes": classes.iter().map(|c| json!({
384 "class_name": c.class_name,
385 "confidence": c.confidence as f64,
386 })).collect::<Vec<_>>(),
387 }],
388 })
389}
390
391#[allow(clippy::too_many_arguments)]
402fn cell_value(
403 row_span: usize,
404 col_span: usize,
405 start_row: usize,
406 end_row: usize,
407 start_col: usize,
408 end_col: usize,
409 text: String,
410 column_header: bool,
411 row_header: bool,
412 row_section: bool,
413 bbox: Option<[f32; 4]>,
414) -> Value {
415 let mut m = serde_json::Map::with_capacity(12);
416 m.insert("row_span".into(), row_span.into());
417 m.insert("col_span".into(), col_span.into());
418 m.insert("start_row_offset_idx".into(), start_row.into());
419 m.insert("end_row_offset_idx".into(), end_row.into());
420 m.insert("start_col_offset_idx".into(), start_col.into());
421 m.insert("end_col_offset_idx".into(), end_col.into());
422 m.insert("text".into(), Value::String(text));
423 m.insert("column_header".into(), column_header.into());
424 m.insert("row_header".into(), row_header.into());
425 m.insert("row_section".into(), row_section.into());
426 m.insert("fillable".into(), false.into());
427 if let Some(b) = bbox {
428 m.insert(
429 "bbox".into(),
430 json!({
431 "l": b[0], "t": b[1], "r": b[2], "b": b[3],
432 "coord_origin": "TOPLEFT",
433 }),
434 );
435 }
436 Value::Object(m)
437}
438
439fn table_data(t: &Table) -> Value {
442 table_data_with(t, false)
443}
444
445fn table_data_with(t: &Table, raw: bool) -> Value {
448 let cell_text = |s: &str| {
449 if raw {
450 s.to_string()
451 } else {
452 unescape_text(&crate::markdown::strip_hard_breaks(s))
453 }
454 };
455 let mut num_rows = t.rows.len();
456 let mut num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
457 let ragged = t.rows.iter().any(|r| r.len() != num_cols);
465 if let Some(first_class) = t.cells.as_ref().filter(|c| ragged && !c.is_empty()) {
466 for c in first_class {
467 num_rows = num_rows.max(c.start_row + c.row_span);
468 num_cols = num_cols.max(c.start_col + c.col_span);
469 }
470 }
471 let mut grid = Vec::with_capacity(num_rows);
472 let mut cells = Vec::new();
473 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
477 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
478 for c in first_class {
479 let idx = cells.len();
480 cells.push(cell_value(
481 c.row_span,
482 c.col_span,
483 c.start_row,
484 c.start_row + c.row_span,
485 c.start_col,
486 c.start_col + c.col_span,
487 cell_text(&c.text),
488 c.column_header,
489 c.row_header,
490 c.row_section,
491 c.bbox,
492 ));
493 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
494 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
495 slot[r * num_cols + k] = Some(idx);
496 }
497 }
498 }
499 for r in 0..num_rows {
500 let mut grid_row = Vec::with_capacity(num_cols);
501 for c in 0..num_cols {
502 grid_row.push(match slot[r * num_cols + c] {
503 Some(i) => cells[i].clone(),
504 None => cell_value(
505 1,
506 1,
507 r,
508 r + 1,
509 c,
510 c + 1,
511 String::new(),
512 false,
513 false,
514 false,
515 None,
516 ),
517 });
518 }
519 grid.push(grid_row);
520 }
521 } else {
522 let s = t.structure.as_ref();
523 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
524 grid.and_then(|g| g.get(r))
525 .and_then(|row| row.get(c))
526 .copied()
527 .unwrap_or(false)
528 };
529 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
530 let (mut r0, mut c0) = (r, c);
531 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
532 c0 -= 1;
533 }
534 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
535 r0 -= 1;
536 }
537 (r0, c0)
538 };
539 let anchors: Vec<(usize, usize)> = (0..num_rows)
542 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
543 .map(|(r, c)| anchor_of(r, c))
544 .collect();
545 let mut extent: Vec<(usize, usize)> = (0..num_rows)
546 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
547 .collect();
548 for (i, &(ar, ac)) in anchors.iter().enumerate() {
549 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
550 let e = &mut extent[ar * num_cols + ac];
551 e.0 = e.0.max(r);
552 e.1 = e.1.max(c);
553 }
554 for (r, row) in t.rows.iter().enumerate() {
555 let mut grid_row = Vec::with_capacity(num_cols);
556 for c in 0..num_cols {
557 let (ar, ac) = anchors[r * num_cols + c];
558 if (ar, ac) == (r, c) {
559 let (er, ec) = extent[r * num_cols + c];
560 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
561 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
562 Some(s) => flag(Some(&s.col_header), r, c),
563 None => r == 0,
564 };
565 slot[r * num_cols + c] = Some(cells.len());
566 cells.push(cell_value(
567 er - r + 1,
568 ec - c + 1,
569 r,
570 er + 1,
571 c,
572 ec + 1,
573 text,
574 column_header,
575 flag(s.map(|s| &s.row_header), r, c),
576 false,
577 None,
578 ));
579 }
580 grid_row.push(match slot[ar * num_cols + ac] {
581 Some(i) => cells[i].clone(),
582 None => Value::Null,
583 });
584 }
585 grid.push(grid_row);
586 }
587 }
588 json!({
589 "table_cells": cells,
590 "num_rows": num_rows,
591 "num_cols": num_cols,
592 "orientation": "rot_0",
593 "grid": grid,
594 })
595}
596
597#[derive(Default)]
598struct Builder {
599 note_keys: bool,
603 texts: Vec<Value>,
604 groups: Vec<Value>,
605 tables: Vec<Value>,
606 pictures: Vec<Value>,
607 field_regions: Vec<Value>,
608 field_items: Vec<Value>,
609 key_value_items: Vec<Value>,
610 pages: Vec<(usize, f64, f64)>,
614 cur_page: usize,
616 cur_w: f64,
617 cur_h: f64,
618 pending_loc: Option<[u16; 4]>,
621 pending_exact: Option<ExactProv>,
624 pending_siblings: Vec<Value>,
628 pending_after: Vec<Value>,
632 pending_body: Vec<Value>,
636 comment_groups: Vec<String>,
640 last_comment_section: Option<(String, String)>,
645 pending_comments: Vec<(String, Vec<usize>)>,
649}
650
651impl Builder {
652 fn take_prov(&mut self, char_len: usize) -> Value {
658 let prov = self.prov_json(char_len, false);
659 self.pending_exact = None;
660 self.pending_loc = None;
661 prov
662 }
663
664 fn prov_json(&self, char_len: usize, span_over_text: bool) -> Value {
669 let r2 = |v: f64| (v * 100.0).round() / 100.0;
670 if let Some(ExactProv {
671 page_no,
672 bbox: [l, t, r, b],
673 bottom_left,
674 charspan,
675 }) = self.pending_exact
676 {
677 let charspan = if span_over_text {
678 [0, char_len]
679 } else {
680 charspan
681 };
682 return json!([{
683 "page_no": page_no,
684 "bbox": {
685 "l": r2(l), "t": r2(t), "r": r2(r), "b": r2(b),
686 "coord_origin": if bottom_left { "BOTTOMLEFT" } else { "TOPLEFT" },
687 },
688 "charspan": charspan,
689 }]);
690 }
691 let Some([x0, y0, x1, y1]) = self.pending_loc else {
692 return json!([]);
693 };
694 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
698 return json!([{
699 "page_no": self.cur_page,
700 "bbox": { "l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT" },
701 "charspan": [0, char_len],
702 }]);
703 }
704 json!([{
705 "page_no": self.cur_page,
706 "bbox": {
707 "l": r2(x0 as f64 * self.cur_w / 512.0),
708 "t": r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
709 "r": r2(x1 as f64 * self.cur_w / 512.0),
710 "b": r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
711 "coord_origin": "BOTTOMLEFT",
712 },
713 "charspan": [0, char_len],
714 }])
715 }
716
717 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
720 if self.pending_loc.is_none() && self.cur_page > 0 {
721 self.pending_loc = loc;
722 }
723 }
724
725 fn link_comments(&mut self) {
733 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
734 .into_iter()
735 .map(|(item, comments)| {
736 let refs = comments
737 .iter()
738 .filter_map(|i| self.comment_groups.get(*i))
739 .map(|r| json!({ "$ref": r }))
740 .collect();
741 (item, refs)
742 })
743 .collect();
744 for (item, comment_refs) in refs {
745 if comment_refs.is_empty() {
746 continue;
747 }
748 let Some(target) = self.item_mut(&item) else {
749 continue;
750 };
751 let Some(obj) = target.as_object_mut() else {
752 continue;
753 };
754 let tail: Vec<(String, Value)> = obj
755 .iter()
756 .skip_while(|(k, _)| k.as_str() != "prov")
757 .skip(1)
758 .map(|(k, v)| (k.clone(), v.clone()))
759 .collect();
760 for (k, _) in &tail {
761 obj.shift_remove(k);
762 }
763 obj.insert("comments".into(), Value::Array(comment_refs));
764 for (k, v) in tail {
765 obj.insert(k, v);
766 }
767 }
768 }
769
770 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
772 let idx = ref_index(self_ref)?;
773 let bucket = if self_ref.starts_with("#/texts/") {
774 &mut self.texts
775 } else if self_ref.starts_with("#/tables/") {
776 &mut self.tables
777 } else if self_ref.starts_with("#/pictures/") {
778 &mut self.pictures
779 } else if self_ref.starts_with("#/groups/") {
780 &mut self.groups
781 } else {
782 return None;
783 };
784 bucket.get_mut(idx)
785 }
786
787 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
791 use crate::tree::TreeKind;
792 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
795 let (mut nt, mut ng, mut ntb, mut np, mut nf, mut nk) = (0, 0, 0, 0, 0, 0);
796 for item in &tree.items {
797 if item.deleted {
798 refs.push(String::new());
799 continue;
800 }
801 let r = match &item.kind {
802 TreeKind::Text { .. } | TreeKind::Code { .. } => {
803 nt += 1;
804 format!("#/texts/{}", nt - 1)
805 }
806 TreeKind::Group { .. } => {
807 ng += 1;
808 format!("#/groups/{}", ng - 1)
809 }
810 TreeKind::Table { .. } => {
811 ntb += 1;
812 format!("#/tables/{}", ntb - 1)
813 }
814 TreeKind::Picture { .. } => {
815 np += 1;
816 format!("#/pictures/{}", np - 1)
817 }
818 TreeKind::FieldRegion { items } => {
819 nt += items
822 .iter()
823 .map(|i| {
824 [&i.marker, &i.key, &i.value]
825 .iter()
826 .filter(|p| p.is_some())
827 .count()
828 })
829 .sum::<usize>();
830 nf += 1;
831 format!("#/field_regions/{}", nf - 1)
832 }
833 TreeKind::KeyValueGraph { .. } => {
834 nk += 1;
835 format!("#/key_value_items/{}", nk - 1)
836 }
837 };
838 refs.push(r);
839 }
840 let ref_of = |id: usize| json!({ "$ref": refs[id] });
841 for (id, item) in tree.items.iter().enumerate() {
842 if item.deleted {
843 continue;
844 }
845 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
846 let children: Vec<Value> = item
850 .children
851 .iter()
852 .filter(|&&c| !tree.items[c].deleted)
853 .map(|&c| ref_of(c))
854 .collect();
855 let layer = item.layer.map_or("body", |l| l.value());
856 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
859 let self_ref = match &item.kind {
860 TreeKind::Text {
861 label,
862 text,
863 orig,
864 formatting,
865 hyperlink,
866 level,
867 list,
868 } => {
869 let mut tail = serde_json::Map::new();
872 if let Some(f) = formatting {
873 tail.insert("formatting".into(), formatting_json(f));
874 }
875 if let Some(h) = hyperlink {
876 tail.insert("hyperlink".into(), json!(h));
877 }
878 if let Some(l) = level {
879 tail.insert("level".into(), json!(l));
880 }
881 if let Some(l) = list {
882 tail.insert("enumerated".into(), json!(l.enumerated));
883 tail.insert("marker".into(), json!(l.marker));
884 }
885 let r = format!("#/texts/{}", self.texts.len());
886 let prov = self.take_prov(text.chars().count());
887 let mut item_json = json!({
888 "self_ref": r,
889 "parent": { "$ref": parent },
890 "children": children,
891 "content_layer": layer,
892 "label": label,
893 "prov": prov,
894 });
895 if !item.comments.is_empty() {
898 item_json["comments"] =
899 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
900 }
901 if let Some(track) = &item.source {
904 item_json["source"] = json!([track_json(track)]);
905 }
906 merge(
907 &mut item_json,
908 json!({
909 "orig": orig.as_deref().unwrap_or(text),
910 "text": text,
911 }),
912 );
913 merge(&mut item_json, Value::Object(tail));
914 if self.note_keys {
915 if !item.notes.is_empty() {
916 item_json["_notes"] = Value::Array(
917 item.notes
918 .iter()
919 .map(|n| json!([n.offset, n.text]))
920 .collect(),
921 );
922 }
923 if item.note_body {
924 item_json["_note_body"] = json!(true);
925 }
926 }
927 self.texts.push(item_json);
928 r
929 }
930 TreeKind::Code {
931 text,
932 orig,
933 language,
934 formatting,
935 hyperlink,
936 } => {
937 let r = format!("#/texts/{}", self.texts.len());
938 let prov = self.take_prov(text.chars().count());
939 let mut item_json = json!({
940 "self_ref": r,
941 "parent": { "$ref": parent },
942 "children": children,
943 "content_layer": layer,
944 "label": "code",
945 "prov": prov,
946 });
947 if !item.comments.is_empty() {
948 item_json["comments"] =
949 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
950 }
951 merge(
952 &mut item_json,
953 json!({
954 "orig": orig.as_deref().unwrap_or(text),
955 "text": text,
956 }),
957 );
958 if let Some(f) = formatting {
959 item_json["formatting"] = formatting_json(f);
960 }
961 if let Some(h) = hyperlink {
962 item_json["hyperlink"] = json!(h);
963 }
964 merge(
965 &mut item_json,
966 json!({
967 "captions": [],
968 "references": [],
969 "footnotes": [],
970 "code_language": code_language(language.as_deref()),
971 }),
972 );
973 self.texts.push(item_json);
974 r
975 }
976 TreeKind::Group { label, name } => {
977 self.pending_exact = None;
978 let r = format!("#/groups/{}", self.groups.len());
979 self.groups.push(json!({
980 "self_ref": r,
981 "parent": { "$ref": parent },
982 "children": children,
983 "content_layer": layer,
984 "name": name,
985 "label": label,
986 }));
987 r
988 }
989 TreeKind::Table {
990 table,
991 rich_cells,
992 captions,
993 } => {
994 let r = self.add_table_with(table, parent, true);
996 let idx = ref_index(&r).expect("table ref");
997 let t = &mut self.tables[idx];
998 t["children"] = Value::Array(children);
999 t["content_layer"] = json!(layer);
1000 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
1001 for &(row, col, group) in rich_cells {
1005 let cell_ref = ref_of(group);
1006 let hit = |c: &Value| {
1007 c["start_row_offset_idx"] == json!(row)
1008 && c["start_col_offset_idx"] == json!(col)
1009 };
1010 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
1011 for c in cells.iter_mut().filter(|c| hit(c)) {
1012 c["ref"] = cell_ref.clone();
1013 }
1014 }
1015 }
1016 r
1017 }
1018 TreeKind::Picture {
1019 captions,
1020 image,
1021 classification,
1022 confidence,
1023 chart,
1024 dpi,
1025 } => {
1026 let mut meta = classification.as_ref().map(|c| {
1031 let mut pred = serde_json::Map::new();
1032 if let Some(conf) = confidence {
1033 pred.insert("confidence".into(), json!(conf));
1034 }
1035 pred.insert("class_name".into(), json!(c));
1036 json!({ "classification": { "predictions": [pred] } })
1037 });
1038 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
1039 if !t.rows.is_empty() {
1040 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
1041 }
1042 }
1043 let prov = self.take_prov(0);
1044 let r = self.push_picture(
1045 prov,
1046 captions.iter().map(|&c| ref_of(c)).collect(),
1047 children,
1048 image.as_ref(),
1049 meta,
1050 parent,
1051 );
1052 if let Some(idx) = ref_index(&r) {
1053 self.pictures[idx]["content_layer"] = json!(layer);
1054 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
1057 img["dpi"] = json!(dpi);
1058 }
1059 }
1060 r
1061 }
1062 TreeKind::FieldRegion { items } => {
1063 self.pending_exact = None;
1064 let r = self.add_field_region(items, parent);
1065 if let Some(region) = self.field_regions.last_mut() {
1066 region["content_layer"] = json!(layer);
1067 }
1068 r
1069 }
1070 TreeKind::KeyValueGraph { cells, links } => {
1071 self.pending_exact = None;
1072 let r = self.add_key_value_graph(cells, links, parent);
1073 if let Some(item) = self.key_value_items.last_mut() {
1074 item["content_layer"] = json!(layer);
1075 }
1076 r
1077 }
1078 };
1079 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1080 }
1081 tree.body
1082 .iter()
1083 .filter(|&&c| !tree.items[c].deleted)
1084 .map(|&c| ref_of(c))
1085 .collect()
1086 }
1087
1088 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1089 match node {
1090 Node::Heading { level: 1, text } => {
1091 Some(self.add_text("title", text, parent, json!({})))
1092 }
1093 Node::Heading { level, text } => Some(self.add_text(
1094 "section_header",
1095 text,
1096 parent,
1097 json!({ "level": level.saturating_sub(1) }),
1098 )),
1099 Node::Caption { text, href } => {
1100 let extra = match href {
1101 Some(url) => json!({ "hyperlink": url }),
1102 None => json!({}),
1103 };
1104 Some(self.add_text("caption", text, parent, extra))
1105 }
1106 Node::LabeledText { label, text, href } => {
1109 let extra = match href {
1110 Some(url) => json!({ "hyperlink": url }),
1111 None => json!({}),
1112 };
1113 Some(self.add_text(label, text, parent, extra))
1114 }
1115 Node::Paragraph { text } => {
1116 let t = text.trim();
1119 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1120 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1121 _ => Some(self.add_text("text", text, parent, json!({}))),
1122 }
1123 }
1124 Node::CheckboxItem { checked, text } => {
1125 let label = if *checked {
1131 "checkbox_selected"
1132 } else {
1133 "checkbox_unselected"
1134 };
1135 Some(self.add_text(label, text, parent, json!({})))
1136 }
1137 Node::Code {
1138 language,
1139 text,
1140 orig,
1141 ..
1142 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1143 Node::Formula {
1146 latex,
1147 orig,
1148 location,
1149 } => {
1150 self.adopt_loc(*location);
1151 Some(self.add_formula_item(latex, orig, parent))
1152 }
1153 Node::CommentSection {
1159 name,
1160 text,
1161 refs_note_text,
1162 grouped,
1163 } => {
1164 if !*grouped {
1165 let child =
1169 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1170 self.comment_groups.push(child.clone());
1171 return Some(child);
1172 }
1173 if let Some((_, self_ref)) = self
1174 .last_comment_section
1175 .as_ref()
1176 .filter(|(n, _)| n == name)
1177 .cloned()
1178 {
1179 let child =
1182 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1183 if let Some(children) =
1184 self.groups[group_index(&self_ref)]["children"].as_array_mut()
1185 {
1186 children.push(json!({ "$ref": child }));
1187 }
1188 self.comment_groups.push(if *refs_note_text {
1189 child
1190 } else {
1191 self_ref.clone()
1192 });
1193 return Some(self_ref);
1194 }
1195 let self_ref = format!("#/groups/{}", self.groups.len());
1196 self.groups.push(Value::Null);
1197 let child =
1198 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1199 self.groups[group_index(&self_ref)] = json!({
1200 "self_ref": self_ref,
1201 "parent": { "$ref": parent },
1202 "children": [{ "$ref": child }],
1203 "content_layer": "notes",
1204 "name": name,
1205 "label": "comment_section",
1206 });
1207 self.last_comment_section = Some((name.clone(), self_ref.clone()));
1208 self.comment_groups.push(if *refs_note_text {
1209 child
1210 } else {
1211 self_ref.clone()
1212 });
1213 Some(self_ref)
1214 }
1215 Node::Commented { comments, inner } => {
1218 let item = self.add_node(inner, parent)?;
1219 if !comments.is_empty() {
1220 self.pending_comments.push((item.clone(), comments.clone()));
1221 }
1222 Some(item)
1223 }
1224 Node::Table(t) => Some(self.add_table(t, parent)),
1225 Node::Picture {
1226 caption,
1227 caption_href,
1228 image,
1229 classification,
1230 caption_parent,
1231 caption_location,
1232 } => Some(self.add_picture(
1233 caption.as_deref(),
1234 caption_href.as_deref(),
1235 image.as_ref(),
1236 classification.as_deref().map(classification_meta),
1237 parent,
1238 (*caption_parent, *caption_location),
1239 )),
1240 Node::Chart {
1245 kind,
1246 table,
1247 caption,
1248 location,
1249 } => {
1250 self.adopt_loc(*location);
1251 let mut meta = json!({
1252 "classification": { "predictions": [{ "class_name": kind }] },
1253 });
1254 if !table.rows.is_empty() {
1255 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1256 }
1257 let mut captions = Vec::new();
1263 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1264 let prov = self.prov_json(unescape_text(cap).chars().count(), true);
1265 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1266 self.pending_siblings.push(json!({ "$ref": cap_ref }));
1267 captions.push(json!({ "$ref": cap_ref }));
1268 }
1269 let prov = self.take_prov(0);
1270 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1271 }
1272 Node::DoclangOnly(_) => None,
1274 Node::Group {
1275 label,
1276 name,
1277 layer,
1278 children,
1279 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1280 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1281 Node::KeyValueGraph { cells, links } => {
1282 Some(self.add_key_value_graph(cells, links, parent))
1283 }
1284 Node::InlineGroup { md_text, .. } => {
1287 Some(self.add_text("text", md_text, parent, json!({})))
1288 }
1289 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1291 Node::Furniture {
1297 layer: ContentLayer::Notes,
1298 inner,
1299 } => {
1300 let item = self.add_node(inner, parent)?;
1301 self.set_layer(&item, "notes");
1302 Some(item)
1303 }
1304 Node::Furniture { .. } => None,
1305 Node::PageFurniture {
1309 footer,
1310 location,
1311 text,
1312 } => {
1313 if self.cur_page > 0 {
1314 self.pending_loc = Some(*location);
1315 }
1316 let label = if *footer {
1317 "page_footer"
1318 } else {
1319 "page_header"
1320 };
1321 let item = self.add_text(label, text, parent, json!({}));
1322 self.pending_loc = None;
1323 self.set_layer(&item, "furniture");
1324 Some(item)
1325 }
1326 Node::FurnitureText { label, text } => {
1329 let item = self.add_text(label, text, parent, json!({}));
1330 self.set_layer(&item, "furniture");
1331 Some(item)
1332 }
1333 Node::PictureChildren(children) => {
1337 let pic = self.pictures.len().checked_sub(1)?;
1338 let pic_ref = format!("#/pictures/{pic}");
1339 let refs = self.walk_into(children, &pic_ref);
1340 if let Some(list) = self.pictures[pic]["children"].as_array_mut() {
1341 list.extend(refs);
1342 }
1343 None
1344 }
1345 Node::Located { location, inner } => {
1350 if self.cur_page > 0 {
1351 self.pending_loc = Some(*location);
1352 }
1353 let r = self.add_node(inner, parent);
1354 self.pending_loc = None;
1355 r
1356 }
1357 Node::Prov {
1358 page_no,
1359 bbox,
1360 charspan,
1361 inner,
1362 ..
1363 } => {
1364 self.pending_exact = Some(ExactProv {
1365 page_no: *page_no,
1366 bbox: bbox.map(f64::from),
1367 bottom_left: false,
1368 charspan: *charspan,
1369 });
1370 let r = self.add_node(inner, parent);
1371 self.pending_exact = None;
1372 r
1373 }
1374 Node::PageBreak => None,
1376 Node::PageInfo {
1379 page_no,
1380 width,
1381 height,
1382 } => {
1383 self.cur_page = *page_no;
1384 self.cur_w = *width as f64;
1385 self.cur_h = *height as f64;
1386 if *page_no > 0 {
1387 self.pages.push((*page_no, self.cur_w, self.cur_h));
1388 }
1389 None
1390 }
1391 Node::ListItem { .. } => None,
1393 }
1394 }
1395
1396 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1400 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1401 self.field_regions.push(Value::Null);
1402 let region_index = self.field_regions.len() - 1;
1403 let mut item_refs = Vec::new();
1404 for item in items {
1405 item_refs.push(json!({ "$ref": self.add_field_item(item, &self_ref) }));
1406 }
1407 self.field_regions[region_index] = json!({
1408 "self_ref": self_ref,
1409 "parent": { "$ref": parent },
1410 "children": item_refs,
1411 "content_layer": "body",
1412 "label": "field_region",
1413 "prov": [],
1414 });
1415 self_ref
1416 }
1417
1418 fn add_key_value_graph(
1422 &mut self,
1423 cells: &[crate::GraphCell],
1424 links: &[crate::GraphLink],
1425 parent: &str,
1426 ) -> String {
1427 let self_ref = format!("#/key_value_items/{}", self.key_value_items.len());
1428 let cells: Vec<Value> = cells
1429 .iter()
1430 .map(|c| {
1431 json!({
1432 "label": c.label,
1433 "cell_id": c.cell_id,
1434 "text": c.text,
1435 "orig": c.orig,
1436 })
1437 })
1438 .collect();
1439 let links: Vec<Value> = links
1440 .iter()
1441 .map(|l| {
1442 json!({
1443 "label": l.label,
1444 "source_cell_id": l.source_cell_id,
1445 "target_cell_id": l.target_cell_id,
1446 })
1447 })
1448 .collect();
1449 self.key_value_items.push(json!({
1450 "self_ref": self_ref,
1451 "parent": { "$ref": parent },
1452 "children": [],
1453 "content_layer": "body",
1454 "label": "key_value_region",
1455 "prov": [],
1456 "captions": [],
1457 "references": [],
1458 "footnotes": [],
1459 "graph": { "cells": cells, "links": links },
1460 }));
1461 self_ref
1462 }
1463
1464 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1465 let self_ref = format!("#/field_items/{}", self.field_items.len());
1466 self.field_items.push(Value::Null);
1467 let item_index = self.field_items.len() - 1;
1468 let mut child_refs = Vec::new();
1469 for (label, text) in [
1470 ("marker", &item.marker),
1471 ("field_key", &item.key),
1472 ("field_value", &item.value),
1473 ] {
1474 if let Some(text) = text {
1475 let extra = match (label, &item.value_kind) {
1478 ("field_value", Some(kind)) => json!({ "kind": kind }),
1479 _ => json!({}),
1480 };
1481 child_refs.push(json!({ "$ref": self.add_text(label, text, &self_ref, extra) }));
1482 }
1483 }
1484 self.field_items[item_index] = json!({
1485 "self_ref": self_ref,
1486 "parent": { "$ref": parent },
1487 "children": child_refs,
1488 "content_layer": "body",
1489 "label": "field_item",
1490 "prov": [],
1491 });
1492 self_ref
1493 }
1494
1495 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1499 let bucket = match self_ref.split('/').nth(1) {
1500 Some("texts") => &mut self.texts,
1501 Some("tables") => &mut self.tables,
1502 Some("pictures") => &mut self.pictures,
1503 Some("groups") => &mut self.groups,
1504 _ => return,
1505 };
1506 if let Some(item) = self_ref
1507 .rsplit('/')
1508 .next()
1509 .and_then(|i| i.parse::<usize>().ok())
1510 .and_then(|i| bucket.get_mut(i))
1511 {
1512 item["content_layer"] = json!(layer);
1513 }
1514 }
1515
1516 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1517 let prov = self.take_prov(unescape_text(text).chars().count());
1518 self.add_text_with(label, text, parent, extra, prov)
1519 }
1520
1521 fn add_text_with(
1524 &mut self,
1525 label: &str,
1526 text: &str,
1527 parent: &str,
1528 extra: Value,
1529 prov: Value,
1530 ) -> String {
1531 let self_ref = format!("#/texts/{}", self.texts.len());
1532 let raw = unescape_text(text);
1533 let mut item = json!({
1534 "self_ref": self_ref,
1535 "parent": { "$ref": parent },
1536 "children": [],
1537 "content_layer": "body",
1538 "label": label,
1539 "prov": prov,
1540 "orig": raw,
1541 "text": raw,
1542 });
1543 merge(&mut item, extra);
1544 self.texts.push(item);
1545 self_ref
1546 }
1547
1548 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
1551 let self_ref = format!("#/texts/{}", self.texts.len());
1552 let prov = self.take_prov(latex.chars().count());
1553 self.texts.push(json!({
1554 "self_ref": self_ref,
1555 "parent": { "$ref": parent },
1556 "children": [],
1557 "content_layer": "body",
1558 "label": "formula",
1559 "prov": prov,
1560 "orig": latex,
1561 "text": latex,
1562 }));
1563 self_ref
1564 }
1565
1566 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
1570 let self_ref = format!("#/texts/{}", self.texts.len());
1571 let prov = self.take_prov(latex.chars().count());
1572 self.texts.push(json!({
1573 "self_ref": self_ref,
1574 "parent": { "$ref": parent },
1575 "children": [],
1576 "content_layer": "body",
1577 "label": "formula",
1578 "prov": prov,
1579 "orig": orig,
1580 "text": latex,
1581 }));
1582 self_ref
1583 }
1584
1585 fn add_code(
1586 &mut self,
1587 text: &str,
1588 language: Option<&str>,
1589 orig: Option<&str>,
1590 parent: &str,
1591 ) -> String {
1592 let self_ref = format!("#/texts/{}", self.texts.len());
1593 let raw = unescape_text(text);
1594 let prov = self.take_prov(raw.chars().count());
1595 self.texts.push(json!({
1596 "self_ref": self_ref,
1597 "parent": { "$ref": parent },
1598 "children": [],
1599 "content_layer": "body",
1600 "label": "code",
1601 "prov": prov,
1602 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
1605 "text": raw,
1606 "captions": [],
1607 "references": [],
1608 "footnotes": [],
1609 "code_language": code_language(language),
1610 }));
1611 self_ref
1612 }
1613
1614 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
1617 let self_ref = format!("#/groups/{}", self.groups.len());
1618 self.groups.push(Value::Null);
1620 let base = level_of(&items[0]);
1621 let mut children = Vec::new();
1622 let mut i = 0;
1623 while i < items.len() {
1624 if !matches!(items[i], Node::ListItem { .. }) {
1627 i += 1;
1628 continue;
1629 }
1630 let lvl = level_of(&items[i]);
1631 if lvl > base {
1632 i += 1;
1634 continue;
1635 }
1636 let item_ref = self.add_list_item(&items[i], &self_ref);
1637 let mut j = i + 1;
1639 while j < items.len() && level_of(&items[j]) > base {
1640 j += 1;
1641 }
1642 if j > i + 1 {
1643 let mut nested = Vec::new();
1644 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
1645 if let Some(idx) = ref_index(&item_ref) {
1647 self.texts[idx]["children"]
1648 .as_array_mut()
1649 .unwrap()
1650 .extend(nested);
1651 }
1652 }
1653 children.push(json!({ "$ref": item_ref }));
1654 i = j;
1655 }
1656 self.groups[group_index(&self_ref)] = json!({
1657 "self_ref": self_ref,
1658 "parent": { "$ref": parent },
1659 "children": children,
1660 "content_layer": "body",
1661 "name": "list",
1662 "label": "list",
1663 });
1664 self_ref
1665 }
1666
1667 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
1668 let Node::ListItem {
1669 ordered,
1670 number,
1671 text,
1672 location,
1673 ..
1674 } = node
1675 else {
1676 unreachable!()
1677 };
1678 self.adopt_loc(*location);
1679 let self_ref = format!("#/texts/{}", self.texts.len());
1680 let raw = unescape_text(text);
1681 let prov = self.take_prov(raw.chars().count());
1682 let marker = if *ordered {
1683 format!("{number}.")
1684 } else {
1685 "-".to_string()
1686 };
1687 self.texts.push(json!({
1688 "self_ref": self_ref,
1689 "parent": { "$ref": parent },
1690 "children": [],
1691 "content_layer": "body",
1692 "label": "list_item",
1693 "prov": prov,
1694 "orig": raw,
1695 "text": raw,
1696 "enumerated": ordered,
1697 "marker": marker,
1698 }));
1699 self_ref
1700 }
1701
1702 fn add_table(&mut self, t: &Table, parent: &str) -> String {
1703 self.add_table_with(t, parent, false)
1704 }
1705
1706 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
1709 let self_ref = format!("#/tables/{}", self.tables.len());
1710 self.adopt_loc(t.location);
1711 let prov = self.take_prov(0);
1712 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
1716 Some(cap) => self.add_caption(
1717 cap,
1718 json!({}),
1719 &self_ref,
1720 parent,
1721 (t.caption_parent, t.caption_location),
1722 ),
1723 None => (Vec::new(), Vec::new()),
1724 };
1725 let data = table_data_with(t, raw);
1726 self.tables.push(json!({
1727 "self_ref": self_ref,
1728 "parent": { "$ref": parent },
1729 "children": children,
1730 "content_layer": "body",
1731 "label": "table",
1732 "prov": prov,
1733 "captions": captions,
1734 "references": [],
1735 "footnotes": [],
1736 "data": data,
1737 "annotations": [],
1738 }));
1739 self_ref
1740 }
1741
1742 fn add_caption(
1751 &mut self,
1752 text: &str,
1753 extra: Value,
1754 self_ref: &str,
1755 parent: &str,
1756 (choice, location): (CaptionParent, Option<[u16; 4]>),
1757 ) -> (Vec<Value>, Vec<Value>) {
1758 let cap_parent = match choice {
1763 CaptionParent::Item => self_ref,
1764 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
1765 CaptionParent::Body => "#/body",
1766 };
1767 if self.cur_page > 0 && location.is_some() {
1768 self.pending_loc = location;
1769 }
1770 let cap_ref = json!({ "$ref": self.add_text("caption", text, cap_parent, extra) });
1771 match choice {
1772 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
1773 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
1776 CaptionParent::Body if parent == "#/body" => {
1777 self.pending_siblings.push(cap_ref.clone())
1778 }
1779 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
1780 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
1783 }
1784 (vec![cap_ref], Vec::new())
1785 }
1786
1787 fn add_picture(
1790 &mut self,
1791 caption: Option<&str>,
1792 caption_href: Option<&str>,
1793 image: Option<&crate::PictureImage>,
1794 meta: Option<Value>,
1795 parent: &str,
1796 caption_at: (CaptionParent, Option<[u16; 4]>),
1797 ) -> String {
1798 let self_ref = format!("#/pictures/{}", self.pictures.len());
1799 let prov = self.take_prov(0);
1802 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
1803 Some(cap) => {
1804 let extra = match caption_href {
1808 Some(href) => json!({ "hyperlink": href }),
1809 None => json!({}),
1810 };
1811 self.add_caption(cap, extra, &self_ref, parent, caption_at)
1812 }
1813 None => (Vec::new(), Vec::new()),
1814 };
1815 self.push_picture(prov, captions, children, image, meta, parent)
1816 }
1817
1818 fn push_picture(
1821 &mut self,
1822 prov: Value,
1823 captions: Vec<Value>,
1824 children: Vec<Value>,
1825 image: Option<&crate::PictureImage>,
1826 meta: Option<Value>,
1827 parent: &str,
1828 ) -> String {
1829 let self_ref = format!("#/pictures/{}", self.pictures.len());
1830 let annotations = meta
1834 .as_ref()
1835 .and_then(|m| m.get("annotations").cloned())
1836 .unwrap_or_else(|| json!([]));
1837 let meta = meta.map(|mut m| {
1838 if let Some(obj) = m.as_object_mut() {
1839 obj.remove("annotations");
1840 }
1841 m
1842 });
1843 let mut item = match meta {
1847 Some(meta) => json!({
1848 "self_ref": self_ref,
1849 "parent": { "$ref": parent },
1850 "children": children,
1851 "content_layer": "body",
1852 "meta": meta,
1853 "label": "picture",
1854 "prov": prov,
1855 "captions": captions,
1856 "references": [],
1857 "footnotes": [],
1858 "annotations": annotations,
1859 }),
1860 None => json!({
1861 "self_ref": self_ref,
1862 "parent": { "$ref": parent },
1863 "children": children,
1864 "content_layer": "body",
1865 "label": "picture",
1866 "prov": prov,
1867 "captions": captions,
1868 "references": [],
1869 "footnotes": [],
1870 "annotations": annotations,
1871 }),
1872 };
1873 if let Some(img) = image {
1878 let image = image_ref_json(img);
1879 if let Some(obj) = item.as_object_mut() {
1880 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
1881 obj.insert("image".into(), image);
1882 obj.insert("annotations".into(), annotations);
1883 }
1884 }
1885 self.pictures.push(item);
1886 self_ref
1887 }
1888
1889 fn add_group(
1890 &mut self,
1891 label: &str,
1892 name: Option<&str>,
1893 layer: Option<ContentLayer>,
1894 nodes: &[Node],
1895 parent: &str,
1896 ) -> String {
1897 let self_ref = format!("#/groups/{}", self.groups.len());
1898 self.groups.push(Value::Null);
1899 let mark = (
1903 self.texts.len(),
1904 self.tables.len(),
1905 self.pictures.len(),
1906 self.groups.len(),
1907 );
1908 let children = self.walk_into(nodes, &self_ref);
1909 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
1910 let content_layer = layer.map_or("body", |l| l.value());
1911 self.groups[group_index(&self_ref)] = json!({
1912 "self_ref": self_ref,
1913 "parent": { "$ref": parent },
1914 "children": children,
1915 "content_layer": content_layer,
1916 "name": name,
1917 "label": label,
1918 });
1919 if layer.is_some() {
1920 let (t, tb, p, g) = mark;
1921 for item in self.texts[t..]
1922 .iter_mut()
1923 .chain(self.tables[tb..].iter_mut())
1924 .chain(self.pictures[p..].iter_mut())
1925 .chain(self.groups[g..].iter_mut())
1926 {
1927 if let Some(obj) = item.as_object_mut() {
1928 obj.insert("content_layer".into(), json!(content_layer));
1929 }
1930 }
1931 }
1932 self_ref
1933 }
1934
1935 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Value> {
1938 let seqs: Option<Vec<usize>> = nodes
1943 .iter()
1944 .map(|n| match n {
1945 Node::Prov { seq: Some(s), .. } => Some(*s),
1946 _ => None,
1947 })
1948 .collect();
1949 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
1950 let mut order: Vec<usize> = (0..nodes.len()).collect();
1951 order.sort_by_key(|&i| seqs[i]);
1952 let mut slots: Vec<Vec<Value>> = vec![Vec::new(); nodes.len()];
1953 for i in order {
1954 if let Some(r) = self.add_node(&nodes[i], parent) {
1955 slots[i].append(&mut self.pending_siblings);
1956 slots[i].push(json!({ "$ref": r }));
1957 slots[i].append(&mut self.pending_after);
1958 }
1959 if parent == "#/body" {
1960 slots[i].append(&mut self.pending_body);
1961 }
1962 }
1963 return slots.into_iter().flatten().collect();
1964 }
1965 let mut children = Vec::new();
1966 let mut i = 0;
1967 while i < nodes.len() {
1968 if matches!(nodes[i], Node::ListItem { .. }) {
1969 let start = i;
1970 i += 1;
1971 loop {
1972 match nodes.get(i) {
1973 Some(Node::ListItem { .. }) => i += 1,
1974 Some(Node::Paragraph { text })
1977 if text.is_empty()
1978 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
1979 {
1980 i += 1
1981 }
1982 _ => break,
1983 }
1984 }
1985 self.add_sibling_lists(&nodes[start..i], parent, &mut children);
1986 } else {
1987 if let Some(r) = self.add_node(&nodes[i], parent) {
1988 children.append(&mut self.pending_siblings);
1989 children.push(json!({ "$ref": r }));
1990 children.append(&mut self.pending_after);
1991 }
1992 i += 1;
1993 }
1994 if parent == "#/body" {
1997 children.append(&mut self.pending_body);
1998 }
1999 }
2000 children
2001 }
2002
2003 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
2008 let base = level_of(&run[0]);
2009 let mut seg = 0;
2010 for k in 0..run.len() {
2011 let Node::ListItem {
2012 first_in_list,
2013 level,
2014 ..
2015 } = &run[k]
2016 else {
2017 continue;
2018 };
2019 if *level != base {
2020 continue; }
2022 if k > seg && *first_in_list {
2023 out.push(json!({ "$ref": self.add_list(&run[seg..k], parent) }));
2024 seg = k;
2025 }
2026 }
2027 out.push(json!({ "$ref": self.add_list(&run[seg..], parent) }));
2028 }
2029}
2030
2031fn level_of(node: &Node) -> u8 {
2032 match node {
2033 Node::ListItem { level, .. } => *level,
2034 _ => 0,
2035 }
2036}
2037
2038fn group_index(self_ref: &str) -> usize {
2039 self_ref.rsplit('/').next().unwrap().parse().unwrap()
2040}
2041
2042fn ref_index(self_ref: &str) -> Option<usize> {
2043 self_ref.rsplit('/').next()?.parse().ok()
2044}
2045
2046fn merge(target: &mut Value, extra: Value) {
2048 if let (Some(t), Some(e)) = (target.as_object_mut(), extra.as_object()) {
2049 for (k, v) in e {
2050 t.insert(k.clone(), v.clone());
2051 }
2052 }
2053}
2054
2055fn unescape_text(s: &str) -> String {
2057 s.replace("<", "<")
2058 .replace(">", ">")
2059 .replace("&", "&")
2060 .replace("\\_", "_")
2061}
2062
2063fn fnv1a(s: &str) -> u64 {
2066 let mut h: u64 = 0xcbf29ce484222325;
2067 for b in s.bytes() {
2068 h ^= b as u64;
2069 h = h.wrapping_mul(0x100000001b3);
2070 }
2071 h
2072}
2073
2074#[cfg(test)]
2075mod tests {
2076 use crate::{
2077 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
2078 };
2079 use serde_json::Value;
2080
2081 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
2082 crate::TableCell {
2083 text: text.into(),
2084 bbox: None,
2085 start_row: row,
2086 start_col: col,
2087 row_span: 1,
2088 col_span,
2089 column_header: row == 0,
2090 row_header: false,
2091 row_section: false,
2092 }
2093 }
2094
2095 #[test]
2101 fn ragged_rows_take_their_grid_from_first_class_cells() {
2102 let cells = vec![
2103 cell_at("h0", 0, 0, 1),
2104 cell_at("h2", 0, 2, 1),
2105 cell_at("wide", 1, 0, 3),
2106 ];
2107 let export = |rows: Vec<Vec<String>>| -> Value {
2108 let mut doc = DoclingDocument::new("t");
2109 doc.push(Node::Table(Table {
2110 rows,
2111 cells: Some(cells.clone()),
2112 ..Table::default()
2113 }));
2114 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2115 v["tables"][0]["data"].clone()
2116 };
2117 let data = export(vec![
2119 vec!["h0".into(), "h2".into()],
2120 vec!["wide".into(), "wide".into(), "wide".into()],
2121 ]);
2122 assert_eq!(data["num_rows"], 2);
2123 assert_eq!(data["num_cols"], 3);
2124 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
2125 let row0: Vec<&str> = data["grid"][0]
2126 .as_array()
2127 .unwrap()
2128 .iter()
2129 .map(|c| c["text"].as_str().unwrap())
2130 .collect();
2131 assert_eq!(row0, ["h0", "", "h2"]);
2132 assert_eq!(data["table_cells"][2]["col_span"], 3);
2133 let data = export(vec![
2136 vec!["h0".into(), "h2".into()],
2137 vec!["wide".into(), "wide".into()],
2138 ]);
2139 assert_eq!(data["num_cols"], 2);
2140 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
2141 }
2142
2143 fn doc_with_image() -> DoclingDocument {
2144 let mut doc = DoclingDocument::new("t");
2145 doc.push(Node::Picture {
2146 caption: Some("Fig 1".into()),
2147 caption_href: None,
2148 image: Some(PictureImage {
2149 dpi: crate::PictureImage::DEFAULT_DPI,
2150 mimetype: "image/png".into(),
2151 width: 4,
2152 height: 2,
2153 data: b"foobar".to_vec(),
2154 }),
2155 classification: None,
2156 caption_parent: Default::default(),
2157 caption_location: None,
2158 });
2159 doc
2160 }
2161
2162 #[test]
2167 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
2168 let mut doc = DoclingDocument::new("t");
2169 doc.push(Node::Heading {
2170 level: 1,
2171 text: "Slide One".into(),
2172 });
2173 doc.push(Node::Furniture {
2174 layer: ContentLayer::Notes,
2175 inner: Box::new(Node::Located {
2176 location: [0, 0, 0, 0],
2177 inner: Box::new(Node::Paragraph {
2178 text: "Speaker note for slide 1.".into(),
2179 }),
2180 }),
2181 });
2182 doc.push(Node::Furniture {
2183 layer: ContentLayer::Furniture,
2184 inner: Box::new(Node::Paragraph {
2185 text: "page header".into(),
2186 }),
2187 });
2188
2189 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2190 let texts = v["texts"].as_array().unwrap();
2191 assert_eq!(
2192 texts
2193 .iter()
2194 .map(|t| (
2195 t["label"].as_str().unwrap(),
2196 t["content_layer"].as_str().unwrap(),
2197 t["text"].as_str().unwrap()
2198 ))
2199 .collect::<Vec<_>>(),
2200 vec![
2201 ("title", "body", "Slide One"),
2202 ("text", "notes", "Speaker note for slide 1."),
2203 ],
2204 "the note is carried on its own layer; the furniture is not carried"
2205 );
2206 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
2208 }
2209
2210 #[test]
2216 fn continuation_flags_become_spanning_cells() {
2217 let mut doc = DoclingDocument::new("t");
2218 let rows = vec![
2220 vec!["merged".to_string(), "merged".into(), "merged".into()],
2221 vec!["merged".to_string(), "merged".into(), "merged".into()],
2222 vec!["a".to_string(), "b".into(), "c".into()],
2223 ];
2224 doc.push(Node::Table(crate::Table {
2225 rows,
2226 location: None,
2227 structure: Some(crate::TableStructure {
2228 header_row: vec![true, false, false],
2229 col_continuation: vec![
2230 vec![false, true, true],
2231 vec![false, true, true],
2232 vec![false, false, false],
2233 ],
2234 row_continuation: vec![
2235 vec![false, false, false],
2236 vec![true, true, true],
2237 vec![false, false, false],
2238 ],
2239 row_header: Vec::new(),
2240 col_header: Vec::new(),
2241 }),
2242 cell_blocks: None,
2243 cells: None,
2244 caption: None,
2245 caption_parent: Default::default(),
2246 caption_location: None,
2247 }));
2248 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2249 let data = &v["tables"][0]["data"];
2250 assert_eq!(data["num_rows"], 3);
2251 assert_eq!(data["num_cols"], 3);
2252 let cells = data["table_cells"].as_array().unwrap();
2253 assert_eq!(
2254 cells.len(),
2255 4,
2256 "one cell for the range, three for the plain row"
2257 );
2258 assert_eq!(
2259 cells[0],
2260 serde_json::json!({
2261 "row_span": 2, "col_span": 3,
2262 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2263 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2264 "text": "merged", "column_header": true, "row_header": false,
2265 "row_section": false, "fillable": false,
2266 })
2267 );
2268 assert_eq!(cells[1]["text"], "a");
2269 assert_eq!(cells[1]["row_span"], 1);
2270 assert_eq!(cells[1]["column_header"], false);
2271 let grid = data["grid"].as_array().unwrap();
2273 assert_eq!(grid.len(), 3);
2274 for (r, row) in grid.iter().take(2).enumerate() {
2275 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2276 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2277 }
2278 }
2279 assert_eq!(grid[2][2]["text"], "c");
2280 }
2281
2282 #[test]
2288 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2289 let mut doc = DoclingDocument::new("t");
2290 doc.push(Node::PageInfo {
2291 page_no: 1,
2292 width: 3.0,
2293 height: 4.0,
2294 });
2295 let table = crate::Table {
2296 rows: vec![vec!["a".to_string(), "b".into()]],
2297 ..Default::default()
2298 };
2299 doc.push(Node::Group {
2300 label: "sheet".into(),
2301 name: Some("Data".into()),
2302 layer: None,
2303 children: vec![
2304 Node::Prov {
2309 page_no: 1,
2310 bbox: [0.0, 0.0, 3.0, 4.0],
2311 charspan: [0, 0],
2312 seq: Some(1),
2313 inner: Box::new(Node::Table(table.clone())),
2314 },
2315 Node::Prov {
2316 page_no: 1,
2317 bbox: [0.0, 1.0, 1.0, 1.0],
2318 charspan: [0, 0],
2319 seq: Some(0),
2320 inner: Box::new(Node::Chart {
2321 kind: "bar_chart".into(),
2322 table,
2323 caption: Some("Sales".into()),
2324 location: Some([0, 128, 170, 128]),
2325 }),
2326 },
2327 ],
2328 });
2329 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2330 assert_eq!(
2331 v["pages"],
2332 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2333 );
2334 assert_eq!(
2335 v["tables"][0]["prov"],
2336 serde_json::json!([{
2337 "page_no": 1,
2338 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2339 "charspan": [0, 0],
2340 }])
2341 );
2342 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2343 let sheet = &v["groups"][0];
2346 assert_eq!(
2347 sheet["children"],
2348 serde_json::json!([
2349 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2350 ])
2351 );
2352 let cap = &v["texts"][0];
2353 assert_eq!(cap["label"], "caption");
2354 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2355 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2356 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2357 let pic = &v["pictures"][0];
2358 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2359 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2360 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2361 assert_eq!(
2362 pic["meta"]["classification"]["predictions"][0]["class_name"],
2363 "bar_chart"
2364 );
2365 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2366 }
2367
2368 #[test]
2372 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2373 let mut doc = DoclingDocument::new("t");
2374 doc.push(Node::PageInfo {
2375 page_no: 1,
2376 width: 12192000.0,
2377 height: 6858000.0,
2378 });
2379 doc.push(Node::Furniture {
2380 layer: ContentLayer::Notes,
2381 inner: Box::new(Node::Located {
2382 location: [0, 0, 0, 0],
2383 inner: Box::new(Node::Paragraph {
2384 text: "a note".into(),
2385 }),
2386 }),
2387 });
2388 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2389 let prov = &v["texts"][0]["prov"][0];
2390 assert_eq!(prov["page_no"], 1);
2391 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2392 assert_eq!(
2393 prov["bbox"],
2394 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2395 );
2396 assert_eq!(
2398 v["pages"]["1"]["size"],
2399 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2400 );
2401 }
2402
2403 #[test]
2413 fn captions_and_checkboxes_carry_their_own_prov() {
2414 let mut doc = DoclingDocument::new("t");
2415 doc.push(Node::PageInfo {
2416 page_no: 1,
2417 width: 512.0,
2418 height: 512.0,
2419 });
2420 doc.push(Node::Located {
2421 location: [64, 64, 448, 256],
2422 inner: Box::new(Node::Picture {
2423 caption: Some("Figure 1".into()),
2424 caption_href: None,
2425 image: None,
2426 classification: None,
2427 caption_parent: CaptionParent::Item,
2428 caption_location: Some([64, 264, 448, 280]),
2429 }),
2430 });
2431 doc.push(Node::PageInfo {
2432 page_no: 2,
2433 width: 512.0,
2434 height: 512.0,
2435 });
2436 doc.push(Node::Located {
2437 location: [64, 120, 448, 300],
2438 inner: Box::new(Node::Table(Table {
2439 rows: vec![vec!["a".into()]],
2440 caption: Some("Table 1".into()),
2441 caption_parent: CaptionParent::Item,
2442 caption_location: Some([64, 100, 300, 112]),
2443 ..Table::default()
2444 })),
2445 });
2446 doc.push(Node::Located {
2447 location: [64, 320, 200, 332],
2448 inner: Box::new(Node::CheckboxItem {
2449 checked: false,
2450 text: "First option".into(),
2451 }),
2452 });
2453 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2454 let texts = v["texts"].as_array().unwrap();
2455 let by_text = |t: &str| texts.iter().find(|x| x["text"] == t).expect(t).clone();
2456 let bbox = |p: &Value| {
2457 let b = &p["prov"][0]["bbox"];
2458 [&b["l"], &b["t"], &b["r"], &b["b"]].map(|x| x.as_f64().unwrap())
2459 };
2460 let fig = by_text("Figure 1");
2463 assert_eq!(fig["prov"][0]["page_no"], 1);
2464 assert_eq!(bbox(&fig), [64.0, 248.0, 448.0, 232.0]);
2465 assert_eq!(fig["prov"][0]["charspan"], serde_json::json!([0, 8]));
2466 assert_eq!(fig["parent"]["$ref"], "#/pictures/0");
2467 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
2468 let tab = by_text("Table 1");
2470 assert_eq!(tab["prov"][0]["page_no"], 2);
2471 assert_eq!(bbox(&tab), [64.0, 412.0, 300.0, 400.0]);
2472 assert_eq!(v["tables"][0]["prov"][0]["bbox"]["t"], 392.0);
2473 let cb = by_text("First option");
2475 assert_eq!(cb["label"], "checkbox_unselected");
2476 assert_eq!(cb["prov"][0]["page_no"], 2);
2477 assert_eq!(bbox(&cb), [64.0, 192.0, 200.0, 180.0]);
2478 assert!(texts
2479 .iter()
2480 .all(|t| !t["prov"].as_array().unwrap().is_empty()));
2481
2482 let mut plain = DoclingDocument::new("t");
2485 plain.push(Node::PageInfo {
2486 page_no: 1,
2487 width: 512.0,
2488 height: 512.0,
2489 });
2490 plain.push(Node::Located {
2491 location: [64, 64, 448, 256],
2492 inner: Box::new(Node::Picture {
2493 caption: Some("Figure 1".into()),
2494 caption_href: None,
2495 image: None,
2496 classification: None,
2497 caption_parent: CaptionParent::Item,
2498 caption_location: None,
2499 }),
2500 });
2501 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2502 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2503 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
2504 }
2505
2506 #[test]
2507 fn page_markers_produce_pages_and_prov() {
2508 let mut doc = DoclingDocument::new("t");
2509 doc.push(Node::PageInfo {
2510 page_no: 1,
2511 width: 512.0,
2512 height: 1024.0,
2513 });
2514 doc.push(Node::Located {
2515 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
2517 text: "hello".into(),
2518 }),
2519 });
2520 doc.push(Node::Table(Table {
2521 rows: vec![vec!["a".into()]],
2522 location: Some([0, 0, 512, 512]),
2523 ..Table::default()
2524 }));
2525 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2526 assert_eq!(v["pages"]["1"]["page_no"], 1);
2527 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
2528 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
2529 let prov = &v["texts"][0]["prov"][0];
2532 assert_eq!(prov["page_no"], 1);
2533 assert_eq!(prov["bbox"]["l"], 128.0);
2534 assert_eq!(prov["bbox"]["t"], 896.0);
2535 assert_eq!(prov["bbox"]["r"], 256.0);
2536 assert_eq!(prov["bbox"]["b"], 768.0);
2537 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
2538 assert_eq!(prov["charspan"][1], 5);
2539 let tprov = &v["tables"][0]["prov"][0];
2541 assert_eq!(tprov["bbox"]["t"], 1024.0);
2542 assert_eq!(tprov["bbox"]["b"], 0.0);
2543 assert_eq!(tprov["charspan"][1], 0);
2544
2545 let mut plain = DoclingDocument::new("t");
2547 plain.push(Node::Located {
2548 location: [1, 2, 3, 4],
2549 inner: Box::new(Node::Paragraph { text: "x".into() }),
2550 });
2551 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2552 assert_eq!(v["pages"], serde_json::json!({}));
2553 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2554 }
2555
2556 #[test]
2559 fn page_furniture_becomes_furniture_layer_text() {
2560 let mut doc = DoclingDocument::new("t");
2561 doc.push(Node::PageInfo {
2562 page_no: 1,
2563 width: 512.0,
2564 height: 512.0,
2565 });
2566 doc.push(Node::PageFurniture {
2567 footer: false,
2568 location: [10, 0, 100, 20],
2569 text: "Chapter 1".into(),
2570 });
2571 doc.push(Node::Paragraph {
2572 text: "body".into(),
2573 });
2574 doc.push(Node::PageFurniture {
2575 footer: true,
2576 location: [400, 490, 500, 512],
2577 text: "1.10.2".into(),
2578 });
2579 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2580 let texts = v["texts"].as_array().unwrap();
2581 assert_eq!(texts.len(), 3);
2582 assert_eq!(texts[0]["label"], "page_header");
2583 assert_eq!(texts[0]["content_layer"], "furniture");
2584 assert_eq!(texts[0]["parent"]["$ref"], "#/body");
2585 assert_eq!(texts[0]["prov"][0]["bbox"]["l"], 10.0);
2586 assert_eq!(texts[1]["content_layer"], "body");
2587 assert_eq!(texts[1]["prov"], serde_json::json!([]));
2588 assert_eq!(texts[2]["label"], "page_footer");
2589 assert_eq!(texts[2]["text"], "1.10.2");
2590 assert_eq!(texts[2]["content_layer"], "furniture");
2591 }
2592
2593 #[test]
2594 fn picture_image_in_markdown_modes_and_json() {
2595 let doc = doc_with_image();
2596 assert!(doc.export_to_markdown().contains("<!-- image -->"));
2598 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
2600 assert!(
2601 md.contains(""),
2602 "got:\n{md}"
2603 );
2604 assert!(files.is_empty());
2605 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
2607 assert!(
2608 md.contains(""),
2609 "got:\n{md}"
2610 );
2611 assert_eq!(
2612 files,
2613 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
2614 );
2615 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2618 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
2619 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
2620 let keys: Vec<&str> = v["pictures"][0]
2621 .as_object()
2622 .unwrap()
2623 .keys()
2624 .map(String::as_str)
2625 .collect();
2626 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
2627 assert_eq!(
2628 v["pictures"][0]["image"]["uri"],
2629 "data:image/png;base64,Zm9vYmFy"
2630 );
2631 }
2632
2633 #[test]
2634 fn exports_docling_schema() {
2635 let mut doc = DoclingDocument::new("t");
2636 doc.push(Node::Heading {
2637 level: 1,
2638 text: "Title".into(),
2639 });
2640 doc.push(Node::Heading {
2641 level: 2,
2642 text: "Sec".into(),
2643 });
2644 doc.push(Node::Paragraph {
2645 text: "Body & more".into(),
2646 }); doc.push(Node::ListItem {
2648 ordered: false,
2649 number: 0,
2650 first_in_list: true,
2651 text: "one".into(),
2652 level: 0,
2653 marker: None,
2654 location: None,
2655 dclx: None,
2656 href: None,
2657 layer: None,
2658 });
2659 doc.push(Node::ListItem {
2660 ordered: false,
2661 number: 0,
2662 first_in_list: false,
2663 text: "two".into(),
2664 level: 0,
2665 marker: None,
2666 location: None,
2667 dclx: None,
2668 href: None,
2669 layer: None,
2670 });
2671 doc.push(Node::Table(Table {
2672 rows: vec![vec!["A".into(), "B".into()]],
2673 location: None,
2674 structure: None,
2675 cell_blocks: None,
2676 cells: None,
2677 caption: None,
2678 caption_parent: Default::default(),
2679 caption_location: None,
2680 }));
2681
2682 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2683 assert_eq!(v["schema_name"], "DoclingDocument");
2684 assert_eq!(v["version"], "1.10.0");
2685 assert_eq!(v["texts"][0]["label"], "title");
2686 assert_eq!(v["texts"][1]["label"], "section_header");
2687 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
2691 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
2692 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
2693 assert_eq!(v["texts"][3]["marker"], "-");
2694 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
2696 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
2697 }
2698 #[test]
2702 fn a_layered_group_stamps_its_whole_subtree() {
2703 let doc = DoclingDocument {
2704 name: "s".into(),
2705 nodes: vec![
2706 Node::Group {
2707 label: "sheet".into(),
2708 name: Some("Sheet1".into()),
2709 layer: None,
2710 children: vec![Node::Paragraph {
2711 text: "visible".into(),
2712 }],
2713 },
2714 Node::Group {
2715 label: "sheet".into(),
2716 name: Some("Sheet2".into()),
2717 layer: Some(ContentLayer::Invisible),
2718 children: vec![Node::Paragraph {
2719 text: "hidden".into(),
2720 }],
2721 },
2722 ],
2723 ..DoclingDocument::new("s")
2724 };
2725 let v = crate::json::to_json(&doc);
2726 assert_eq!(v["groups"][0]["label"], "sheet");
2727 assert_eq!(v["groups"][0]["name"], "Sheet1");
2728 assert_eq!(v["groups"][0]["content_layer"], "body");
2729 assert_eq!(v["texts"][0]["content_layer"], "body");
2730 assert_eq!(v["groups"][1]["name"], "Sheet2");
2731 assert_eq!(v["groups"][1]["content_layer"], "invisible");
2732 assert_eq!(v["texts"][1]["content_layer"], "invisible");
2733 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
2735 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
2736 }
2737
2738 #[test]
2748 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
2749 use crate::tree::{ItemTree, TreeKind};
2750 let mut t = ItemTree::default();
2751 let text = |txt: &str| TreeKind::Text {
2752 label: "text".into(),
2753 text: txt.into(),
2754 orig: None,
2755 formatting: None,
2756 hyperlink: None,
2757 level: None,
2758 list: None,
2759 };
2760 let a = t.add(None, None, text("a"));
2761 let blank = t.add(None, None, text(""));
2762 let b = t.add(None, None, text("b"));
2763 t.delete(blank);
2764 let group = t.add(
2765 None,
2766 Some(ContentLayer::Notes),
2767 TreeKind::Group {
2768 label: "comment_section".into(),
2769 name: "comment-0".into(),
2770 },
2771 );
2772 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
2773 t.items[a].comments.push(group);
2774 t.add(
2775 None,
2776 None,
2777 TreeKind::Picture {
2778 captions: Vec::new(),
2779 image: None,
2780 classification: Some("bar_chart".into()),
2781 confidence: None,
2782 chart: Some(Table {
2783 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
2784 ..Table::default()
2785 }),
2786 dpi: None,
2787 },
2788 );
2789 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
2790 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
2791 let mut doc = DoclingDocument::new("t");
2792 doc.tree = Some(t);
2793 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2794 let texts = v["texts"].as_array().unwrap();
2795 assert_eq!(texts.len(), 3);
2796 assert_eq!(texts[1]["text"], "b");
2797 assert_eq!(texts[1]["self_ref"], "#/texts/1");
2798 assert_eq!(
2799 v["body"]["children"],
2800 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
2801 );
2802 let keys: Vec<&str> = texts[0]
2803 .as_object()
2804 .unwrap()
2805 .keys()
2806 .map(String::as_str)
2807 .collect();
2808 assert_eq!(
2809 keys,
2810 vec![
2811 "self_ref",
2812 "parent",
2813 "children",
2814 "content_layer",
2815 "label",
2816 "prov",
2817 "comments",
2818 "orig",
2819 "text"
2820 ]
2821 );
2822 assert_eq!(
2823 texts[0]["comments"],
2824 serde_json::json!([{"$ref": "#/groups/0"}])
2825 );
2826 assert!(texts[1].get("comments").is_none());
2827 let meta = &v["pictures"][0]["meta"];
2828 assert_eq!(
2829 meta["classification"]["predictions"][0]["class_name"],
2830 "bar_chart"
2831 );
2832 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
2833 }
2834
2835 #[test]
2841 fn tree_items_carry_exact_provenance_and_dpi() {
2842 use crate::tree::{ItemTree, TreeKind, TreeProv};
2843 let text = |label: &str, t: &str| TreeKind::Text {
2844 label: label.into(),
2845 text: t.into(),
2846 orig: None,
2847 formatting: None,
2848 hyperlink: None,
2849 level: None,
2850 list: None,
2851 };
2852 let mut t = ItemTree::default();
2853 let slide = t.add(
2854 None,
2855 None,
2856 TreeKind::Group {
2857 label: "chapter".into(),
2858 name: "slide-0".into(),
2859 },
2860 );
2861 t.add_with_prov(
2862 Some(slide),
2863 None,
2864 text("paragraph", "héllo"),
2865 TreeProv {
2866 page_no: 1,
2867 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
2868 bottom_left: true,
2869 charspan: [0, 5],
2870 },
2871 );
2872 t.add_with_prov(
2873 Some(slide),
2874 None,
2875 TreeKind::Picture {
2876 captions: Vec::new(),
2877 image: Some(crate::PictureImage {
2878 dpi: crate::PictureImage::DEFAULT_DPI,
2879 mimetype: "image/png".into(),
2880 width: 2,
2881 height: 2,
2882 data: vec![0],
2883 }),
2884 classification: None,
2885 confidence: None,
2886 chart: None,
2887 dpi: Some(300),
2888 },
2889 TreeProv {
2890 page_no: 1,
2891 bbox: [0.0; 4],
2892 bottom_left: false,
2893 charspan: [0, 0],
2894 },
2895 );
2896 t.add(
2897 Some(slide),
2898 Some(ContentLayer::Notes),
2899 text("text", "no geometry"),
2900 );
2901 let mut doc = DoclingDocument::new("t");
2902 doc.push(Node::PageInfo {
2903 page_no: 1,
2904 width: 9144000.0,
2905 height: 6858000.0,
2906 });
2907 doc.tree = Some(t);
2908 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2909 assert_eq!(
2910 v["texts"][0]["prov"],
2911 serde_json::json!([{
2912 "page_no": 1,
2913 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
2914 "charspan": [0, 5],
2915 }])
2916 );
2917 assert_eq!(v["texts"][0]["label"], "paragraph");
2918 assert_eq!(
2919 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
2920 "TOPLEFT"
2921 );
2922 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
2923 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
2924 assert_eq!(v["texts"][1]["content_layer"], "notes");
2925 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
2926 assert_eq!(v["pages"]["1"]["page_no"], 1);
2927 }
2928
2929 #[test]
2934 fn provenance_boxes_are_clamped_to_their_page() {
2935 let mut doc = DoclingDocument::new("t");
2936 doc.push(Node::PageInfo {
2937 page_no: 1,
2938 width: 10.0,
2939 height: 8.0,
2940 });
2941 doc.push(Node::Prov {
2942 page_no: 1,
2943 bbox: [-1.0, 2.0, 12.0, 9.5],
2944 charspan: [0, 1],
2945 seq: None,
2946 inner: Box::new(Node::Paragraph { text: "x".into() }),
2947 });
2948 let mut table = Table {
2949 rows: vec![vec!["a".into()]],
2950 ..Table::default()
2951 };
2952 table.cells = Some(vec![crate::TableCell {
2953 text: "a".into(),
2954 bbox: Some([1.0, 1.0, 11.0, 9.0]),
2955 start_row: 0,
2956 start_col: 0,
2957 row_span: 1,
2958 col_span: 1,
2959 column_header: false,
2960 row_header: false,
2961 row_section: false,
2962 }]);
2963 doc.push(Node::Prov {
2964 page_no: 1,
2965 bbox: [0.0, 0.0, 10.0, 8.0],
2966 charspan: [0, 0],
2967 seq: None,
2968 inner: Box::new(Node::Table(table)),
2969 });
2970 doc.push(Node::Prov {
2971 page_no: 7,
2972 bbox: [-5.0, 0.0, 50.0, 50.0],
2973 charspan: [0, 1],
2974 seq: None,
2975 inner: Box::new(Node::Paragraph { text: "y".into() }),
2976 });
2977 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2978 assert_eq!(
2979 v["texts"][0]["prov"][0]["bbox"],
2980 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
2981 );
2982 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
2983 assert_eq!(
2984 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
2985 (Some(1.0), Some(10.0), Some(8.0))
2986 );
2987 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
2988 assert_eq!(
2989 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
2990 "page 7 is not described"
2991 );
2992 }
2993
2994 #[test]
3001 fn misplaced_list_items_are_wrapped_like_docling_core() {
3002 use crate::tree::{ItemTree, ListMeta, TreeKind};
3003 let mut t = ItemTree::default();
3004 let li = |txt: &str| TreeKind::Text {
3005 label: "list_item".into(),
3006 text: txt.into(),
3007 orig: None,
3008 formatting: None,
3009 hyperlink: None,
3010 level: None,
3011 list: Some(ListMeta {
3012 enumerated: false,
3013 marker: "-".into(),
3014 }),
3015 };
3016 let group = |label: &str, name: &str| TreeKind::Group {
3017 label: label.into(),
3018 name: name.into(),
3019 };
3020 t.add(None, None, li("A"));
3021 t.add(None, None, li("B"));
3022 t.add(
3023 None,
3024 None,
3025 TreeKind::Text {
3026 label: "text".into(),
3027 text: "plain".into(),
3028 orig: None,
3029 formatting: None,
3030 hyperlink: None,
3031 level: None,
3032 list: None,
3033 },
3034 );
3035 let cell = t.add(None, None, group("unspecified", "cell"));
3036 t.add(Some(cell), None, li("C"));
3037 t.add(Some(cell), None, li("D"));
3038 let list = t.add(None, None, group("list", "list"));
3039 t.add(Some(list), None, li("E"));
3040 let doc = DoclingDocument {
3041 tree: Some(t),
3042 ..DoclingDocument::new("t")
3043 };
3044 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3045 let refs = |x: &Value| -> Vec<String> {
3046 x.as_array()
3047 .unwrap()
3048 .iter()
3049 .map(|r| r["$ref"].as_str().unwrap().to_string())
3050 .collect()
3051 };
3052 assert_eq!(
3053 refs(&v["body"]["children"]),
3054 ["#/groups/4", "#/texts/0", "#/groups/0", "#/groups/1"]
3055 );
3056 assert_eq!(
3057 refs(&v["groups"][0]["children"]),
3058 ["#/groups/3", "#/groups/2"]
3059 );
3060 for (g, name, kids) in [
3061 (2, "group", vec!["#/texts/2"]),
3062 (3, "group", vec!["#/texts/3"]),
3063 (4, "group", vec!["#/texts/4", "#/texts/5"]),
3064 (1, "list", vec!["#/texts/1"]),
3065 ] {
3066 assert_eq!(v["groups"][g]["label"], "list");
3067 assert_eq!(v["groups"][g]["name"], name);
3068 assert_eq!(refs(&v["groups"][g]["children"]), kids);
3069 }
3070 let texts: Vec<&str> = v["texts"]
3071 .as_array()
3072 .unwrap()
3073 .iter()
3074 .map(|t| t["text"].as_str().unwrap())
3075 .collect();
3076 assert_eq!(texts, ["plain", "E", "D", "C", "A", "B"]);
3077 assert_eq!(v["texts"][2]["parent"]["$ref"], "#/groups/2");
3078 }
3079
3080 #[test]
3083 fn a_deleted_child_is_not_written_as_an_empty_ref() {
3084 use crate::tree::{ItemTree, TreeKind};
3085 let mut t = ItemTree::default();
3086 let text = |txt: &str| TreeKind::Text {
3087 label: "text".into(),
3088 text: txt.into(),
3089 orig: None,
3090 formatting: None,
3091 hyperlink: None,
3092 level: None,
3093 list: None,
3094 };
3095 let g = t.add(
3096 None,
3097 None,
3098 TreeKind::Group {
3099 label: "unspecified".into(),
3100 name: "g".into(),
3101 },
3102 );
3103 let gone = t.add(Some(g), None, text(""));
3104 t.add(Some(g), None, text("kept"));
3105 t.items[gone].deleted = true; let doc = DoclingDocument {
3107 tree: Some(t),
3108 ..DoclingDocument::new("t")
3109 };
3110 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3111 assert_eq!(
3112 v["groups"][0]["children"],
3113 serde_json::json!([{"$ref": "#/texts/0"}])
3114 );
3115 }
3116
3117 #[test]
3118 fn a_backend_item_tree_is_written_verbatim() {
3119 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
3120 let mut t = ItemTree::default();
3121 let text = |label: &str, txt: &str| TreeKind::Text {
3122 label: label.into(),
3123 text: txt.into(),
3124 orig: None,
3125 formatting: None,
3126 hyperlink: None,
3127 level: None,
3128 list: None,
3129 };
3130 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
3131 let h = t.add(None, None, text("title", "Heading"));
3132 let group = t.add(
3133 Some(h),
3134 None,
3135 TreeKind::Group {
3136 label: "inline".into(),
3137 name: "group".into(),
3138 },
3139 );
3140 t.add(
3141 Some(group),
3142 None,
3143 TreeKind::Text {
3144 label: "text".into(),
3145 text: "bold".into(),
3146 orig: None,
3147 formatting: Some(Formatting {
3148 bold: true,
3149 ..Formatting::default()
3150 }),
3151 hyperlink: Some("https://example.com/".into()),
3152 level: None,
3153 list: None,
3154 },
3155 );
3156 t.add(
3157 Some(group),
3158 None,
3159 TreeKind::Code {
3160 text: "x = 1".into(),
3161 orig: None,
3162 language: Some("python".into()),
3163 formatting: None,
3164 hyperlink: None,
3165 },
3166 );
3167 let sub = t.add(
3168 Some(h),
3169 None,
3170 TreeKind::Text {
3171 label: "section_header".into(),
3172 text: "Sub".into(),
3173 orig: Some("Sub\u{2019}".into()),
3174 formatting: None,
3175 hyperlink: None,
3176 level: Some(1),
3177 list: None,
3178 },
3179 );
3180 let item = t.add(
3181 Some(sub),
3182 None,
3183 TreeKind::Text {
3184 label: "list_item".into(),
3185 text: "item".into(),
3186 orig: None,
3187 formatting: None,
3188 hyperlink: None,
3189 level: None,
3190 list: Some(ListMeta {
3191 enumerated: true,
3192 marker: "3.".into(),
3193 }),
3194 },
3195 );
3196 let _region = t.add(
3197 Some(sub),
3198 None,
3199 TreeKind::FieldRegion {
3200 items: vec![crate::FieldItem {
3201 marker: None,
3202 key: Some("Name".into()),
3203 value: Some("Duck".into()),
3204 value_kind: Some("read_only".into()),
3205 }],
3206 },
3207 );
3208 let table = t.add(
3209 Some(sub),
3210 None,
3211 TreeKind::Table {
3212 table: Table {
3213 rows: vec![vec!["a \n<".into(), "b".into()]],
3214 cells: Some(vec![
3215 crate::TableCell {
3216 text: "a \n<".into(),
3217 bbox: None,
3218 start_row: 0,
3219 start_col: 0,
3220 row_span: 3,
3221 col_span: 1,
3222 column_header: false,
3223 row_header: true,
3224 row_section: false,
3225 },
3226 crate::TableCell {
3227 text: "b".into(),
3228 bbox: None,
3229 start_row: 0,
3230 start_col: 1,
3231 row_span: 1,
3232 col_span: 1,
3233 column_header: false,
3234 row_header: false,
3235 row_section: false,
3236 },
3237 ]),
3238 ..Table::default()
3239 },
3240 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
3242 },
3243 );
3244 let cell_group = t.add(
3245 Some(table),
3246 None,
3247 TreeKind::Group {
3248 label: "unspecified".into(),
3249 name: "rich_cell_group_1_0_0".into(),
3250 },
3251 );
3252 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
3253 *rich_cells = vec![(0, 1, cell_group)];
3254 }
3255 let after = t.add(Some(sub), None, text("text", "after the region"));
3256 let _ = (title, after);
3257 let list = t.add(
3261 Some(sub),
3262 None,
3263 TreeKind::Group {
3264 label: "list".into(),
3265 name: "list".into(),
3266 },
3267 );
3268 t.reparent(item, Some(list));
3269
3270 let doc = DoclingDocument {
3271 tree: Some(t),
3272 ..DoclingDocument::new("t")
3273 };
3274 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3275 let texts: Vec<&str> = v["texts"]
3277 .as_array()
3278 .unwrap()
3279 .iter()
3280 .map(|t| t["text"].as_str().unwrap())
3281 .collect();
3282 assert_eq!(
3283 texts,
3284 [
3285 "Page",
3286 "Heading",
3287 "bold",
3288 "x = 1",
3289 "Sub",
3290 "item",
3291 "Name",
3292 "Duck",
3293 "after the region"
3294 ]
3295 );
3296 assert_eq!(
3297 v["body"]["children"],
3298 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
3299 );
3300 assert_eq!(v["texts"][0]["content_layer"], "furniture");
3301 assert_eq!(
3302 v["texts"][1]["children"],
3303 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
3304 );
3305 let bold = &v["texts"][2];
3306 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
3307 let keys: Vec<&str> = bold
3308 .as_object()
3309 .unwrap()
3310 .keys()
3311 .map(String::as_str)
3312 .collect();
3313 assert_eq!(
3314 keys,
3315 [
3316 "self_ref",
3317 "parent",
3318 "children",
3319 "content_layer",
3320 "label",
3321 "prov",
3322 "orig",
3323 "text",
3324 "formatting",
3325 "hyperlink"
3326 ]
3327 );
3328 assert_eq!(
3329 bold["formatting"],
3330 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
3331 );
3332 let code = &v["texts"][3];
3333 assert_eq!(code["label"], "code");
3334 assert_eq!(code["code_language"], "Python");
3335 let sub = &v["texts"][4];
3336 assert_eq!(sub["orig"], "Sub\u{2019}");
3337 assert_eq!(sub["level"], 1);
3338 let item = &v["texts"][5];
3339 let keys: Vec<&str> = item
3340 .as_object()
3341 .unwrap()
3342 .keys()
3343 .map(String::as_str)
3344 .collect();
3345 assert_eq!(
3346 keys,
3347 [
3348 "self_ref",
3349 "parent",
3350 "children",
3351 "content_layer",
3352 "label",
3353 "prov",
3354 "orig",
3355 "text",
3356 "enumerated",
3357 "marker"
3358 ]
3359 );
3360 assert_eq!(item["marker"], "3.");
3361 assert_eq!(v["texts"][7]["kind"], "read_only");
3362 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
3363 let table = &v["tables"][0];
3364 assert_eq!(
3365 table["children"],
3366 serde_json::json!([{"$ref": "#/groups/1"}])
3367 );
3368 let cells = table["data"]["table_cells"].as_array().unwrap();
3369 assert_eq!(
3370 cells[0]["text"], "a \n<",
3371 "raw cell text is written verbatim"
3372 );
3373 assert_eq!(
3374 cells[0]["end_row_offset_idx"], 3,
3375 "declared spans are not clamped"
3376 );
3377 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
3378 assert!(cells[0].get("ref").is_none());
3379 assert!(
3380 table["data"]["grid"][0][1].get("ref").is_none(),
3381 "the grid shows plain cells"
3382 );
3383 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
3384 }
3385
3386 #[test]
3390 fn a_comment_section_can_be_referenced_by_its_note_text() {
3391 let doc = DoclingDocument {
3392 name: "c".into(),
3393 nodes: vec![
3394 Node::Commented {
3395 comments: vec![0],
3396 inner: Box::new(Node::Paragraph {
3397 text: "annotated".into(),
3398 }),
3399 },
3400 Node::CommentSection {
3401 name: "comment-Sheet1-A1".into(),
3402 text: "[author: A]: note".into(),
3403 refs_note_text: true,
3404 grouped: true,
3405 },
3406 ],
3407 ..DoclingDocument::new("c")
3408 };
3409 let v = crate::json::to_json(&doc);
3410 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
3411 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
3412 }
3413
3414 #[test]
3418 fn comment_sections_link_back_to_their_items() {
3419 let doc = DoclingDocument {
3420 name: "c".into(),
3421 nodes: vec![
3422 Node::Commented {
3423 comments: vec![0],
3424 inner: Box::new(Node::Paragraph {
3425 text: "annotated".into(),
3426 }),
3427 },
3428 Node::Paragraph {
3429 text: "plain".into(),
3430 },
3431 Node::CommentSection {
3432 name: "comment-7".into(),
3433 text: "[time: t]: note".into(),
3434 refs_note_text: false,
3435 grouped: true,
3436 },
3437 ],
3438 ..DoclingDocument::new("c")
3439 };
3440 let v = crate::json::to_json(&doc);
3441 assert_eq!(v["groups"][0]["label"], "comment_section");
3443 assert_eq!(v["groups"][0]["name"], "comment-7");
3444 assert_eq!(v["groups"][0]["content_layer"], "notes");
3445 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
3446 assert_eq!(v["texts"][2]["content_layer"], "notes");
3447 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
3449 assert!(v["texts"][1].get("comments").is_none());
3450 let keys: Vec<&str> = v["texts"][0]
3452 .as_object()
3453 .unwrap()
3454 .keys()
3455 .map(String::as_str)
3456 .collect();
3457 assert_eq!(
3458 &keys[keys.len() - 4..],
3459 &["prov", "comments", "orig", "text"]
3460 );
3461 }
3462
3463 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
3464 Node::Picture {
3465 caption: Some(caption.into()),
3466 caption_href: None,
3467 image: None,
3468 classification: None,
3469 caption_parent,
3470 caption_location: None,
3471 }
3472 }
3473
3474 fn group(children: Vec<Node>) -> Node {
3475 Node::Group {
3476 label: "section".into(),
3477 name: None,
3478 layer: None,
3479 children,
3480 }
3481 }
3482
3483 fn refs(v: &Value) -> Vec<&str> {
3484 v.as_array()
3485 .unwrap()
3486 .iter()
3487 .map(|r| r["$ref"].as_str().unwrap())
3488 .collect()
3489 }
3490
3491 #[test]
3496 fn a_body_caption_follows_the_enclosing_top_level_item() {
3497 let mut doc = DoclingDocument::new("t");
3498 doc.push(picture("top", CaptionParent::Body));
3499 doc.push(group(vec![
3500 Node::Paragraph { text: "p".into() },
3501 picture("nested", CaptionParent::Body),
3502 ]));
3503 doc.push(Node::Paragraph {
3504 text: "after".into(),
3505 });
3506 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3507 assert_eq!(
3508 refs(&v["body"]["children"]),
3509 [
3510 "#/texts/0",
3511 "#/pictures/0",
3512 "#/groups/0",
3513 "#/texts/2",
3514 "#/texts/3"
3515 ]
3516 );
3517 assert_eq!(
3518 refs(&v["groups"][0]["children"]),
3519 ["#/texts/1", "#/pictures/1"]
3520 );
3521 for (cap, pic) in [(0, 0), (2, 1)] {
3522 assert_eq!(v["texts"][cap]["label"], "caption");
3523 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
3524 assert_eq!(
3525 refs(&v["pictures"][pic]["captions"]),
3526 [format!("#/texts/{cap}")]
3527 );
3528 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
3529 }
3530 }
3531
3532 #[test]
3535 fn an_item_caption_is_the_items_first_child() {
3536 let mut doc = DoclingDocument::new("t");
3537 doc.push(picture("fig", CaptionParent::Item));
3538 doc.push(Node::Table(Table {
3539 rows: vec![vec!["a".into()]],
3540 caption: Some("tab".into()),
3541 caption_parent: CaptionParent::Item,
3542 ..Table::default()
3543 }));
3544 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3545 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
3546 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
3547 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
3548 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3549 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
3550 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
3551 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
3552 }
3553
3554 #[test]
3560 fn picture_children_hang_off_the_picture_after_its_caption() {
3561 let mut doc = DoclingDocument::new("t");
3562 doc.push(picture("fig", CaptionParent::Item));
3563 doc.push(Node::PictureChildren(vec![
3564 Node::Heading {
3565 level: 2,
3566 text: "in-figure title".into(),
3567 },
3568 Node::Paragraph {
3569 text: "axis label".into(),
3570 },
3571 Node::ListItem {
3572 ordered: false,
3573 number: 0,
3574 first_in_list: true,
3575 text: "callout".into(),
3576 level: 0,
3577 marker: None,
3578 location: None,
3579 dclx: None,
3580 href: None,
3581 layer: None,
3582 },
3583 ]));
3584 doc.push(Node::Paragraph {
3585 text: "after".into(),
3586 });
3587 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3588 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/texts/4"]);
3589 assert_eq!(
3590 refs(&v["pictures"][0]["children"]),
3591 ["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"]
3592 );
3593 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3594 assert_eq!(v["texts"][1]["label"], "section_header");
3595 assert_eq!(v["texts"][2]["label"], "text");
3596 for t in 1..=2 {
3597 assert_eq!(v["texts"][t]["parent"]["$ref"], "#/pictures/0");
3598 assert_eq!(v["texts"][t]["content_layer"], "body");
3599 }
3600 assert_eq!(v["groups"][0]["parent"]["$ref"], "#/pictures/0");
3601 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
3602 let md = doc.export_to_markdown();
3603 assert!(
3604 !md.contains("axis label") && !md.contains("callout"),
3605 "{md}"
3606 );
3607 assert!(md.contains("after"), "{md}");
3608 }
3609
3610 #[test]
3614 fn a_container_caption_is_the_items_sibling() {
3615 let mut doc = DoclingDocument::new("t");
3616 doc.push(group(vec![
3617 picture("chart", CaptionParent::Container),
3618 Node::Table(Table {
3619 rows: vec![vec!["a".into()]],
3620 caption: Some("figcaption".into()),
3621 caption_parent: CaptionParent::ContainerAfter,
3622 ..Table::default()
3623 }),
3624 ]));
3625 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3626 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
3627 assert_eq!(
3628 refs(&v["groups"][0]["children"]),
3629 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
3630 );
3631 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
3632 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
3633 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
3634 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
3635 }
3636
3637 #[test]
3642 fn a_labeled_text_keeps_its_label_and_hyperlink_out_of_the_text() {
3643 let mut doc = DoclingDocument::new("t");
3644 doc.push(Node::LabeledText {
3645 label: "footnote".into(),
3646 text: "1 https://example.com/a\\_b".into(),
3647 href: Some("https://example.com/a_b".into()),
3648 });
3649 doc.push(Node::LabeledText {
3650 label: "footnote".into(),
3651 text: "© 2022 the authors".into(),
3652 href: None,
3653 });
3654 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3655 let t = &v["texts"][0];
3656 assert_eq!(t["label"], "footnote");
3657 assert_eq!(t["text"], "1 https://example.com/a_b");
3658 assert_eq!(t["orig"], "1 https://example.com/a_b");
3659 assert_eq!(t["hyperlink"], "https://example.com/a_b");
3660 assert_eq!(v["texts"][1]["label"], "footnote");
3661 assert!(v["texts"][1].get("hyperlink").is_none());
3662 assert_eq!(
3663 doc.export_to_markdown(),
3664 "[1 https://example.com/a\\_b](https://example.com/a_b)\n\n© 2022 the authors\n"
3665 );
3666 assert_eq!(
3667 doc.export_to_text(),
3668 "1 https://example.com/a_b\n\n© 2022 the authors"
3669 );
3670 }
3671}