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