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 } => {
1392 let r = format!("#/texts/{}", self.texts.len());
1393 let prov = self.take_prov(text.chars().count());
1394 let mut item_json = json!({
1395 "self_ref": r,
1396 "parent": { "$ref": parent },
1397 "children": children,
1398 "content_layer": layer,
1399 "label": "code",
1400 "prov": prov,
1401 });
1402 if !item.comments.is_empty() {
1403 item_json["comments"] =
1404 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
1405 }
1406 merge(
1407 &mut item_json,
1408 json!({
1409 "orig": orig.as_deref().unwrap_or(text),
1410 "text": text,
1411 }),
1412 );
1413 if let Some(f) = formatting {
1414 item_json["formatting"] = formatting_json(f);
1415 }
1416 if let Some(h) = hyperlink {
1417 item_json["hyperlink"] = json!(h);
1418 }
1419 merge(
1420 &mut item_json,
1421 json!({
1422 "captions": [],
1423 "references": [],
1424 "footnotes": [],
1425 "code_language": code_language(language.as_deref()),
1426 }),
1427 );
1428 self.texts.push(Item::json(item_json));
1429 r
1430 }
1431 TreeKind::Group { label, name } => {
1432 self.pending_exact = None;
1433 let r = format!("#/groups/{}", self.groups.len());
1434 self.groups.push(json!({
1435 "self_ref": r,
1436 "parent": { "$ref": parent },
1437 "children": children,
1438 "content_layer": layer,
1439 "name": name,
1440 "label": label,
1441 }));
1442 r
1443 }
1444 TreeKind::Table {
1445 table,
1446 rich_cells,
1447 captions,
1448 } => {
1449 let r = self.add_table_with(table, parent, true);
1451 let idx = ref_index(&r).expect("table ref");
1452 let t = &mut self.tables[idx];
1453 t["children"] = Value::Array(children);
1454 t["content_layer"] = json!(layer);
1455 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
1456 for &(row, col, group) in rich_cells {
1460 let cell_ref = ref_of(group);
1461 let hit = |c: &Value| {
1462 c["start_row_offset_idx"] == json!(row)
1463 && c["start_col_offset_idx"] == json!(col)
1464 };
1465 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
1466 for c in cells.iter_mut().filter(|c| hit(c)) {
1467 c["ref"] = cell_ref.clone();
1468 }
1469 }
1470 }
1471 r
1472 }
1473 TreeKind::Picture {
1474 captions,
1475 image,
1476 classification,
1477 extra_classes,
1478 description,
1479 confidence,
1480 chart,
1481 dpi,
1482 } => {
1483 let mut meta = classification.as_ref().map(|c| {
1488 let mut pred = serde_json::Map::new();
1489 if let Some(conf) = confidence {
1490 pred.insert("confidence".into(), json!(conf));
1491 }
1492 pred.insert("class_name".into(), json!(c));
1493 let mut preds = vec![Value::Object(pred)];
1494 preds.extend(extra_classes.iter().map(|c| json!({ "class_name": c })));
1495 json!({ "classification": { "predictions": preds } })
1496 });
1497 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
1498 if !t.rows.is_empty() {
1499 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
1500 }
1501 }
1502 if let Some(desc) = description {
1506 let mut d = description_meta(desc);
1507 let mut merged = Map::new();
1508 merged.insert("description".into(), d["description"].take());
1509 if let Some(Value::Object(rest)) = meta {
1510 merged.extend(rest);
1511 }
1512 merged.insert("annotations".into(), d["annotations"].take());
1513 meta = Some(Value::Object(merged));
1514 }
1515 let prov = self.take_prov(0);
1516 let r = self.push_picture(
1517 prov,
1518 captions.iter().map(|&c| ref_of(c)).collect(),
1519 children,
1520 image.as_ref(),
1521 meta,
1522 parent,
1523 );
1524 if let Some(idx) = ref_index(&r) {
1525 self.pictures[idx]["content_layer"] = json!(layer);
1526 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
1529 img["dpi"] = json!(dpi);
1530 }
1531 }
1532 r
1533 }
1534 TreeKind::FieldRegion { items } => {
1535 self.pending_exact = None;
1536 let r = self.add_field_region(items, parent);
1537 if let Some(region) = self.field_regions.last_mut() {
1538 region["content_layer"] = json!(layer);
1539 }
1540 r
1541 }
1542 TreeKind::KeyValueGraph { cells, links } => {
1543 self.pending_exact = None;
1544 let r = self.add_key_value_graph(cells, links, parent);
1545 if let Some(item) = self.key_value_items.last_mut() {
1546 item["content_layer"] = json!(layer);
1547 }
1548 r
1549 }
1550 };
1551 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1552 }
1553 tree.body
1554 .iter()
1555 .filter(|&&c| !tree.items[c].deleted)
1556 .map(|&c| ref_of(c))
1557 .collect()
1558 }
1559
1560 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1561 match node {
1562 Node::Heading { level: 1, text } => {
1563 Some(self.add_text("title", text, parent, json!({})))
1564 }
1565 Node::Heading { level, text } => Some(self.add_text(
1566 "section_header",
1567 text,
1568 parent,
1569 json!({ "level": level.saturating_sub(1) }),
1570 )),
1571 Node::Caption { text, href } => {
1572 let extra = match href {
1573 Some(url) => json!({ "hyperlink": url }),
1574 None => json!({}),
1575 };
1576 Some(self.add_text("caption", text, parent, extra))
1577 }
1578 Node::LabeledText { label, text, href } => {
1581 let extra = match href {
1582 Some(url) => json!({ "hyperlink": url }),
1583 None => json!({}),
1584 };
1585 Some(self.add_text(label, text, parent, extra))
1586 }
1587 Node::Paragraph { text } => {
1588 let t = text.trim();
1591 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1592 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1593 _ => Some(self.add_text("text", text, parent, json!({}))),
1594 }
1595 }
1596 Node::CheckboxItem { checked, text } => {
1597 let label = if *checked {
1603 "checkbox_selected"
1604 } else {
1605 "checkbox_unselected"
1606 };
1607 Some(self.add_text(label, text, parent, json!({})))
1608 }
1609 Node::Code {
1610 language,
1611 text,
1612 orig,
1613 ..
1614 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1615 Node::Formula {
1618 latex,
1619 orig,
1620 location,
1621 } => {
1622 self.adopt_loc(*location);
1623 Some(self.add_formula_item(latex, orig, parent))
1624 }
1625 Node::CommentSection {
1631 name,
1632 text,
1633 refs_note_text,
1634 grouped,
1635 } => {
1636 if !*grouped {
1637 let child =
1641 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1642 self.comment_groups.push(child.clone());
1643 return Some(child);
1644 }
1645 if let Some((_, self_ref)) = self
1646 .last_comment_section
1647 .as_ref()
1648 .filter(|(n, _)| n == name)
1649 .cloned()
1650 {
1651 let child =
1654 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1655 if let Some(children) =
1656 self.groups[group_index(&self_ref)]["children"].as_array_mut()
1657 {
1658 children.push(ref_value(child.as_str()));
1659 }
1660 self.comment_groups.push(if *refs_note_text {
1661 child
1662 } else {
1663 self_ref.clone()
1664 });
1665 return Some(self_ref);
1666 }
1667 let self_ref = format!("#/groups/{}", self.groups.len());
1668 self.groups.push(Value::Null);
1669 let child =
1670 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1671 self.groups[group_index(&self_ref)] = json!({
1672 "self_ref": self_ref,
1673 "parent": { "$ref": parent },
1674 "children": [{ "$ref": child }],
1675 "content_layer": "notes",
1676 "name": name,
1677 "label": "comment_section",
1678 });
1679 self.last_comment_section = Some((name.clone(), self_ref.clone()));
1680 self.comment_groups.push(if *refs_note_text {
1681 child
1682 } else {
1683 self_ref.clone()
1684 });
1685 Some(self_ref)
1686 }
1687 Node::Commented { comments, inner } => {
1690 let item = self.add_node(inner, parent)?;
1691 if !comments.is_empty() {
1692 self.pending_comments.push((item.clone(), comments.clone()));
1693 }
1694 Some(item)
1695 }
1696 Node::Table(t) => Some(self.add_table(t, parent)),
1697 Node::Picture {
1698 caption,
1699 caption_href,
1700 image,
1701 classification,
1702 description,
1703 caption_parent,
1704 caption_location,
1705 } => Some(self.add_picture(
1706 caption.as_deref(),
1707 caption_href.as_deref(),
1708 image.as_ref(),
1709 picture_meta(classification.as_deref(), description.as_ref()),
1710 parent,
1711 (*caption_parent, *caption_location),
1712 )),
1713 Node::Chart {
1718 kind,
1719 table,
1720 caption,
1721 location,
1722 } => {
1723 self.adopt_loc(*location);
1724 let mut meta = json!({
1725 "classification": { "predictions": [{ "class_name": kind }] },
1726 });
1727 if !table.rows.is_empty() {
1728 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1729 }
1730 let mut captions = Vec::new();
1736 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1737 let prov = self.prov_slot(unescape_text(cap).chars().count(), true);
1738 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1739 self.pending_siblings.push(ref_value(cap_ref.as_str()));
1740 captions.push(ref_value(cap_ref));
1741 }
1742 let prov = self.take_prov(0);
1743 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1744 }
1745 Node::DoclangOnly(_) => None,
1747 Node::Group {
1748 label,
1749 name,
1750 layer,
1751 children,
1752 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1753 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1754 Node::KeyValueGraph { cells, links } => {
1755 Some(self.add_key_value_graph(cells, links, parent))
1756 }
1757 Node::InlineGroup { md_text, .. } => {
1760 Some(self.add_text("text", md_text, parent, json!({})))
1761 }
1762 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1764 Node::Furniture {
1770 layer: ContentLayer::Notes,
1771 inner,
1772 } => {
1773 let item = self.add_node(inner, parent)?;
1774 self.set_layer(&item, "notes");
1775 Some(item)
1776 }
1777 Node::Furniture { .. } => None,
1778 Node::PageFurniture {
1782 footer,
1783 location,
1784 text,
1785 } => {
1786 if self.cur_page > 0 {
1787 self.pending_loc = Some(*location);
1788 }
1789 let label = if *footer {
1790 "page_footer"
1791 } else {
1792 "page_header"
1793 };
1794 let item = self.add_text(label, text, parent, json!({}));
1795 self.pending_loc = None;
1796 self.set_layer(&item, "furniture");
1797 Some(item)
1798 }
1799 Node::FurnitureText { label, text } => {
1802 let item = self.add_text(label, text, parent, json!({}));
1803 self.set_layer(&item, "furniture");
1804 Some(item)
1805 }
1806 Node::PictureChildren(children) => {
1810 let pic = self.pictures.len().checked_sub(1)?;
1811 let pic_ref = format!("#/pictures/{pic}");
1812 let refs = self.walk_into(children, &pic_ref);
1813 if let Some(list) = self.pictures[pic]["children"].as_array_mut() {
1814 list.extend(refs.into_iter().map(Child::into_value));
1815 }
1816 None
1817 }
1818 Node::Located { location, inner } => {
1823 if self.cur_page > 0 {
1824 self.pending_loc = Some(*location);
1825 }
1826 let r = self.add_node(inner, parent);
1827 self.pending_loc = None;
1828 r
1829 }
1830 Node::Track { track, cue, inner } => {
1831 self.pending_track = Some((track.clone(), cue.clone()));
1832 let r = self.add_node(inner, parent);
1833 self.pending_track = None;
1834 r
1835 }
1836 Node::Prov {
1837 page_no,
1838 bbox,
1839 charspan,
1840 inner,
1841 ..
1842 } => {
1843 self.pending_exact = Some(ExactProv {
1844 page_no: *page_no,
1845 bbox: bbox.map(f64::from),
1846 bottom_left: false,
1847 charspan: *charspan,
1848 });
1849 let r = self.add_node(inner, parent);
1850 self.pending_exact = None;
1851 r
1852 }
1853 Node::PageBreak => None,
1855 Node::PageInfo {
1858 page_no,
1859 width,
1860 height,
1861 } => {
1862 self.cur_page = *page_no;
1863 self.cur_w = *width as f64;
1864 self.cur_h = *height as f64;
1865 if *page_no > 0 {
1866 self.pages.push((*page_no, self.cur_w, self.cur_h));
1867 }
1868 None
1869 }
1870 Node::ListItem { .. } => None,
1872 }
1873 }
1874
1875 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1879 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1880 self.field_regions.push(Value::Null);
1881 let region_index = self.field_regions.len() - 1;
1882 let mut item_refs = Vec::new();
1883 for item in items {
1884 item_refs.push(ref_value(self.add_field_item(item, &self_ref)));
1885 }
1886 self.field_regions[region_index] = json!({
1887 "self_ref": self_ref,
1888 "parent": { "$ref": parent },
1889 "children": item_refs,
1890 "content_layer": "body",
1891 "label": "field_region",
1892 "prov": [],
1893 });
1894 self_ref
1895 }
1896
1897 fn add_key_value_graph(
1901 &mut self,
1902 cells: &[crate::GraphCell],
1903 links: &[crate::GraphLink],
1904 parent: &str,
1905 ) -> String {
1906 let self_ref = format!("#/key_value_items/{}", self.key_value_items.len());
1907 let cells: Vec<Value> = cells
1908 .iter()
1909 .map(|c| {
1910 json!({
1911 "label": c.label,
1912 "cell_id": c.cell_id,
1913 "text": c.text,
1914 "orig": c.orig,
1915 })
1916 })
1917 .collect();
1918 let links: Vec<Value> = links
1919 .iter()
1920 .map(|l| {
1921 json!({
1922 "label": l.label,
1923 "source_cell_id": l.source_cell_id,
1924 "target_cell_id": l.target_cell_id,
1925 })
1926 })
1927 .collect();
1928 self.key_value_items.push(json!({
1929 "self_ref": self_ref,
1930 "parent": { "$ref": parent },
1931 "children": [],
1932 "content_layer": "body",
1933 "label": "key_value_region",
1934 "prov": [],
1935 "captions": [],
1936 "references": [],
1937 "footnotes": [],
1938 "graph": { "cells": cells, "links": links },
1939 }));
1940 self_ref
1941 }
1942
1943 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1944 let self_ref = format!("#/field_items/{}", self.field_items.len());
1945 self.field_items.push(Value::Null);
1946 let item_index = self.field_items.len() - 1;
1947 let mut child_refs = Vec::new();
1948 for (label, text) in [
1949 ("marker", &item.marker),
1950 ("field_key", &item.key),
1951 ("field_value", &item.value),
1952 ] {
1953 if let Some(text) = text {
1954 let extra = match (label, &item.value_kind) {
1957 ("field_value", Some(kind)) => json!({ "kind": kind }),
1958 _ => json!({}),
1959 };
1960 child_refs.push(ref_value(self.add_text(label, text, &self_ref, extra)));
1961 }
1962 }
1963 self.field_items[item_index] = json!({
1964 "self_ref": self_ref,
1965 "parent": { "$ref": parent },
1966 "children": child_refs,
1967 "content_layer": "body",
1968 "label": "field_item",
1969 "prov": [],
1970 });
1971 self_ref
1972 }
1973
1974 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1978 if let Some(item) = self.item_mut(self_ref) {
1979 item["content_layer"] = json!(layer);
1980 }
1981 }
1982
1983 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1984 let prov = self.take_prov_slot(unescape_text(text).chars().count());
1985 self.add_text_with(label, text, parent, extra, prov)
1986 }
1987
1988 fn add_text_with(
1991 &mut self,
1992 label: &str,
1993 text: &str,
1994 parent: &str,
1995 extra: Value,
1996 prov: ProvSlot,
1997 ) -> String {
1998 let index = self.texts.len();
1999 let self_ref = format!("#/texts/{index}");
2000 let mut item = match self.pending_track.take() {
2005 None => Item::Text(Box::new(TextItem {
2006 parent: parent.into(),
2007 label: label.into(),
2008 prov,
2009 text: unescape_text(text).into(),
2010 })),
2011 Some((track, cue)) => {
2012 let mut m = serde_json::Map::with_capacity(9);
2013 m.insert("self_ref".into(), Value::String(self_ref.clone()));
2014 m.insert("parent".into(), ref_value(parent));
2015 m.insert("children".into(), Value::Array(Vec::new()));
2016 m.insert("content_layer".into(), "body".into());
2017 m.insert("label".into(), label.into());
2018 m.insert("prov".into(), prov.to_value());
2019 m.insert("source".into(), Value::Array(vec![track_json(&track)]));
2021 m.insert("orig".into(), Value::String(cue.clone()));
2022 m.insert("text".into(), Value::String(cue));
2023 Item::json(Value::Object(m))
2024 }
2025 };
2026 if extra.as_object().is_some_and(|e| !e.is_empty()) {
2027 merge(item.value_mut(index), extra);
2028 }
2029 self.texts.push(item);
2030 self_ref
2031 }
2032
2033 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
2036 let self_ref = format!("#/texts/{}", self.texts.len());
2037 let prov = self.take_prov(latex.chars().count());
2038 self.texts.push(Item::json(json!({
2039 "self_ref": self_ref,
2040 "parent": { "$ref": parent },
2041 "children": [],
2042 "content_layer": "body",
2043 "label": "formula",
2044 "prov": prov,
2045 "orig": latex,
2046 "text": latex,
2047 })));
2048 self_ref
2049 }
2050
2051 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
2055 let self_ref = format!("#/texts/{}", self.texts.len());
2056 let prov = self.take_prov(latex.chars().count());
2057 self.texts.push(Item::json(json!({
2058 "self_ref": self_ref,
2059 "parent": { "$ref": parent },
2060 "children": [],
2061 "content_layer": "body",
2062 "label": "formula",
2063 "prov": prov,
2064 "orig": orig,
2065 "text": latex,
2066 })));
2067 self_ref
2068 }
2069
2070 fn add_code(
2071 &mut self,
2072 text: &str,
2073 language: Option<&str>,
2074 orig: Option<&str>,
2075 parent: &str,
2076 ) -> String {
2077 let self_ref = format!("#/texts/{}", self.texts.len());
2078 let raw = unescape_text(text);
2079 let prov = self.take_prov(raw.chars().count());
2080 self.texts.push(Item::json(json!({
2081 "self_ref": self_ref,
2082 "parent": { "$ref": parent },
2083 "children": [],
2084 "content_layer": "body",
2085 "label": "code",
2086 "prov": prov,
2087 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
2090 "text": raw,
2091 "captions": [],
2092 "references": [],
2093 "footnotes": [],
2094 "code_language": code_language(language),
2095 })));
2096 self_ref
2097 }
2098
2099 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
2102 let self_ref = format!("#/groups/{}", self.groups.len());
2103 self.groups.push(Value::Null);
2105 let base = level_of(&items[0]);
2106 let mut children = Vec::new();
2107 let mut i = 0;
2108 while i < items.len() {
2109 if !matches!(items[i], Node::ListItem { .. }) {
2112 i += 1;
2113 continue;
2114 }
2115 let lvl = level_of(&items[i]);
2116 if lvl > base {
2117 i += 1;
2119 continue;
2120 }
2121 let item_ref = self.add_list_item(&items[i], &self_ref);
2122 let mut j = i + 1;
2124 while j < items.len() && level_of(&items[j]) > base {
2125 j += 1;
2126 }
2127 if j > i + 1 {
2128 let mut nested = Vec::new();
2129 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
2130 if let Some(idx) = ref_index(&item_ref) {
2132 self.texts[idx].value_mut(idx)["children"]
2133 .as_array_mut()
2134 .unwrap()
2135 .extend(nested);
2136 }
2137 }
2138 children.push(ref_value(item_ref));
2139 i = j;
2140 }
2141 self.groups[group_index(&self_ref)] = json!({
2142 "self_ref": self_ref,
2143 "parent": { "$ref": parent },
2144 "children": children,
2145 "content_layer": "body",
2146 "name": "list",
2147 "label": "list",
2148 });
2149 self_ref
2150 }
2151
2152 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
2153 let Node::ListItem {
2154 ordered,
2155 number,
2156 text,
2157 location,
2158 ..
2159 } = node
2160 else {
2161 unreachable!()
2162 };
2163 self.adopt_loc(*location);
2164 let self_ref = format!("#/texts/{}", self.texts.len());
2165 let raw = unescape_text(text);
2166 let prov = self.take_prov(raw.chars().count());
2167 let marker = if *ordered {
2168 format!("{number}.")
2169 } else {
2170 "-".to_string()
2171 };
2172 self.texts.push(Item::json(json!({
2173 "self_ref": self_ref,
2174 "parent": { "$ref": parent },
2175 "children": [],
2176 "content_layer": "body",
2177 "label": "list_item",
2178 "prov": prov,
2179 "orig": raw,
2180 "text": raw,
2181 "enumerated": ordered,
2182 "marker": marker,
2183 })));
2184 self_ref
2185 }
2186
2187 fn add_table(&mut self, t: &Table, parent: &str) -> String {
2188 self.add_table_with(t, parent, false)
2189 }
2190
2191 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
2194 let self_ref = format!("#/tables/{}", self.tables.len());
2195 self.adopt_loc(t.location);
2196 let prov = self.take_prov(0);
2197 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
2201 Some(cap) => self.add_caption(
2202 cap,
2203 json!({}),
2204 &self_ref,
2205 parent,
2206 (t.caption_parent, t.caption_location),
2207 ),
2208 None => (Vec::new(), Vec::new()),
2209 };
2210 let data = table_data_with(t, raw);
2211 self.tables.push(json!({
2212 "self_ref": self_ref,
2213 "parent": { "$ref": parent },
2214 "children": children,
2215 "content_layer": "body",
2216 "label": "table",
2217 "prov": prov,
2218 "captions": captions,
2219 "references": [],
2220 "footnotes": [],
2221 "data": data,
2222 "annotations": [],
2223 }));
2224 self_ref
2225 }
2226
2227 fn add_caption(
2236 &mut self,
2237 text: &str,
2238 extra: Value,
2239 self_ref: &str,
2240 parent: &str,
2241 (choice, location): (CaptionParent, Option<[u16; 4]>),
2242 ) -> (Vec<Value>, Vec<Value>) {
2243 let cap_parent = match choice {
2248 CaptionParent::Item => self_ref,
2249 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
2250 CaptionParent::Body => "#/body",
2251 };
2252 if self.cur_page > 0 && location.is_some() {
2253 self.pending_loc = location;
2254 }
2255 let cap_ref = ref_value(self.add_text("caption", text, cap_parent, extra));
2256 match choice {
2257 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
2258 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
2261 CaptionParent::Body if parent == "#/body" => {
2262 self.pending_siblings.push(cap_ref.clone())
2263 }
2264 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
2265 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
2268 }
2269 (vec![cap_ref], Vec::new())
2270 }
2271
2272 fn add_picture(
2275 &mut self,
2276 caption: Option<&str>,
2277 caption_href: Option<&str>,
2278 image: Option<&crate::PictureImage>,
2279 meta: Option<Value>,
2280 parent: &str,
2281 caption_at: (CaptionParent, Option<[u16; 4]>),
2282 ) -> String {
2283 let self_ref = format!("#/pictures/{}", self.pictures.len());
2284 let prov = self.take_prov(0);
2287 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
2288 Some(cap) => {
2289 let extra = match caption_href {
2293 Some(href) => json!({ "hyperlink": href }),
2294 None => json!({}),
2295 };
2296 self.add_caption(cap, extra, &self_ref, parent, caption_at)
2297 }
2298 None => (Vec::new(), Vec::new()),
2299 };
2300 self.push_picture(prov, captions, children, image, meta, parent)
2301 }
2302
2303 fn push_picture(
2306 &mut self,
2307 prov: Value,
2308 captions: Vec<Value>,
2309 children: Vec<Value>,
2310 image: Option<&crate::PictureImage>,
2311 meta: Option<Value>,
2312 parent: &str,
2313 ) -> String {
2314 let self_ref = format!("#/pictures/{}", self.pictures.len());
2315 let annotations = meta
2319 .as_ref()
2320 .and_then(|m| m.get("annotations").cloned())
2321 .unwrap_or_else(|| json!([]));
2322 let meta = meta.map(|mut m| {
2323 if let Some(obj) = m.as_object_mut() {
2324 obj.remove("annotations");
2325 }
2326 m
2327 });
2328 let mut item = match meta {
2332 Some(meta) => json!({
2333 "self_ref": self_ref,
2334 "parent": { "$ref": parent },
2335 "children": children,
2336 "content_layer": "body",
2337 "meta": meta,
2338 "label": "picture",
2339 "prov": prov,
2340 "captions": captions,
2341 "references": [],
2342 "footnotes": [],
2343 "annotations": annotations,
2344 }),
2345 None => json!({
2346 "self_ref": self_ref,
2347 "parent": { "$ref": parent },
2348 "children": children,
2349 "content_layer": "body",
2350 "label": "picture",
2351 "prov": prov,
2352 "captions": captions,
2353 "references": [],
2354 "footnotes": [],
2355 "annotations": annotations,
2356 }),
2357 };
2358 if let Some(img) = image {
2363 let image = image_ref_json(img);
2364 if let Some(obj) = item.as_object_mut() {
2365 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
2366 obj.insert("image".into(), image);
2367 obj.insert("annotations".into(), annotations);
2368 }
2369 }
2370 self.pictures.push(item);
2371 self_ref
2372 }
2373
2374 fn add_group(
2375 &mut self,
2376 label: &str,
2377 name: Option<&str>,
2378 layer: Option<ContentLayer>,
2379 nodes: &[Node],
2380 parent: &str,
2381 ) -> String {
2382 let self_ref = format!("#/groups/{}", self.groups.len());
2383 self.groups.push(Value::Null);
2384 let mark = (
2388 self.texts.len(),
2389 self.tables.len(),
2390 self.pictures.len(),
2391 self.groups.len(),
2392 );
2393 let children: Vec<Value> = self
2394 .walk_into(nodes, &self_ref)
2395 .into_iter()
2396 .map(Child::into_value)
2397 .collect();
2398 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
2399 let content_layer = layer.map_or("body", |l| l.value());
2400 self.groups[group_index(&self_ref)] = json!({
2401 "self_ref": self_ref,
2402 "parent": { "$ref": parent },
2403 "children": children,
2404 "content_layer": content_layer,
2405 "name": name,
2406 "label": label,
2407 });
2408 if layer.is_some() {
2409 let (t, tb, p, g) = mark;
2410 for item in self.texts[t..]
2411 .iter_mut()
2412 .enumerate()
2413 .map(|(k, item)| item.value_mut(t + k))
2414 .chain(self.tables[tb..].iter_mut())
2415 .chain(self.pictures[p..].iter_mut())
2416 .chain(self.groups[g..].iter_mut())
2417 {
2418 if let Some(obj) = item.as_object_mut() {
2419 obj.insert("content_layer".into(), json!(content_layer));
2420 }
2421 }
2422 }
2423 self_ref
2424 }
2425
2426 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Child> {
2429 let seqs: Option<Vec<usize>> = nodes
2434 .iter()
2435 .map(|n| match n {
2436 Node::Prov { seq: Some(s), .. } => Some(*s),
2437 _ => None,
2438 })
2439 .collect();
2440 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
2441 let mut order: Vec<usize> = (0..nodes.len()).collect();
2442 order.sort_by_key(|&i| seqs[i]);
2443 let mut slots: Vec<Vec<Child>> = (0..nodes.len()).map(|_| Vec::new()).collect();
2444 for i in order {
2445 if let Some(r) = self.add_node(&nodes[i], parent) {
2446 slots[i].extend(self.pending_siblings.drain(..).map(Child::json));
2447 slots[i].push(Child::Ref(r.into()));
2448 slots[i].extend(self.pending_after.drain(..).map(Child::json));
2449 }
2450 if parent == "#/body" {
2451 slots[i].extend(self.pending_body.drain(..).map(Child::json));
2452 }
2453 }
2454 return slots.into_iter().flatten().collect();
2455 }
2456 let mut children = Vec::new();
2457 let mut i = 0;
2458 while i < nodes.len() {
2459 if matches!(nodes[i], Node::ListItem { .. }) {
2460 let start = i;
2461 i += 1;
2462 loop {
2463 match nodes.get(i) {
2464 Some(Node::ListItem { .. }) => i += 1,
2465 Some(Node::Paragraph { text })
2468 if text.is_empty()
2469 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
2470 {
2471 i += 1
2472 }
2473 _ => break,
2474 }
2475 }
2476 let mut lists = Vec::new();
2477 self.add_sibling_lists(&nodes[start..i], parent, &mut lists);
2478 children.extend(lists.into_iter().map(Child::json));
2479 } else {
2480 if let Some(r) = self.add_node(&nodes[i], parent) {
2481 children.extend(self.pending_siblings.drain(..).map(Child::json));
2482 children.push(Child::Ref(r.into()));
2483 children.extend(self.pending_after.drain(..).map(Child::json));
2484 }
2485 i += 1;
2486 }
2487 if parent == "#/body" {
2490 children.extend(self.pending_body.drain(..).map(Child::json));
2491 }
2492 }
2493 children
2494 }
2495
2496 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
2501 let base = level_of(&run[0]);
2502 let mut seg = 0;
2503 for k in 0..run.len() {
2504 let Node::ListItem {
2505 first_in_list,
2506 level,
2507 ..
2508 } = &run[k]
2509 else {
2510 continue;
2511 };
2512 if *level != base {
2513 continue; }
2515 if k > seg && *first_in_list {
2516 out.push(ref_value(self.add_list(&run[seg..k], parent)));
2517 seg = k;
2518 }
2519 }
2520 out.push(ref_value(self.add_list(&run[seg..], parent)));
2521 }
2522}
2523
2524fn level_of(node: &Node) -> u8 {
2525 match node {
2526 Node::ListItem { level, .. } => *level,
2527 _ => 0,
2528 }
2529}
2530
2531fn group_index(self_ref: &str) -> usize {
2532 self_ref.rsplit('/').next().unwrap().parse().unwrap()
2533}
2534
2535fn ref_index(self_ref: &str) -> Option<usize> {
2536 self_ref.rsplit('/').next()?.parse().ok()
2537}
2538
2539fn merge(target: &mut Value, extra: Value) {
2541 if let (Some(t), Value::Object(e)) = (target.as_object_mut(), extra) {
2542 t.extend(e);
2543 }
2544}
2545
2546fn object<const N: usize>(entries: [(&str, Value); N]) -> Value {
2552 let mut m = serde_json::Map::with_capacity(N);
2553 for (k, v) in entries {
2554 m.insert(k.to_owned(), v);
2555 }
2556 Value::Object(m)
2557}
2558
2559fn ref_value(r: impl Into<String>) -> Value {
2561 object([("$ref", Value::String(r.into()))])
2562}
2563
2564fn unescape_text(s: &str) -> String {
2566 s.replace("<", "<")
2567 .replace(">", ">")
2568 .replace("&", "&")
2569 .replace("\\_", "_")
2570}
2571
2572fn fnv1a(s: &str) -> u64 {
2575 let mut h: u64 = 0xcbf29ce484222325;
2576 for b in s.bytes() {
2577 h ^= b as u64;
2578 h = h.wrapping_mul(0x100000001b3);
2579 }
2580 h
2581}
2582
2583#[cfg(test)]
2584mod tests {
2585 use crate::{
2586 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
2587 };
2588 use serde_json::Value;
2589
2590 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
2591 crate::TableCell {
2592 text: text.into(),
2593 bbox: None,
2594 start_row: row,
2595 start_col: col,
2596 row_span: 1,
2597 col_span,
2598 column_header: row == 0,
2599 row_header: false,
2600 row_section: false,
2601 }
2602 }
2603
2604 #[test]
2610 fn ragged_rows_take_their_grid_from_first_class_cells() {
2611 let cells = vec![
2612 cell_at("h0", 0, 0, 1),
2613 cell_at("h2", 0, 2, 1),
2614 cell_at("wide", 1, 0, 3),
2615 ];
2616 let export = |rows: Vec<Vec<String>>| -> Value {
2617 let mut doc = DoclingDocument::new("t");
2618 doc.push(Node::Table(Table {
2619 rows,
2620 cells: Some(cells.clone()),
2621 ..Table::default()
2622 }));
2623 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2624 v["tables"][0]["data"].clone()
2625 };
2626 let data = export(vec![
2628 vec!["h0".into(), "h2".into()],
2629 vec!["wide".into(), "wide".into(), "wide".into()],
2630 ]);
2631 assert_eq!(data["num_rows"], 2);
2632 assert_eq!(data["num_cols"], 3);
2633 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
2634 let row0: Vec<&str> = data["grid"][0]
2635 .as_array()
2636 .unwrap()
2637 .iter()
2638 .map(|c| c["text"].as_str().unwrap())
2639 .collect();
2640 assert_eq!(row0, ["h0", "", "h2"]);
2641 assert_eq!(data["table_cells"][2]["col_span"], 3);
2642 let data = export(vec![
2645 vec!["h0".into(), "h2".into()],
2646 vec!["wide".into(), "wide".into()],
2647 ]);
2648 assert_eq!(data["num_cols"], 2);
2649 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
2650 }
2651
2652 fn doc_with_image() -> DoclingDocument {
2653 let mut doc = DoclingDocument::new("t");
2654 doc.push(Node::Picture {
2655 caption: Some("Fig 1".into()),
2656 caption_href: None,
2657 image: Some(PictureImage {
2658 dpi: crate::PictureImage::DEFAULT_DPI,
2659 mimetype: "image/png".into(),
2660 width: 4,
2661 height: 2,
2662 data: b"foobar".to_vec(),
2663 }),
2664 classification: None,
2665 description: None,
2666 caption_parent: Default::default(),
2667 caption_location: None,
2668 });
2669 doc
2670 }
2671
2672 #[test]
2677 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
2678 let mut doc = DoclingDocument::new("t");
2679 doc.push(Node::Heading {
2680 level: 1,
2681 text: "Slide One".into(),
2682 });
2683 doc.push(Node::Furniture {
2684 layer: ContentLayer::Notes,
2685 inner: Box::new(Node::Located {
2686 location: [0, 0, 0, 0],
2687 inner: Box::new(Node::Paragraph {
2688 text: "Speaker note for slide 1.".into(),
2689 }),
2690 }),
2691 });
2692 doc.push(Node::Furniture {
2693 layer: ContentLayer::Furniture,
2694 inner: Box::new(Node::Paragraph {
2695 text: "page header".into(),
2696 }),
2697 });
2698
2699 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2700 let texts = v["texts"].as_array().unwrap();
2701 assert_eq!(
2702 texts
2703 .iter()
2704 .map(|t| (
2705 t["label"].as_str().unwrap(),
2706 t["content_layer"].as_str().unwrap(),
2707 t["text"].as_str().unwrap()
2708 ))
2709 .collect::<Vec<_>>(),
2710 vec![
2711 ("title", "body", "Slide One"),
2712 ("text", "notes", "Speaker note for slide 1."),
2713 ],
2714 "the note is carried on its own layer; the furniture is not carried"
2715 );
2716 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
2718 }
2719
2720 #[test]
2726 fn continuation_flags_become_spanning_cells() {
2727 let mut doc = DoclingDocument::new("t");
2728 let rows = vec![
2730 vec!["merged".to_string(), "merged".into(), "merged".into()],
2731 vec!["merged".to_string(), "merged".into(), "merged".into()],
2732 vec!["a".to_string(), "b".into(), "c".into()],
2733 ];
2734 doc.push(Node::Table(crate::Table {
2735 rows,
2736 location: None,
2737 structure: Some(crate::TableStructure {
2738 header_row: vec![true, false, false],
2739 col_continuation: vec![
2740 vec![false, true, true],
2741 vec![false, true, true],
2742 vec![false, false, false],
2743 ],
2744 row_continuation: vec![
2745 vec![false, false, false],
2746 vec![true, true, true],
2747 vec![false, false, false],
2748 ],
2749 row_header: Vec::new(),
2750 col_header: Vec::new(),
2751 }),
2752 cell_blocks: None,
2753 cells: None,
2754 caption: None,
2755 caption_parent: Default::default(),
2756 caption_location: None,
2757 }));
2758 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2759 let data = &v["tables"][0]["data"];
2760 assert_eq!(data["num_rows"], 3);
2761 assert_eq!(data["num_cols"], 3);
2762 let cells = data["table_cells"].as_array().unwrap();
2763 assert_eq!(
2764 cells.len(),
2765 4,
2766 "one cell for the range, three for the plain row"
2767 );
2768 assert_eq!(
2769 cells[0],
2770 serde_json::json!({
2771 "row_span": 2, "col_span": 3,
2772 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2773 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2774 "text": "merged", "column_header": true, "row_header": false,
2775 "row_section": false, "fillable": false,
2776 })
2777 );
2778 assert_eq!(cells[1]["text"], "a");
2779 assert_eq!(cells[1]["row_span"], 1);
2780 assert_eq!(cells[1]["column_header"], false);
2781 let grid = data["grid"].as_array().unwrap();
2783 assert_eq!(grid.len(), 3);
2784 for (r, row) in grid.iter().take(2).enumerate() {
2785 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2786 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2787 }
2788 }
2789 assert_eq!(grid[2][2]["text"], "c");
2790 }
2791
2792 #[test]
2798 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2799 let mut doc = DoclingDocument::new("t");
2800 doc.push(Node::PageInfo {
2801 page_no: 1,
2802 width: 3.0,
2803 height: 4.0,
2804 });
2805 let table = crate::Table {
2806 rows: vec![vec!["a".to_string(), "b".into()]],
2807 ..Default::default()
2808 };
2809 doc.push(Node::Group {
2810 label: "sheet".into(),
2811 name: Some("Data".into()),
2812 layer: None,
2813 children: vec![
2814 Node::Prov {
2819 page_no: 1,
2820 bbox: [0.0, 0.0, 3.0, 4.0],
2821 charspan: [0, 0],
2822 seq: Some(1),
2823 inner: Box::new(Node::Table(table.clone())),
2824 },
2825 Node::Prov {
2826 page_no: 1,
2827 bbox: [0.0, 1.0, 1.0, 1.0],
2828 charspan: [0, 0],
2829 seq: Some(0),
2830 inner: Box::new(Node::Chart {
2831 kind: "bar_chart".into(),
2832 table,
2833 caption: Some("Sales".into()),
2834 location: Some([0, 128, 170, 128]),
2835 }),
2836 },
2837 ],
2838 });
2839 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2840 assert_eq!(
2841 v["pages"],
2842 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2843 );
2844 assert_eq!(
2845 v["tables"][0]["prov"],
2846 serde_json::json!([{
2847 "page_no": 1,
2848 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2849 "charspan": [0, 0],
2850 }])
2851 );
2852 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2853 let sheet = &v["groups"][0];
2856 assert_eq!(
2857 sheet["children"],
2858 serde_json::json!([
2859 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2860 ])
2861 );
2862 let cap = &v["texts"][0];
2863 assert_eq!(cap["label"], "caption");
2864 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2865 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2866 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2867 let pic = &v["pictures"][0];
2868 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2869 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2870 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2871 assert_eq!(
2872 pic["meta"]["classification"]["predictions"][0]["class_name"],
2873 "bar_chart"
2874 );
2875 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2876 }
2877
2878 #[test]
2882 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2883 let mut doc = DoclingDocument::new("t");
2884 doc.push(Node::PageInfo {
2885 page_no: 1,
2886 width: 12192000.0,
2887 height: 6858000.0,
2888 });
2889 doc.push(Node::Furniture {
2890 layer: ContentLayer::Notes,
2891 inner: Box::new(Node::Located {
2892 location: [0, 0, 0, 0],
2893 inner: Box::new(Node::Paragraph {
2894 text: "a note".into(),
2895 }),
2896 }),
2897 });
2898 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2899 let prov = &v["texts"][0]["prov"][0];
2900 assert_eq!(prov["page_no"], 1);
2901 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2902 assert_eq!(
2903 prov["bbox"],
2904 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2905 );
2906 assert_eq!(
2908 v["pages"]["1"]["size"],
2909 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2910 );
2911 }
2912
2913 #[test]
2923 fn captions_and_checkboxes_carry_their_own_prov() {
2924 let mut doc = DoclingDocument::new("t");
2925 doc.push(Node::PageInfo {
2926 page_no: 1,
2927 width: 512.0,
2928 height: 512.0,
2929 });
2930 doc.push(Node::Located {
2931 location: [64, 64, 448, 256],
2932 inner: Box::new(Node::Picture {
2933 caption: Some("Figure 1".into()),
2934 caption_href: None,
2935 image: None,
2936 classification: None,
2937 description: None,
2938 caption_parent: CaptionParent::Item,
2939 caption_location: Some([64, 264, 448, 280]),
2940 }),
2941 });
2942 doc.push(Node::PageInfo {
2943 page_no: 2,
2944 width: 512.0,
2945 height: 512.0,
2946 });
2947 doc.push(Node::Located {
2948 location: [64, 120, 448, 300],
2949 inner: Box::new(Node::Table(Table {
2950 rows: vec![vec!["a".into()]],
2951 caption: Some("Table 1".into()),
2952 caption_parent: CaptionParent::Item,
2953 caption_location: Some([64, 100, 300, 112]),
2954 ..Table::default()
2955 })),
2956 });
2957 doc.push(Node::Located {
2958 location: [64, 320, 200, 332],
2959 inner: Box::new(Node::CheckboxItem {
2960 checked: false,
2961 text: "First option".into(),
2962 }),
2963 });
2964 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2965 let texts = v["texts"].as_array().unwrap();
2966 let by_text = |t: &str| texts.iter().find(|x| x["text"] == t).expect(t).clone();
2967 let bbox = |p: &Value| {
2968 let b = &p["prov"][0]["bbox"];
2969 [&b["l"], &b["t"], &b["r"], &b["b"]].map(|x| x.as_f64().unwrap())
2970 };
2971 let fig = by_text("Figure 1");
2974 assert_eq!(fig["prov"][0]["page_no"], 1);
2975 assert_eq!(bbox(&fig), [64.0, 248.0, 448.0, 232.0]);
2976 assert_eq!(fig["prov"][0]["charspan"], serde_json::json!([0, 8]));
2977 assert_eq!(fig["parent"]["$ref"], "#/pictures/0");
2978 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
2979 let tab = by_text("Table 1");
2981 assert_eq!(tab["prov"][0]["page_no"], 2);
2982 assert_eq!(bbox(&tab), [64.0, 412.0, 300.0, 400.0]);
2983 assert_eq!(v["tables"][0]["prov"][0]["bbox"]["t"], 392.0);
2984 let cb = by_text("First option");
2986 assert_eq!(cb["label"], "checkbox_unselected");
2987 assert_eq!(cb["prov"][0]["page_no"], 2);
2988 assert_eq!(bbox(&cb), [64.0, 192.0, 200.0, 180.0]);
2989 assert!(texts
2990 .iter()
2991 .all(|t| !t["prov"].as_array().unwrap().is_empty()));
2992
2993 let mut plain = DoclingDocument::new("t");
2996 plain.push(Node::PageInfo {
2997 page_no: 1,
2998 width: 512.0,
2999 height: 512.0,
3000 });
3001 plain.push(Node::Located {
3002 location: [64, 64, 448, 256],
3003 inner: Box::new(Node::Picture {
3004 caption: Some("Figure 1".into()),
3005 caption_href: None,
3006 image: None,
3007 classification: None,
3008 description: None,
3009 caption_parent: CaptionParent::Item,
3010 caption_location: None,
3011 }),
3012 });
3013 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
3014 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
3015 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
3016 }
3017
3018 #[test]
3019 fn page_markers_produce_pages_and_prov() {
3020 let mut doc = DoclingDocument::new("t");
3021 doc.push(Node::PageInfo {
3022 page_no: 1,
3023 width: 512.0,
3024 height: 1024.0,
3025 });
3026 doc.push(Node::Located {
3027 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
3029 text: "hello".into(),
3030 }),
3031 });
3032 doc.push(Node::Table(Table {
3033 rows: vec![vec!["a".into()]],
3034 location: Some([0, 0, 512, 512]),
3035 ..Table::default()
3036 }));
3037 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3038 assert_eq!(v["pages"]["1"]["page_no"], 1);
3039 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
3040 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
3041 let prov = &v["texts"][0]["prov"][0];
3044 assert_eq!(prov["page_no"], 1);
3045 assert_eq!(prov["bbox"]["l"], 128.0);
3046 assert_eq!(prov["bbox"]["t"], 896.0);
3047 assert_eq!(prov["bbox"]["r"], 256.0);
3048 assert_eq!(prov["bbox"]["b"], 768.0);
3049 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
3050 assert_eq!(prov["charspan"][1], 5);
3051 let tprov = &v["tables"][0]["prov"][0];
3053 assert_eq!(tprov["bbox"]["t"], 1024.0);
3054 assert_eq!(tprov["bbox"]["b"], 0.0);
3055 assert_eq!(tprov["charspan"][1], 0);
3056
3057 let mut plain = DoclingDocument::new("t");
3059 plain.push(Node::Located {
3060 location: [1, 2, 3, 4],
3061 inner: Box::new(Node::Paragraph { text: "x".into() }),
3062 });
3063 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
3064 assert_eq!(v["pages"], serde_json::json!({}));
3065 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
3066 }
3067
3068 #[test]
3071 fn page_furniture_becomes_furniture_layer_text() {
3072 let mut doc = DoclingDocument::new("t");
3073 doc.push(Node::PageInfo {
3074 page_no: 1,
3075 width: 512.0,
3076 height: 512.0,
3077 });
3078 doc.push(Node::PageFurniture {
3079 footer: false,
3080 location: [10, 0, 100, 20],
3081 text: "Chapter 1".into(),
3082 });
3083 doc.push(Node::Paragraph {
3084 text: "body".into(),
3085 });
3086 doc.push(Node::PageFurniture {
3087 footer: true,
3088 location: [400, 490, 500, 512],
3089 text: "1.10.2".into(),
3090 });
3091 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3092 let texts = v["texts"].as_array().unwrap();
3093 assert_eq!(texts.len(), 3);
3094 assert_eq!(texts[0]["label"], "page_header");
3095 assert_eq!(texts[0]["content_layer"], "furniture");
3096 assert_eq!(texts[0]["parent"]["$ref"], "#/body");
3097 assert_eq!(texts[0]["prov"][0]["bbox"]["l"], 10.0);
3098 assert_eq!(texts[1]["content_layer"], "body");
3099 assert_eq!(texts[1]["prov"], serde_json::json!([]));
3100 assert_eq!(texts[2]["label"], "page_footer");
3101 assert_eq!(texts[2]["text"], "1.10.2");
3102 assert_eq!(texts[2]["content_layer"], "furniture");
3103 }
3104
3105 #[test]
3106 fn picture_image_in_markdown_modes_and_json() {
3107 let doc = doc_with_image();
3108 assert!(doc.export_to_markdown().contains("<!-- image -->"));
3110 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
3112 assert!(
3113 md.contains(""),
3114 "got:\n{md}"
3115 );
3116 assert!(files.is_empty());
3117 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
3119 assert!(
3120 md.contains(""),
3121 "got:\n{md}"
3122 );
3123 assert_eq!(
3124 files,
3125 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
3126 );
3127 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3130 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
3131 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
3132 let keys: Vec<&str> = v["pictures"][0]
3133 .as_object()
3134 .unwrap()
3135 .keys()
3136 .map(String::as_str)
3137 .collect();
3138 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
3139 assert_eq!(
3140 v["pictures"][0]["image"]["uri"],
3141 "data:image/png;base64,Zm9vYmFy"
3142 );
3143 }
3144
3145 #[test]
3146 fn exports_docling_schema() {
3147 let mut doc = DoclingDocument::new("t");
3148 doc.push(Node::Heading {
3149 level: 1,
3150 text: "Title".into(),
3151 });
3152 doc.push(Node::Heading {
3153 level: 2,
3154 text: "Sec".into(),
3155 });
3156 doc.push(Node::Paragraph {
3157 text: "Body & more".into(),
3158 }); doc.push(Node::ListItem {
3160 ordered: false,
3161 number: 0,
3162 first_in_list: true,
3163 text: "one".into(),
3164 level: 0,
3165 marker: None,
3166 location: None,
3167 dclx: None,
3168 href: None,
3169 layer: None,
3170 });
3171 doc.push(Node::ListItem {
3172 ordered: false,
3173 number: 0,
3174 first_in_list: false,
3175 text: "two".into(),
3176 level: 0,
3177 marker: None,
3178 location: None,
3179 dclx: None,
3180 href: None,
3181 layer: None,
3182 });
3183 doc.push(Node::Table(Table {
3184 rows: vec![vec!["A".into(), "B".into()]],
3185 location: None,
3186 structure: None,
3187 cell_blocks: None,
3188 cells: None,
3189 caption: None,
3190 caption_parent: Default::default(),
3191 caption_location: None,
3192 }));
3193
3194 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3195 assert_eq!(v["schema_name"], "DoclingDocument");
3196 assert_eq!(v["version"], "1.10.0");
3197 assert_eq!(v["texts"][0]["label"], "title");
3198 assert_eq!(v["texts"][1]["label"], "section_header");
3199 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
3203 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
3204 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
3205 assert_eq!(v["texts"][3]["marker"], "-");
3206 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
3208 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
3209 }
3210 #[test]
3214 fn a_layered_group_stamps_its_whole_subtree() {
3215 let doc = DoclingDocument {
3216 name: "s".into(),
3217 nodes: vec![
3218 Node::Group {
3219 label: "sheet".into(),
3220 name: Some("Sheet1".into()),
3221 layer: None,
3222 children: vec![Node::Paragraph {
3223 text: "visible".into(),
3224 }],
3225 },
3226 Node::Group {
3227 label: "sheet".into(),
3228 name: Some("Sheet2".into()),
3229 layer: Some(ContentLayer::Invisible),
3230 children: vec![Node::Paragraph {
3231 text: "hidden".into(),
3232 }],
3233 },
3234 ],
3235 ..DoclingDocument::new("s")
3236 };
3237 let v = crate::json::to_json(&doc);
3238 assert_eq!(v["groups"][0]["label"], "sheet");
3239 assert_eq!(v["groups"][0]["name"], "Sheet1");
3240 assert_eq!(v["groups"][0]["content_layer"], "body");
3241 assert_eq!(v["texts"][0]["content_layer"], "body");
3242 assert_eq!(v["groups"][1]["name"], "Sheet2");
3243 assert_eq!(v["groups"][1]["content_layer"], "invisible");
3244 assert_eq!(v["texts"][1]["content_layer"], "invisible");
3245 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
3247 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
3248 }
3249
3250 #[test]
3260 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
3261 use crate::tree::{ItemTree, TreeKind};
3262 let mut t = ItemTree::default();
3263 let text = |txt: &str| TreeKind::Text {
3264 label: "text".into(),
3265 text: txt.into(),
3266 orig: None,
3267 formatting: None,
3268 hyperlink: None,
3269 level: None,
3270 list: None,
3271 };
3272 let a = t.add(None, None, text("a"));
3273 let blank = t.add(None, None, text(""));
3274 let b = t.add(None, None, text("b"));
3275 t.delete(blank);
3276 let group = t.add(
3277 None,
3278 Some(ContentLayer::Notes),
3279 TreeKind::Group {
3280 label: "comment_section".into(),
3281 name: "comment-0".into(),
3282 },
3283 );
3284 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
3285 t.items[a].comments.push(group);
3286 t.add(
3287 None,
3288 None,
3289 TreeKind::Picture {
3290 captions: Vec::new(),
3291 image: None,
3292 extra_classes: Vec::new(),
3293 classification: Some("bar_chart".into()),
3294 description: None,
3295 confidence: None,
3296 chart: Some(Table {
3297 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
3298 ..Table::default()
3299 }),
3300 dpi: None,
3301 },
3302 );
3303 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
3304 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
3305 let mut doc = DoclingDocument::new("t");
3306 doc.tree = Some(t);
3307 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3308 let texts = v["texts"].as_array().unwrap();
3309 assert_eq!(texts.len(), 3);
3310 assert_eq!(texts[1]["text"], "b");
3311 assert_eq!(texts[1]["self_ref"], "#/texts/1");
3312 assert_eq!(
3313 v["body"]["children"],
3314 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
3315 );
3316 let keys: Vec<&str> = texts[0]
3317 .as_object()
3318 .unwrap()
3319 .keys()
3320 .map(String::as_str)
3321 .collect();
3322 assert_eq!(
3323 keys,
3324 vec![
3325 "self_ref",
3326 "parent",
3327 "children",
3328 "content_layer",
3329 "label",
3330 "prov",
3331 "comments",
3332 "orig",
3333 "text"
3334 ]
3335 );
3336 assert_eq!(
3337 texts[0]["comments"],
3338 serde_json::json!([{"$ref": "#/groups/0"}])
3339 );
3340 assert!(texts[1].get("comments").is_none());
3341 let meta = &v["pictures"][0]["meta"];
3342 assert_eq!(
3343 meta["classification"]["predictions"][0]["class_name"],
3344 "bar_chart"
3345 );
3346 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
3347 }
3348
3349 #[test]
3355 fn tree_items_carry_exact_provenance_and_dpi() {
3356 use crate::tree::{ItemTree, TreeKind, TreeProv};
3357 let text = |label: &str, t: &str| TreeKind::Text {
3358 label: label.into(),
3359 text: t.into(),
3360 orig: None,
3361 formatting: None,
3362 hyperlink: None,
3363 level: None,
3364 list: None,
3365 };
3366 let mut t = ItemTree::default();
3367 let slide = t.add(
3368 None,
3369 None,
3370 TreeKind::Group {
3371 label: "chapter".into(),
3372 name: "slide-0".into(),
3373 },
3374 );
3375 t.add_with_prov(
3376 Some(slide),
3377 None,
3378 text("paragraph", "héllo"),
3379 TreeProv {
3380 page_no: 1,
3381 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
3382 bottom_left: true,
3383 charspan: [0, 5],
3384 },
3385 );
3386 t.add_with_prov(
3387 Some(slide),
3388 None,
3389 TreeKind::Picture {
3390 captions: Vec::new(),
3391 image: Some(crate::PictureImage {
3392 dpi: crate::PictureImage::DEFAULT_DPI,
3393 mimetype: "image/png".into(),
3394 width: 2,
3395 height: 2,
3396 data: vec![0],
3397 }),
3398 extra_classes: Vec::new(),
3399 classification: None,
3400 description: None,
3401 confidence: None,
3402 chart: None,
3403 dpi: Some(300),
3404 },
3405 TreeProv {
3406 page_no: 1,
3407 bbox: [0.0; 4],
3408 bottom_left: false,
3409 charspan: [0, 0],
3410 },
3411 );
3412 t.add(
3413 Some(slide),
3414 Some(ContentLayer::Notes),
3415 text("text", "no geometry"),
3416 );
3417 let mut doc = DoclingDocument::new("t");
3418 doc.push(Node::PageInfo {
3419 page_no: 1,
3420 width: 9144000.0,
3421 height: 6858000.0,
3422 });
3423 doc.tree = Some(t);
3424 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3425 assert_eq!(
3426 v["texts"][0]["prov"],
3427 serde_json::json!([{
3428 "page_no": 1,
3429 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
3430 "charspan": [0, 5],
3431 }])
3432 );
3433 assert_eq!(v["texts"][0]["label"], "paragraph");
3434 assert_eq!(
3435 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
3436 "TOPLEFT"
3437 );
3438 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
3439 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
3440 assert_eq!(v["texts"][1]["content_layer"], "notes");
3441 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
3442 assert_eq!(v["pages"]["1"]["page_no"], 1);
3443 }
3444
3445 #[test]
3450 fn provenance_boxes_are_clamped_to_their_page() {
3451 let mut doc = DoclingDocument::new("t");
3452 doc.push(Node::PageInfo {
3453 page_no: 1,
3454 width: 10.0,
3455 height: 8.0,
3456 });
3457 doc.push(Node::Prov {
3458 page_no: 1,
3459 bbox: [-1.0, 2.0, 12.0, 9.5],
3460 charspan: [0, 1],
3461 seq: None,
3462 inner: Box::new(Node::Paragraph { text: "x".into() }),
3463 });
3464 let mut table = Table {
3465 rows: vec![vec!["a".into()]],
3466 ..Table::default()
3467 };
3468 table.cells = Some(vec![crate::TableCell {
3469 text: "a".into(),
3470 bbox: Some([1.0, 1.0, 11.0, 9.0]),
3471 start_row: 0,
3472 start_col: 0,
3473 row_span: 1,
3474 col_span: 1,
3475 column_header: false,
3476 row_header: false,
3477 row_section: false,
3478 }]);
3479 doc.push(Node::Prov {
3480 page_no: 1,
3481 bbox: [0.0, 0.0, 10.0, 8.0],
3482 charspan: [0, 0],
3483 seq: None,
3484 inner: Box::new(Node::Table(table)),
3485 });
3486 doc.push(Node::Prov {
3487 page_no: 7,
3488 bbox: [-5.0, 0.0, 50.0, 50.0],
3489 charspan: [0, 1],
3490 seq: None,
3491 inner: Box::new(Node::Paragraph { text: "y".into() }),
3492 });
3493 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3494 assert_eq!(
3495 v["texts"][0]["prov"][0]["bbox"],
3496 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
3497 );
3498 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
3499 assert_eq!(
3500 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
3501 (Some(1.0), Some(10.0), Some(8.0))
3502 );
3503 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
3504 assert_eq!(
3505 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
3506 "page 7 is not described"
3507 );
3508 }
3509
3510 #[test]
3517 fn misplaced_list_items_are_wrapped_like_docling_core() {
3518 use crate::tree::{ItemTree, ListMeta, TreeKind};
3519 let mut t = ItemTree::default();
3520 let li = |txt: &str| TreeKind::Text {
3521 label: "list_item".into(),
3522 text: txt.into(),
3523 orig: None,
3524 formatting: None,
3525 hyperlink: None,
3526 level: None,
3527 list: Some(ListMeta {
3528 enumerated: false,
3529 marker: "-".into(),
3530 }),
3531 };
3532 let group = |label: &str, name: &str| TreeKind::Group {
3533 label: label.into(),
3534 name: name.into(),
3535 };
3536 t.add(None, None, li("A"));
3537 t.add(None, None, li("B"));
3538 t.add(
3539 None,
3540 None,
3541 TreeKind::Text {
3542 label: "text".into(),
3543 text: "plain".into(),
3544 orig: None,
3545 formatting: None,
3546 hyperlink: None,
3547 level: None,
3548 list: None,
3549 },
3550 );
3551 let cell = t.add(None, None, group("unspecified", "cell"));
3552 t.add(Some(cell), None, li("C"));
3553 t.add(Some(cell), None, li("D"));
3554 let list = t.add(None, None, group("list", "list"));
3555 t.add(Some(list), None, li("E"));
3556 let doc = DoclingDocument {
3557 tree: Some(t),
3558 ..DoclingDocument::new("t")
3559 };
3560 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3561 let refs = |x: &Value| -> Vec<String> {
3562 x.as_array()
3563 .unwrap()
3564 .iter()
3565 .map(|r| r["$ref"].as_str().unwrap().to_string())
3566 .collect()
3567 };
3568 assert_eq!(
3569 refs(&v["body"]["children"]),
3570 ["#/groups/4", "#/texts/0", "#/groups/0", "#/groups/1"]
3571 );
3572 assert_eq!(
3573 refs(&v["groups"][0]["children"]),
3574 ["#/groups/3", "#/groups/2"]
3575 );
3576 for (g, name, kids) in [
3577 (2, "group", vec!["#/texts/2"]),
3578 (3, "group", vec!["#/texts/3"]),
3579 (4, "group", vec!["#/texts/4", "#/texts/5"]),
3580 (1, "list", vec!["#/texts/1"]),
3581 ] {
3582 assert_eq!(v["groups"][g]["label"], "list");
3583 assert_eq!(v["groups"][g]["name"], name);
3584 assert_eq!(refs(&v["groups"][g]["children"]), kids);
3585 }
3586 let texts: Vec<&str> = v["texts"]
3587 .as_array()
3588 .unwrap()
3589 .iter()
3590 .map(|t| t["text"].as_str().unwrap())
3591 .collect();
3592 assert_eq!(texts, ["plain", "E", "D", "C", "A", "B"]);
3593 assert_eq!(v["texts"][2]["parent"]["$ref"], "#/groups/2");
3594 }
3595
3596 #[test]
3599 fn a_deleted_child_is_not_written_as_an_empty_ref() {
3600 use crate::tree::{ItemTree, TreeKind};
3601 let mut t = ItemTree::default();
3602 let text = |txt: &str| TreeKind::Text {
3603 label: "text".into(),
3604 text: txt.into(),
3605 orig: None,
3606 formatting: None,
3607 hyperlink: None,
3608 level: None,
3609 list: None,
3610 };
3611 let g = t.add(
3612 None,
3613 None,
3614 TreeKind::Group {
3615 label: "unspecified".into(),
3616 name: "g".into(),
3617 },
3618 );
3619 let gone = t.add(Some(g), None, text(""));
3620 t.add(Some(g), None, text("kept"));
3621 t.items[gone].deleted = true; let doc = DoclingDocument {
3623 tree: Some(t),
3624 ..DoclingDocument::new("t")
3625 };
3626 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3627 assert_eq!(
3628 v["groups"][0]["children"],
3629 serde_json::json!([{"$ref": "#/texts/0"}])
3630 );
3631 }
3632
3633 #[test]
3634 fn a_backend_item_tree_is_written_verbatim() {
3635 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
3636 let mut t = ItemTree::default();
3637 let text = |label: &str, txt: &str| TreeKind::Text {
3638 label: label.into(),
3639 text: txt.into(),
3640 orig: None,
3641 formatting: None,
3642 hyperlink: None,
3643 level: None,
3644 list: None,
3645 };
3646 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
3647 let h = t.add(None, None, text("title", "Heading"));
3648 let group = t.add(
3649 Some(h),
3650 None,
3651 TreeKind::Group {
3652 label: "inline".into(),
3653 name: "group".into(),
3654 },
3655 );
3656 t.add(
3657 Some(group),
3658 None,
3659 TreeKind::Text {
3660 label: "text".into(),
3661 text: "bold".into(),
3662 orig: None,
3663 formatting: Some(Formatting {
3664 bold: true,
3665 ..Formatting::default()
3666 }),
3667 hyperlink: Some("https://example.com/".into()),
3668 level: None,
3669 list: None,
3670 },
3671 );
3672 t.add(
3673 Some(group),
3674 None,
3675 TreeKind::Code {
3676 text: "x = 1".into(),
3677 orig: None,
3678 language: Some("python".into()),
3679 formatting: None,
3680 hyperlink: None,
3681 },
3682 );
3683 let sub = t.add(
3684 Some(h),
3685 None,
3686 TreeKind::Text {
3687 label: "section_header".into(),
3688 text: "Sub".into(),
3689 orig: Some("Sub\u{2019}".into()),
3690 formatting: None,
3691 hyperlink: None,
3692 level: Some(1),
3693 list: None,
3694 },
3695 );
3696 let item = t.add(
3697 Some(sub),
3698 None,
3699 TreeKind::Text {
3700 label: "list_item".into(),
3701 text: "item".into(),
3702 orig: None,
3703 formatting: None,
3704 hyperlink: None,
3705 level: None,
3706 list: Some(ListMeta {
3707 enumerated: true,
3708 marker: "3.".into(),
3709 }),
3710 },
3711 );
3712 let _region = t.add(
3713 Some(sub),
3714 None,
3715 TreeKind::FieldRegion {
3716 items: vec![crate::FieldItem {
3717 marker: None,
3718 key: Some("Name".into()),
3719 value: Some("Duck".into()),
3720 value_kind: Some("read_only".into()),
3721 }],
3722 },
3723 );
3724 let table = t.add(
3725 Some(sub),
3726 None,
3727 TreeKind::Table {
3728 table: Table {
3729 rows: vec![vec!["a \n<".into(), "b".into()]],
3730 cells: Some(vec![
3731 crate::TableCell {
3732 text: "a \n<".into(),
3733 bbox: None,
3734 start_row: 0,
3735 start_col: 0,
3736 row_span: 3,
3737 col_span: 1,
3738 column_header: false,
3739 row_header: true,
3740 row_section: false,
3741 },
3742 crate::TableCell {
3743 text: "b".into(),
3744 bbox: None,
3745 start_row: 0,
3746 start_col: 1,
3747 row_span: 1,
3748 col_span: 1,
3749 column_header: false,
3750 row_header: false,
3751 row_section: false,
3752 },
3753 ]),
3754 ..Table::default()
3755 },
3756 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
3758 },
3759 );
3760 let cell_group = t.add(
3761 Some(table),
3762 None,
3763 TreeKind::Group {
3764 label: "unspecified".into(),
3765 name: "rich_cell_group_1_0_0".into(),
3766 },
3767 );
3768 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
3769 *rich_cells = vec![(0, 1, cell_group)];
3770 }
3771 let after = t.add(Some(sub), None, text("text", "after the region"));
3772 let _ = (title, after);
3773 let list = t.add(
3777 Some(sub),
3778 None,
3779 TreeKind::Group {
3780 label: "list".into(),
3781 name: "list".into(),
3782 },
3783 );
3784 t.reparent(item, Some(list));
3785
3786 let doc = DoclingDocument {
3787 tree: Some(t),
3788 ..DoclingDocument::new("t")
3789 };
3790 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3791 let texts: Vec<&str> = v["texts"]
3793 .as_array()
3794 .unwrap()
3795 .iter()
3796 .map(|t| t["text"].as_str().unwrap())
3797 .collect();
3798 assert_eq!(
3799 texts,
3800 [
3801 "Page",
3802 "Heading",
3803 "bold",
3804 "x = 1",
3805 "Sub",
3806 "item",
3807 "Name",
3808 "Duck",
3809 "after the region"
3810 ]
3811 );
3812 assert_eq!(
3813 v["body"]["children"],
3814 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
3815 );
3816 assert_eq!(v["texts"][0]["content_layer"], "furniture");
3817 assert_eq!(
3818 v["texts"][1]["children"],
3819 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
3820 );
3821 let bold = &v["texts"][2];
3822 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
3823 let keys: Vec<&str> = bold
3824 .as_object()
3825 .unwrap()
3826 .keys()
3827 .map(String::as_str)
3828 .collect();
3829 assert_eq!(
3830 keys,
3831 [
3832 "self_ref",
3833 "parent",
3834 "children",
3835 "content_layer",
3836 "label",
3837 "prov",
3838 "orig",
3839 "text",
3840 "formatting",
3841 "hyperlink"
3842 ]
3843 );
3844 assert_eq!(
3845 bold["formatting"],
3846 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
3847 );
3848 let code = &v["texts"][3];
3849 assert_eq!(code["label"], "code");
3850 assert_eq!(code["code_language"], "Python");
3851 let sub = &v["texts"][4];
3852 assert_eq!(sub["orig"], "Sub\u{2019}");
3853 assert_eq!(sub["level"], 1);
3854 let item = &v["texts"][5];
3855 let keys: Vec<&str> = item
3856 .as_object()
3857 .unwrap()
3858 .keys()
3859 .map(String::as_str)
3860 .collect();
3861 assert_eq!(
3862 keys,
3863 [
3864 "self_ref",
3865 "parent",
3866 "children",
3867 "content_layer",
3868 "label",
3869 "prov",
3870 "orig",
3871 "text",
3872 "enumerated",
3873 "marker"
3874 ]
3875 );
3876 assert_eq!(item["marker"], "3.");
3877 assert_eq!(v["texts"][7]["kind"], "read_only");
3878 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
3879 let table = &v["tables"][0];
3880 assert_eq!(
3881 table["children"],
3882 serde_json::json!([{"$ref": "#/groups/1"}])
3883 );
3884 let cells = table["data"]["table_cells"].as_array().unwrap();
3885 assert_eq!(
3886 cells[0]["text"], "a \n<",
3887 "raw cell text is written verbatim"
3888 );
3889 assert_eq!(
3890 cells[0]["end_row_offset_idx"], 3,
3891 "declared spans are not clamped"
3892 );
3893 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
3894 assert!(cells[0].get("ref").is_none());
3895 assert!(
3896 table["data"]["grid"][0][1].get("ref").is_none(),
3897 "the grid shows plain cells"
3898 );
3899 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
3900 }
3901
3902 #[test]
3906 fn a_comment_section_can_be_referenced_by_its_note_text() {
3907 let doc = DoclingDocument {
3908 name: "c".into(),
3909 nodes: vec![
3910 Node::Commented {
3911 comments: vec![0],
3912 inner: Box::new(Node::Paragraph {
3913 text: "annotated".into(),
3914 }),
3915 },
3916 Node::CommentSection {
3917 name: "comment-Sheet1-A1".into(),
3918 text: "[author: A]: note".into(),
3919 refs_note_text: true,
3920 grouped: true,
3921 },
3922 ],
3923 ..DoclingDocument::new("c")
3924 };
3925 let v = crate::json::to_json(&doc);
3926 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
3927 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
3928 }
3929
3930 #[test]
3934 fn comment_sections_link_back_to_their_items() {
3935 let doc = DoclingDocument {
3936 name: "c".into(),
3937 nodes: vec![
3938 Node::Commented {
3939 comments: vec![0],
3940 inner: Box::new(Node::Paragraph {
3941 text: "annotated".into(),
3942 }),
3943 },
3944 Node::Paragraph {
3945 text: "plain".into(),
3946 },
3947 Node::CommentSection {
3948 name: "comment-7".into(),
3949 text: "[time: t]: note".into(),
3950 refs_note_text: false,
3951 grouped: true,
3952 },
3953 ],
3954 ..DoclingDocument::new("c")
3955 };
3956 let v = crate::json::to_json(&doc);
3957 assert_eq!(v["groups"][0]["label"], "comment_section");
3959 assert_eq!(v["groups"][0]["name"], "comment-7");
3960 assert_eq!(v["groups"][0]["content_layer"], "notes");
3961 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
3962 assert_eq!(v["texts"][2]["content_layer"], "notes");
3963 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
3965 assert!(v["texts"][1].get("comments").is_none());
3966 let keys: Vec<&str> = v["texts"][0]
3968 .as_object()
3969 .unwrap()
3970 .keys()
3971 .map(String::as_str)
3972 .collect();
3973 assert_eq!(
3974 &keys[keys.len() - 4..],
3975 &["prov", "comments", "orig", "text"]
3976 );
3977 }
3978
3979 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
3980 Node::Picture {
3981 caption: Some(caption.into()),
3982 caption_href: None,
3983 image: None,
3984 classification: None,
3985 description: None,
3986 caption_parent,
3987 caption_location: None,
3988 }
3989 }
3990
3991 fn group(children: Vec<Node>) -> Node {
3992 Node::Group {
3993 label: "section".into(),
3994 name: None,
3995 layer: None,
3996 children,
3997 }
3998 }
3999
4000 fn refs(v: &Value) -> Vec<&str> {
4001 v.as_array()
4002 .unwrap()
4003 .iter()
4004 .map(|r| r["$ref"].as_str().unwrap())
4005 .collect()
4006 }
4007
4008 #[test]
4013 fn a_body_caption_follows_the_enclosing_top_level_item() {
4014 let mut doc = DoclingDocument::new("t");
4015 doc.push(picture("top", CaptionParent::Body));
4016 doc.push(group(vec![
4017 Node::Paragraph { text: "p".into() },
4018 picture("nested", CaptionParent::Body),
4019 ]));
4020 doc.push(Node::Paragraph {
4021 text: "after".into(),
4022 });
4023 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4024 assert_eq!(
4025 refs(&v["body"]["children"]),
4026 [
4027 "#/texts/0",
4028 "#/pictures/0",
4029 "#/groups/0",
4030 "#/texts/2",
4031 "#/texts/3"
4032 ]
4033 );
4034 assert_eq!(
4035 refs(&v["groups"][0]["children"]),
4036 ["#/texts/1", "#/pictures/1"]
4037 );
4038 for (cap, pic) in [(0, 0), (2, 1)] {
4039 assert_eq!(v["texts"][cap]["label"], "caption");
4040 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
4041 assert_eq!(
4042 refs(&v["pictures"][pic]["captions"]),
4043 [format!("#/texts/{cap}")]
4044 );
4045 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
4046 }
4047 }
4048
4049 #[test]
4052 fn an_item_caption_is_the_items_first_child() {
4053 let mut doc = DoclingDocument::new("t");
4054 doc.push(picture("fig", CaptionParent::Item));
4055 doc.push(Node::Table(Table {
4056 rows: vec![vec!["a".into()]],
4057 caption: Some("tab".into()),
4058 caption_parent: CaptionParent::Item,
4059 ..Table::default()
4060 }));
4061 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4062 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
4063 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
4064 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
4065 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
4066 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
4067 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
4068 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
4069 }
4070
4071 #[test]
4077 fn picture_children_hang_off_the_picture_after_its_caption() {
4078 let mut doc = DoclingDocument::new("t");
4079 doc.push(picture("fig", CaptionParent::Item));
4080 doc.push(Node::PictureChildren(vec![
4081 Node::Heading {
4082 level: 2,
4083 text: "in-figure title".into(),
4084 },
4085 Node::Paragraph {
4086 text: "axis label".into(),
4087 },
4088 Node::ListItem {
4089 ordered: false,
4090 number: 0,
4091 first_in_list: true,
4092 text: "callout".into(),
4093 level: 0,
4094 marker: None,
4095 location: None,
4096 dclx: None,
4097 href: None,
4098 layer: None,
4099 },
4100 ]));
4101 doc.push(Node::Paragraph {
4102 text: "after".into(),
4103 });
4104 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4105 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/texts/4"]);
4106 assert_eq!(
4107 refs(&v["pictures"][0]["children"]),
4108 ["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"]
4109 );
4110 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
4111 assert_eq!(v["texts"][1]["label"], "section_header");
4112 assert_eq!(v["texts"][2]["label"], "text");
4113 for t in 1..=2 {
4114 assert_eq!(v["texts"][t]["parent"]["$ref"], "#/pictures/0");
4115 assert_eq!(v["texts"][t]["content_layer"], "body");
4116 }
4117 assert_eq!(v["groups"][0]["parent"]["$ref"], "#/pictures/0");
4118 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
4119 let md = doc.export_to_markdown();
4120 assert!(
4121 !md.contains("axis label") && !md.contains("callout"),
4122 "{md}"
4123 );
4124 assert!(md.contains("after"), "{md}");
4125 }
4126
4127 #[test]
4131 fn a_container_caption_is_the_items_sibling() {
4132 let mut doc = DoclingDocument::new("t");
4133 doc.push(group(vec![
4134 picture("chart", CaptionParent::Container),
4135 Node::Table(Table {
4136 rows: vec![vec!["a".into()]],
4137 caption: Some("figcaption".into()),
4138 caption_parent: CaptionParent::ContainerAfter,
4139 ..Table::default()
4140 }),
4141 ]));
4142 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4143 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
4144 assert_eq!(
4145 refs(&v["groups"][0]["children"]),
4146 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
4147 );
4148 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
4149 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
4150 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
4151 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
4152 }
4153
4154 #[test]
4159 fn a_labeled_text_keeps_its_label_and_hyperlink_out_of_the_text() {
4160 let mut doc = DoclingDocument::new("t");
4161 doc.push(Node::LabeledText {
4162 label: "footnote".into(),
4163 text: "1 https://example.com/a\\_b".into(),
4164 href: Some("https://example.com/a_b".into()),
4165 });
4166 doc.push(Node::LabeledText {
4167 label: "footnote".into(),
4168 text: "© 2022 the authors".into(),
4169 href: None,
4170 });
4171 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4172 let t = &v["texts"][0];
4173 assert_eq!(t["label"], "footnote");
4174 assert_eq!(t["text"], "1 https://example.com/a_b");
4175 assert_eq!(t["orig"], "1 https://example.com/a_b");
4176 assert_eq!(t["hyperlink"], "https://example.com/a_b");
4177 assert_eq!(v["texts"][1]["label"], "footnote");
4178 assert!(v["texts"][1].get("hyperlink").is_none());
4179 assert_eq!(
4180 doc.export_to_markdown(),
4181 "[1 https://example.com/a\\_b](https://example.com/a_b)\n\n© 2022 the authors\n"
4182 );
4183 assert_eq!(
4184 doc.export_to_text(),
4185 "1 https://example.com/a_b\n\n© 2022 the authors"
4186 );
4187 }
4188}