1use serde_json::{json, Map, 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
124type PageSizes = std::collections::HashMap<usize, (f64, f64)>;
126
127fn page_sizes(pages: &[(usize, f64, f64)]) -> PageSizes {
128 let mut sizes = PageSizes::with_capacity(pages.len());
129 for &(p, w, h) in pages {
130 sizes.entry(p).or_insert((w, h));
131 }
132 sizes
133}
134
135fn clamp_boxes_to_pages(b: &mut Builder) {
142 if b.pages.is_empty() {
143 return;
144 }
145 let sizes = page_sizes(&b.pages);
147 for item in &mut b.texts {
148 match item {
149 Item::Text(t) => t.prov.clamp(&sizes),
150 Item::Json(v) => clamp_item(v, &sizes),
151 }
152 }
153 for item in b
154 .pictures
155 .iter_mut()
156 .chain(b.tables.iter_mut())
157 .chain(b.key_value_items.iter_mut())
158 {
159 clamp_item(item, &sizes);
160 }
161}
162
163fn clamp_item(item: &mut Value, sizes: &PageSizes) {
164 let size = |page_no: &Value| -> Option<(f64, f64)> {
165 let n = page_no.as_u64()? as usize;
166 sizes.get(&n).copied()
167 };
168 let clamp_bbox = |bbox: &mut Value, (w, h): (f64, f64)| {
169 for (key, hi) in [("l", w), ("r", w), ("t", h), ("b", h)] {
170 if let Some(v) = bbox.get(key).and_then(Value::as_f64) {
171 bbox[key] = json!(round2(v.clamp(0.0, hi.max(0.0))));
172 }
173 }
174 };
175 let mut table_page: Option<Option<(f64, f64)>> = None;
176 if let Some(provs) = item.get_mut("prov").and_then(Value::as_array_mut) {
177 let mut page_nos: Vec<u64> = Vec::new();
178 for prov in provs.iter_mut() {
179 if let Some(n) = prov.get("page_no").and_then(Value::as_u64) {
180 page_nos.push(n);
181 }
182 let Some(sz) = prov.get("page_no").and_then(size) else {
183 continue;
184 };
185 if let Some(bbox) = prov.get_mut("bbox") {
186 clamp_bbox(bbox, sz);
187 }
188 }
189 page_nos.sort_unstable();
190 page_nos.dedup();
191 if let [only] = page_nos[..] {
192 table_page = Some(size(&json!(only)));
193 }
194 }
195 if let (Some(Some(sz)), Some(data)) = (table_page, item.get_mut("data")) {
197 for key in ["table_cells", "grid"] {
198 let Some(rows) = data.get_mut(key).and_then(Value::as_array_mut) else {
199 continue;
200 };
201 for entry in rows.iter_mut() {
202 let cells: Vec<&mut Value> = match entry {
203 Value::Array(row) => row.iter_mut().collect(),
204 other => vec![other],
205 };
206 for cell in cells {
207 if let Some(bbox) = cell.get_mut("bbox").filter(|b| b.is_object()) {
208 clamp_bbox(bbox, sz);
209 }
210 }
211 }
212 }
213 }
214}
215
216fn formatting_json(f: &crate::tree::Formatting) -> Value {
218 json!({
219 "bold": f.bold,
220 "italic": f.italic,
221 "underline": f.underline,
222 "strikethrough": f.strikethrough,
223 "script": match f.script {
224 crate::Script::Baseline => "baseline",
225 crate::Script::Sub => "sub",
226 crate::Script::Super => "super",
227 },
228 })
229}
230
231fn track_json(t: &crate::tree::TreeTrack) -> Value {
234 let mut m = serde_json::Map::new();
235 m.insert("kind".into(), json!("track"));
236 m.insert("start_time".into(), json!(t.start_time));
237 m.insert("end_time".into(), json!(t.end_time));
238 if let Some(id) = &t.identifier {
239 m.insert("identifier".into(), json!(id));
240 }
241 if let Some(v) = &t.voice {
242 m.insert("voice".into(), json!(v));
243 }
244 Value::Object(m)
245}
246
247fn image_ref_json(img: &crate::PictureImage) -> Value {
251 let (mimetype, uri) = crate::pixel_digest::docling_data_uri(img);
252 json!({
253 "mimetype": mimetype,
254 "dpi": img.dpi,
255 "size": { "width": img.width as f64, "height": img.height as f64 },
256 "uri": uri,
257 })
258}
259
260pub fn to_json(doc: &DoclingDocument) -> Value {
262 build_json(doc, false)
263}
264
265pub(crate) fn to_json_with_notes(doc: &DoclingDocument) -> Value {
270 build_json(doc, true)
271}
272
273fn build_parts(doc: &DoclingDocument, note_keys: bool) -> Parts {
274 let mut b = Builder {
275 note_keys,
276 ..Builder::default()
277 };
278 let body = match &doc.tree {
282 Some(tree) => {
283 for n in &doc.nodes {
286 if let Node::PageInfo {
287 page_no,
288 width,
289 height,
290 } = n
291 {
292 if *page_no > 0 {
293 b.pages.push((*page_no, *width as f64, *height as f64));
294 }
295 }
296 }
297 let mut tree = tree.clone();
301 tree.wrap_misplaced_list_items();
302 b.write_tree(&tree).into_iter().map(Child::json).collect()
303 }
304 None => b.walk_into(&doc.nodes, "#/body"),
305 };
306 b.link_comments();
307
308 clamp_boxes_to_pages(&mut b);
317
318 let pages = b
319 .pages
320 .iter()
321 .map(|(n, w, h)| {
322 let r2 = |v: f64| (v * 100.0).round() / 100.0;
323 let mut page = serde_json::Map::new();
326 page.insert("size".into(), json!({ "width": r2(*w), "height": r2(*h) }));
327 if let Some(img) = doc.page_images.get(n) {
328 page.insert("image".into(), image_ref_json(img));
329 }
330 page.insert("page_no".into(), json!(n));
331 (n.to_string(), Value::Object(page))
332 })
333 .collect::<serde_json::Map<String, Value>>();
334 let fields = (!b.field_regions.is_empty()).then_some((b.field_regions, b.field_items));
337 Parts {
338 name: doc.name.clone(),
339 metadata: doc
340 .metadata_output
341 .in_json()
342 .then(|| doc.metadata.to_json())
343 .filter(|m| m.as_object().is_some_and(|o| !o.is_empty())),
344 origin: json!({
345 "mimetype": "text/plain",
346 "binary_hash": fnv1a(&doc.name),
347 "filename": doc.name,
348 }),
349 furniture: json!({
350 "self_ref": "#/furniture",
351 "children": [],
352 "content_layer": "furniture",
353 "name": "_root_",
354 "label": "unspecified",
355 }),
356 body,
357 groups: b.groups,
358 texts: b.texts,
359 pictures: b.pictures,
360 tables: b.tables,
361 key_value_items: b.key_value_items,
362 fields,
363 pages,
364 }
365}
366
367fn build_json(doc: &DoclingDocument, note_keys: bool) -> Value {
368 build_parts(doc, note_keys).into_value()
369}
370
371struct Parts {
376 name: String,
377 metadata: Option<Value>,
380 origin: Value,
381 furniture: Value,
382 body: Vec<Child>,
384 groups: Vec<Value>,
385 texts: Vec<Item>,
386 pictures: Vec<Value>,
387 tables: Vec<Value>,
388 key_value_items: Vec<Value>,
389 fields: Option<(Vec<Value>, Vec<Value>)>,
392 pages: serde_json::Map<String, Value>,
393}
394
395impl Parts {
396 fn into_value(self) -> Value {
397 let mut out = serde_json::Map::with_capacity(15);
399 out.insert("schema_name".into(), "DoclingDocument".into());
400 out.insert("version".into(), SCHEMA_VERSION.into());
401 out.insert("name".into(), Value::String(self.name));
402 out.insert("origin".into(), self.origin);
403 if let Some(m) = self.metadata {
404 out.insert("metadata".into(), m);
405 }
406 out.insert("furniture".into(), self.furniture);
407 out.insert(
408 "body".into(),
409 object([
410 ("self_ref", "#/body".into()),
411 (
412 "children",
413 Value::Array(self.body.into_iter().map(Child::into_value).collect()),
414 ),
415 ("content_layer", "body".into()),
416 ("name", "_root_".into()),
417 ("label", "unspecified".into()),
418 ]),
419 );
420 out.insert("groups".into(), Value::Array(self.groups));
421 out.insert(
422 "texts".into(),
423 Value::Array(
424 self.texts
425 .into_iter()
426 .enumerate()
427 .map(|(i, item)| item.into_value(i))
428 .collect(),
429 ),
430 );
431 out.insert("pictures".into(), Value::Array(self.pictures));
432 out.insert("tables".into(), Value::Array(self.tables));
433 out.insert("key_value_items".into(), Value::Array(self.key_value_items));
434 out.insert("form_items".into(), Value::Array(Vec::new()));
435 if let Some((regions, items)) = self.fields {
436 out.insert("field_regions".into(), Value::Array(regions));
437 out.insert("field_items".into(), Value::Array(items));
438 }
439 out.insert("pages".into(), Value::Object(self.pages));
440 Value::Object(out)
441 }
442}
443
444impl serde::Serialize for Parts {
445 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
446 use serde::ser::SerializeMap;
447 let mut m = s.serialize_map(Some(
448 13 + 2 * usize::from(self.fields.is_some()) + usize::from(self.metadata.is_some()),
449 ))?;
450 m.serialize_entry("schema_name", "DoclingDocument")?;
451 m.serialize_entry("version", SCHEMA_VERSION)?;
452 m.serialize_entry("name", &self.name)?;
453 m.serialize_entry("origin", &self.origin)?;
454 if let Some(meta) = &self.metadata {
455 m.serialize_entry("metadata", meta)?;
456 }
457 m.serialize_entry("furniture", &self.furniture)?;
458 m.serialize_entry("body", &Body(&self.body))?;
459 m.serialize_entry("groups", &self.groups)?;
460 m.serialize_entry("texts", &Texts(&self.texts))?;
461 m.serialize_entry("pictures", &self.pictures)?;
462 m.serialize_entry("tables", &self.tables)?;
463 m.serialize_entry("key_value_items", &self.key_value_items)?;
464 m.serialize_entry("form_items", &[(); 0])?;
465 if let Some((regions, items)) = &self.fields {
466 m.serialize_entry("field_regions", regions)?;
467 m.serialize_entry("field_items", items)?;
468 }
469 m.serialize_entry("pages", &self.pages)?;
470 m.end()
471 }
472}
473
474struct Body<'a>(&'a [Child]);
476
477impl serde::Serialize for Body<'_> {
478 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
479 use serde::ser::SerializeMap;
480 let mut m = s.serialize_map(Some(5))?;
481 m.serialize_entry("self_ref", "#/body")?;
482 m.serialize_entry("children", self.0)?;
483 m.serialize_entry("content_layer", "body")?;
484 m.serialize_entry("name", "_root_")?;
485 m.serialize_entry("label", "unspecified")?;
486 m.end()
487 }
488}
489
490pub fn write_json<W: std::io::Write>(
497 doc: &DoclingDocument,
498 w: W,
499 pretty: bool,
500) -> serde_json::Result<()> {
501 use serde::Serialize;
502 let parts = build_parts(doc, false);
503 if pretty {
504 parts.serialize(&mut serde_json::Serializer::pretty(w))
505 } else {
506 parts.serialize(&mut serde_json::Serializer::new(w))
507 }
508}
509
510fn classification_meta(classes: &[crate::PictureClass]) -> Value {
517 json!({
518 "classification": {
519 "predictions": classes.iter().map(|c| json!({
520 "confidence": c.confidence as f64,
521 "created_by": "DocumentPictureClassifier",
522 "class_name": c.class_name,
523 })).collect::<Vec<_>>(),
524 },
525 "annotations": [{
526 "kind": "classification",
527 "provenance": "DocumentPictureClassifier",
528 "predicted_classes": classes.iter().map(|c| json!({
529 "class_name": c.class_name,
530 "confidence": c.confidence as f64,
531 })).collect::<Vec<_>>(),
532 }],
533 })
534}
535
536fn description_meta(desc: &crate::PictureDescription) -> Value {
544 json!({
545 "description": {
546 "created_by": desc.provenance,
547 "text": desc.text,
548 },
549 "annotations": [{
550 "kind": "description",
551 "text": desc.text,
552 "provenance": desc.provenance,
553 }],
554 })
555}
556
557fn picture_meta(
563 classes: Option<&[crate::PictureClass]>,
564 desc: Option<&crate::PictureDescription>,
565) -> Option<Value> {
566 let mut meta = Map::new();
567 let mut annotations = Vec::new();
568 if let Some(desc) = desc {
569 let mut m = description_meta(desc);
570 annotations.push(m["annotations"][0].take());
571 meta.insert("description".into(), m["description"].take());
572 }
573 if let Some(classes) = classes {
574 let mut m = classification_meta(classes);
575 annotations.insert(0, m["annotations"][0].take());
576 meta.insert("classification".into(), m["classification"].take());
577 }
578 if meta.is_empty() {
579 return None;
580 }
581 meta.insert("annotations".into(), Value::Array(annotations));
582 Some(Value::Object(meta))
583}
584
585#[allow(clippy::too_many_arguments)]
596fn cell_value(
597 row_span: usize,
598 col_span: usize,
599 start_row: usize,
600 end_row: usize,
601 start_col: usize,
602 end_col: usize,
603 text: String,
604 column_header: bool,
605 row_header: bool,
606 row_section: bool,
607 bbox: Option<[f32; 4]>,
608) -> Value {
609 let mut m = serde_json::Map::with_capacity(12);
610 m.insert("row_span".into(), row_span.into());
611 m.insert("col_span".into(), col_span.into());
612 m.insert("start_row_offset_idx".into(), start_row.into());
613 m.insert("end_row_offset_idx".into(), end_row.into());
614 m.insert("start_col_offset_idx".into(), start_col.into());
615 m.insert("end_col_offset_idx".into(), end_col.into());
616 m.insert("text".into(), Value::String(text));
617 m.insert("column_header".into(), column_header.into());
618 m.insert("row_header".into(), row_header.into());
619 m.insert("row_section".into(), row_section.into());
620 m.insert("fillable".into(), false.into());
621 if let Some(b) = bbox {
622 m.insert(
623 "bbox".into(),
624 json!({
625 "l": b[0], "t": b[1], "r": b[2], "b": b[3],
626 "coord_origin": "TOPLEFT",
627 }),
628 );
629 }
630 Value::Object(m)
631}
632
633fn table_data(t: &Table) -> Value {
636 table_data_with(t, false)
637}
638
639fn table_data_with(t: &Table, raw: bool) -> Value {
642 let cell_text = |s: &str| {
643 if raw {
644 s.to_string()
645 } else {
646 unescape_text(&crate::markdown::strip_hard_breaks(s))
647 }
648 };
649 let mut num_rows = t.rows.len();
650 let mut num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
651 let ragged = t.rows.iter().any(|r| r.len() != num_cols);
659 if let Some(first_class) = t.cells.as_ref().filter(|c| ragged && !c.is_empty()) {
660 for c in first_class {
661 num_rows = num_rows.max(c.start_row + c.row_span);
662 num_cols = num_cols.max(c.start_col + c.col_span);
663 }
664 }
665 let mut grid = Vec::with_capacity(num_rows);
666 let mut cells = Vec::new();
667 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
671 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
672 for c in first_class {
673 let idx = cells.len();
674 cells.push(cell_value(
675 c.row_span,
676 c.col_span,
677 c.start_row,
678 c.start_row + c.row_span,
679 c.start_col,
680 c.start_col + c.col_span,
681 cell_text(&c.text),
682 c.column_header,
683 c.row_header,
684 c.row_section,
685 c.bbox,
686 ));
687 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
688 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
689 slot[r * num_cols + k] = Some(idx);
690 }
691 }
692 }
693 for r in 0..num_rows {
694 let mut grid_row = Vec::with_capacity(num_cols);
695 for c in 0..num_cols {
696 grid_row.push(match slot[r * num_cols + c] {
697 Some(i) => cells[i].clone(),
698 None => cell_value(
699 1,
700 1,
701 r,
702 r + 1,
703 c,
704 c + 1,
705 String::new(),
706 false,
707 false,
708 false,
709 None,
710 ),
711 });
712 }
713 grid.push(grid_row);
714 }
715 } else {
716 let s = t.structure.as_ref();
717 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
718 grid.and_then(|g| g.get(r))
719 .and_then(|row| row.get(c))
720 .copied()
721 .unwrap_or(false)
722 };
723 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
724 let (mut r0, mut c0) = (r, c);
725 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
726 c0 -= 1;
727 }
728 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
729 r0 -= 1;
730 }
731 (r0, c0)
732 };
733 let anchors: Vec<(usize, usize)> = (0..num_rows)
736 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
737 .map(|(r, c)| anchor_of(r, c))
738 .collect();
739 let mut extent: Vec<(usize, usize)> = (0..num_rows)
740 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
741 .collect();
742 for (i, &(ar, ac)) in anchors.iter().enumerate() {
743 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
744 let e = &mut extent[ar * num_cols + ac];
745 e.0 = e.0.max(r);
746 e.1 = e.1.max(c);
747 }
748 for (r, row) in t.rows.iter().enumerate() {
749 let mut grid_row = Vec::with_capacity(num_cols);
750 for c in 0..num_cols {
751 let (ar, ac) = anchors[r * num_cols + c];
752 if (ar, ac) == (r, c) {
753 let (er, ec) = extent[r * num_cols + c];
754 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
755 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
756 Some(s) => flag(Some(&s.col_header), r, c),
757 None => r == 0,
758 };
759 slot[r * num_cols + c] = Some(cells.len());
760 cells.push(cell_value(
761 er - r + 1,
762 ec - c + 1,
763 r,
764 er + 1,
765 c,
766 ec + 1,
767 text,
768 column_header,
769 flag(s.map(|s| &s.row_header), r, c),
770 false,
771 None,
772 ));
773 }
774 grid_row.push(match slot[ar * num_cols + ac] {
775 Some(i) => cells[i].clone(),
776 None => Value::Null,
777 });
778 }
779 grid.push(grid_row);
780 }
781 }
782 json!({
783 "table_cells": cells,
784 "num_rows": num_rows,
785 "num_cols": num_cols,
786 "orientation": "rot_0",
787 "grid": grid,
788 })
789}
790
791#[derive(Clone, Copy)]
795enum ProvSlot {
796 None,
797 One {
798 page_no: usize,
799 bbox: [f64; 4],
801 bottom_left: bool,
802 charspan: [usize; 2],
803 },
804}
805
806impl ProvSlot {
807 fn origin(bottom_left: bool) -> &'static str {
808 if bottom_left {
809 "BOTTOMLEFT"
810 } else {
811 "TOPLEFT"
812 }
813 }
814
815 fn to_value(self) -> Value {
818 let ProvSlot::One {
819 page_no,
820 bbox: [l, t, r, b],
821 bottom_left,
822 charspan,
823 } = self
824 else {
825 return Value::Array(Vec::new());
826 };
827 let bbox = object([
828 ("l", l.into()),
829 ("t", t.into()),
830 ("r", r.into()),
831 ("b", b.into()),
832 ("coord_origin", Self::origin(bottom_left).into()),
833 ]);
834 let span = Value::Array(vec![charspan[0].into(), charspan[1].into()]);
835 Value::Array(vec![object([
836 ("page_no", page_no.into()),
837 ("bbox", bbox),
838 ("charspan", span),
839 ])])
840 }
841
842 fn clamp(&mut self, sizes: &PageSizes) {
844 if let ProvSlot::One { page_no, bbox, .. } = self {
845 if let Some(&(w, h)) = sizes.get(page_no) {
846 for (v, hi) in bbox.iter_mut().zip([w, h, w, h]) {
847 if v.is_finite() {
850 *v = round2(v.clamp(0.0, hi.max(0.0)));
851 }
852 }
853 }
854 }
855 }
856}
857
858impl serde::Serialize for ProvSlot {
859 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
860 use serde::ser::{SerializeMap, SerializeSeq};
861 let ProvSlot::One {
862 page_no,
863 bbox: [l, t, r, b],
864 bottom_left,
865 charspan,
866 } = *self
867 else {
868 return s.serialize_seq(Some(0))?.end();
869 };
870 struct Bbox([f64; 4], &'static str);
871 impl serde::Serialize for Bbox {
872 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
873 let mut m = s.serialize_map(Some(5))?;
874 for (k, v) in ["l", "t", "r", "b"].iter().zip(self.0) {
875 m.serialize_entry(k, &v)?;
876 }
877 m.serialize_entry("coord_origin", self.1)?;
878 m.end()
879 }
880 }
881 struct Entry(usize, Bbox, [usize; 2]);
882 impl serde::Serialize for Entry {
883 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
884 let mut m = s.serialize_map(Some(3))?;
885 m.serialize_entry("page_no", &self.0)?;
886 m.serialize_entry("bbox", &self.1)?;
887 m.serialize_entry("charspan", &self.2)?;
888 m.end()
889 }
890 }
891 let mut seq = s.serialize_seq(Some(1))?;
892 seq.serialize_element(&Entry(
893 page_no,
894 Bbox([l, t, r, b], Self::origin(bottom_left)),
895 charspan,
896 ))?;
897 seq.end()
898 }
899}
900
901struct TextItem {
908 parent: Box<str>,
909 label: Box<str>,
910 prov: ProvSlot,
911 text: Box<str>,
912}
913
914impl TextItem {
915 fn into_value(self, index: usize) -> Value {
916 let text = String::from(self.text);
917 object([
918 ("self_ref", Value::String(format!("#/texts/{index}"))),
919 ("parent", ref_value(self.parent)),
920 ("children", Value::Array(Vec::new())),
921 ("content_layer", "body".into()),
922 ("label", Value::String(self.label.into())),
923 ("prov", self.prov.to_value()),
924 ("orig", Value::String(text.clone())),
925 ("text", Value::String(text)),
926 ])
927 }
928}
929
930enum Item {
934 Text(Box<TextItem>),
935 Json(Box<Value>),
936}
937
938impl Item {
939 fn json(v: Value) -> Self {
940 Item::Json(Box::new(v))
941 }
942
943 fn value_mut(&mut self, index: usize) -> &mut Value {
946 if let Item::Text(t) = self {
947 let t = std::mem::replace(
948 t,
949 Box::new(TextItem {
950 parent: "".into(),
951 label: "".into(),
952 prov: ProvSlot::None,
953 text: "".into(),
954 }),
955 );
956 *self = Item::json(t.into_value(index));
957 }
958 match self {
959 Item::Json(v) => v,
960 Item::Text(_) => unreachable!("converted above"),
961 }
962 }
963
964 fn into_value(self, index: usize) -> Value {
965 match self {
966 Item::Text(t) => t.into_value(index),
967 Item::Json(v) => *v,
968 }
969 }
970}
971
972struct Texts<'a>(&'a [Item]);
974
975impl serde::Serialize for Texts<'_> {
976 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
977 use serde::ser::{SerializeMap, SerializeSeq};
978 struct Text<'a>(usize, &'a TextItem);
979 impl serde::Serialize for Text<'_> {
980 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
981 let Text(i, t) = *self;
982 let mut m = s.serialize_map(Some(8))?;
983 m.serialize_entry("self_ref", &format_args!("#/texts/{i}"))?;
984 m.serialize_entry("parent", &Child::Ref(t.parent.clone()))?;
985 m.serialize_entry("children", &[(); 0])?;
986 m.serialize_entry("content_layer", "body")?;
987 m.serialize_entry("label", &t.label)?;
988 m.serialize_entry("prov", &t.prov)?;
989 m.serialize_entry("orig", &t.text)?;
990 m.serialize_entry("text", &t.text)?;
991 m.end()
992 }
993 }
994 let mut seq = s.serialize_seq(Some(self.0.len()))?;
995 for (i, item) in self.0.iter().enumerate() {
996 match item {
997 Item::Text(t) => seq.serialize_element(&Text(i, t))?,
998 Item::Json(v) => seq.serialize_element(v)?,
999 }
1000 }
1001 seq.end()
1002 }
1003}
1004
1005enum Child {
1009 Ref(Box<str>),
1010 Json(Box<Value>),
1011}
1012
1013impl Child {
1014 fn json(v: Value) -> Self {
1015 Child::Json(Box::new(v))
1016 }
1017
1018 fn into_value(self) -> Value {
1019 match self {
1020 Child::Ref(r) => ref_value(r),
1021 Child::Json(v) => *v,
1022 }
1023 }
1024}
1025
1026impl serde::Serialize for Child {
1027 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1028 use serde::ser::SerializeMap;
1029 match self {
1030 Child::Ref(r) => {
1031 let mut m = s.serialize_map(Some(1))?;
1032 m.serialize_entry("$ref", r)?;
1033 m.end()
1034 }
1035 Child::Json(v) => v.serialize(s),
1036 }
1037 }
1038}
1039
1040fn round2(v: f64) -> f64 {
1041 (v * 100.0).round() / 100.0
1042}
1043
1044#[derive(Default)]
1045struct Builder {
1046 note_keys: bool,
1050 texts: Vec<Item>,
1051 groups: Vec<Value>,
1052 tables: Vec<Value>,
1053 pictures: Vec<Value>,
1054 field_regions: Vec<Value>,
1055 field_items: Vec<Value>,
1056 key_value_items: Vec<Value>,
1057 pages: Vec<(usize, f64, f64)>,
1061 cur_page: usize,
1063 cur_w: f64,
1064 cur_h: f64,
1065 pending_loc: Option<[u16; 4]>,
1068 pending_exact: Option<ExactProv>,
1071 pending_track: Option<(crate::tree::TreeTrack, String)>,
1074 pending_siblings: Vec<Value>,
1078 pending_after: Vec<Value>,
1082 pending_body: Vec<Value>,
1086 comment_groups: Vec<String>,
1090 last_comment_section: Option<(String, String)>,
1095 pending_comments: Vec<(String, Vec<usize>)>,
1099}
1100
1101impl Builder {
1102 fn take_prov(&mut self, char_len: usize) -> Value {
1108 self.take_prov_slot(char_len).to_value()
1109 }
1110
1111 fn take_prov_slot(&mut self, char_len: usize) -> ProvSlot {
1113 let prov = self.prov_slot(char_len, false);
1114 self.pending_exact = None;
1115 self.pending_loc = None;
1116 prov
1117 }
1118
1119 fn prov_slot(&self, char_len: usize, span_over_text: bool) -> ProvSlot {
1124 let r2 = round2;
1125 if let Some(ExactProv {
1126 page_no,
1127 bbox: [l, t, r, b],
1128 bottom_left,
1129 charspan,
1130 }) = self.pending_exact
1131 {
1132 let charspan = if span_over_text {
1133 [0, char_len]
1134 } else {
1135 charspan
1136 };
1137 return ProvSlot::One {
1138 page_no,
1139 bbox: [r2(l), r2(t), r2(r), r2(b)],
1140 bottom_left,
1141 charspan,
1142 };
1143 }
1144 let Some([x0, y0, x1, y1]) = self.pending_loc else {
1145 return ProvSlot::None;
1146 };
1147 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
1151 return ProvSlot::One {
1152 page_no: self.cur_page,
1153 bbox: [0.0; 4],
1154 bottom_left: true,
1155 charspan: [0, char_len],
1156 };
1157 }
1158 ProvSlot::One {
1159 page_no: self.cur_page,
1160 bbox: [
1161 r2(x0 as f64 * self.cur_w / 512.0),
1162 r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
1163 r2(x1 as f64 * self.cur_w / 512.0),
1164 r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
1165 ],
1166 bottom_left: true,
1167 charspan: [0, char_len],
1168 }
1169 }
1170
1171 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
1174 if self.pending_loc.is_none() && self.cur_page > 0 {
1175 self.pending_loc = loc;
1176 }
1177 }
1178
1179 fn link_comments(&mut self) {
1187 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
1188 .into_iter()
1189 .map(|(item, comments)| {
1190 let refs = comments
1191 .iter()
1192 .filter_map(|i| self.comment_groups.get(*i))
1193 .map(ref_value)
1194 .collect();
1195 (item, refs)
1196 })
1197 .collect();
1198 for (item, comment_refs) in refs {
1199 if comment_refs.is_empty() {
1200 continue;
1201 }
1202 let Some(target) = self.item_mut(&item) else {
1203 continue;
1204 };
1205 let Some(obj) = target.as_object_mut() else {
1206 continue;
1207 };
1208 let tail: Vec<(String, Value)> = obj
1209 .iter()
1210 .skip_while(|(k, _)| k.as_str() != "prov")
1211 .skip(1)
1212 .map(|(k, v)| (k.clone(), v.clone()))
1213 .collect();
1214 for (k, _) in &tail {
1215 obj.shift_remove(k);
1216 }
1217 obj.insert("comments".into(), Value::Array(comment_refs));
1218 for (k, v) in tail {
1219 obj.insert(k, v);
1220 }
1221 }
1222 }
1223
1224 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
1226 let idx = ref_index(self_ref)?;
1227 if self_ref.starts_with("#/texts/") {
1228 return self.texts.get_mut(idx).map(|item| item.value_mut(idx));
1229 }
1230 let bucket = if self_ref.starts_with("#/tables/") {
1231 &mut self.tables
1232 } else if self_ref.starts_with("#/pictures/") {
1233 &mut self.pictures
1234 } else if self_ref.starts_with("#/groups/") {
1235 &mut self.groups
1236 } else {
1237 return None;
1238 };
1239 bucket.get_mut(idx)
1240 }
1241
1242 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
1246 use crate::tree::TreeKind;
1247 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
1250 let (mut nt, mut ng, mut ntb, mut np, mut nf, mut nk) = (0, 0, 0, 0, 0, 0);
1251 for item in &tree.items {
1252 if item.deleted {
1253 refs.push(String::new());
1254 continue;
1255 }
1256 let r = match &item.kind {
1257 TreeKind::Text { .. } | TreeKind::Code { .. } => {
1258 nt += 1;
1259 format!("#/texts/{}", nt - 1)
1260 }
1261 TreeKind::Group { .. } => {
1262 ng += 1;
1263 format!("#/groups/{}", ng - 1)
1264 }
1265 TreeKind::Table { .. } => {
1266 ntb += 1;
1267 format!("#/tables/{}", ntb - 1)
1268 }
1269 TreeKind::Picture { .. } => {
1270 np += 1;
1271 format!("#/pictures/{}", np - 1)
1272 }
1273 TreeKind::FieldRegion { items } => {
1274 nt += items
1277 .iter()
1278 .map(|i| {
1279 [&i.marker, &i.key, &i.value]
1280 .iter()
1281 .filter(|p| p.is_some())
1282 .count()
1283 })
1284 .sum::<usize>();
1285 nf += 1;
1286 format!("#/field_regions/{}", nf - 1)
1287 }
1288 TreeKind::KeyValueGraph { .. } => {
1289 nk += 1;
1290 format!("#/key_value_items/{}", nk - 1)
1291 }
1292 };
1293 refs.push(r);
1294 }
1295 let ref_of = |id: usize| ref_value(refs[id].as_str());
1296 for (id, item) in tree.items.iter().enumerate() {
1297 if item.deleted {
1298 continue;
1299 }
1300 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
1301 let children: Vec<Value> = item
1305 .children
1306 .iter()
1307 .filter(|&&c| !tree.items[c].deleted)
1308 .map(|&c| ref_of(c))
1309 .collect();
1310 let layer = item.layer.map_or("body", |l| l.value());
1311 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
1314 let self_ref = match &item.kind {
1315 TreeKind::Text {
1316 label,
1317 text,
1318 orig,
1319 formatting,
1320 hyperlink,
1321 level,
1322 list,
1323 } => {
1324 let mut tail = serde_json::Map::new();
1327 if let Some(f) = formatting {
1328 tail.insert("formatting".into(), formatting_json(f));
1329 }
1330 if let Some(h) = hyperlink {
1331 tail.insert("hyperlink".into(), json!(h));
1332 }
1333 if let Some(l) = level {
1334 tail.insert("level".into(), json!(l));
1335 }
1336 if let Some(l) = list {
1337 tail.insert("enumerated".into(), json!(l.enumerated));
1338 tail.insert("marker".into(), json!(l.marker));
1339 }
1340 let r = format!("#/texts/{}", self.texts.len());
1341 let prov = self.take_prov(text.chars().count());
1342 let mut item_json = json!({
1343 "self_ref": r,
1344 "parent": { "$ref": parent },
1345 "children": children,
1346 "content_layer": layer,
1347 "label": label,
1348 "prov": prov,
1349 });
1350 if !item.comments.is_empty() {
1353 item_json["comments"] =
1354 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
1355 }
1356 if let Some(track) = &item.source {
1359 item_json["source"] = json!([track_json(track)]);
1360 }
1361 merge(
1362 &mut item_json,
1363 json!({
1364 "orig": orig.as_deref().unwrap_or(text),
1365 "text": text,
1366 }),
1367 );
1368 merge(&mut item_json, Value::Object(tail));
1369 if self.note_keys {
1370 if !item.notes.is_empty() {
1371 item_json["_notes"] = Value::Array(
1372 item.notes
1373 .iter()
1374 .map(|n| json!([n.offset, n.text]))
1375 .collect(),
1376 );
1377 }
1378 if item.note_body {
1379 item_json["_note_body"] = json!(true);
1380 }
1381 }
1382 self.texts.push(Item::json(item_json));
1383 r
1384 }
1385 TreeKind::Code {
1386 text,
1387 orig,
1388 language,
1389 formatting,
1390 hyperlink,
1391 captions,
1392 } => {
1393 let r = format!("#/texts/{}", self.texts.len());
1394 let prov = self.take_prov(text.chars().count());
1395 let mut item_json = json!({
1396 "self_ref": r,
1397 "parent": { "$ref": parent },
1398 "children": children,
1399 "content_layer": layer,
1400 "label": "code",
1401 "prov": prov,
1402 });
1403 if !item.comments.is_empty() {
1404 item_json["comments"] =
1405 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
1406 }
1407 merge(
1408 &mut item_json,
1409 json!({
1410 "orig": orig.as_deref().unwrap_or(text),
1411 "text": text,
1412 }),
1413 );
1414 if let Some(f) = formatting {
1415 item_json["formatting"] = formatting_json(f);
1416 }
1417 if let Some(h) = hyperlink {
1418 item_json["hyperlink"] = json!(h);
1419 }
1420 merge(
1421 &mut item_json,
1422 json!({
1423 "captions": captions.iter().map(|&c| ref_of(c)).collect::<Vec<_>>(),
1424 "references": [],
1425 "footnotes": [],
1426 "code_language": code_language(language.as_deref()),
1427 }),
1428 );
1429 self.texts.push(Item::json(item_json));
1430 r
1431 }
1432 TreeKind::Group { label, name } => {
1433 self.pending_exact = None;
1434 let r = format!("#/groups/{}", self.groups.len());
1435 self.groups.push(json!({
1436 "self_ref": r,
1437 "parent": { "$ref": parent },
1438 "children": children,
1439 "content_layer": layer,
1440 "name": name,
1441 "label": label,
1442 }));
1443 r
1444 }
1445 TreeKind::Table {
1446 table,
1447 rich_cells,
1448 captions,
1449 } => {
1450 let r = self.add_table_with(table, parent, true);
1452 let idx = ref_index(&r).expect("table ref");
1453 let t = &mut self.tables[idx];
1454 t["children"] = Value::Array(children);
1455 t["content_layer"] = json!(layer);
1456 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
1457 for &(row, col, group) in rich_cells {
1461 let cell_ref = ref_of(group);
1462 let hit = |c: &Value| {
1463 c["start_row_offset_idx"] == json!(row)
1464 && c["start_col_offset_idx"] == json!(col)
1465 };
1466 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
1467 for c in cells.iter_mut().filter(|c| hit(c)) {
1468 c["ref"] = cell_ref.clone();
1469 }
1470 }
1471 }
1472 r
1473 }
1474 TreeKind::Picture {
1475 captions,
1476 image,
1477 classification,
1478 extra_classes,
1479 description,
1480 confidence,
1481 chart,
1482 dpi,
1483 } => {
1484 let mut meta = classification.as_ref().map(|c| {
1489 let mut pred = serde_json::Map::new();
1490 if let Some(conf) = confidence {
1491 pred.insert("confidence".into(), json!(conf));
1492 }
1493 pred.insert("class_name".into(), json!(c));
1494 let mut preds = vec![Value::Object(pred)];
1495 preds.extend(extra_classes.iter().map(|c| json!({ "class_name": c })));
1496 json!({ "classification": { "predictions": preds } })
1497 });
1498 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
1499 if !t.rows.is_empty() {
1500 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
1501 }
1502 }
1503 if let Some(desc) = description {
1507 let mut d = description_meta(desc);
1508 let mut merged = Map::new();
1509 merged.insert("description".into(), d["description"].take());
1510 if let Some(Value::Object(rest)) = meta {
1511 merged.extend(rest);
1512 }
1513 merged.insert("annotations".into(), d["annotations"].take());
1514 meta = Some(Value::Object(merged));
1515 }
1516 let prov = self.take_prov(0);
1517 let r = self.push_picture(
1518 prov,
1519 captions.iter().map(|&c| ref_of(c)).collect(),
1520 children,
1521 image.as_ref(),
1522 meta,
1523 parent,
1524 );
1525 if let Some(idx) = ref_index(&r) {
1526 self.pictures[idx]["content_layer"] = json!(layer);
1527 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
1530 img["dpi"] = json!(dpi);
1531 }
1532 }
1533 r
1534 }
1535 TreeKind::FieldRegion { items } => {
1536 self.pending_exact = None;
1537 let r = self.add_field_region(items, parent);
1538 if let Some(region) = self.field_regions.last_mut() {
1539 region["content_layer"] = json!(layer);
1540 }
1541 r
1542 }
1543 TreeKind::KeyValueGraph { cells, links } => {
1544 self.pending_exact = None;
1545 let r = self.add_key_value_graph(cells, links, parent);
1546 if let Some(item) = self.key_value_items.last_mut() {
1547 item["content_layer"] = json!(layer);
1548 }
1549 r
1550 }
1551 };
1552 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1553 }
1554 tree.body
1555 .iter()
1556 .filter(|&&c| !tree.items[c].deleted)
1557 .map(|&c| ref_of(c))
1558 .collect()
1559 }
1560
1561 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1562 match node {
1563 Node::Heading { level: 1, text } => {
1564 Some(self.add_text("title", text, parent, json!({})))
1565 }
1566 Node::Heading { level, text } => Some(self.add_text(
1567 "section_header",
1568 text,
1569 parent,
1570 json!({ "level": level.saturating_sub(1) }),
1571 )),
1572 Node::Caption { text, href } => {
1573 let extra = match href {
1574 Some(url) => json!({ "hyperlink": url }),
1575 None => json!({}),
1576 };
1577 Some(self.add_text("caption", text, parent, extra))
1578 }
1579 Node::LabeledText { label, text, href } => {
1582 let extra = match href {
1583 Some(url) => json!({ "hyperlink": url }),
1584 None => json!({}),
1585 };
1586 Some(self.add_text(label, text, parent, extra))
1587 }
1588 Node::Paragraph { text } | Node::ListText { text, .. } => {
1589 let t = text.trim();
1592 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1593 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1594 _ => Some(self.add_text("text", text, parent, json!({}))),
1595 }
1596 }
1597 Node::CheckboxItem { checked, text } => {
1598 let label = if *checked {
1604 "checkbox_selected"
1605 } else {
1606 "checkbox_unselected"
1607 };
1608 Some(self.add_text(label, text, parent, json!({})))
1609 }
1610 Node::Code {
1611 language,
1612 text,
1613 orig,
1614 ..
1615 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1616 Node::Formula {
1619 latex,
1620 orig,
1621 location,
1622 } => {
1623 self.adopt_loc(*location);
1624 Some(self.add_formula_item(latex, orig, parent))
1625 }
1626 Node::CommentSection {
1632 name,
1633 text,
1634 refs_note_text,
1635 grouped,
1636 } => {
1637 if !*grouped {
1638 let child =
1642 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1643 self.comment_groups.push(child.clone());
1644 return Some(child);
1645 }
1646 if let Some((_, self_ref)) = self
1647 .last_comment_section
1648 .as_ref()
1649 .filter(|(n, _)| n == name)
1650 .cloned()
1651 {
1652 let child =
1655 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1656 if let Some(children) =
1657 self.groups[group_index(&self_ref)]["children"].as_array_mut()
1658 {
1659 children.push(ref_value(child.as_str()));
1660 }
1661 self.comment_groups.push(if *refs_note_text {
1662 child
1663 } else {
1664 self_ref.clone()
1665 });
1666 return Some(self_ref);
1667 }
1668 let self_ref = format!("#/groups/{}", self.groups.len());
1669 self.groups.push(Value::Null);
1670 let child =
1671 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1672 self.groups[group_index(&self_ref)] = json!({
1673 "self_ref": self_ref,
1674 "parent": { "$ref": parent },
1675 "children": [{ "$ref": child }],
1676 "content_layer": "notes",
1677 "name": name,
1678 "label": "comment_section",
1679 });
1680 self.last_comment_section = Some((name.clone(), self_ref.clone()));
1681 self.comment_groups.push(if *refs_note_text {
1682 child
1683 } else {
1684 self_ref.clone()
1685 });
1686 Some(self_ref)
1687 }
1688 Node::Commented { comments, inner } => {
1691 let item = self.add_node(inner, parent)?;
1692 if !comments.is_empty() {
1693 self.pending_comments.push((item.clone(), comments.clone()));
1694 }
1695 Some(item)
1696 }
1697 Node::Table(t) => Some(self.add_table(t, parent)),
1698 Node::Picture {
1699 caption,
1700 caption_href,
1701 image,
1702 classification,
1703 description,
1704 caption_parent,
1705 caption_location,
1706 } => Some(self.add_picture(
1707 caption.as_deref(),
1708 caption_href.as_deref(),
1709 image.as_ref(),
1710 picture_meta(classification.as_deref(), description.as_ref()),
1711 parent,
1712 (*caption_parent, *caption_location),
1713 )),
1714 Node::Chart {
1719 kind,
1720 table,
1721 caption,
1722 location,
1723 } => {
1724 self.adopt_loc(*location);
1725 let mut meta = json!({
1726 "classification": { "predictions": [{ "class_name": kind }] },
1727 });
1728 if !table.rows.is_empty() {
1729 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1730 }
1731 let mut captions = Vec::new();
1737 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1738 let prov = self.prov_slot(unescape_text(cap).chars().count(), true);
1739 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1740 self.pending_siblings.push(ref_value(cap_ref.as_str()));
1741 captions.push(ref_value(cap_ref));
1742 }
1743 let prov = self.take_prov(0);
1744 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1745 }
1746 Node::DoclangOnly(_) => None,
1748 Node::Group {
1749 label,
1750 name,
1751 layer,
1752 children,
1753 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1754 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1755 Node::KeyValueGraph { cells, links } => {
1756 Some(self.add_key_value_graph(cells, links, parent))
1757 }
1758 Node::InlineGroup { md_text, .. } => {
1761 Some(self.add_text("text", md_text, parent, json!({})))
1762 }
1763 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1765 Node::Furniture {
1771 layer: ContentLayer::Notes,
1772 inner,
1773 } => {
1774 let item = self.add_node(inner, parent)?;
1775 self.set_layer(&item, "notes");
1776 Some(item)
1777 }
1778 Node::Furniture { .. } => None,
1779 Node::PageFurniture {
1783 footer,
1784 location,
1785 text,
1786 } => {
1787 if self.cur_page > 0 {
1788 self.pending_loc = Some(*location);
1789 }
1790 let label = if *footer {
1791 "page_footer"
1792 } else {
1793 "page_header"
1794 };
1795 let item = self.add_text(label, text, parent, json!({}));
1796 self.pending_loc = None;
1797 self.set_layer(&item, "furniture");
1798 Some(item)
1799 }
1800 Node::FurnitureText { label, text } => {
1803 let item = self.add_text(label, text, parent, json!({}));
1804 self.set_layer(&item, "furniture");
1805 Some(item)
1806 }
1807 Node::PictureChildren(children) => {
1811 let pic = self.pictures.len().checked_sub(1)?;
1812 let pic_ref = format!("#/pictures/{pic}");
1813 let refs = self.walk_into(children, &pic_ref);
1814 if let Some(list) = self.pictures[pic]["children"].as_array_mut() {
1815 list.extend(refs.into_iter().map(Child::into_value));
1816 }
1817 None
1818 }
1819 Node::Located { location, inner } => {
1824 if self.cur_page > 0 {
1825 self.pending_loc = Some(*location);
1826 }
1827 let r = self.add_node(inner, parent);
1828 self.pending_loc = None;
1829 r
1830 }
1831 Node::Track { track, cue, inner } => {
1832 self.pending_track = Some((track.clone(), cue.clone()));
1833 let r = self.add_node(inner, parent);
1834 self.pending_track = None;
1835 r
1836 }
1837 Node::Prov {
1838 page_no,
1839 bbox,
1840 charspan,
1841 inner,
1842 ..
1843 } => {
1844 self.pending_exact = Some(ExactProv {
1845 page_no: *page_no,
1846 bbox: bbox.map(f64::from),
1847 bottom_left: false,
1848 charspan: *charspan,
1849 });
1850 let r = self.add_node(inner, parent);
1851 self.pending_exact = None;
1852 r
1853 }
1854 Node::PageBreak => None,
1856 Node::PageInfo {
1859 page_no,
1860 width,
1861 height,
1862 } => {
1863 self.cur_page = *page_no;
1864 self.cur_w = *width as f64;
1865 self.cur_h = *height as f64;
1866 if *page_no > 0 {
1867 self.pages.push((*page_no, self.cur_w, self.cur_h));
1868 }
1869 None
1870 }
1871 Node::ListItem { .. } => None,
1873 }
1874 }
1875
1876 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1880 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1881 self.field_regions.push(Value::Null);
1882 let region_index = self.field_regions.len() - 1;
1883 let mut item_refs = Vec::new();
1884 for item in items {
1885 item_refs.push(ref_value(self.add_field_item(item, &self_ref)));
1886 }
1887 self.field_regions[region_index] = json!({
1888 "self_ref": self_ref,
1889 "parent": { "$ref": parent },
1890 "children": item_refs,
1891 "content_layer": "body",
1892 "label": "field_region",
1893 "prov": [],
1894 });
1895 self_ref
1896 }
1897
1898 fn add_key_value_graph(
1902 &mut self,
1903 cells: &[crate::GraphCell],
1904 links: &[crate::GraphLink],
1905 parent: &str,
1906 ) -> String {
1907 let self_ref = format!("#/key_value_items/{}", self.key_value_items.len());
1908 let cells: Vec<Value> = cells
1909 .iter()
1910 .map(|c| {
1911 json!({
1912 "label": c.label,
1913 "cell_id": c.cell_id,
1914 "text": c.text,
1915 "orig": c.orig,
1916 })
1917 })
1918 .collect();
1919 let links: Vec<Value> = links
1920 .iter()
1921 .map(|l| {
1922 json!({
1923 "label": l.label,
1924 "source_cell_id": l.source_cell_id,
1925 "target_cell_id": l.target_cell_id,
1926 })
1927 })
1928 .collect();
1929 self.key_value_items.push(json!({
1930 "self_ref": self_ref,
1931 "parent": { "$ref": parent },
1932 "children": [],
1933 "content_layer": "body",
1934 "label": "key_value_region",
1935 "prov": [],
1936 "captions": [],
1937 "references": [],
1938 "footnotes": [],
1939 "graph": { "cells": cells, "links": links },
1940 }));
1941 self_ref
1942 }
1943
1944 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1945 let self_ref = format!("#/field_items/{}", self.field_items.len());
1946 self.field_items.push(Value::Null);
1947 let item_index = self.field_items.len() - 1;
1948 let mut child_refs = Vec::new();
1949 for (label, text) in [
1950 ("marker", &item.marker),
1951 ("field_key", &item.key),
1952 ("field_value", &item.value),
1953 ] {
1954 if let Some(text) = text {
1955 let extra = match (label, &item.value_kind) {
1958 ("field_value", Some(kind)) => json!({ "kind": kind }),
1959 _ => json!({}),
1960 };
1961 child_refs.push(ref_value(self.add_text(label, text, &self_ref, extra)));
1962 }
1963 }
1964 self.field_items[item_index] = json!({
1965 "self_ref": self_ref,
1966 "parent": { "$ref": parent },
1967 "children": child_refs,
1968 "content_layer": "body",
1969 "label": "field_item",
1970 "prov": [],
1971 });
1972 self_ref
1973 }
1974
1975 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1979 if let Some(item) = self.item_mut(self_ref) {
1980 item["content_layer"] = json!(layer);
1981 }
1982 }
1983
1984 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1985 let prov = self.take_prov_slot(unescape_text(text).chars().count());
1986 self.add_text_with(label, text, parent, extra, prov)
1987 }
1988
1989 fn add_text_with(
1992 &mut self,
1993 label: &str,
1994 text: &str,
1995 parent: &str,
1996 extra: Value,
1997 prov: ProvSlot,
1998 ) -> String {
1999 let index = self.texts.len();
2000 let self_ref = format!("#/texts/{index}");
2001 let mut item = match self.pending_track.take() {
2006 None => Item::Text(Box::new(TextItem {
2007 parent: parent.into(),
2008 label: label.into(),
2009 prov,
2010 text: unescape_text(text).into(),
2011 })),
2012 Some((track, cue)) => {
2013 let mut m = serde_json::Map::with_capacity(9);
2014 m.insert("self_ref".into(), Value::String(self_ref.clone()));
2015 m.insert("parent".into(), ref_value(parent));
2016 m.insert("children".into(), Value::Array(Vec::new()));
2017 m.insert("content_layer".into(), "body".into());
2018 m.insert("label".into(), label.into());
2019 m.insert("prov".into(), prov.to_value());
2020 m.insert("source".into(), Value::Array(vec![track_json(&track)]));
2022 m.insert("orig".into(), Value::String(cue.clone()));
2023 m.insert("text".into(), Value::String(cue));
2024 Item::json(Value::Object(m))
2025 }
2026 };
2027 if extra.as_object().is_some_and(|e| !e.is_empty()) {
2028 merge(item.value_mut(index), extra);
2029 }
2030 self.texts.push(item);
2031 self_ref
2032 }
2033
2034 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
2037 let self_ref = format!("#/texts/{}", self.texts.len());
2038 let prov = self.take_prov(latex.chars().count());
2039 self.texts.push(Item::json(json!({
2040 "self_ref": self_ref,
2041 "parent": { "$ref": parent },
2042 "children": [],
2043 "content_layer": "body",
2044 "label": "formula",
2045 "prov": prov,
2046 "orig": latex,
2047 "text": latex,
2048 })));
2049 self_ref
2050 }
2051
2052 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
2056 let self_ref = format!("#/texts/{}", self.texts.len());
2057 let prov = self.take_prov(latex.chars().count());
2058 self.texts.push(Item::json(json!({
2059 "self_ref": self_ref,
2060 "parent": { "$ref": parent },
2061 "children": [],
2062 "content_layer": "body",
2063 "label": "formula",
2064 "prov": prov,
2065 "orig": orig,
2066 "text": latex,
2067 })));
2068 self_ref
2069 }
2070
2071 fn add_code(
2072 &mut self,
2073 text: &str,
2074 language: Option<&str>,
2075 orig: Option<&str>,
2076 parent: &str,
2077 ) -> String {
2078 let self_ref = format!("#/texts/{}", self.texts.len());
2079 let raw = unescape_text(text);
2080 let prov = self.take_prov(raw.chars().count());
2081 self.texts.push(Item::json(json!({
2082 "self_ref": self_ref,
2083 "parent": { "$ref": parent },
2084 "children": [],
2085 "content_layer": "body",
2086 "label": "code",
2087 "prov": prov,
2088 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
2091 "text": raw,
2092 "captions": [],
2093 "references": [],
2094 "footnotes": [],
2095 "code_language": code_language(language),
2096 })));
2097 self_ref
2098 }
2099
2100 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
2103 let self_ref = format!("#/groups/{}", self.groups.len());
2104 self.groups.push(Value::Null);
2106 let base = level_of(&items[0]);
2107 let mut children = Vec::new();
2108 let mut i = 0;
2109 while i < items.len() {
2110 if !matches!(items[i], Node::ListItem { .. }) {
2113 i += 1;
2114 continue;
2115 }
2116 let lvl = level_of(&items[i]);
2117 if lvl > base {
2118 i += 1;
2120 continue;
2121 }
2122 let item_ref = self.add_list_item(&items[i], &self_ref);
2123 let mut j = i + 1;
2125 while j < items.len() && level_of(&items[j]) > base {
2126 j += 1;
2127 }
2128 if j > i + 1 {
2129 let mut nested = Vec::new();
2130 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
2131 if let Some(idx) = ref_index(&item_ref) {
2133 self.texts[idx].value_mut(idx)["children"]
2134 .as_array_mut()
2135 .unwrap()
2136 .extend(nested);
2137 }
2138 }
2139 children.push(ref_value(item_ref));
2140 i = j;
2141 }
2142 self.groups[group_index(&self_ref)] = json!({
2143 "self_ref": self_ref,
2144 "parent": { "$ref": parent },
2145 "children": children,
2146 "content_layer": "body",
2147 "name": "list",
2148 "label": "list",
2149 });
2150 self_ref
2151 }
2152
2153 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
2154 let Node::ListItem {
2155 ordered,
2156 number,
2157 text,
2158 location,
2159 ..
2160 } = node
2161 else {
2162 unreachable!()
2163 };
2164 self.adopt_loc(*location);
2165 let self_ref = format!("#/texts/{}", self.texts.len());
2166 let raw = unescape_text(text);
2167 let prov = self.take_prov(raw.chars().count());
2168 let marker = if *ordered {
2169 format!("{number}.")
2170 } else {
2171 "-".to_string()
2172 };
2173 self.texts.push(Item::json(json!({
2174 "self_ref": self_ref,
2175 "parent": { "$ref": parent },
2176 "children": [],
2177 "content_layer": "body",
2178 "label": "list_item",
2179 "prov": prov,
2180 "orig": raw,
2181 "text": raw,
2182 "enumerated": ordered,
2183 "marker": marker,
2184 })));
2185 self_ref
2186 }
2187
2188 fn add_table(&mut self, t: &Table, parent: &str) -> String {
2189 self.add_table_with(t, parent, false)
2190 }
2191
2192 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
2195 let self_ref = format!("#/tables/{}", self.tables.len());
2196 self.adopt_loc(t.location);
2197 let prov = self.take_prov(0);
2198 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
2202 Some(cap) => self.add_caption(
2203 cap,
2204 json!({}),
2205 &self_ref,
2206 parent,
2207 (t.caption_parent, t.caption_location),
2208 ),
2209 None => (Vec::new(), Vec::new()),
2210 };
2211 let data = table_data_with(t, raw);
2212 self.tables.push(json!({
2213 "self_ref": self_ref,
2214 "parent": { "$ref": parent },
2215 "children": children,
2216 "content_layer": "body",
2217 "label": "table",
2218 "prov": prov,
2219 "captions": captions,
2220 "references": [],
2221 "footnotes": [],
2222 "data": data,
2223 "annotations": [],
2224 }));
2225 self_ref
2226 }
2227
2228 fn add_caption(
2237 &mut self,
2238 text: &str,
2239 extra: Value,
2240 self_ref: &str,
2241 parent: &str,
2242 (choice, location): (CaptionParent, Option<[u16; 4]>),
2243 ) -> (Vec<Value>, Vec<Value>) {
2244 let cap_parent = match choice {
2249 CaptionParent::Item => self_ref,
2250 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
2251 CaptionParent::Body => "#/body",
2252 };
2253 if self.cur_page > 0 && location.is_some() {
2254 self.pending_loc = location;
2255 }
2256 let cap_ref = ref_value(self.add_text("caption", text, cap_parent, extra));
2257 match choice {
2258 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
2259 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
2262 CaptionParent::Body if parent == "#/body" => {
2263 self.pending_siblings.push(cap_ref.clone())
2264 }
2265 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
2266 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
2269 }
2270 (vec![cap_ref], Vec::new())
2271 }
2272
2273 fn add_picture(
2276 &mut self,
2277 caption: Option<&str>,
2278 caption_href: Option<&str>,
2279 image: Option<&crate::PictureImage>,
2280 meta: Option<Value>,
2281 parent: &str,
2282 caption_at: (CaptionParent, Option<[u16; 4]>),
2283 ) -> String {
2284 let self_ref = format!("#/pictures/{}", self.pictures.len());
2285 let prov = self.take_prov(0);
2288 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
2289 Some(cap) => {
2290 let extra = match caption_href {
2294 Some(href) => json!({ "hyperlink": href }),
2295 None => json!({}),
2296 };
2297 self.add_caption(cap, extra, &self_ref, parent, caption_at)
2298 }
2299 None => (Vec::new(), Vec::new()),
2300 };
2301 self.push_picture(prov, captions, children, image, meta, parent)
2302 }
2303
2304 fn push_picture(
2307 &mut self,
2308 prov: Value,
2309 captions: Vec<Value>,
2310 children: Vec<Value>,
2311 image: Option<&crate::PictureImage>,
2312 meta: Option<Value>,
2313 parent: &str,
2314 ) -> String {
2315 let self_ref = format!("#/pictures/{}", self.pictures.len());
2316 let annotations = meta
2320 .as_ref()
2321 .and_then(|m| m.get("annotations").cloned())
2322 .unwrap_or_else(|| json!([]));
2323 let meta = meta.map(|mut m| {
2324 if let Some(obj) = m.as_object_mut() {
2325 obj.remove("annotations");
2326 }
2327 m
2328 });
2329 let mut item = match meta {
2333 Some(meta) => json!({
2334 "self_ref": self_ref,
2335 "parent": { "$ref": parent },
2336 "children": children,
2337 "content_layer": "body",
2338 "meta": meta,
2339 "label": "picture",
2340 "prov": prov,
2341 "captions": captions,
2342 "references": [],
2343 "footnotes": [],
2344 "annotations": annotations,
2345 }),
2346 None => json!({
2347 "self_ref": self_ref,
2348 "parent": { "$ref": parent },
2349 "children": children,
2350 "content_layer": "body",
2351 "label": "picture",
2352 "prov": prov,
2353 "captions": captions,
2354 "references": [],
2355 "footnotes": [],
2356 "annotations": annotations,
2357 }),
2358 };
2359 if let Some(img) = image {
2364 let image = image_ref_json(img);
2365 if let Some(obj) = item.as_object_mut() {
2366 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
2367 obj.insert("image".into(), image);
2368 obj.insert("annotations".into(), annotations);
2369 }
2370 }
2371 self.pictures.push(item);
2372 self_ref
2373 }
2374
2375 fn add_group(
2376 &mut self,
2377 label: &str,
2378 name: Option<&str>,
2379 layer: Option<ContentLayer>,
2380 nodes: &[Node],
2381 parent: &str,
2382 ) -> String {
2383 let self_ref = format!("#/groups/{}", self.groups.len());
2384 self.groups.push(Value::Null);
2385 let mark = (
2389 self.texts.len(),
2390 self.tables.len(),
2391 self.pictures.len(),
2392 self.groups.len(),
2393 );
2394 let children: Vec<Value> = self
2395 .walk_into(nodes, &self_ref)
2396 .into_iter()
2397 .map(Child::into_value)
2398 .collect();
2399 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
2400 let content_layer = layer.map_or("body", |l| l.value());
2401 self.groups[group_index(&self_ref)] = json!({
2402 "self_ref": self_ref,
2403 "parent": { "$ref": parent },
2404 "children": children,
2405 "content_layer": content_layer,
2406 "name": name,
2407 "label": label,
2408 });
2409 if layer.is_some() {
2410 let (t, tb, p, g) = mark;
2411 for item in self.texts[t..]
2412 .iter_mut()
2413 .enumerate()
2414 .map(|(k, item)| item.value_mut(t + k))
2415 .chain(self.tables[tb..].iter_mut())
2416 .chain(self.pictures[p..].iter_mut())
2417 .chain(self.groups[g..].iter_mut())
2418 {
2419 if let Some(obj) = item.as_object_mut() {
2420 obj.insert("content_layer".into(), json!(content_layer));
2421 }
2422 }
2423 }
2424 self_ref
2425 }
2426
2427 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Child> {
2430 let seqs: Option<Vec<usize>> = nodes
2435 .iter()
2436 .map(|n| match n {
2437 Node::Prov { seq: Some(s), .. } => Some(*s),
2438 _ => None,
2439 })
2440 .collect();
2441 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
2442 let mut order: Vec<usize> = (0..nodes.len()).collect();
2443 order.sort_by_key(|&i| seqs[i]);
2444 let mut slots: Vec<Vec<Child>> = (0..nodes.len()).map(|_| Vec::new()).collect();
2445 for i in order {
2446 if let Some(r) = self.add_node(&nodes[i], parent) {
2447 slots[i].extend(self.pending_siblings.drain(..).map(Child::json));
2448 slots[i].push(Child::Ref(r.into()));
2449 slots[i].extend(self.pending_after.drain(..).map(Child::json));
2450 }
2451 if parent == "#/body" {
2452 slots[i].extend(self.pending_body.drain(..).map(Child::json));
2453 }
2454 }
2455 return slots.into_iter().flatten().collect();
2456 }
2457 let mut children = Vec::new();
2458 let mut i = 0;
2459 while i < nodes.len() {
2460 if matches!(nodes[i], Node::ListItem { .. }) {
2461 let start = i;
2462 i += 1;
2463 loop {
2464 match nodes.get(i) {
2465 Some(Node::ListItem { .. }) => i += 1,
2466 Some(Node::Paragraph { text })
2469 if text.is_empty()
2470 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
2471 {
2472 i += 1
2473 }
2474 _ => break,
2475 }
2476 }
2477 let mut lists = Vec::new();
2478 self.add_sibling_lists(&nodes[start..i], parent, &mut lists);
2479 children.extend(lists.into_iter().map(Child::json));
2480 } else {
2481 if let Some(r) = self.add_node(&nodes[i], parent) {
2482 children.extend(self.pending_siblings.drain(..).map(Child::json));
2483 children.push(Child::Ref(r.into()));
2484 children.extend(self.pending_after.drain(..).map(Child::json));
2485 }
2486 i += 1;
2487 }
2488 if parent == "#/body" {
2491 children.extend(self.pending_body.drain(..).map(Child::json));
2492 }
2493 }
2494 children
2495 }
2496
2497 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
2502 let base = level_of(&run[0]);
2503 let mut seg = 0;
2504 for k in 0..run.len() {
2505 let Node::ListItem {
2506 first_in_list,
2507 level,
2508 ..
2509 } = &run[k]
2510 else {
2511 continue;
2512 };
2513 if *level != base {
2514 continue; }
2516 if k > seg && *first_in_list {
2517 out.push(ref_value(self.add_list(&run[seg..k], parent)));
2518 seg = k;
2519 }
2520 }
2521 out.push(ref_value(self.add_list(&run[seg..], parent)));
2522 }
2523}
2524
2525fn level_of(node: &Node) -> u8 {
2526 match node {
2527 Node::ListItem { level, .. } => *level,
2528 _ => 0,
2529 }
2530}
2531
2532fn group_index(self_ref: &str) -> usize {
2533 self_ref.rsplit('/').next().unwrap().parse().unwrap()
2534}
2535
2536fn ref_index(self_ref: &str) -> Option<usize> {
2537 self_ref.rsplit('/').next()?.parse().ok()
2538}
2539
2540fn merge(target: &mut Value, extra: Value) {
2542 if let (Some(t), Value::Object(e)) = (target.as_object_mut(), extra) {
2543 t.extend(e);
2544 }
2545}
2546
2547fn object<const N: usize>(entries: [(&str, Value); N]) -> Value {
2553 let mut m = serde_json::Map::with_capacity(N);
2554 for (k, v) in entries {
2555 m.insert(k.to_owned(), v);
2556 }
2557 Value::Object(m)
2558}
2559
2560fn ref_value(r: impl Into<String>) -> Value {
2562 object([("$ref", Value::String(r.into()))])
2563}
2564
2565fn unescape_text(s: &str) -> String {
2567 s.replace("<", "<")
2568 .replace(">", ">")
2569 .replace("&", "&")
2570 .replace("\\_", "_")
2571}
2572
2573fn fnv1a(s: &str) -> u64 {
2576 let mut h: u64 = 0xcbf29ce484222325;
2577 for b in s.bytes() {
2578 h ^= b as u64;
2579 h = h.wrapping_mul(0x100000001b3);
2580 }
2581 h
2582}
2583
2584#[cfg(test)]
2585mod tests {
2586 use crate::{
2587 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
2588 };
2589 use serde_json::Value;
2590
2591 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
2592 crate::TableCell {
2593 text: text.into(),
2594 bbox: None,
2595 start_row: row,
2596 start_col: col,
2597 row_span: 1,
2598 col_span,
2599 column_header: row == 0,
2600 row_header: false,
2601 row_section: false,
2602 }
2603 }
2604
2605 #[test]
2611 fn ragged_rows_take_their_grid_from_first_class_cells() {
2612 let cells = vec![
2613 cell_at("h0", 0, 0, 1),
2614 cell_at("h2", 0, 2, 1),
2615 cell_at("wide", 1, 0, 3),
2616 ];
2617 let export = |rows: Vec<Vec<String>>| -> Value {
2618 let mut doc = DoclingDocument::new("t");
2619 doc.push(Node::Table(Table {
2620 rows,
2621 cells: Some(cells.clone()),
2622 ..Table::default()
2623 }));
2624 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2625 v["tables"][0]["data"].clone()
2626 };
2627 let data = export(vec![
2629 vec!["h0".into(), "h2".into()],
2630 vec!["wide".into(), "wide".into(), "wide".into()],
2631 ]);
2632 assert_eq!(data["num_rows"], 2);
2633 assert_eq!(data["num_cols"], 3);
2634 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
2635 let row0: Vec<&str> = data["grid"][0]
2636 .as_array()
2637 .unwrap()
2638 .iter()
2639 .map(|c| c["text"].as_str().unwrap())
2640 .collect();
2641 assert_eq!(row0, ["h0", "", "h2"]);
2642 assert_eq!(data["table_cells"][2]["col_span"], 3);
2643 let data = export(vec![
2646 vec!["h0".into(), "h2".into()],
2647 vec!["wide".into(), "wide".into()],
2648 ]);
2649 assert_eq!(data["num_cols"], 2);
2650 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
2651 }
2652
2653 fn doc_with_image() -> DoclingDocument {
2654 let mut doc = DoclingDocument::new("t");
2655 doc.push(Node::Picture {
2656 caption: Some("Fig 1".into()),
2657 caption_href: None,
2658 image: Some(PictureImage {
2659 dpi: crate::PictureImage::DEFAULT_DPI,
2660 mimetype: "image/png".into(),
2661 width: 4,
2662 height: 2,
2663 data: b"foobar".to_vec(),
2664 }),
2665 classification: None,
2666 description: None,
2667 caption_parent: Default::default(),
2668 caption_location: None,
2669 });
2670 doc
2671 }
2672
2673 #[test]
2678 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
2679 let mut doc = DoclingDocument::new("t");
2680 doc.push(Node::Heading {
2681 level: 1,
2682 text: "Slide One".into(),
2683 });
2684 doc.push(Node::Furniture {
2685 layer: ContentLayer::Notes,
2686 inner: Box::new(Node::Located {
2687 location: [0, 0, 0, 0],
2688 inner: Box::new(Node::Paragraph {
2689 text: "Speaker note for slide 1.".into(),
2690 }),
2691 }),
2692 });
2693 doc.push(Node::Furniture {
2694 layer: ContentLayer::Furniture,
2695 inner: Box::new(Node::Paragraph {
2696 text: "page header".into(),
2697 }),
2698 });
2699
2700 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2701 let texts = v["texts"].as_array().unwrap();
2702 assert_eq!(
2703 texts
2704 .iter()
2705 .map(|t| (
2706 t["label"].as_str().unwrap(),
2707 t["content_layer"].as_str().unwrap(),
2708 t["text"].as_str().unwrap()
2709 ))
2710 .collect::<Vec<_>>(),
2711 vec![
2712 ("title", "body", "Slide One"),
2713 ("text", "notes", "Speaker note for slide 1."),
2714 ],
2715 "the note is carried on its own layer; the furniture is not carried"
2716 );
2717 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
2719 }
2720
2721 #[test]
2727 fn continuation_flags_become_spanning_cells() {
2728 let mut doc = DoclingDocument::new("t");
2729 let rows = vec![
2731 vec!["merged".to_string(), "merged".into(), "merged".into()],
2732 vec!["merged".to_string(), "merged".into(), "merged".into()],
2733 vec!["a".to_string(), "b".into(), "c".into()],
2734 ];
2735 doc.push(Node::Table(crate::Table {
2736 rows,
2737 location: None,
2738 structure: Some(crate::TableStructure {
2739 header_row: vec![true, false, false],
2740 col_continuation: vec![
2741 vec![false, true, true],
2742 vec![false, true, true],
2743 vec![false, false, false],
2744 ],
2745 row_continuation: vec![
2746 vec![false, false, false],
2747 vec![true, true, true],
2748 vec![false, false, false],
2749 ],
2750 row_header: Vec::new(),
2751 col_header: Vec::new(),
2752 }),
2753 cell_blocks: None,
2754 cells: None,
2755 caption: None,
2756 caption_parent: Default::default(),
2757 caption_location: None,
2758 }));
2759 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2760 let data = &v["tables"][0]["data"];
2761 assert_eq!(data["num_rows"], 3);
2762 assert_eq!(data["num_cols"], 3);
2763 let cells = data["table_cells"].as_array().unwrap();
2764 assert_eq!(
2765 cells.len(),
2766 4,
2767 "one cell for the range, three for the plain row"
2768 );
2769 assert_eq!(
2770 cells[0],
2771 serde_json::json!({
2772 "row_span": 2, "col_span": 3,
2773 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2774 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2775 "text": "merged", "column_header": true, "row_header": false,
2776 "row_section": false, "fillable": false,
2777 })
2778 );
2779 assert_eq!(cells[1]["text"], "a");
2780 assert_eq!(cells[1]["row_span"], 1);
2781 assert_eq!(cells[1]["column_header"], false);
2782 let grid = data["grid"].as_array().unwrap();
2784 assert_eq!(grid.len(), 3);
2785 for (r, row) in grid.iter().take(2).enumerate() {
2786 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2787 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2788 }
2789 }
2790 assert_eq!(grid[2][2]["text"], "c");
2791 }
2792
2793 #[test]
2799 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2800 let mut doc = DoclingDocument::new("t");
2801 doc.push(Node::PageInfo {
2802 page_no: 1,
2803 width: 3.0,
2804 height: 4.0,
2805 });
2806 let table = crate::Table {
2807 rows: vec![vec!["a".to_string(), "b".into()]],
2808 ..Default::default()
2809 };
2810 doc.push(Node::Group {
2811 label: "sheet".into(),
2812 name: Some("Data".into()),
2813 layer: None,
2814 children: vec![
2815 Node::Prov {
2820 page_no: 1,
2821 bbox: [0.0, 0.0, 3.0, 4.0],
2822 charspan: [0, 0],
2823 seq: Some(1),
2824 inner: Box::new(Node::Table(table.clone())),
2825 },
2826 Node::Prov {
2827 page_no: 1,
2828 bbox: [0.0, 1.0, 1.0, 1.0],
2829 charspan: [0, 0],
2830 seq: Some(0),
2831 inner: Box::new(Node::Chart {
2832 kind: "bar_chart".into(),
2833 table,
2834 caption: Some("Sales".into()),
2835 location: Some([0, 128, 170, 128]),
2836 }),
2837 },
2838 ],
2839 });
2840 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2841 assert_eq!(
2842 v["pages"],
2843 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2844 );
2845 assert_eq!(
2846 v["tables"][0]["prov"],
2847 serde_json::json!([{
2848 "page_no": 1,
2849 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2850 "charspan": [0, 0],
2851 }])
2852 );
2853 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2854 let sheet = &v["groups"][0];
2857 assert_eq!(
2858 sheet["children"],
2859 serde_json::json!([
2860 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2861 ])
2862 );
2863 let cap = &v["texts"][0];
2864 assert_eq!(cap["label"], "caption");
2865 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2866 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2867 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2868 let pic = &v["pictures"][0];
2869 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2870 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2871 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2872 assert_eq!(
2873 pic["meta"]["classification"]["predictions"][0]["class_name"],
2874 "bar_chart"
2875 );
2876 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2877 }
2878
2879 #[test]
2883 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2884 let mut doc = DoclingDocument::new("t");
2885 doc.push(Node::PageInfo {
2886 page_no: 1,
2887 width: 12192000.0,
2888 height: 6858000.0,
2889 });
2890 doc.push(Node::Furniture {
2891 layer: ContentLayer::Notes,
2892 inner: Box::new(Node::Located {
2893 location: [0, 0, 0, 0],
2894 inner: Box::new(Node::Paragraph {
2895 text: "a note".into(),
2896 }),
2897 }),
2898 });
2899 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2900 let prov = &v["texts"][0]["prov"][0];
2901 assert_eq!(prov["page_no"], 1);
2902 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2903 assert_eq!(
2904 prov["bbox"],
2905 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2906 );
2907 assert_eq!(
2909 v["pages"]["1"]["size"],
2910 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2911 );
2912 }
2913
2914 #[test]
2924 fn captions_and_checkboxes_carry_their_own_prov() {
2925 let mut doc = DoclingDocument::new("t");
2926 doc.push(Node::PageInfo {
2927 page_no: 1,
2928 width: 512.0,
2929 height: 512.0,
2930 });
2931 doc.push(Node::Located {
2932 location: [64, 64, 448, 256],
2933 inner: Box::new(Node::Picture {
2934 caption: Some("Figure 1".into()),
2935 caption_href: None,
2936 image: None,
2937 classification: None,
2938 description: None,
2939 caption_parent: CaptionParent::Item,
2940 caption_location: Some([64, 264, 448, 280]),
2941 }),
2942 });
2943 doc.push(Node::PageInfo {
2944 page_no: 2,
2945 width: 512.0,
2946 height: 512.0,
2947 });
2948 doc.push(Node::Located {
2949 location: [64, 120, 448, 300],
2950 inner: Box::new(Node::Table(Table {
2951 rows: vec![vec!["a".into()]],
2952 caption: Some("Table 1".into()),
2953 caption_parent: CaptionParent::Item,
2954 caption_location: Some([64, 100, 300, 112]),
2955 ..Table::default()
2956 })),
2957 });
2958 doc.push(Node::Located {
2959 location: [64, 320, 200, 332],
2960 inner: Box::new(Node::CheckboxItem {
2961 checked: false,
2962 text: "First option".into(),
2963 }),
2964 });
2965 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2966 let texts = v["texts"].as_array().unwrap();
2967 let by_text = |t: &str| texts.iter().find(|x| x["text"] == t).expect(t).clone();
2968 let bbox = |p: &Value| {
2969 let b = &p["prov"][0]["bbox"];
2970 [&b["l"], &b["t"], &b["r"], &b["b"]].map(|x| x.as_f64().unwrap())
2971 };
2972 let fig = by_text("Figure 1");
2975 assert_eq!(fig["prov"][0]["page_no"], 1);
2976 assert_eq!(bbox(&fig), [64.0, 248.0, 448.0, 232.0]);
2977 assert_eq!(fig["prov"][0]["charspan"], serde_json::json!([0, 8]));
2978 assert_eq!(fig["parent"]["$ref"], "#/pictures/0");
2979 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
2980 let tab = by_text("Table 1");
2982 assert_eq!(tab["prov"][0]["page_no"], 2);
2983 assert_eq!(bbox(&tab), [64.0, 412.0, 300.0, 400.0]);
2984 assert_eq!(v["tables"][0]["prov"][0]["bbox"]["t"], 392.0);
2985 let cb = by_text("First option");
2987 assert_eq!(cb["label"], "checkbox_unselected");
2988 assert_eq!(cb["prov"][0]["page_no"], 2);
2989 assert_eq!(bbox(&cb), [64.0, 192.0, 200.0, 180.0]);
2990 assert!(texts
2991 .iter()
2992 .all(|t| !t["prov"].as_array().unwrap().is_empty()));
2993
2994 let mut plain = DoclingDocument::new("t");
2997 plain.push(Node::PageInfo {
2998 page_no: 1,
2999 width: 512.0,
3000 height: 512.0,
3001 });
3002 plain.push(Node::Located {
3003 location: [64, 64, 448, 256],
3004 inner: Box::new(Node::Picture {
3005 caption: Some("Figure 1".into()),
3006 caption_href: None,
3007 image: None,
3008 classification: None,
3009 description: None,
3010 caption_parent: CaptionParent::Item,
3011 caption_location: None,
3012 }),
3013 });
3014 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
3015 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
3016 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
3017 }
3018
3019 #[test]
3020 fn page_markers_produce_pages_and_prov() {
3021 let mut doc = DoclingDocument::new("t");
3022 doc.push(Node::PageInfo {
3023 page_no: 1,
3024 width: 512.0,
3025 height: 1024.0,
3026 });
3027 doc.push(Node::Located {
3028 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
3030 text: "hello".into(),
3031 }),
3032 });
3033 doc.push(Node::Table(Table {
3034 rows: vec![vec!["a".into()]],
3035 location: Some([0, 0, 512, 512]),
3036 ..Table::default()
3037 }));
3038 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3039 assert_eq!(v["pages"]["1"]["page_no"], 1);
3040 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
3041 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
3042 let prov = &v["texts"][0]["prov"][0];
3045 assert_eq!(prov["page_no"], 1);
3046 assert_eq!(prov["bbox"]["l"], 128.0);
3047 assert_eq!(prov["bbox"]["t"], 896.0);
3048 assert_eq!(prov["bbox"]["r"], 256.0);
3049 assert_eq!(prov["bbox"]["b"], 768.0);
3050 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
3051 assert_eq!(prov["charspan"][1], 5);
3052 let tprov = &v["tables"][0]["prov"][0];
3054 assert_eq!(tprov["bbox"]["t"], 1024.0);
3055 assert_eq!(tprov["bbox"]["b"], 0.0);
3056 assert_eq!(tprov["charspan"][1], 0);
3057
3058 let mut plain = DoclingDocument::new("t");
3060 plain.push(Node::Located {
3061 location: [1, 2, 3, 4],
3062 inner: Box::new(Node::Paragraph { text: "x".into() }),
3063 });
3064 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
3065 assert_eq!(v["pages"], serde_json::json!({}));
3066 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
3067 }
3068
3069 #[test]
3072 fn page_furniture_becomes_furniture_layer_text() {
3073 let mut doc = DoclingDocument::new("t");
3074 doc.push(Node::PageInfo {
3075 page_no: 1,
3076 width: 512.0,
3077 height: 512.0,
3078 });
3079 doc.push(Node::PageFurniture {
3080 footer: false,
3081 location: [10, 0, 100, 20],
3082 text: "Chapter 1".into(),
3083 });
3084 doc.push(Node::Paragraph {
3085 text: "body".into(),
3086 });
3087 doc.push(Node::PageFurniture {
3088 footer: true,
3089 location: [400, 490, 500, 512],
3090 text: "1.10.2".into(),
3091 });
3092 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3093 let texts = v["texts"].as_array().unwrap();
3094 assert_eq!(texts.len(), 3);
3095 assert_eq!(texts[0]["label"], "page_header");
3096 assert_eq!(texts[0]["content_layer"], "furniture");
3097 assert_eq!(texts[0]["parent"]["$ref"], "#/body");
3098 assert_eq!(texts[0]["prov"][0]["bbox"]["l"], 10.0);
3099 assert_eq!(texts[1]["content_layer"], "body");
3100 assert_eq!(texts[1]["prov"], serde_json::json!([]));
3101 assert_eq!(texts[2]["label"], "page_footer");
3102 assert_eq!(texts[2]["text"], "1.10.2");
3103 assert_eq!(texts[2]["content_layer"], "furniture");
3104 }
3105
3106 #[test]
3107 fn picture_image_in_markdown_modes_and_json() {
3108 let doc = doc_with_image();
3109 assert!(doc.export_to_markdown().contains("<!-- image -->"));
3111 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
3113 assert!(
3114 md.contains(""),
3115 "got:\n{md}"
3116 );
3117 assert!(files.is_empty());
3118 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
3120 assert!(
3121 md.contains(""),
3122 "got:\n{md}"
3123 );
3124 assert_eq!(
3125 files,
3126 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
3127 );
3128 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3131 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
3132 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
3133 let keys: Vec<&str> = v["pictures"][0]
3134 .as_object()
3135 .unwrap()
3136 .keys()
3137 .map(String::as_str)
3138 .collect();
3139 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
3140 assert_eq!(
3141 v["pictures"][0]["image"]["uri"],
3142 "data:image/png;base64,Zm9vYmFy"
3143 );
3144 }
3145
3146 #[test]
3147 fn exports_docling_schema() {
3148 let mut doc = DoclingDocument::new("t");
3149 doc.push(Node::Heading {
3150 level: 1,
3151 text: "Title".into(),
3152 });
3153 doc.push(Node::Heading {
3154 level: 2,
3155 text: "Sec".into(),
3156 });
3157 doc.push(Node::Paragraph {
3158 text: "Body & more".into(),
3159 }); doc.push(Node::ListItem {
3161 ordered: false,
3162 number: 0,
3163 first_in_list: true,
3164 text: "one".into(),
3165 level: 0,
3166 marker: None,
3167 location: None,
3168 dclx: None,
3169 href: None,
3170 layer: None,
3171 });
3172 doc.push(Node::ListItem {
3173 ordered: false,
3174 number: 0,
3175 first_in_list: false,
3176 text: "two".into(),
3177 level: 0,
3178 marker: None,
3179 location: None,
3180 dclx: None,
3181 href: None,
3182 layer: None,
3183 });
3184 doc.push(Node::Table(Table {
3185 rows: vec![vec!["A".into(), "B".into()]],
3186 location: None,
3187 structure: None,
3188 cell_blocks: None,
3189 cells: None,
3190 caption: None,
3191 caption_parent: Default::default(),
3192 caption_location: None,
3193 }));
3194
3195 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3196 assert_eq!(v["schema_name"], "DoclingDocument");
3197 assert_eq!(v["version"], "1.10.0");
3198 assert_eq!(v["texts"][0]["label"], "title");
3199 assert_eq!(v["texts"][1]["label"], "section_header");
3200 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
3204 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
3205 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
3206 assert_eq!(v["texts"][3]["marker"], "-");
3207 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
3209 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
3210 }
3211 #[test]
3215 fn a_layered_group_stamps_its_whole_subtree() {
3216 let doc = DoclingDocument {
3217 name: "s".into(),
3218 nodes: vec![
3219 Node::Group {
3220 label: "sheet".into(),
3221 name: Some("Sheet1".into()),
3222 layer: None,
3223 children: vec![Node::Paragraph {
3224 text: "visible".into(),
3225 }],
3226 },
3227 Node::Group {
3228 label: "sheet".into(),
3229 name: Some("Sheet2".into()),
3230 layer: Some(ContentLayer::Invisible),
3231 children: vec![Node::Paragraph {
3232 text: "hidden".into(),
3233 }],
3234 },
3235 ],
3236 ..DoclingDocument::new("s")
3237 };
3238 let v = crate::json::to_json(&doc);
3239 assert_eq!(v["groups"][0]["label"], "sheet");
3240 assert_eq!(v["groups"][0]["name"], "Sheet1");
3241 assert_eq!(v["groups"][0]["content_layer"], "body");
3242 assert_eq!(v["texts"][0]["content_layer"], "body");
3243 assert_eq!(v["groups"][1]["name"], "Sheet2");
3244 assert_eq!(v["groups"][1]["content_layer"], "invisible");
3245 assert_eq!(v["texts"][1]["content_layer"], "invisible");
3246 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
3248 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
3249 }
3250
3251 #[test]
3261 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
3262 use crate::tree::{ItemTree, TreeKind};
3263 let mut t = ItemTree::default();
3264 let text = |txt: &str| TreeKind::Text {
3265 label: "text".into(),
3266 text: txt.into(),
3267 orig: None,
3268 formatting: None,
3269 hyperlink: None,
3270 level: None,
3271 list: None,
3272 };
3273 let a = t.add(None, None, text("a"));
3274 let blank = t.add(None, None, text(""));
3275 let b = t.add(None, None, text("b"));
3276 t.delete(blank);
3277 let group = t.add(
3278 None,
3279 Some(ContentLayer::Notes),
3280 TreeKind::Group {
3281 label: "comment_section".into(),
3282 name: "comment-0".into(),
3283 },
3284 );
3285 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
3286 t.items[a].comments.push(group);
3287 t.add(
3288 None,
3289 None,
3290 TreeKind::Picture {
3291 captions: Vec::new(),
3292 image: None,
3293 extra_classes: Vec::new(),
3294 classification: Some("bar_chart".into()),
3295 description: None,
3296 confidence: None,
3297 chart: Some(Table {
3298 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
3299 ..Table::default()
3300 }),
3301 dpi: None,
3302 },
3303 );
3304 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
3305 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
3306 let mut doc = DoclingDocument::new("t");
3307 doc.tree = Some(t);
3308 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3309 let texts = v["texts"].as_array().unwrap();
3310 assert_eq!(texts.len(), 3);
3311 assert_eq!(texts[1]["text"], "b");
3312 assert_eq!(texts[1]["self_ref"], "#/texts/1");
3313 assert_eq!(
3314 v["body"]["children"],
3315 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
3316 );
3317 let keys: Vec<&str> = texts[0]
3318 .as_object()
3319 .unwrap()
3320 .keys()
3321 .map(String::as_str)
3322 .collect();
3323 assert_eq!(
3324 keys,
3325 vec![
3326 "self_ref",
3327 "parent",
3328 "children",
3329 "content_layer",
3330 "label",
3331 "prov",
3332 "comments",
3333 "orig",
3334 "text"
3335 ]
3336 );
3337 assert_eq!(
3338 texts[0]["comments"],
3339 serde_json::json!([{"$ref": "#/groups/0"}])
3340 );
3341 assert!(texts[1].get("comments").is_none());
3342 let meta = &v["pictures"][0]["meta"];
3343 assert_eq!(
3344 meta["classification"]["predictions"][0]["class_name"],
3345 "bar_chart"
3346 );
3347 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
3348 }
3349
3350 #[test]
3356 fn tree_items_carry_exact_provenance_and_dpi() {
3357 use crate::tree::{ItemTree, TreeKind, TreeProv};
3358 let text = |label: &str, t: &str| TreeKind::Text {
3359 label: label.into(),
3360 text: t.into(),
3361 orig: None,
3362 formatting: None,
3363 hyperlink: None,
3364 level: None,
3365 list: None,
3366 };
3367 let mut t = ItemTree::default();
3368 let slide = t.add(
3369 None,
3370 None,
3371 TreeKind::Group {
3372 label: "chapter".into(),
3373 name: "slide-0".into(),
3374 },
3375 );
3376 t.add_with_prov(
3377 Some(slide),
3378 None,
3379 text("paragraph", "héllo"),
3380 TreeProv {
3381 page_no: 1,
3382 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
3383 bottom_left: true,
3384 charspan: [0, 5],
3385 },
3386 );
3387 t.add_with_prov(
3388 Some(slide),
3389 None,
3390 TreeKind::Picture {
3391 captions: Vec::new(),
3392 image: Some(crate::PictureImage {
3393 dpi: crate::PictureImage::DEFAULT_DPI,
3394 mimetype: "image/png".into(),
3395 width: 2,
3396 height: 2,
3397 data: vec![0],
3398 }),
3399 extra_classes: Vec::new(),
3400 classification: None,
3401 description: None,
3402 confidence: None,
3403 chart: None,
3404 dpi: Some(300),
3405 },
3406 TreeProv {
3407 page_no: 1,
3408 bbox: [0.0; 4],
3409 bottom_left: false,
3410 charspan: [0, 0],
3411 },
3412 );
3413 t.add(
3414 Some(slide),
3415 Some(ContentLayer::Notes),
3416 text("text", "no geometry"),
3417 );
3418 let mut doc = DoclingDocument::new("t");
3419 doc.push(Node::PageInfo {
3420 page_no: 1,
3421 width: 9144000.0,
3422 height: 6858000.0,
3423 });
3424 doc.tree = Some(t);
3425 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3426 assert_eq!(
3427 v["texts"][0]["prov"],
3428 serde_json::json!([{
3429 "page_no": 1,
3430 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
3431 "charspan": [0, 5],
3432 }])
3433 );
3434 assert_eq!(v["texts"][0]["label"], "paragraph");
3435 assert_eq!(
3436 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
3437 "TOPLEFT"
3438 );
3439 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
3440 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
3441 assert_eq!(v["texts"][1]["content_layer"], "notes");
3442 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
3443 assert_eq!(v["pages"]["1"]["page_no"], 1);
3444 }
3445
3446 #[test]
3451 fn provenance_boxes_are_clamped_to_their_page() {
3452 let mut doc = DoclingDocument::new("t");
3453 doc.push(Node::PageInfo {
3454 page_no: 1,
3455 width: 10.0,
3456 height: 8.0,
3457 });
3458 doc.push(Node::Prov {
3459 page_no: 1,
3460 bbox: [-1.0, 2.0, 12.0, 9.5],
3461 charspan: [0, 1],
3462 seq: None,
3463 inner: Box::new(Node::Paragraph { text: "x".into() }),
3464 });
3465 let mut table = Table {
3466 rows: vec![vec!["a".into()]],
3467 ..Table::default()
3468 };
3469 table.cells = Some(vec![crate::TableCell {
3470 text: "a".into(),
3471 bbox: Some([1.0, 1.0, 11.0, 9.0]),
3472 start_row: 0,
3473 start_col: 0,
3474 row_span: 1,
3475 col_span: 1,
3476 column_header: false,
3477 row_header: false,
3478 row_section: false,
3479 }]);
3480 doc.push(Node::Prov {
3481 page_no: 1,
3482 bbox: [0.0, 0.0, 10.0, 8.0],
3483 charspan: [0, 0],
3484 seq: None,
3485 inner: Box::new(Node::Table(table)),
3486 });
3487 doc.push(Node::Prov {
3488 page_no: 7,
3489 bbox: [-5.0, 0.0, 50.0, 50.0],
3490 charspan: [0, 1],
3491 seq: None,
3492 inner: Box::new(Node::Paragraph { text: "y".into() }),
3493 });
3494 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3495 assert_eq!(
3496 v["texts"][0]["prov"][0]["bbox"],
3497 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
3498 );
3499 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
3500 assert_eq!(
3501 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
3502 (Some(1.0), Some(10.0), Some(8.0))
3503 );
3504 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
3505 assert_eq!(
3506 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
3507 "page 7 is not described"
3508 );
3509 }
3510
3511 #[test]
3518 fn misplaced_list_items_are_wrapped_like_docling_core() {
3519 use crate::tree::{ItemTree, ListMeta, TreeKind};
3520 let mut t = ItemTree::default();
3521 let li = |txt: &str| TreeKind::Text {
3522 label: "list_item".into(),
3523 text: txt.into(),
3524 orig: None,
3525 formatting: None,
3526 hyperlink: None,
3527 level: None,
3528 list: Some(ListMeta {
3529 enumerated: false,
3530 marker: "-".into(),
3531 }),
3532 };
3533 let group = |label: &str, name: &str| TreeKind::Group {
3534 label: label.into(),
3535 name: name.into(),
3536 };
3537 t.add(None, None, li("A"));
3538 t.add(None, None, li("B"));
3539 t.add(
3540 None,
3541 None,
3542 TreeKind::Text {
3543 label: "text".into(),
3544 text: "plain".into(),
3545 orig: None,
3546 formatting: None,
3547 hyperlink: None,
3548 level: None,
3549 list: None,
3550 },
3551 );
3552 let cell = t.add(None, None, group("unspecified", "cell"));
3553 t.add(Some(cell), None, li("C"));
3554 t.add(Some(cell), None, li("D"));
3555 let list = t.add(None, None, group("list", "list"));
3556 t.add(Some(list), None, li("E"));
3557 let doc = DoclingDocument {
3558 tree: Some(t),
3559 ..DoclingDocument::new("t")
3560 };
3561 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3562 let refs = |x: &Value| -> Vec<String> {
3563 x.as_array()
3564 .unwrap()
3565 .iter()
3566 .map(|r| r["$ref"].as_str().unwrap().to_string())
3567 .collect()
3568 };
3569 assert_eq!(
3570 refs(&v["body"]["children"]),
3571 ["#/groups/4", "#/texts/0", "#/groups/0", "#/groups/1"]
3572 );
3573 assert_eq!(
3574 refs(&v["groups"][0]["children"]),
3575 ["#/groups/3", "#/groups/2"]
3576 );
3577 for (g, name, kids) in [
3578 (2, "group", vec!["#/texts/2"]),
3579 (3, "group", vec!["#/texts/3"]),
3580 (4, "group", vec!["#/texts/4", "#/texts/5"]),
3581 (1, "list", vec!["#/texts/1"]),
3582 ] {
3583 assert_eq!(v["groups"][g]["label"], "list");
3584 assert_eq!(v["groups"][g]["name"], name);
3585 assert_eq!(refs(&v["groups"][g]["children"]), kids);
3586 }
3587 let texts: Vec<&str> = v["texts"]
3588 .as_array()
3589 .unwrap()
3590 .iter()
3591 .map(|t| t["text"].as_str().unwrap())
3592 .collect();
3593 assert_eq!(texts, ["plain", "E", "D", "C", "A", "B"]);
3594 assert_eq!(v["texts"][2]["parent"]["$ref"], "#/groups/2");
3595 }
3596
3597 #[test]
3600 fn a_deleted_child_is_not_written_as_an_empty_ref() {
3601 use crate::tree::{ItemTree, TreeKind};
3602 let mut t = ItemTree::default();
3603 let text = |txt: &str| TreeKind::Text {
3604 label: "text".into(),
3605 text: txt.into(),
3606 orig: None,
3607 formatting: None,
3608 hyperlink: None,
3609 level: None,
3610 list: None,
3611 };
3612 let g = t.add(
3613 None,
3614 None,
3615 TreeKind::Group {
3616 label: "unspecified".into(),
3617 name: "g".into(),
3618 },
3619 );
3620 let gone = t.add(Some(g), None, text(""));
3621 t.add(Some(g), None, text("kept"));
3622 t.items[gone].deleted = true; let doc = DoclingDocument {
3624 tree: Some(t),
3625 ..DoclingDocument::new("t")
3626 };
3627 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3628 assert_eq!(
3629 v["groups"][0]["children"],
3630 serde_json::json!([{"$ref": "#/texts/0"}])
3631 );
3632 }
3633
3634 #[test]
3635 fn a_backend_item_tree_is_written_verbatim() {
3636 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
3637 let mut t = ItemTree::default();
3638 let text = |label: &str, txt: &str| TreeKind::Text {
3639 label: label.into(),
3640 text: txt.into(),
3641 orig: None,
3642 formatting: None,
3643 hyperlink: None,
3644 level: None,
3645 list: None,
3646 };
3647 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
3648 let h = t.add(None, None, text("title", "Heading"));
3649 let group = t.add(
3650 Some(h),
3651 None,
3652 TreeKind::Group {
3653 label: "inline".into(),
3654 name: "group".into(),
3655 },
3656 );
3657 t.add(
3658 Some(group),
3659 None,
3660 TreeKind::Text {
3661 label: "text".into(),
3662 text: "bold".into(),
3663 orig: None,
3664 formatting: Some(Formatting {
3665 bold: true,
3666 ..Formatting::default()
3667 }),
3668 hyperlink: Some("https://example.com/".into()),
3669 level: None,
3670 list: None,
3671 },
3672 );
3673 t.add(
3674 Some(group),
3675 None,
3676 TreeKind::Code {
3677 text: "x = 1".into(),
3678 orig: None,
3679 language: Some("python".into()),
3680 formatting: None,
3681 hyperlink: None,
3682 captions: Vec::new(),
3683 },
3684 );
3685 let sub = t.add(
3686 Some(h),
3687 None,
3688 TreeKind::Text {
3689 label: "section_header".into(),
3690 text: "Sub".into(),
3691 orig: Some("Sub\u{2019}".into()),
3692 formatting: None,
3693 hyperlink: None,
3694 level: Some(1),
3695 list: None,
3696 },
3697 );
3698 let item = t.add(
3699 Some(sub),
3700 None,
3701 TreeKind::Text {
3702 label: "list_item".into(),
3703 text: "item".into(),
3704 orig: None,
3705 formatting: None,
3706 hyperlink: None,
3707 level: None,
3708 list: Some(ListMeta {
3709 enumerated: true,
3710 marker: "3.".into(),
3711 }),
3712 },
3713 );
3714 let _region = t.add(
3715 Some(sub),
3716 None,
3717 TreeKind::FieldRegion {
3718 items: vec![crate::FieldItem {
3719 marker: None,
3720 key: Some("Name".into()),
3721 value: Some("Duck".into()),
3722 value_kind: Some("read_only".into()),
3723 }],
3724 },
3725 );
3726 let table = t.add(
3727 Some(sub),
3728 None,
3729 TreeKind::Table {
3730 table: Table {
3731 rows: vec![vec!["a \n<".into(), "b".into()]],
3732 cells: Some(vec![
3733 crate::TableCell {
3734 text: "a \n<".into(),
3735 bbox: None,
3736 start_row: 0,
3737 start_col: 0,
3738 row_span: 3,
3739 col_span: 1,
3740 column_header: false,
3741 row_header: true,
3742 row_section: false,
3743 },
3744 crate::TableCell {
3745 text: "b".into(),
3746 bbox: None,
3747 start_row: 0,
3748 start_col: 1,
3749 row_span: 1,
3750 col_span: 1,
3751 column_header: false,
3752 row_header: false,
3753 row_section: false,
3754 },
3755 ]),
3756 ..Table::default()
3757 },
3758 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
3760 },
3761 );
3762 let cell_group = t.add(
3763 Some(table),
3764 None,
3765 TreeKind::Group {
3766 label: "unspecified".into(),
3767 name: "rich_cell_group_1_0_0".into(),
3768 },
3769 );
3770 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
3771 *rich_cells = vec![(0, 1, cell_group)];
3772 }
3773 let after = t.add(Some(sub), None, text("text", "after the region"));
3774 let _ = (title, after);
3775 let list = t.add(
3779 Some(sub),
3780 None,
3781 TreeKind::Group {
3782 label: "list".into(),
3783 name: "list".into(),
3784 },
3785 );
3786 t.reparent(item, Some(list));
3787
3788 let doc = DoclingDocument {
3789 tree: Some(t),
3790 ..DoclingDocument::new("t")
3791 };
3792 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3793 let texts: Vec<&str> = v["texts"]
3795 .as_array()
3796 .unwrap()
3797 .iter()
3798 .map(|t| t["text"].as_str().unwrap())
3799 .collect();
3800 assert_eq!(
3801 texts,
3802 [
3803 "Page",
3804 "Heading",
3805 "bold",
3806 "x = 1",
3807 "Sub",
3808 "item",
3809 "Name",
3810 "Duck",
3811 "after the region"
3812 ]
3813 );
3814 assert_eq!(
3815 v["body"]["children"],
3816 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
3817 );
3818 assert_eq!(v["texts"][0]["content_layer"], "furniture");
3819 assert_eq!(
3820 v["texts"][1]["children"],
3821 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
3822 );
3823 let bold = &v["texts"][2];
3824 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
3825 let keys: Vec<&str> = bold
3826 .as_object()
3827 .unwrap()
3828 .keys()
3829 .map(String::as_str)
3830 .collect();
3831 assert_eq!(
3832 keys,
3833 [
3834 "self_ref",
3835 "parent",
3836 "children",
3837 "content_layer",
3838 "label",
3839 "prov",
3840 "orig",
3841 "text",
3842 "formatting",
3843 "hyperlink"
3844 ]
3845 );
3846 assert_eq!(
3847 bold["formatting"],
3848 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
3849 );
3850 let code = &v["texts"][3];
3851 assert_eq!(code["label"], "code");
3852 assert_eq!(code["code_language"], "Python");
3853 let sub = &v["texts"][4];
3854 assert_eq!(sub["orig"], "Sub\u{2019}");
3855 assert_eq!(sub["level"], 1);
3856 let item = &v["texts"][5];
3857 let keys: Vec<&str> = item
3858 .as_object()
3859 .unwrap()
3860 .keys()
3861 .map(String::as_str)
3862 .collect();
3863 assert_eq!(
3864 keys,
3865 [
3866 "self_ref",
3867 "parent",
3868 "children",
3869 "content_layer",
3870 "label",
3871 "prov",
3872 "orig",
3873 "text",
3874 "enumerated",
3875 "marker"
3876 ]
3877 );
3878 assert_eq!(item["marker"], "3.");
3879 assert_eq!(v["texts"][7]["kind"], "read_only");
3880 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
3881 let table = &v["tables"][0];
3882 assert_eq!(
3883 table["children"],
3884 serde_json::json!([{"$ref": "#/groups/1"}])
3885 );
3886 let cells = table["data"]["table_cells"].as_array().unwrap();
3887 assert_eq!(
3888 cells[0]["text"], "a \n<",
3889 "raw cell text is written verbatim"
3890 );
3891 assert_eq!(
3892 cells[0]["end_row_offset_idx"], 3,
3893 "declared spans are not clamped"
3894 );
3895 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
3896 assert!(cells[0].get("ref").is_none());
3897 assert!(
3898 table["data"]["grid"][0][1].get("ref").is_none(),
3899 "the grid shows plain cells"
3900 );
3901 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
3902 }
3903
3904 #[test]
3908 fn a_comment_section_can_be_referenced_by_its_note_text() {
3909 let doc = DoclingDocument {
3910 name: "c".into(),
3911 nodes: vec![
3912 Node::Commented {
3913 comments: vec![0],
3914 inner: Box::new(Node::Paragraph {
3915 text: "annotated".into(),
3916 }),
3917 },
3918 Node::CommentSection {
3919 name: "comment-Sheet1-A1".into(),
3920 text: "[author: A]: note".into(),
3921 refs_note_text: true,
3922 grouped: true,
3923 },
3924 ],
3925 ..DoclingDocument::new("c")
3926 };
3927 let v = crate::json::to_json(&doc);
3928 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
3929 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
3930 }
3931
3932 #[test]
3936 fn comment_sections_link_back_to_their_items() {
3937 let doc = DoclingDocument {
3938 name: "c".into(),
3939 nodes: vec![
3940 Node::Commented {
3941 comments: vec![0],
3942 inner: Box::new(Node::Paragraph {
3943 text: "annotated".into(),
3944 }),
3945 },
3946 Node::Paragraph {
3947 text: "plain".into(),
3948 },
3949 Node::CommentSection {
3950 name: "comment-7".into(),
3951 text: "[time: t]: note".into(),
3952 refs_note_text: false,
3953 grouped: true,
3954 },
3955 ],
3956 ..DoclingDocument::new("c")
3957 };
3958 let v = crate::json::to_json(&doc);
3959 assert_eq!(v["groups"][0]["label"], "comment_section");
3961 assert_eq!(v["groups"][0]["name"], "comment-7");
3962 assert_eq!(v["groups"][0]["content_layer"], "notes");
3963 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
3964 assert_eq!(v["texts"][2]["content_layer"], "notes");
3965 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
3967 assert!(v["texts"][1].get("comments").is_none());
3968 let keys: Vec<&str> = v["texts"][0]
3970 .as_object()
3971 .unwrap()
3972 .keys()
3973 .map(String::as_str)
3974 .collect();
3975 assert_eq!(
3976 &keys[keys.len() - 4..],
3977 &["prov", "comments", "orig", "text"]
3978 );
3979 }
3980
3981 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
3982 Node::Picture {
3983 caption: Some(caption.into()),
3984 caption_href: None,
3985 image: None,
3986 classification: None,
3987 description: None,
3988 caption_parent,
3989 caption_location: None,
3990 }
3991 }
3992
3993 fn group(children: Vec<Node>) -> Node {
3994 Node::Group {
3995 label: "section".into(),
3996 name: None,
3997 layer: None,
3998 children,
3999 }
4000 }
4001
4002 fn refs(v: &Value) -> Vec<&str> {
4003 v.as_array()
4004 .unwrap()
4005 .iter()
4006 .map(|r| r["$ref"].as_str().unwrap())
4007 .collect()
4008 }
4009
4010 #[test]
4015 fn a_body_caption_follows_the_enclosing_top_level_item() {
4016 let mut doc = DoclingDocument::new("t");
4017 doc.push(picture("top", CaptionParent::Body));
4018 doc.push(group(vec![
4019 Node::Paragraph { text: "p".into() },
4020 picture("nested", CaptionParent::Body),
4021 ]));
4022 doc.push(Node::Paragraph {
4023 text: "after".into(),
4024 });
4025 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4026 assert_eq!(
4027 refs(&v["body"]["children"]),
4028 [
4029 "#/texts/0",
4030 "#/pictures/0",
4031 "#/groups/0",
4032 "#/texts/2",
4033 "#/texts/3"
4034 ]
4035 );
4036 assert_eq!(
4037 refs(&v["groups"][0]["children"]),
4038 ["#/texts/1", "#/pictures/1"]
4039 );
4040 for (cap, pic) in [(0, 0), (2, 1)] {
4041 assert_eq!(v["texts"][cap]["label"], "caption");
4042 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
4043 assert_eq!(
4044 refs(&v["pictures"][pic]["captions"]),
4045 [format!("#/texts/{cap}")]
4046 );
4047 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
4048 }
4049 }
4050
4051 #[test]
4054 fn an_item_caption_is_the_items_first_child() {
4055 let mut doc = DoclingDocument::new("t");
4056 doc.push(picture("fig", CaptionParent::Item));
4057 doc.push(Node::Table(Table {
4058 rows: vec![vec!["a".into()]],
4059 caption: Some("tab".into()),
4060 caption_parent: CaptionParent::Item,
4061 ..Table::default()
4062 }));
4063 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4064 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
4065 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
4066 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
4067 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
4068 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
4069 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
4070 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
4071 }
4072
4073 #[test]
4079 fn picture_children_hang_off_the_picture_after_its_caption() {
4080 let mut doc = DoclingDocument::new("t");
4081 doc.push(picture("fig", CaptionParent::Item));
4082 doc.push(Node::PictureChildren(vec![
4083 Node::Heading {
4084 level: 2,
4085 text: "in-figure title".into(),
4086 },
4087 Node::Paragraph {
4088 text: "axis label".into(),
4089 },
4090 Node::ListItem {
4091 ordered: false,
4092 number: 0,
4093 first_in_list: true,
4094 text: "callout".into(),
4095 level: 0,
4096 marker: None,
4097 location: None,
4098 dclx: None,
4099 href: None,
4100 layer: None,
4101 },
4102 ]));
4103 doc.push(Node::Paragraph {
4104 text: "after".into(),
4105 });
4106 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4107 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/texts/4"]);
4108 assert_eq!(
4109 refs(&v["pictures"][0]["children"]),
4110 ["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"]
4111 );
4112 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
4113 assert_eq!(v["texts"][1]["label"], "section_header");
4114 assert_eq!(v["texts"][2]["label"], "text");
4115 for t in 1..=2 {
4116 assert_eq!(v["texts"][t]["parent"]["$ref"], "#/pictures/0");
4117 assert_eq!(v["texts"][t]["content_layer"], "body");
4118 }
4119 assert_eq!(v["groups"][0]["parent"]["$ref"], "#/pictures/0");
4120 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
4121 let md = doc.export_to_markdown();
4122 assert!(
4123 !md.contains("axis label") && !md.contains("callout"),
4124 "{md}"
4125 );
4126 assert!(md.contains("after"), "{md}");
4127 }
4128
4129 #[test]
4133 fn a_container_caption_is_the_items_sibling() {
4134 let mut doc = DoclingDocument::new("t");
4135 doc.push(group(vec![
4136 picture("chart", CaptionParent::Container),
4137 Node::Table(Table {
4138 rows: vec![vec!["a".into()]],
4139 caption: Some("figcaption".into()),
4140 caption_parent: CaptionParent::ContainerAfter,
4141 ..Table::default()
4142 }),
4143 ]));
4144 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4145 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
4146 assert_eq!(
4147 refs(&v["groups"][0]["children"]),
4148 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
4149 );
4150 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
4151 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
4152 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
4153 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
4154 }
4155
4156 #[test]
4161 fn a_labeled_text_keeps_its_label_and_hyperlink_out_of_the_text() {
4162 let mut doc = DoclingDocument::new("t");
4163 doc.push(Node::LabeledText {
4164 label: "footnote".into(),
4165 text: "1 https://example.com/a\\_b".into(),
4166 href: Some("https://example.com/a_b".into()),
4167 });
4168 doc.push(Node::LabeledText {
4169 label: "footnote".into(),
4170 text: "© 2022 the authors".into(),
4171 href: None,
4172 });
4173 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4174 let t = &v["texts"][0];
4175 assert_eq!(t["label"], "footnote");
4176 assert_eq!(t["text"], "1 https://example.com/a_b");
4177 assert_eq!(t["orig"], "1 https://example.com/a_b");
4178 assert_eq!(t["hyperlink"], "https://example.com/a_b");
4179 assert_eq!(v["texts"][1]["label"], "footnote");
4180 assert!(v["texts"][1].get("hyperlink").is_none());
4181 assert_eq!(
4182 doc.export_to_markdown(),
4183 "[1 https://example.com/a\\_b](https://example.com/a_b)\n\n© 2022 the authors\n"
4184 );
4185 assert_eq!(
4186 doc.export_to_text(),
4187 "1 https://example.com/a_b\n\n© 2022 the authors"
4188 );
4189 }
4190}