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_track: Option<(crate::tree::TreeTrack, String)>,
627 pending_siblings: Vec<Value>,
631 pending_after: Vec<Value>,
635 pending_body: Vec<Value>,
639 comment_groups: Vec<String>,
643 last_comment_section: Option<(String, String)>,
648 pending_comments: Vec<(String, Vec<usize>)>,
652}
653
654impl Builder {
655 fn take_prov(&mut self, char_len: usize) -> Value {
661 let prov = self.prov_json(char_len, false);
662 self.pending_exact = None;
663 self.pending_loc = None;
664 prov
665 }
666
667 fn prov_json(&self, char_len: usize, span_over_text: bool) -> Value {
672 let r2 = |v: f64| (v * 100.0).round() / 100.0;
673 if let Some(ExactProv {
674 page_no,
675 bbox: [l, t, r, b],
676 bottom_left,
677 charspan,
678 }) = self.pending_exact
679 {
680 let charspan = if span_over_text {
681 [0, char_len]
682 } else {
683 charspan
684 };
685 return json!([{
686 "page_no": page_no,
687 "bbox": {
688 "l": r2(l), "t": r2(t), "r": r2(r), "b": r2(b),
689 "coord_origin": if bottom_left { "BOTTOMLEFT" } else { "TOPLEFT" },
690 },
691 "charspan": charspan,
692 }]);
693 }
694 let Some([x0, y0, x1, y1]) = self.pending_loc else {
695 return json!([]);
696 };
697 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
701 return json!([{
702 "page_no": self.cur_page,
703 "bbox": { "l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT" },
704 "charspan": [0, char_len],
705 }]);
706 }
707 json!([{
708 "page_no": self.cur_page,
709 "bbox": {
710 "l": r2(x0 as f64 * self.cur_w / 512.0),
711 "t": r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
712 "r": r2(x1 as f64 * self.cur_w / 512.0),
713 "b": r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
714 "coord_origin": "BOTTOMLEFT",
715 },
716 "charspan": [0, char_len],
717 }])
718 }
719
720 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
723 if self.pending_loc.is_none() && self.cur_page > 0 {
724 self.pending_loc = loc;
725 }
726 }
727
728 fn link_comments(&mut self) {
736 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
737 .into_iter()
738 .map(|(item, comments)| {
739 let refs = comments
740 .iter()
741 .filter_map(|i| self.comment_groups.get(*i))
742 .map(|r| json!({ "$ref": r }))
743 .collect();
744 (item, refs)
745 })
746 .collect();
747 for (item, comment_refs) in refs {
748 if comment_refs.is_empty() {
749 continue;
750 }
751 let Some(target) = self.item_mut(&item) else {
752 continue;
753 };
754 let Some(obj) = target.as_object_mut() else {
755 continue;
756 };
757 let tail: Vec<(String, Value)> = obj
758 .iter()
759 .skip_while(|(k, _)| k.as_str() != "prov")
760 .skip(1)
761 .map(|(k, v)| (k.clone(), v.clone()))
762 .collect();
763 for (k, _) in &tail {
764 obj.shift_remove(k);
765 }
766 obj.insert("comments".into(), Value::Array(comment_refs));
767 for (k, v) in tail {
768 obj.insert(k, v);
769 }
770 }
771 }
772
773 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
775 let idx = ref_index(self_ref)?;
776 let bucket = if self_ref.starts_with("#/texts/") {
777 &mut self.texts
778 } else if self_ref.starts_with("#/tables/") {
779 &mut self.tables
780 } else if self_ref.starts_with("#/pictures/") {
781 &mut self.pictures
782 } else if self_ref.starts_with("#/groups/") {
783 &mut self.groups
784 } else {
785 return None;
786 };
787 bucket.get_mut(idx)
788 }
789
790 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
794 use crate::tree::TreeKind;
795 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
798 let (mut nt, mut ng, mut ntb, mut np, mut nf, mut nk) = (0, 0, 0, 0, 0, 0);
799 for item in &tree.items {
800 if item.deleted {
801 refs.push(String::new());
802 continue;
803 }
804 let r = match &item.kind {
805 TreeKind::Text { .. } | TreeKind::Code { .. } => {
806 nt += 1;
807 format!("#/texts/{}", nt - 1)
808 }
809 TreeKind::Group { .. } => {
810 ng += 1;
811 format!("#/groups/{}", ng - 1)
812 }
813 TreeKind::Table { .. } => {
814 ntb += 1;
815 format!("#/tables/{}", ntb - 1)
816 }
817 TreeKind::Picture { .. } => {
818 np += 1;
819 format!("#/pictures/{}", np - 1)
820 }
821 TreeKind::FieldRegion { items } => {
822 nt += items
825 .iter()
826 .map(|i| {
827 [&i.marker, &i.key, &i.value]
828 .iter()
829 .filter(|p| p.is_some())
830 .count()
831 })
832 .sum::<usize>();
833 nf += 1;
834 format!("#/field_regions/{}", nf - 1)
835 }
836 TreeKind::KeyValueGraph { .. } => {
837 nk += 1;
838 format!("#/key_value_items/{}", nk - 1)
839 }
840 };
841 refs.push(r);
842 }
843 let ref_of = |id: usize| json!({ "$ref": refs[id] });
844 for (id, item) in tree.items.iter().enumerate() {
845 if item.deleted {
846 continue;
847 }
848 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
849 let children: Vec<Value> = item
853 .children
854 .iter()
855 .filter(|&&c| !tree.items[c].deleted)
856 .map(|&c| ref_of(c))
857 .collect();
858 let layer = item.layer.map_or("body", |l| l.value());
859 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
862 let self_ref = match &item.kind {
863 TreeKind::Text {
864 label,
865 text,
866 orig,
867 formatting,
868 hyperlink,
869 level,
870 list,
871 } => {
872 let mut tail = serde_json::Map::new();
875 if let Some(f) = formatting {
876 tail.insert("formatting".into(), formatting_json(f));
877 }
878 if let Some(h) = hyperlink {
879 tail.insert("hyperlink".into(), json!(h));
880 }
881 if let Some(l) = level {
882 tail.insert("level".into(), json!(l));
883 }
884 if let Some(l) = list {
885 tail.insert("enumerated".into(), json!(l.enumerated));
886 tail.insert("marker".into(), json!(l.marker));
887 }
888 let r = format!("#/texts/{}", self.texts.len());
889 let prov = self.take_prov(text.chars().count());
890 let mut item_json = json!({
891 "self_ref": r,
892 "parent": { "$ref": parent },
893 "children": children,
894 "content_layer": layer,
895 "label": label,
896 "prov": prov,
897 });
898 if !item.comments.is_empty() {
901 item_json["comments"] =
902 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
903 }
904 if let Some(track) = &item.source {
907 item_json["source"] = json!([track_json(track)]);
908 }
909 merge(
910 &mut item_json,
911 json!({
912 "orig": orig.as_deref().unwrap_or(text),
913 "text": text,
914 }),
915 );
916 merge(&mut item_json, Value::Object(tail));
917 if self.note_keys {
918 if !item.notes.is_empty() {
919 item_json["_notes"] = Value::Array(
920 item.notes
921 .iter()
922 .map(|n| json!([n.offset, n.text]))
923 .collect(),
924 );
925 }
926 if item.note_body {
927 item_json["_note_body"] = json!(true);
928 }
929 }
930 self.texts.push(item_json);
931 r
932 }
933 TreeKind::Code {
934 text,
935 orig,
936 language,
937 formatting,
938 hyperlink,
939 } => {
940 let r = format!("#/texts/{}", self.texts.len());
941 let prov = self.take_prov(text.chars().count());
942 let mut item_json = json!({
943 "self_ref": r,
944 "parent": { "$ref": parent },
945 "children": children,
946 "content_layer": layer,
947 "label": "code",
948 "prov": prov,
949 });
950 if !item.comments.is_empty() {
951 item_json["comments"] =
952 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
953 }
954 merge(
955 &mut item_json,
956 json!({
957 "orig": orig.as_deref().unwrap_or(text),
958 "text": text,
959 }),
960 );
961 if let Some(f) = formatting {
962 item_json["formatting"] = formatting_json(f);
963 }
964 if let Some(h) = hyperlink {
965 item_json["hyperlink"] = json!(h);
966 }
967 merge(
968 &mut item_json,
969 json!({
970 "captions": [],
971 "references": [],
972 "footnotes": [],
973 "code_language": code_language(language.as_deref()),
974 }),
975 );
976 self.texts.push(item_json);
977 r
978 }
979 TreeKind::Group { label, name } => {
980 self.pending_exact = None;
981 let r = format!("#/groups/{}", self.groups.len());
982 self.groups.push(json!({
983 "self_ref": r,
984 "parent": { "$ref": parent },
985 "children": children,
986 "content_layer": layer,
987 "name": name,
988 "label": label,
989 }));
990 r
991 }
992 TreeKind::Table {
993 table,
994 rich_cells,
995 captions,
996 } => {
997 let r = self.add_table_with(table, parent, true);
999 let idx = ref_index(&r).expect("table ref");
1000 let t = &mut self.tables[idx];
1001 t["children"] = Value::Array(children);
1002 t["content_layer"] = json!(layer);
1003 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
1004 for &(row, col, group) in rich_cells {
1008 let cell_ref = ref_of(group);
1009 let hit = |c: &Value| {
1010 c["start_row_offset_idx"] == json!(row)
1011 && c["start_col_offset_idx"] == json!(col)
1012 };
1013 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
1014 for c in cells.iter_mut().filter(|c| hit(c)) {
1015 c["ref"] = cell_ref.clone();
1016 }
1017 }
1018 }
1019 r
1020 }
1021 TreeKind::Picture {
1022 captions,
1023 image,
1024 classification,
1025 confidence,
1026 chart,
1027 dpi,
1028 } => {
1029 let mut meta = classification.as_ref().map(|c| {
1034 let mut pred = serde_json::Map::new();
1035 if let Some(conf) = confidence {
1036 pred.insert("confidence".into(), json!(conf));
1037 }
1038 pred.insert("class_name".into(), json!(c));
1039 json!({ "classification": { "predictions": [pred] } })
1040 });
1041 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
1042 if !t.rows.is_empty() {
1043 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
1044 }
1045 }
1046 let prov = self.take_prov(0);
1047 let r = self.push_picture(
1048 prov,
1049 captions.iter().map(|&c| ref_of(c)).collect(),
1050 children,
1051 image.as_ref(),
1052 meta,
1053 parent,
1054 );
1055 if let Some(idx) = ref_index(&r) {
1056 self.pictures[idx]["content_layer"] = json!(layer);
1057 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
1060 img["dpi"] = json!(dpi);
1061 }
1062 }
1063 r
1064 }
1065 TreeKind::FieldRegion { items } => {
1066 self.pending_exact = None;
1067 let r = self.add_field_region(items, parent);
1068 if let Some(region) = self.field_regions.last_mut() {
1069 region["content_layer"] = json!(layer);
1070 }
1071 r
1072 }
1073 TreeKind::KeyValueGraph { cells, links } => {
1074 self.pending_exact = None;
1075 let r = self.add_key_value_graph(cells, links, parent);
1076 if let Some(item) = self.key_value_items.last_mut() {
1077 item["content_layer"] = json!(layer);
1078 }
1079 r
1080 }
1081 };
1082 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1083 }
1084 tree.body
1085 .iter()
1086 .filter(|&&c| !tree.items[c].deleted)
1087 .map(|&c| ref_of(c))
1088 .collect()
1089 }
1090
1091 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1092 match node {
1093 Node::Heading { level: 1, text } => {
1094 Some(self.add_text("title", text, parent, json!({})))
1095 }
1096 Node::Heading { level, text } => Some(self.add_text(
1097 "section_header",
1098 text,
1099 parent,
1100 json!({ "level": level.saturating_sub(1) }),
1101 )),
1102 Node::Caption { text, href } => {
1103 let extra = match href {
1104 Some(url) => json!({ "hyperlink": url }),
1105 None => json!({}),
1106 };
1107 Some(self.add_text("caption", text, parent, extra))
1108 }
1109 Node::LabeledText { label, text, href } => {
1112 let extra = match href {
1113 Some(url) => json!({ "hyperlink": url }),
1114 None => json!({}),
1115 };
1116 Some(self.add_text(label, text, parent, extra))
1117 }
1118 Node::Paragraph { text } => {
1119 let t = text.trim();
1122 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1123 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1124 _ => Some(self.add_text("text", text, parent, json!({}))),
1125 }
1126 }
1127 Node::CheckboxItem { checked, text } => {
1128 let label = if *checked {
1134 "checkbox_selected"
1135 } else {
1136 "checkbox_unselected"
1137 };
1138 Some(self.add_text(label, text, parent, json!({})))
1139 }
1140 Node::Code {
1141 language,
1142 text,
1143 orig,
1144 ..
1145 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1146 Node::Formula {
1149 latex,
1150 orig,
1151 location,
1152 } => {
1153 self.adopt_loc(*location);
1154 Some(self.add_formula_item(latex, orig, parent))
1155 }
1156 Node::CommentSection {
1162 name,
1163 text,
1164 refs_note_text,
1165 grouped,
1166 } => {
1167 if !*grouped {
1168 let child =
1172 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1173 self.comment_groups.push(child.clone());
1174 return Some(child);
1175 }
1176 if let Some((_, self_ref)) = self
1177 .last_comment_section
1178 .as_ref()
1179 .filter(|(n, _)| n == name)
1180 .cloned()
1181 {
1182 let child =
1185 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1186 if let Some(children) =
1187 self.groups[group_index(&self_ref)]["children"].as_array_mut()
1188 {
1189 children.push(json!({ "$ref": child }));
1190 }
1191 self.comment_groups.push(if *refs_note_text {
1192 child
1193 } else {
1194 self_ref.clone()
1195 });
1196 return Some(self_ref);
1197 }
1198 let self_ref = format!("#/groups/{}", self.groups.len());
1199 self.groups.push(Value::Null);
1200 let child =
1201 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1202 self.groups[group_index(&self_ref)] = json!({
1203 "self_ref": self_ref,
1204 "parent": { "$ref": parent },
1205 "children": [{ "$ref": child }],
1206 "content_layer": "notes",
1207 "name": name,
1208 "label": "comment_section",
1209 });
1210 self.last_comment_section = Some((name.clone(), self_ref.clone()));
1211 self.comment_groups.push(if *refs_note_text {
1212 child
1213 } else {
1214 self_ref.clone()
1215 });
1216 Some(self_ref)
1217 }
1218 Node::Commented { comments, inner } => {
1221 let item = self.add_node(inner, parent)?;
1222 if !comments.is_empty() {
1223 self.pending_comments.push((item.clone(), comments.clone()));
1224 }
1225 Some(item)
1226 }
1227 Node::Table(t) => Some(self.add_table(t, parent)),
1228 Node::Picture {
1229 caption,
1230 caption_href,
1231 image,
1232 classification,
1233 caption_parent,
1234 caption_location,
1235 } => Some(self.add_picture(
1236 caption.as_deref(),
1237 caption_href.as_deref(),
1238 image.as_ref(),
1239 classification.as_deref().map(classification_meta),
1240 parent,
1241 (*caption_parent, *caption_location),
1242 )),
1243 Node::Chart {
1248 kind,
1249 table,
1250 caption,
1251 location,
1252 } => {
1253 self.adopt_loc(*location);
1254 let mut meta = json!({
1255 "classification": { "predictions": [{ "class_name": kind }] },
1256 });
1257 if !table.rows.is_empty() {
1258 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1259 }
1260 let mut captions = Vec::new();
1266 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1267 let prov = self.prov_json(unescape_text(cap).chars().count(), true);
1268 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1269 self.pending_siblings.push(json!({ "$ref": cap_ref }));
1270 captions.push(json!({ "$ref": cap_ref }));
1271 }
1272 let prov = self.take_prov(0);
1273 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1274 }
1275 Node::DoclangOnly(_) => None,
1277 Node::Group {
1278 label,
1279 name,
1280 layer,
1281 children,
1282 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1283 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1284 Node::KeyValueGraph { cells, links } => {
1285 Some(self.add_key_value_graph(cells, links, parent))
1286 }
1287 Node::InlineGroup { md_text, .. } => {
1290 Some(self.add_text("text", md_text, parent, json!({})))
1291 }
1292 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1294 Node::Furniture {
1300 layer: ContentLayer::Notes,
1301 inner,
1302 } => {
1303 let item = self.add_node(inner, parent)?;
1304 self.set_layer(&item, "notes");
1305 Some(item)
1306 }
1307 Node::Furniture { .. } => None,
1308 Node::PageFurniture {
1312 footer,
1313 location,
1314 text,
1315 } => {
1316 if self.cur_page > 0 {
1317 self.pending_loc = Some(*location);
1318 }
1319 let label = if *footer {
1320 "page_footer"
1321 } else {
1322 "page_header"
1323 };
1324 let item = self.add_text(label, text, parent, json!({}));
1325 self.pending_loc = None;
1326 self.set_layer(&item, "furniture");
1327 Some(item)
1328 }
1329 Node::FurnitureText { label, text } => {
1332 let item = self.add_text(label, text, parent, json!({}));
1333 self.set_layer(&item, "furniture");
1334 Some(item)
1335 }
1336 Node::PictureChildren(children) => {
1340 let pic = self.pictures.len().checked_sub(1)?;
1341 let pic_ref = format!("#/pictures/{pic}");
1342 let refs = self.walk_into(children, &pic_ref);
1343 if let Some(list) = self.pictures[pic]["children"].as_array_mut() {
1344 list.extend(refs);
1345 }
1346 None
1347 }
1348 Node::Located { location, inner } => {
1353 if self.cur_page > 0 {
1354 self.pending_loc = Some(*location);
1355 }
1356 let r = self.add_node(inner, parent);
1357 self.pending_loc = None;
1358 r
1359 }
1360 Node::Track { track, cue, inner } => {
1361 self.pending_track = Some((track.clone(), cue.clone()));
1362 let r = self.add_node(inner, parent);
1363 self.pending_track = None;
1364 r
1365 }
1366 Node::Prov {
1367 page_no,
1368 bbox,
1369 charspan,
1370 inner,
1371 ..
1372 } => {
1373 self.pending_exact = Some(ExactProv {
1374 page_no: *page_no,
1375 bbox: bbox.map(f64::from),
1376 bottom_left: false,
1377 charspan: *charspan,
1378 });
1379 let r = self.add_node(inner, parent);
1380 self.pending_exact = None;
1381 r
1382 }
1383 Node::PageBreak => None,
1385 Node::PageInfo {
1388 page_no,
1389 width,
1390 height,
1391 } => {
1392 self.cur_page = *page_no;
1393 self.cur_w = *width as f64;
1394 self.cur_h = *height as f64;
1395 if *page_no > 0 {
1396 self.pages.push((*page_no, self.cur_w, self.cur_h));
1397 }
1398 None
1399 }
1400 Node::ListItem { .. } => None,
1402 }
1403 }
1404
1405 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1409 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1410 self.field_regions.push(Value::Null);
1411 let region_index = self.field_regions.len() - 1;
1412 let mut item_refs = Vec::new();
1413 for item in items {
1414 item_refs.push(json!({ "$ref": self.add_field_item(item, &self_ref) }));
1415 }
1416 self.field_regions[region_index] = json!({
1417 "self_ref": self_ref,
1418 "parent": { "$ref": parent },
1419 "children": item_refs,
1420 "content_layer": "body",
1421 "label": "field_region",
1422 "prov": [],
1423 });
1424 self_ref
1425 }
1426
1427 fn add_key_value_graph(
1431 &mut self,
1432 cells: &[crate::GraphCell],
1433 links: &[crate::GraphLink],
1434 parent: &str,
1435 ) -> String {
1436 let self_ref = format!("#/key_value_items/{}", self.key_value_items.len());
1437 let cells: Vec<Value> = cells
1438 .iter()
1439 .map(|c| {
1440 json!({
1441 "label": c.label,
1442 "cell_id": c.cell_id,
1443 "text": c.text,
1444 "orig": c.orig,
1445 })
1446 })
1447 .collect();
1448 let links: Vec<Value> = links
1449 .iter()
1450 .map(|l| {
1451 json!({
1452 "label": l.label,
1453 "source_cell_id": l.source_cell_id,
1454 "target_cell_id": l.target_cell_id,
1455 })
1456 })
1457 .collect();
1458 self.key_value_items.push(json!({
1459 "self_ref": self_ref,
1460 "parent": { "$ref": parent },
1461 "children": [],
1462 "content_layer": "body",
1463 "label": "key_value_region",
1464 "prov": [],
1465 "captions": [],
1466 "references": [],
1467 "footnotes": [],
1468 "graph": { "cells": cells, "links": links },
1469 }));
1470 self_ref
1471 }
1472
1473 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1474 let self_ref = format!("#/field_items/{}", self.field_items.len());
1475 self.field_items.push(Value::Null);
1476 let item_index = self.field_items.len() - 1;
1477 let mut child_refs = Vec::new();
1478 for (label, text) in [
1479 ("marker", &item.marker),
1480 ("field_key", &item.key),
1481 ("field_value", &item.value),
1482 ] {
1483 if let Some(text) = text {
1484 let extra = match (label, &item.value_kind) {
1487 ("field_value", Some(kind)) => json!({ "kind": kind }),
1488 _ => json!({}),
1489 };
1490 child_refs.push(json!({ "$ref": self.add_text(label, text, &self_ref, extra) }));
1491 }
1492 }
1493 self.field_items[item_index] = json!({
1494 "self_ref": self_ref,
1495 "parent": { "$ref": parent },
1496 "children": child_refs,
1497 "content_layer": "body",
1498 "label": "field_item",
1499 "prov": [],
1500 });
1501 self_ref
1502 }
1503
1504 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1508 let bucket = match self_ref.split('/').nth(1) {
1509 Some("texts") => &mut self.texts,
1510 Some("tables") => &mut self.tables,
1511 Some("pictures") => &mut self.pictures,
1512 Some("groups") => &mut self.groups,
1513 _ => return,
1514 };
1515 if let Some(item) = self_ref
1516 .rsplit('/')
1517 .next()
1518 .and_then(|i| i.parse::<usize>().ok())
1519 .and_then(|i| bucket.get_mut(i))
1520 {
1521 item["content_layer"] = json!(layer);
1522 }
1523 }
1524
1525 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1526 let prov = self.take_prov(unescape_text(text).chars().count());
1527 self.add_text_with(label, text, parent, extra, prov)
1528 }
1529
1530 fn add_text_with(
1533 &mut self,
1534 label: &str,
1535 text: &str,
1536 parent: &str,
1537 extra: Value,
1538 prov: Value,
1539 ) -> String {
1540 let self_ref = format!("#/texts/{}", self.texts.len());
1541 let (raw, track) = match self.pending_track.take() {
1545 Some((track, cue)) => (cue, Some(track)),
1546 None => (unescape_text(text), None),
1547 };
1548 let mut item = json!({
1549 "self_ref": self_ref,
1550 "parent": { "$ref": parent },
1551 "children": [],
1552 "content_layer": "body",
1553 "label": label,
1554 "prov": prov,
1555 });
1556 if let Some(track) = track {
1559 item["source"] = json!([track_json(&track)]);
1560 }
1561 item["orig"] = json!(raw);
1562 item["text"] = json!(raw);
1563 merge(&mut item, extra);
1564 self.texts.push(item);
1565 self_ref
1566 }
1567
1568 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
1571 let self_ref = format!("#/texts/{}", self.texts.len());
1572 let prov = self.take_prov(latex.chars().count());
1573 self.texts.push(json!({
1574 "self_ref": self_ref,
1575 "parent": { "$ref": parent },
1576 "children": [],
1577 "content_layer": "body",
1578 "label": "formula",
1579 "prov": prov,
1580 "orig": latex,
1581 "text": latex,
1582 }));
1583 self_ref
1584 }
1585
1586 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
1590 let self_ref = format!("#/texts/{}", self.texts.len());
1591 let prov = self.take_prov(latex.chars().count());
1592 self.texts.push(json!({
1593 "self_ref": self_ref,
1594 "parent": { "$ref": parent },
1595 "children": [],
1596 "content_layer": "body",
1597 "label": "formula",
1598 "prov": prov,
1599 "orig": orig,
1600 "text": latex,
1601 }));
1602 self_ref
1603 }
1604
1605 fn add_code(
1606 &mut self,
1607 text: &str,
1608 language: Option<&str>,
1609 orig: Option<&str>,
1610 parent: &str,
1611 ) -> String {
1612 let self_ref = format!("#/texts/{}", self.texts.len());
1613 let raw = unescape_text(text);
1614 let prov = self.take_prov(raw.chars().count());
1615 self.texts.push(json!({
1616 "self_ref": self_ref,
1617 "parent": { "$ref": parent },
1618 "children": [],
1619 "content_layer": "body",
1620 "label": "code",
1621 "prov": prov,
1622 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
1625 "text": raw,
1626 "captions": [],
1627 "references": [],
1628 "footnotes": [],
1629 "code_language": code_language(language),
1630 }));
1631 self_ref
1632 }
1633
1634 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
1637 let self_ref = format!("#/groups/{}", self.groups.len());
1638 self.groups.push(Value::Null);
1640 let base = level_of(&items[0]);
1641 let mut children = Vec::new();
1642 let mut i = 0;
1643 while i < items.len() {
1644 if !matches!(items[i], Node::ListItem { .. }) {
1647 i += 1;
1648 continue;
1649 }
1650 let lvl = level_of(&items[i]);
1651 if lvl > base {
1652 i += 1;
1654 continue;
1655 }
1656 let item_ref = self.add_list_item(&items[i], &self_ref);
1657 let mut j = i + 1;
1659 while j < items.len() && level_of(&items[j]) > base {
1660 j += 1;
1661 }
1662 if j > i + 1 {
1663 let mut nested = Vec::new();
1664 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
1665 if let Some(idx) = ref_index(&item_ref) {
1667 self.texts[idx]["children"]
1668 .as_array_mut()
1669 .unwrap()
1670 .extend(nested);
1671 }
1672 }
1673 children.push(json!({ "$ref": item_ref }));
1674 i = j;
1675 }
1676 self.groups[group_index(&self_ref)] = json!({
1677 "self_ref": self_ref,
1678 "parent": { "$ref": parent },
1679 "children": children,
1680 "content_layer": "body",
1681 "name": "list",
1682 "label": "list",
1683 });
1684 self_ref
1685 }
1686
1687 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
1688 let Node::ListItem {
1689 ordered,
1690 number,
1691 text,
1692 location,
1693 ..
1694 } = node
1695 else {
1696 unreachable!()
1697 };
1698 self.adopt_loc(*location);
1699 let self_ref = format!("#/texts/{}", self.texts.len());
1700 let raw = unescape_text(text);
1701 let prov = self.take_prov(raw.chars().count());
1702 let marker = if *ordered {
1703 format!("{number}.")
1704 } else {
1705 "-".to_string()
1706 };
1707 self.texts.push(json!({
1708 "self_ref": self_ref,
1709 "parent": { "$ref": parent },
1710 "children": [],
1711 "content_layer": "body",
1712 "label": "list_item",
1713 "prov": prov,
1714 "orig": raw,
1715 "text": raw,
1716 "enumerated": ordered,
1717 "marker": marker,
1718 }));
1719 self_ref
1720 }
1721
1722 fn add_table(&mut self, t: &Table, parent: &str) -> String {
1723 self.add_table_with(t, parent, false)
1724 }
1725
1726 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
1729 let self_ref = format!("#/tables/{}", self.tables.len());
1730 self.adopt_loc(t.location);
1731 let prov = self.take_prov(0);
1732 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
1736 Some(cap) => self.add_caption(
1737 cap,
1738 json!({}),
1739 &self_ref,
1740 parent,
1741 (t.caption_parent, t.caption_location),
1742 ),
1743 None => (Vec::new(), Vec::new()),
1744 };
1745 let data = table_data_with(t, raw);
1746 self.tables.push(json!({
1747 "self_ref": self_ref,
1748 "parent": { "$ref": parent },
1749 "children": children,
1750 "content_layer": "body",
1751 "label": "table",
1752 "prov": prov,
1753 "captions": captions,
1754 "references": [],
1755 "footnotes": [],
1756 "data": data,
1757 "annotations": [],
1758 }));
1759 self_ref
1760 }
1761
1762 fn add_caption(
1771 &mut self,
1772 text: &str,
1773 extra: Value,
1774 self_ref: &str,
1775 parent: &str,
1776 (choice, location): (CaptionParent, Option<[u16; 4]>),
1777 ) -> (Vec<Value>, Vec<Value>) {
1778 let cap_parent = match choice {
1783 CaptionParent::Item => self_ref,
1784 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
1785 CaptionParent::Body => "#/body",
1786 };
1787 if self.cur_page > 0 && location.is_some() {
1788 self.pending_loc = location;
1789 }
1790 let cap_ref = json!({ "$ref": self.add_text("caption", text, cap_parent, extra) });
1791 match choice {
1792 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
1793 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
1796 CaptionParent::Body if parent == "#/body" => {
1797 self.pending_siblings.push(cap_ref.clone())
1798 }
1799 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
1800 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
1803 }
1804 (vec![cap_ref], Vec::new())
1805 }
1806
1807 fn add_picture(
1810 &mut self,
1811 caption: Option<&str>,
1812 caption_href: Option<&str>,
1813 image: Option<&crate::PictureImage>,
1814 meta: Option<Value>,
1815 parent: &str,
1816 caption_at: (CaptionParent, Option<[u16; 4]>),
1817 ) -> String {
1818 let self_ref = format!("#/pictures/{}", self.pictures.len());
1819 let prov = self.take_prov(0);
1822 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
1823 Some(cap) => {
1824 let extra = match caption_href {
1828 Some(href) => json!({ "hyperlink": href }),
1829 None => json!({}),
1830 };
1831 self.add_caption(cap, extra, &self_ref, parent, caption_at)
1832 }
1833 None => (Vec::new(), Vec::new()),
1834 };
1835 self.push_picture(prov, captions, children, image, meta, parent)
1836 }
1837
1838 fn push_picture(
1841 &mut self,
1842 prov: Value,
1843 captions: Vec<Value>,
1844 children: Vec<Value>,
1845 image: Option<&crate::PictureImage>,
1846 meta: Option<Value>,
1847 parent: &str,
1848 ) -> String {
1849 let self_ref = format!("#/pictures/{}", self.pictures.len());
1850 let annotations = meta
1854 .as_ref()
1855 .and_then(|m| m.get("annotations").cloned())
1856 .unwrap_or_else(|| json!([]));
1857 let meta = meta.map(|mut m| {
1858 if let Some(obj) = m.as_object_mut() {
1859 obj.remove("annotations");
1860 }
1861 m
1862 });
1863 let mut item = match meta {
1867 Some(meta) => json!({
1868 "self_ref": self_ref,
1869 "parent": { "$ref": parent },
1870 "children": children,
1871 "content_layer": "body",
1872 "meta": meta,
1873 "label": "picture",
1874 "prov": prov,
1875 "captions": captions,
1876 "references": [],
1877 "footnotes": [],
1878 "annotations": annotations,
1879 }),
1880 None => json!({
1881 "self_ref": self_ref,
1882 "parent": { "$ref": parent },
1883 "children": children,
1884 "content_layer": "body",
1885 "label": "picture",
1886 "prov": prov,
1887 "captions": captions,
1888 "references": [],
1889 "footnotes": [],
1890 "annotations": annotations,
1891 }),
1892 };
1893 if let Some(img) = image {
1898 let image = image_ref_json(img);
1899 if let Some(obj) = item.as_object_mut() {
1900 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
1901 obj.insert("image".into(), image);
1902 obj.insert("annotations".into(), annotations);
1903 }
1904 }
1905 self.pictures.push(item);
1906 self_ref
1907 }
1908
1909 fn add_group(
1910 &mut self,
1911 label: &str,
1912 name: Option<&str>,
1913 layer: Option<ContentLayer>,
1914 nodes: &[Node],
1915 parent: &str,
1916 ) -> String {
1917 let self_ref = format!("#/groups/{}", self.groups.len());
1918 self.groups.push(Value::Null);
1919 let mark = (
1923 self.texts.len(),
1924 self.tables.len(),
1925 self.pictures.len(),
1926 self.groups.len(),
1927 );
1928 let children = self.walk_into(nodes, &self_ref);
1929 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
1930 let content_layer = layer.map_or("body", |l| l.value());
1931 self.groups[group_index(&self_ref)] = json!({
1932 "self_ref": self_ref,
1933 "parent": { "$ref": parent },
1934 "children": children,
1935 "content_layer": content_layer,
1936 "name": name,
1937 "label": label,
1938 });
1939 if layer.is_some() {
1940 let (t, tb, p, g) = mark;
1941 for item in self.texts[t..]
1942 .iter_mut()
1943 .chain(self.tables[tb..].iter_mut())
1944 .chain(self.pictures[p..].iter_mut())
1945 .chain(self.groups[g..].iter_mut())
1946 {
1947 if let Some(obj) = item.as_object_mut() {
1948 obj.insert("content_layer".into(), json!(content_layer));
1949 }
1950 }
1951 }
1952 self_ref
1953 }
1954
1955 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Value> {
1958 let seqs: Option<Vec<usize>> = nodes
1963 .iter()
1964 .map(|n| match n {
1965 Node::Prov { seq: Some(s), .. } => Some(*s),
1966 _ => None,
1967 })
1968 .collect();
1969 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
1970 let mut order: Vec<usize> = (0..nodes.len()).collect();
1971 order.sort_by_key(|&i| seqs[i]);
1972 let mut slots: Vec<Vec<Value>> = vec![Vec::new(); nodes.len()];
1973 for i in order {
1974 if let Some(r) = self.add_node(&nodes[i], parent) {
1975 slots[i].append(&mut self.pending_siblings);
1976 slots[i].push(json!({ "$ref": r }));
1977 slots[i].append(&mut self.pending_after);
1978 }
1979 if parent == "#/body" {
1980 slots[i].append(&mut self.pending_body);
1981 }
1982 }
1983 return slots.into_iter().flatten().collect();
1984 }
1985 let mut children = Vec::new();
1986 let mut i = 0;
1987 while i < nodes.len() {
1988 if matches!(nodes[i], Node::ListItem { .. }) {
1989 let start = i;
1990 i += 1;
1991 loop {
1992 match nodes.get(i) {
1993 Some(Node::ListItem { .. }) => i += 1,
1994 Some(Node::Paragraph { text })
1997 if text.is_empty()
1998 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
1999 {
2000 i += 1
2001 }
2002 _ => break,
2003 }
2004 }
2005 self.add_sibling_lists(&nodes[start..i], parent, &mut children);
2006 } else {
2007 if let Some(r) = self.add_node(&nodes[i], parent) {
2008 children.append(&mut self.pending_siblings);
2009 children.push(json!({ "$ref": r }));
2010 children.append(&mut self.pending_after);
2011 }
2012 i += 1;
2013 }
2014 if parent == "#/body" {
2017 children.append(&mut self.pending_body);
2018 }
2019 }
2020 children
2021 }
2022
2023 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
2028 let base = level_of(&run[0]);
2029 let mut seg = 0;
2030 for k in 0..run.len() {
2031 let Node::ListItem {
2032 first_in_list,
2033 level,
2034 ..
2035 } = &run[k]
2036 else {
2037 continue;
2038 };
2039 if *level != base {
2040 continue; }
2042 if k > seg && *first_in_list {
2043 out.push(json!({ "$ref": self.add_list(&run[seg..k], parent) }));
2044 seg = k;
2045 }
2046 }
2047 out.push(json!({ "$ref": self.add_list(&run[seg..], parent) }));
2048 }
2049}
2050
2051fn level_of(node: &Node) -> u8 {
2052 match node {
2053 Node::ListItem { level, .. } => *level,
2054 _ => 0,
2055 }
2056}
2057
2058fn group_index(self_ref: &str) -> usize {
2059 self_ref.rsplit('/').next().unwrap().parse().unwrap()
2060}
2061
2062fn ref_index(self_ref: &str) -> Option<usize> {
2063 self_ref.rsplit('/').next()?.parse().ok()
2064}
2065
2066fn merge(target: &mut Value, extra: Value) {
2068 if let (Some(t), Some(e)) = (target.as_object_mut(), extra.as_object()) {
2069 for (k, v) in e {
2070 t.insert(k.clone(), v.clone());
2071 }
2072 }
2073}
2074
2075fn unescape_text(s: &str) -> String {
2077 s.replace("<", "<")
2078 .replace(">", ">")
2079 .replace("&", "&")
2080 .replace("\\_", "_")
2081}
2082
2083fn fnv1a(s: &str) -> u64 {
2086 let mut h: u64 = 0xcbf29ce484222325;
2087 for b in s.bytes() {
2088 h ^= b as u64;
2089 h = h.wrapping_mul(0x100000001b3);
2090 }
2091 h
2092}
2093
2094#[cfg(test)]
2095mod tests {
2096 use crate::{
2097 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
2098 };
2099 use serde_json::Value;
2100
2101 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
2102 crate::TableCell {
2103 text: text.into(),
2104 bbox: None,
2105 start_row: row,
2106 start_col: col,
2107 row_span: 1,
2108 col_span,
2109 column_header: row == 0,
2110 row_header: false,
2111 row_section: false,
2112 }
2113 }
2114
2115 #[test]
2121 fn ragged_rows_take_their_grid_from_first_class_cells() {
2122 let cells = vec![
2123 cell_at("h0", 0, 0, 1),
2124 cell_at("h2", 0, 2, 1),
2125 cell_at("wide", 1, 0, 3),
2126 ];
2127 let export = |rows: Vec<Vec<String>>| -> Value {
2128 let mut doc = DoclingDocument::new("t");
2129 doc.push(Node::Table(Table {
2130 rows,
2131 cells: Some(cells.clone()),
2132 ..Table::default()
2133 }));
2134 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2135 v["tables"][0]["data"].clone()
2136 };
2137 let data = export(vec![
2139 vec!["h0".into(), "h2".into()],
2140 vec!["wide".into(), "wide".into(), "wide".into()],
2141 ]);
2142 assert_eq!(data["num_rows"], 2);
2143 assert_eq!(data["num_cols"], 3);
2144 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
2145 let row0: Vec<&str> = data["grid"][0]
2146 .as_array()
2147 .unwrap()
2148 .iter()
2149 .map(|c| c["text"].as_str().unwrap())
2150 .collect();
2151 assert_eq!(row0, ["h0", "", "h2"]);
2152 assert_eq!(data["table_cells"][2]["col_span"], 3);
2153 let data = export(vec![
2156 vec!["h0".into(), "h2".into()],
2157 vec!["wide".into(), "wide".into()],
2158 ]);
2159 assert_eq!(data["num_cols"], 2);
2160 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
2161 }
2162
2163 fn doc_with_image() -> DoclingDocument {
2164 let mut doc = DoclingDocument::new("t");
2165 doc.push(Node::Picture {
2166 caption: Some("Fig 1".into()),
2167 caption_href: None,
2168 image: Some(PictureImage {
2169 dpi: crate::PictureImage::DEFAULT_DPI,
2170 mimetype: "image/png".into(),
2171 width: 4,
2172 height: 2,
2173 data: b"foobar".to_vec(),
2174 }),
2175 classification: None,
2176 caption_parent: Default::default(),
2177 caption_location: None,
2178 });
2179 doc
2180 }
2181
2182 #[test]
2187 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
2188 let mut doc = DoclingDocument::new("t");
2189 doc.push(Node::Heading {
2190 level: 1,
2191 text: "Slide One".into(),
2192 });
2193 doc.push(Node::Furniture {
2194 layer: ContentLayer::Notes,
2195 inner: Box::new(Node::Located {
2196 location: [0, 0, 0, 0],
2197 inner: Box::new(Node::Paragraph {
2198 text: "Speaker note for slide 1.".into(),
2199 }),
2200 }),
2201 });
2202 doc.push(Node::Furniture {
2203 layer: ContentLayer::Furniture,
2204 inner: Box::new(Node::Paragraph {
2205 text: "page header".into(),
2206 }),
2207 });
2208
2209 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2210 let texts = v["texts"].as_array().unwrap();
2211 assert_eq!(
2212 texts
2213 .iter()
2214 .map(|t| (
2215 t["label"].as_str().unwrap(),
2216 t["content_layer"].as_str().unwrap(),
2217 t["text"].as_str().unwrap()
2218 ))
2219 .collect::<Vec<_>>(),
2220 vec![
2221 ("title", "body", "Slide One"),
2222 ("text", "notes", "Speaker note for slide 1."),
2223 ],
2224 "the note is carried on its own layer; the furniture is not carried"
2225 );
2226 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
2228 }
2229
2230 #[test]
2236 fn continuation_flags_become_spanning_cells() {
2237 let mut doc = DoclingDocument::new("t");
2238 let rows = vec![
2240 vec!["merged".to_string(), "merged".into(), "merged".into()],
2241 vec!["merged".to_string(), "merged".into(), "merged".into()],
2242 vec!["a".to_string(), "b".into(), "c".into()],
2243 ];
2244 doc.push(Node::Table(crate::Table {
2245 rows,
2246 location: None,
2247 structure: Some(crate::TableStructure {
2248 header_row: vec![true, false, false],
2249 col_continuation: vec![
2250 vec![false, true, true],
2251 vec![false, true, true],
2252 vec![false, false, false],
2253 ],
2254 row_continuation: vec![
2255 vec![false, false, false],
2256 vec![true, true, true],
2257 vec![false, false, false],
2258 ],
2259 row_header: Vec::new(),
2260 col_header: Vec::new(),
2261 }),
2262 cell_blocks: None,
2263 cells: None,
2264 caption: None,
2265 caption_parent: Default::default(),
2266 caption_location: None,
2267 }));
2268 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2269 let data = &v["tables"][0]["data"];
2270 assert_eq!(data["num_rows"], 3);
2271 assert_eq!(data["num_cols"], 3);
2272 let cells = data["table_cells"].as_array().unwrap();
2273 assert_eq!(
2274 cells.len(),
2275 4,
2276 "one cell for the range, three for the plain row"
2277 );
2278 assert_eq!(
2279 cells[0],
2280 serde_json::json!({
2281 "row_span": 2, "col_span": 3,
2282 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2283 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2284 "text": "merged", "column_header": true, "row_header": false,
2285 "row_section": false, "fillable": false,
2286 })
2287 );
2288 assert_eq!(cells[1]["text"], "a");
2289 assert_eq!(cells[1]["row_span"], 1);
2290 assert_eq!(cells[1]["column_header"], false);
2291 let grid = data["grid"].as_array().unwrap();
2293 assert_eq!(grid.len(), 3);
2294 for (r, row) in grid.iter().take(2).enumerate() {
2295 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2296 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2297 }
2298 }
2299 assert_eq!(grid[2][2]["text"], "c");
2300 }
2301
2302 #[test]
2308 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2309 let mut doc = DoclingDocument::new("t");
2310 doc.push(Node::PageInfo {
2311 page_no: 1,
2312 width: 3.0,
2313 height: 4.0,
2314 });
2315 let table = crate::Table {
2316 rows: vec![vec!["a".to_string(), "b".into()]],
2317 ..Default::default()
2318 };
2319 doc.push(Node::Group {
2320 label: "sheet".into(),
2321 name: Some("Data".into()),
2322 layer: None,
2323 children: vec![
2324 Node::Prov {
2329 page_no: 1,
2330 bbox: [0.0, 0.0, 3.0, 4.0],
2331 charspan: [0, 0],
2332 seq: Some(1),
2333 inner: Box::new(Node::Table(table.clone())),
2334 },
2335 Node::Prov {
2336 page_no: 1,
2337 bbox: [0.0, 1.0, 1.0, 1.0],
2338 charspan: [0, 0],
2339 seq: Some(0),
2340 inner: Box::new(Node::Chart {
2341 kind: "bar_chart".into(),
2342 table,
2343 caption: Some("Sales".into()),
2344 location: Some([0, 128, 170, 128]),
2345 }),
2346 },
2347 ],
2348 });
2349 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2350 assert_eq!(
2351 v["pages"],
2352 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2353 );
2354 assert_eq!(
2355 v["tables"][0]["prov"],
2356 serde_json::json!([{
2357 "page_no": 1,
2358 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2359 "charspan": [0, 0],
2360 }])
2361 );
2362 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2363 let sheet = &v["groups"][0];
2366 assert_eq!(
2367 sheet["children"],
2368 serde_json::json!([
2369 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2370 ])
2371 );
2372 let cap = &v["texts"][0];
2373 assert_eq!(cap["label"], "caption");
2374 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2375 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2376 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2377 let pic = &v["pictures"][0];
2378 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2379 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2380 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2381 assert_eq!(
2382 pic["meta"]["classification"]["predictions"][0]["class_name"],
2383 "bar_chart"
2384 );
2385 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2386 }
2387
2388 #[test]
2392 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2393 let mut doc = DoclingDocument::new("t");
2394 doc.push(Node::PageInfo {
2395 page_no: 1,
2396 width: 12192000.0,
2397 height: 6858000.0,
2398 });
2399 doc.push(Node::Furniture {
2400 layer: ContentLayer::Notes,
2401 inner: Box::new(Node::Located {
2402 location: [0, 0, 0, 0],
2403 inner: Box::new(Node::Paragraph {
2404 text: "a note".into(),
2405 }),
2406 }),
2407 });
2408 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2409 let prov = &v["texts"][0]["prov"][0];
2410 assert_eq!(prov["page_no"], 1);
2411 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2412 assert_eq!(
2413 prov["bbox"],
2414 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2415 );
2416 assert_eq!(
2418 v["pages"]["1"]["size"],
2419 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2420 );
2421 }
2422
2423 #[test]
2433 fn captions_and_checkboxes_carry_their_own_prov() {
2434 let mut doc = DoclingDocument::new("t");
2435 doc.push(Node::PageInfo {
2436 page_no: 1,
2437 width: 512.0,
2438 height: 512.0,
2439 });
2440 doc.push(Node::Located {
2441 location: [64, 64, 448, 256],
2442 inner: Box::new(Node::Picture {
2443 caption: Some("Figure 1".into()),
2444 caption_href: None,
2445 image: None,
2446 classification: None,
2447 caption_parent: CaptionParent::Item,
2448 caption_location: Some([64, 264, 448, 280]),
2449 }),
2450 });
2451 doc.push(Node::PageInfo {
2452 page_no: 2,
2453 width: 512.0,
2454 height: 512.0,
2455 });
2456 doc.push(Node::Located {
2457 location: [64, 120, 448, 300],
2458 inner: Box::new(Node::Table(Table {
2459 rows: vec![vec!["a".into()]],
2460 caption: Some("Table 1".into()),
2461 caption_parent: CaptionParent::Item,
2462 caption_location: Some([64, 100, 300, 112]),
2463 ..Table::default()
2464 })),
2465 });
2466 doc.push(Node::Located {
2467 location: [64, 320, 200, 332],
2468 inner: Box::new(Node::CheckboxItem {
2469 checked: false,
2470 text: "First option".into(),
2471 }),
2472 });
2473 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2474 let texts = v["texts"].as_array().unwrap();
2475 let by_text = |t: &str| texts.iter().find(|x| x["text"] == t).expect(t).clone();
2476 let bbox = |p: &Value| {
2477 let b = &p["prov"][0]["bbox"];
2478 [&b["l"], &b["t"], &b["r"], &b["b"]].map(|x| x.as_f64().unwrap())
2479 };
2480 let fig = by_text("Figure 1");
2483 assert_eq!(fig["prov"][0]["page_no"], 1);
2484 assert_eq!(bbox(&fig), [64.0, 248.0, 448.0, 232.0]);
2485 assert_eq!(fig["prov"][0]["charspan"], serde_json::json!([0, 8]));
2486 assert_eq!(fig["parent"]["$ref"], "#/pictures/0");
2487 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
2488 let tab = by_text("Table 1");
2490 assert_eq!(tab["prov"][0]["page_no"], 2);
2491 assert_eq!(bbox(&tab), [64.0, 412.0, 300.0, 400.0]);
2492 assert_eq!(v["tables"][0]["prov"][0]["bbox"]["t"], 392.0);
2493 let cb = by_text("First option");
2495 assert_eq!(cb["label"], "checkbox_unselected");
2496 assert_eq!(cb["prov"][0]["page_no"], 2);
2497 assert_eq!(bbox(&cb), [64.0, 192.0, 200.0, 180.0]);
2498 assert!(texts
2499 .iter()
2500 .all(|t| !t["prov"].as_array().unwrap().is_empty()));
2501
2502 let mut plain = DoclingDocument::new("t");
2505 plain.push(Node::PageInfo {
2506 page_no: 1,
2507 width: 512.0,
2508 height: 512.0,
2509 });
2510 plain.push(Node::Located {
2511 location: [64, 64, 448, 256],
2512 inner: Box::new(Node::Picture {
2513 caption: Some("Figure 1".into()),
2514 caption_href: None,
2515 image: None,
2516 classification: None,
2517 caption_parent: CaptionParent::Item,
2518 caption_location: None,
2519 }),
2520 });
2521 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2522 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2523 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
2524 }
2525
2526 #[test]
2527 fn page_markers_produce_pages_and_prov() {
2528 let mut doc = DoclingDocument::new("t");
2529 doc.push(Node::PageInfo {
2530 page_no: 1,
2531 width: 512.0,
2532 height: 1024.0,
2533 });
2534 doc.push(Node::Located {
2535 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
2537 text: "hello".into(),
2538 }),
2539 });
2540 doc.push(Node::Table(Table {
2541 rows: vec![vec!["a".into()]],
2542 location: Some([0, 0, 512, 512]),
2543 ..Table::default()
2544 }));
2545 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2546 assert_eq!(v["pages"]["1"]["page_no"], 1);
2547 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
2548 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
2549 let prov = &v["texts"][0]["prov"][0];
2552 assert_eq!(prov["page_no"], 1);
2553 assert_eq!(prov["bbox"]["l"], 128.0);
2554 assert_eq!(prov["bbox"]["t"], 896.0);
2555 assert_eq!(prov["bbox"]["r"], 256.0);
2556 assert_eq!(prov["bbox"]["b"], 768.0);
2557 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
2558 assert_eq!(prov["charspan"][1], 5);
2559 let tprov = &v["tables"][0]["prov"][0];
2561 assert_eq!(tprov["bbox"]["t"], 1024.0);
2562 assert_eq!(tprov["bbox"]["b"], 0.0);
2563 assert_eq!(tprov["charspan"][1], 0);
2564
2565 let mut plain = DoclingDocument::new("t");
2567 plain.push(Node::Located {
2568 location: [1, 2, 3, 4],
2569 inner: Box::new(Node::Paragraph { text: "x".into() }),
2570 });
2571 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2572 assert_eq!(v["pages"], serde_json::json!({}));
2573 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2574 }
2575
2576 #[test]
2579 fn page_furniture_becomes_furniture_layer_text() {
2580 let mut doc = DoclingDocument::new("t");
2581 doc.push(Node::PageInfo {
2582 page_no: 1,
2583 width: 512.0,
2584 height: 512.0,
2585 });
2586 doc.push(Node::PageFurniture {
2587 footer: false,
2588 location: [10, 0, 100, 20],
2589 text: "Chapter 1".into(),
2590 });
2591 doc.push(Node::Paragraph {
2592 text: "body".into(),
2593 });
2594 doc.push(Node::PageFurniture {
2595 footer: true,
2596 location: [400, 490, 500, 512],
2597 text: "1.10.2".into(),
2598 });
2599 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2600 let texts = v["texts"].as_array().unwrap();
2601 assert_eq!(texts.len(), 3);
2602 assert_eq!(texts[0]["label"], "page_header");
2603 assert_eq!(texts[0]["content_layer"], "furniture");
2604 assert_eq!(texts[0]["parent"]["$ref"], "#/body");
2605 assert_eq!(texts[0]["prov"][0]["bbox"]["l"], 10.0);
2606 assert_eq!(texts[1]["content_layer"], "body");
2607 assert_eq!(texts[1]["prov"], serde_json::json!([]));
2608 assert_eq!(texts[2]["label"], "page_footer");
2609 assert_eq!(texts[2]["text"], "1.10.2");
2610 assert_eq!(texts[2]["content_layer"], "furniture");
2611 }
2612
2613 #[test]
2614 fn picture_image_in_markdown_modes_and_json() {
2615 let doc = doc_with_image();
2616 assert!(doc.export_to_markdown().contains("<!-- image -->"));
2618 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
2620 assert!(
2621 md.contains(""),
2622 "got:\n{md}"
2623 );
2624 assert!(files.is_empty());
2625 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
2627 assert!(
2628 md.contains(""),
2629 "got:\n{md}"
2630 );
2631 assert_eq!(
2632 files,
2633 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
2634 );
2635 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2638 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
2639 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
2640 let keys: Vec<&str> = v["pictures"][0]
2641 .as_object()
2642 .unwrap()
2643 .keys()
2644 .map(String::as_str)
2645 .collect();
2646 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
2647 assert_eq!(
2648 v["pictures"][0]["image"]["uri"],
2649 "data:image/png;base64,Zm9vYmFy"
2650 );
2651 }
2652
2653 #[test]
2654 fn exports_docling_schema() {
2655 let mut doc = DoclingDocument::new("t");
2656 doc.push(Node::Heading {
2657 level: 1,
2658 text: "Title".into(),
2659 });
2660 doc.push(Node::Heading {
2661 level: 2,
2662 text: "Sec".into(),
2663 });
2664 doc.push(Node::Paragraph {
2665 text: "Body & more".into(),
2666 }); doc.push(Node::ListItem {
2668 ordered: false,
2669 number: 0,
2670 first_in_list: true,
2671 text: "one".into(),
2672 level: 0,
2673 marker: None,
2674 location: None,
2675 dclx: None,
2676 href: None,
2677 layer: None,
2678 });
2679 doc.push(Node::ListItem {
2680 ordered: false,
2681 number: 0,
2682 first_in_list: false,
2683 text: "two".into(),
2684 level: 0,
2685 marker: None,
2686 location: None,
2687 dclx: None,
2688 href: None,
2689 layer: None,
2690 });
2691 doc.push(Node::Table(Table {
2692 rows: vec![vec!["A".into(), "B".into()]],
2693 location: None,
2694 structure: None,
2695 cell_blocks: None,
2696 cells: None,
2697 caption: None,
2698 caption_parent: Default::default(),
2699 caption_location: None,
2700 }));
2701
2702 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2703 assert_eq!(v["schema_name"], "DoclingDocument");
2704 assert_eq!(v["version"], "1.10.0");
2705 assert_eq!(v["texts"][0]["label"], "title");
2706 assert_eq!(v["texts"][1]["label"], "section_header");
2707 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
2711 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
2712 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
2713 assert_eq!(v["texts"][3]["marker"], "-");
2714 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
2716 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
2717 }
2718 #[test]
2722 fn a_layered_group_stamps_its_whole_subtree() {
2723 let doc = DoclingDocument {
2724 name: "s".into(),
2725 nodes: vec![
2726 Node::Group {
2727 label: "sheet".into(),
2728 name: Some("Sheet1".into()),
2729 layer: None,
2730 children: vec![Node::Paragraph {
2731 text: "visible".into(),
2732 }],
2733 },
2734 Node::Group {
2735 label: "sheet".into(),
2736 name: Some("Sheet2".into()),
2737 layer: Some(ContentLayer::Invisible),
2738 children: vec![Node::Paragraph {
2739 text: "hidden".into(),
2740 }],
2741 },
2742 ],
2743 ..DoclingDocument::new("s")
2744 };
2745 let v = crate::json::to_json(&doc);
2746 assert_eq!(v["groups"][0]["label"], "sheet");
2747 assert_eq!(v["groups"][0]["name"], "Sheet1");
2748 assert_eq!(v["groups"][0]["content_layer"], "body");
2749 assert_eq!(v["texts"][0]["content_layer"], "body");
2750 assert_eq!(v["groups"][1]["name"], "Sheet2");
2751 assert_eq!(v["groups"][1]["content_layer"], "invisible");
2752 assert_eq!(v["texts"][1]["content_layer"], "invisible");
2753 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
2755 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
2756 }
2757
2758 #[test]
2768 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
2769 use crate::tree::{ItemTree, TreeKind};
2770 let mut t = ItemTree::default();
2771 let text = |txt: &str| TreeKind::Text {
2772 label: "text".into(),
2773 text: txt.into(),
2774 orig: None,
2775 formatting: None,
2776 hyperlink: None,
2777 level: None,
2778 list: None,
2779 };
2780 let a = t.add(None, None, text("a"));
2781 let blank = t.add(None, None, text(""));
2782 let b = t.add(None, None, text("b"));
2783 t.delete(blank);
2784 let group = t.add(
2785 None,
2786 Some(ContentLayer::Notes),
2787 TreeKind::Group {
2788 label: "comment_section".into(),
2789 name: "comment-0".into(),
2790 },
2791 );
2792 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
2793 t.items[a].comments.push(group);
2794 t.add(
2795 None,
2796 None,
2797 TreeKind::Picture {
2798 captions: Vec::new(),
2799 image: None,
2800 classification: Some("bar_chart".into()),
2801 confidence: None,
2802 chart: Some(Table {
2803 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
2804 ..Table::default()
2805 }),
2806 dpi: None,
2807 },
2808 );
2809 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
2810 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
2811 let mut doc = DoclingDocument::new("t");
2812 doc.tree = Some(t);
2813 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2814 let texts = v["texts"].as_array().unwrap();
2815 assert_eq!(texts.len(), 3);
2816 assert_eq!(texts[1]["text"], "b");
2817 assert_eq!(texts[1]["self_ref"], "#/texts/1");
2818 assert_eq!(
2819 v["body"]["children"],
2820 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
2821 );
2822 let keys: Vec<&str> = texts[0]
2823 .as_object()
2824 .unwrap()
2825 .keys()
2826 .map(String::as_str)
2827 .collect();
2828 assert_eq!(
2829 keys,
2830 vec![
2831 "self_ref",
2832 "parent",
2833 "children",
2834 "content_layer",
2835 "label",
2836 "prov",
2837 "comments",
2838 "orig",
2839 "text"
2840 ]
2841 );
2842 assert_eq!(
2843 texts[0]["comments"],
2844 serde_json::json!([{"$ref": "#/groups/0"}])
2845 );
2846 assert!(texts[1].get("comments").is_none());
2847 let meta = &v["pictures"][0]["meta"];
2848 assert_eq!(
2849 meta["classification"]["predictions"][0]["class_name"],
2850 "bar_chart"
2851 );
2852 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
2853 }
2854
2855 #[test]
2861 fn tree_items_carry_exact_provenance_and_dpi() {
2862 use crate::tree::{ItemTree, TreeKind, TreeProv};
2863 let text = |label: &str, t: &str| TreeKind::Text {
2864 label: label.into(),
2865 text: t.into(),
2866 orig: None,
2867 formatting: None,
2868 hyperlink: None,
2869 level: None,
2870 list: None,
2871 };
2872 let mut t = ItemTree::default();
2873 let slide = t.add(
2874 None,
2875 None,
2876 TreeKind::Group {
2877 label: "chapter".into(),
2878 name: "slide-0".into(),
2879 },
2880 );
2881 t.add_with_prov(
2882 Some(slide),
2883 None,
2884 text("paragraph", "héllo"),
2885 TreeProv {
2886 page_no: 1,
2887 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
2888 bottom_left: true,
2889 charspan: [0, 5],
2890 },
2891 );
2892 t.add_with_prov(
2893 Some(slide),
2894 None,
2895 TreeKind::Picture {
2896 captions: Vec::new(),
2897 image: Some(crate::PictureImage {
2898 dpi: crate::PictureImage::DEFAULT_DPI,
2899 mimetype: "image/png".into(),
2900 width: 2,
2901 height: 2,
2902 data: vec![0],
2903 }),
2904 classification: None,
2905 confidence: None,
2906 chart: None,
2907 dpi: Some(300),
2908 },
2909 TreeProv {
2910 page_no: 1,
2911 bbox: [0.0; 4],
2912 bottom_left: false,
2913 charspan: [0, 0],
2914 },
2915 );
2916 t.add(
2917 Some(slide),
2918 Some(ContentLayer::Notes),
2919 text("text", "no geometry"),
2920 );
2921 let mut doc = DoclingDocument::new("t");
2922 doc.push(Node::PageInfo {
2923 page_no: 1,
2924 width: 9144000.0,
2925 height: 6858000.0,
2926 });
2927 doc.tree = Some(t);
2928 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2929 assert_eq!(
2930 v["texts"][0]["prov"],
2931 serde_json::json!([{
2932 "page_no": 1,
2933 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
2934 "charspan": [0, 5],
2935 }])
2936 );
2937 assert_eq!(v["texts"][0]["label"], "paragraph");
2938 assert_eq!(
2939 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
2940 "TOPLEFT"
2941 );
2942 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
2943 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
2944 assert_eq!(v["texts"][1]["content_layer"], "notes");
2945 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
2946 assert_eq!(v["pages"]["1"]["page_no"], 1);
2947 }
2948
2949 #[test]
2954 fn provenance_boxes_are_clamped_to_their_page() {
2955 let mut doc = DoclingDocument::new("t");
2956 doc.push(Node::PageInfo {
2957 page_no: 1,
2958 width: 10.0,
2959 height: 8.0,
2960 });
2961 doc.push(Node::Prov {
2962 page_no: 1,
2963 bbox: [-1.0, 2.0, 12.0, 9.5],
2964 charspan: [0, 1],
2965 seq: None,
2966 inner: Box::new(Node::Paragraph { text: "x".into() }),
2967 });
2968 let mut table = Table {
2969 rows: vec![vec!["a".into()]],
2970 ..Table::default()
2971 };
2972 table.cells = Some(vec![crate::TableCell {
2973 text: "a".into(),
2974 bbox: Some([1.0, 1.0, 11.0, 9.0]),
2975 start_row: 0,
2976 start_col: 0,
2977 row_span: 1,
2978 col_span: 1,
2979 column_header: false,
2980 row_header: false,
2981 row_section: false,
2982 }]);
2983 doc.push(Node::Prov {
2984 page_no: 1,
2985 bbox: [0.0, 0.0, 10.0, 8.0],
2986 charspan: [0, 0],
2987 seq: None,
2988 inner: Box::new(Node::Table(table)),
2989 });
2990 doc.push(Node::Prov {
2991 page_no: 7,
2992 bbox: [-5.0, 0.0, 50.0, 50.0],
2993 charspan: [0, 1],
2994 seq: None,
2995 inner: Box::new(Node::Paragraph { text: "y".into() }),
2996 });
2997 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2998 assert_eq!(
2999 v["texts"][0]["prov"][0]["bbox"],
3000 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
3001 );
3002 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
3003 assert_eq!(
3004 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
3005 (Some(1.0), Some(10.0), Some(8.0))
3006 );
3007 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
3008 assert_eq!(
3009 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
3010 "page 7 is not described"
3011 );
3012 }
3013
3014 #[test]
3021 fn misplaced_list_items_are_wrapped_like_docling_core() {
3022 use crate::tree::{ItemTree, ListMeta, TreeKind};
3023 let mut t = ItemTree::default();
3024 let li = |txt: &str| TreeKind::Text {
3025 label: "list_item".into(),
3026 text: txt.into(),
3027 orig: None,
3028 formatting: None,
3029 hyperlink: None,
3030 level: None,
3031 list: Some(ListMeta {
3032 enumerated: false,
3033 marker: "-".into(),
3034 }),
3035 };
3036 let group = |label: &str, name: &str| TreeKind::Group {
3037 label: label.into(),
3038 name: name.into(),
3039 };
3040 t.add(None, None, li("A"));
3041 t.add(None, None, li("B"));
3042 t.add(
3043 None,
3044 None,
3045 TreeKind::Text {
3046 label: "text".into(),
3047 text: "plain".into(),
3048 orig: None,
3049 formatting: None,
3050 hyperlink: None,
3051 level: None,
3052 list: None,
3053 },
3054 );
3055 let cell = t.add(None, None, group("unspecified", "cell"));
3056 t.add(Some(cell), None, li("C"));
3057 t.add(Some(cell), None, li("D"));
3058 let list = t.add(None, None, group("list", "list"));
3059 t.add(Some(list), None, li("E"));
3060 let doc = DoclingDocument {
3061 tree: Some(t),
3062 ..DoclingDocument::new("t")
3063 };
3064 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3065 let refs = |x: &Value| -> Vec<String> {
3066 x.as_array()
3067 .unwrap()
3068 .iter()
3069 .map(|r| r["$ref"].as_str().unwrap().to_string())
3070 .collect()
3071 };
3072 assert_eq!(
3073 refs(&v["body"]["children"]),
3074 ["#/groups/4", "#/texts/0", "#/groups/0", "#/groups/1"]
3075 );
3076 assert_eq!(
3077 refs(&v["groups"][0]["children"]),
3078 ["#/groups/3", "#/groups/2"]
3079 );
3080 for (g, name, kids) in [
3081 (2, "group", vec!["#/texts/2"]),
3082 (3, "group", vec!["#/texts/3"]),
3083 (4, "group", vec!["#/texts/4", "#/texts/5"]),
3084 (1, "list", vec!["#/texts/1"]),
3085 ] {
3086 assert_eq!(v["groups"][g]["label"], "list");
3087 assert_eq!(v["groups"][g]["name"], name);
3088 assert_eq!(refs(&v["groups"][g]["children"]), kids);
3089 }
3090 let texts: Vec<&str> = v["texts"]
3091 .as_array()
3092 .unwrap()
3093 .iter()
3094 .map(|t| t["text"].as_str().unwrap())
3095 .collect();
3096 assert_eq!(texts, ["plain", "E", "D", "C", "A", "B"]);
3097 assert_eq!(v["texts"][2]["parent"]["$ref"], "#/groups/2");
3098 }
3099
3100 #[test]
3103 fn a_deleted_child_is_not_written_as_an_empty_ref() {
3104 use crate::tree::{ItemTree, TreeKind};
3105 let mut t = ItemTree::default();
3106 let text = |txt: &str| TreeKind::Text {
3107 label: "text".into(),
3108 text: txt.into(),
3109 orig: None,
3110 formatting: None,
3111 hyperlink: None,
3112 level: None,
3113 list: None,
3114 };
3115 let g = t.add(
3116 None,
3117 None,
3118 TreeKind::Group {
3119 label: "unspecified".into(),
3120 name: "g".into(),
3121 },
3122 );
3123 let gone = t.add(Some(g), None, text(""));
3124 t.add(Some(g), None, text("kept"));
3125 t.items[gone].deleted = true; let doc = DoclingDocument {
3127 tree: Some(t),
3128 ..DoclingDocument::new("t")
3129 };
3130 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3131 assert_eq!(
3132 v["groups"][0]["children"],
3133 serde_json::json!([{"$ref": "#/texts/0"}])
3134 );
3135 }
3136
3137 #[test]
3138 fn a_backend_item_tree_is_written_verbatim() {
3139 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
3140 let mut t = ItemTree::default();
3141 let text = |label: &str, txt: &str| TreeKind::Text {
3142 label: label.into(),
3143 text: txt.into(),
3144 orig: None,
3145 formatting: None,
3146 hyperlink: None,
3147 level: None,
3148 list: None,
3149 };
3150 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
3151 let h = t.add(None, None, text("title", "Heading"));
3152 let group = t.add(
3153 Some(h),
3154 None,
3155 TreeKind::Group {
3156 label: "inline".into(),
3157 name: "group".into(),
3158 },
3159 );
3160 t.add(
3161 Some(group),
3162 None,
3163 TreeKind::Text {
3164 label: "text".into(),
3165 text: "bold".into(),
3166 orig: None,
3167 formatting: Some(Formatting {
3168 bold: true,
3169 ..Formatting::default()
3170 }),
3171 hyperlink: Some("https://example.com/".into()),
3172 level: None,
3173 list: None,
3174 },
3175 );
3176 t.add(
3177 Some(group),
3178 None,
3179 TreeKind::Code {
3180 text: "x = 1".into(),
3181 orig: None,
3182 language: Some("python".into()),
3183 formatting: None,
3184 hyperlink: None,
3185 },
3186 );
3187 let sub = t.add(
3188 Some(h),
3189 None,
3190 TreeKind::Text {
3191 label: "section_header".into(),
3192 text: "Sub".into(),
3193 orig: Some("Sub\u{2019}".into()),
3194 formatting: None,
3195 hyperlink: None,
3196 level: Some(1),
3197 list: None,
3198 },
3199 );
3200 let item = t.add(
3201 Some(sub),
3202 None,
3203 TreeKind::Text {
3204 label: "list_item".into(),
3205 text: "item".into(),
3206 orig: None,
3207 formatting: None,
3208 hyperlink: None,
3209 level: None,
3210 list: Some(ListMeta {
3211 enumerated: true,
3212 marker: "3.".into(),
3213 }),
3214 },
3215 );
3216 let _region = t.add(
3217 Some(sub),
3218 None,
3219 TreeKind::FieldRegion {
3220 items: vec![crate::FieldItem {
3221 marker: None,
3222 key: Some("Name".into()),
3223 value: Some("Duck".into()),
3224 value_kind: Some("read_only".into()),
3225 }],
3226 },
3227 );
3228 let table = t.add(
3229 Some(sub),
3230 None,
3231 TreeKind::Table {
3232 table: Table {
3233 rows: vec![vec!["a \n<".into(), "b".into()]],
3234 cells: Some(vec![
3235 crate::TableCell {
3236 text: "a \n<".into(),
3237 bbox: None,
3238 start_row: 0,
3239 start_col: 0,
3240 row_span: 3,
3241 col_span: 1,
3242 column_header: false,
3243 row_header: true,
3244 row_section: false,
3245 },
3246 crate::TableCell {
3247 text: "b".into(),
3248 bbox: None,
3249 start_row: 0,
3250 start_col: 1,
3251 row_span: 1,
3252 col_span: 1,
3253 column_header: false,
3254 row_header: false,
3255 row_section: false,
3256 },
3257 ]),
3258 ..Table::default()
3259 },
3260 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
3262 },
3263 );
3264 let cell_group = t.add(
3265 Some(table),
3266 None,
3267 TreeKind::Group {
3268 label: "unspecified".into(),
3269 name: "rich_cell_group_1_0_0".into(),
3270 },
3271 );
3272 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
3273 *rich_cells = vec![(0, 1, cell_group)];
3274 }
3275 let after = t.add(Some(sub), None, text("text", "after the region"));
3276 let _ = (title, after);
3277 let list = t.add(
3281 Some(sub),
3282 None,
3283 TreeKind::Group {
3284 label: "list".into(),
3285 name: "list".into(),
3286 },
3287 );
3288 t.reparent(item, Some(list));
3289
3290 let doc = DoclingDocument {
3291 tree: Some(t),
3292 ..DoclingDocument::new("t")
3293 };
3294 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3295 let texts: Vec<&str> = v["texts"]
3297 .as_array()
3298 .unwrap()
3299 .iter()
3300 .map(|t| t["text"].as_str().unwrap())
3301 .collect();
3302 assert_eq!(
3303 texts,
3304 [
3305 "Page",
3306 "Heading",
3307 "bold",
3308 "x = 1",
3309 "Sub",
3310 "item",
3311 "Name",
3312 "Duck",
3313 "after the region"
3314 ]
3315 );
3316 assert_eq!(
3317 v["body"]["children"],
3318 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
3319 );
3320 assert_eq!(v["texts"][0]["content_layer"], "furniture");
3321 assert_eq!(
3322 v["texts"][1]["children"],
3323 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
3324 );
3325 let bold = &v["texts"][2];
3326 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
3327 let keys: Vec<&str> = bold
3328 .as_object()
3329 .unwrap()
3330 .keys()
3331 .map(String::as_str)
3332 .collect();
3333 assert_eq!(
3334 keys,
3335 [
3336 "self_ref",
3337 "parent",
3338 "children",
3339 "content_layer",
3340 "label",
3341 "prov",
3342 "orig",
3343 "text",
3344 "formatting",
3345 "hyperlink"
3346 ]
3347 );
3348 assert_eq!(
3349 bold["formatting"],
3350 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
3351 );
3352 let code = &v["texts"][3];
3353 assert_eq!(code["label"], "code");
3354 assert_eq!(code["code_language"], "Python");
3355 let sub = &v["texts"][4];
3356 assert_eq!(sub["orig"], "Sub\u{2019}");
3357 assert_eq!(sub["level"], 1);
3358 let item = &v["texts"][5];
3359 let keys: Vec<&str> = item
3360 .as_object()
3361 .unwrap()
3362 .keys()
3363 .map(String::as_str)
3364 .collect();
3365 assert_eq!(
3366 keys,
3367 [
3368 "self_ref",
3369 "parent",
3370 "children",
3371 "content_layer",
3372 "label",
3373 "prov",
3374 "orig",
3375 "text",
3376 "enumerated",
3377 "marker"
3378 ]
3379 );
3380 assert_eq!(item["marker"], "3.");
3381 assert_eq!(v["texts"][7]["kind"], "read_only");
3382 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
3383 let table = &v["tables"][0];
3384 assert_eq!(
3385 table["children"],
3386 serde_json::json!([{"$ref": "#/groups/1"}])
3387 );
3388 let cells = table["data"]["table_cells"].as_array().unwrap();
3389 assert_eq!(
3390 cells[0]["text"], "a \n<",
3391 "raw cell text is written verbatim"
3392 );
3393 assert_eq!(
3394 cells[0]["end_row_offset_idx"], 3,
3395 "declared spans are not clamped"
3396 );
3397 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
3398 assert!(cells[0].get("ref").is_none());
3399 assert!(
3400 table["data"]["grid"][0][1].get("ref").is_none(),
3401 "the grid shows plain cells"
3402 );
3403 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
3404 }
3405
3406 #[test]
3410 fn a_comment_section_can_be_referenced_by_its_note_text() {
3411 let doc = DoclingDocument {
3412 name: "c".into(),
3413 nodes: vec![
3414 Node::Commented {
3415 comments: vec![0],
3416 inner: Box::new(Node::Paragraph {
3417 text: "annotated".into(),
3418 }),
3419 },
3420 Node::CommentSection {
3421 name: "comment-Sheet1-A1".into(),
3422 text: "[author: A]: note".into(),
3423 refs_note_text: true,
3424 grouped: true,
3425 },
3426 ],
3427 ..DoclingDocument::new("c")
3428 };
3429 let v = crate::json::to_json(&doc);
3430 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
3431 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
3432 }
3433
3434 #[test]
3438 fn comment_sections_link_back_to_their_items() {
3439 let doc = DoclingDocument {
3440 name: "c".into(),
3441 nodes: vec![
3442 Node::Commented {
3443 comments: vec![0],
3444 inner: Box::new(Node::Paragraph {
3445 text: "annotated".into(),
3446 }),
3447 },
3448 Node::Paragraph {
3449 text: "plain".into(),
3450 },
3451 Node::CommentSection {
3452 name: "comment-7".into(),
3453 text: "[time: t]: note".into(),
3454 refs_note_text: false,
3455 grouped: true,
3456 },
3457 ],
3458 ..DoclingDocument::new("c")
3459 };
3460 let v = crate::json::to_json(&doc);
3461 assert_eq!(v["groups"][0]["label"], "comment_section");
3463 assert_eq!(v["groups"][0]["name"], "comment-7");
3464 assert_eq!(v["groups"][0]["content_layer"], "notes");
3465 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
3466 assert_eq!(v["texts"][2]["content_layer"], "notes");
3467 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
3469 assert!(v["texts"][1].get("comments").is_none());
3470 let keys: Vec<&str> = v["texts"][0]
3472 .as_object()
3473 .unwrap()
3474 .keys()
3475 .map(String::as_str)
3476 .collect();
3477 assert_eq!(
3478 &keys[keys.len() - 4..],
3479 &["prov", "comments", "orig", "text"]
3480 );
3481 }
3482
3483 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
3484 Node::Picture {
3485 caption: Some(caption.into()),
3486 caption_href: None,
3487 image: None,
3488 classification: None,
3489 caption_parent,
3490 caption_location: None,
3491 }
3492 }
3493
3494 fn group(children: Vec<Node>) -> Node {
3495 Node::Group {
3496 label: "section".into(),
3497 name: None,
3498 layer: None,
3499 children,
3500 }
3501 }
3502
3503 fn refs(v: &Value) -> Vec<&str> {
3504 v.as_array()
3505 .unwrap()
3506 .iter()
3507 .map(|r| r["$ref"].as_str().unwrap())
3508 .collect()
3509 }
3510
3511 #[test]
3516 fn a_body_caption_follows_the_enclosing_top_level_item() {
3517 let mut doc = DoclingDocument::new("t");
3518 doc.push(picture("top", CaptionParent::Body));
3519 doc.push(group(vec![
3520 Node::Paragraph { text: "p".into() },
3521 picture("nested", CaptionParent::Body),
3522 ]));
3523 doc.push(Node::Paragraph {
3524 text: "after".into(),
3525 });
3526 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3527 assert_eq!(
3528 refs(&v["body"]["children"]),
3529 [
3530 "#/texts/0",
3531 "#/pictures/0",
3532 "#/groups/0",
3533 "#/texts/2",
3534 "#/texts/3"
3535 ]
3536 );
3537 assert_eq!(
3538 refs(&v["groups"][0]["children"]),
3539 ["#/texts/1", "#/pictures/1"]
3540 );
3541 for (cap, pic) in [(0, 0), (2, 1)] {
3542 assert_eq!(v["texts"][cap]["label"], "caption");
3543 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
3544 assert_eq!(
3545 refs(&v["pictures"][pic]["captions"]),
3546 [format!("#/texts/{cap}")]
3547 );
3548 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
3549 }
3550 }
3551
3552 #[test]
3555 fn an_item_caption_is_the_items_first_child() {
3556 let mut doc = DoclingDocument::new("t");
3557 doc.push(picture("fig", CaptionParent::Item));
3558 doc.push(Node::Table(Table {
3559 rows: vec![vec!["a".into()]],
3560 caption: Some("tab".into()),
3561 caption_parent: CaptionParent::Item,
3562 ..Table::default()
3563 }));
3564 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3565 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
3566 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
3567 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
3568 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3569 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
3570 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
3571 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
3572 }
3573
3574 #[test]
3580 fn picture_children_hang_off_the_picture_after_its_caption() {
3581 let mut doc = DoclingDocument::new("t");
3582 doc.push(picture("fig", CaptionParent::Item));
3583 doc.push(Node::PictureChildren(vec![
3584 Node::Heading {
3585 level: 2,
3586 text: "in-figure title".into(),
3587 },
3588 Node::Paragraph {
3589 text: "axis label".into(),
3590 },
3591 Node::ListItem {
3592 ordered: false,
3593 number: 0,
3594 first_in_list: true,
3595 text: "callout".into(),
3596 level: 0,
3597 marker: None,
3598 location: None,
3599 dclx: None,
3600 href: None,
3601 layer: None,
3602 },
3603 ]));
3604 doc.push(Node::Paragraph {
3605 text: "after".into(),
3606 });
3607 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3608 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/texts/4"]);
3609 assert_eq!(
3610 refs(&v["pictures"][0]["children"]),
3611 ["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"]
3612 );
3613 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3614 assert_eq!(v["texts"][1]["label"], "section_header");
3615 assert_eq!(v["texts"][2]["label"], "text");
3616 for t in 1..=2 {
3617 assert_eq!(v["texts"][t]["parent"]["$ref"], "#/pictures/0");
3618 assert_eq!(v["texts"][t]["content_layer"], "body");
3619 }
3620 assert_eq!(v["groups"][0]["parent"]["$ref"], "#/pictures/0");
3621 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
3622 let md = doc.export_to_markdown();
3623 assert!(
3624 !md.contains("axis label") && !md.contains("callout"),
3625 "{md}"
3626 );
3627 assert!(md.contains("after"), "{md}");
3628 }
3629
3630 #[test]
3634 fn a_container_caption_is_the_items_sibling() {
3635 let mut doc = DoclingDocument::new("t");
3636 doc.push(group(vec![
3637 picture("chart", CaptionParent::Container),
3638 Node::Table(Table {
3639 rows: vec![vec!["a".into()]],
3640 caption: Some("figcaption".into()),
3641 caption_parent: CaptionParent::ContainerAfter,
3642 ..Table::default()
3643 }),
3644 ]));
3645 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3646 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
3647 assert_eq!(
3648 refs(&v["groups"][0]["children"]),
3649 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
3650 );
3651 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
3652 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
3653 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
3654 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
3655 }
3656
3657 #[test]
3662 fn a_labeled_text_keeps_its_label_and_hyperlink_out_of_the_text() {
3663 let mut doc = DoclingDocument::new("t");
3664 doc.push(Node::LabeledText {
3665 label: "footnote".into(),
3666 text: "1 https://example.com/a\\_b".into(),
3667 href: Some("https://example.com/a_b".into()),
3668 });
3669 doc.push(Node::LabeledText {
3670 label: "footnote".into(),
3671 text: "© 2022 the authors".into(),
3672 href: None,
3673 });
3674 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3675 let t = &v["texts"][0];
3676 assert_eq!(t["label"], "footnote");
3677 assert_eq!(t["text"], "1 https://example.com/a_b");
3678 assert_eq!(t["orig"], "1 https://example.com/a_b");
3679 assert_eq!(t["hyperlink"], "https://example.com/a_b");
3680 assert_eq!(v["texts"][1]["label"], "footnote");
3681 assert!(v["texts"][1].get("hyperlink").is_none());
3682 assert_eq!(
3683 doc.export_to_markdown(),
3684 "[1 https://example.com/a\\_b](https://example.com/a_b)\n\n© 2022 the authors\n"
3685 );
3686 assert_eq!(
3687 doc.export_to_text(),
3688 "1 https://example.com/a_b\n\n© 2022 the authors"
3689 );
3690 }
3691}