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 origin: json!({
340 "mimetype": "text/plain",
341 "binary_hash": fnv1a(&doc.name),
342 "filename": doc.name,
343 }),
344 furniture: json!({
345 "self_ref": "#/furniture",
346 "children": [],
347 "content_layer": "furniture",
348 "name": "_root_",
349 "label": "unspecified",
350 }),
351 body,
352 groups: b.groups,
353 texts: b.texts,
354 pictures: b.pictures,
355 tables: b.tables,
356 key_value_items: b.key_value_items,
357 fields,
358 pages,
359 }
360}
361
362fn build_json(doc: &DoclingDocument, note_keys: bool) -> Value {
363 build_parts(doc, note_keys).into_value()
364}
365
366struct Parts {
371 name: String,
372 origin: Value,
373 furniture: Value,
374 body: Vec<Child>,
376 groups: Vec<Value>,
377 texts: Vec<Item>,
378 pictures: Vec<Value>,
379 tables: Vec<Value>,
380 key_value_items: Vec<Value>,
381 fields: Option<(Vec<Value>, Vec<Value>)>,
384 pages: serde_json::Map<String, Value>,
385}
386
387impl Parts {
388 fn into_value(self) -> Value {
389 let mut out = serde_json::Map::with_capacity(15);
391 out.insert("schema_name".into(), "DoclingDocument".into());
392 out.insert("version".into(), SCHEMA_VERSION.into());
393 out.insert("name".into(), Value::String(self.name));
394 out.insert("origin".into(), self.origin);
395 out.insert("furniture".into(), self.furniture);
396 out.insert(
397 "body".into(),
398 object([
399 ("self_ref", "#/body".into()),
400 (
401 "children",
402 Value::Array(self.body.into_iter().map(Child::into_value).collect()),
403 ),
404 ("content_layer", "body".into()),
405 ("name", "_root_".into()),
406 ("label", "unspecified".into()),
407 ]),
408 );
409 out.insert("groups".into(), Value::Array(self.groups));
410 out.insert(
411 "texts".into(),
412 Value::Array(
413 self.texts
414 .into_iter()
415 .enumerate()
416 .map(|(i, item)| item.into_value(i))
417 .collect(),
418 ),
419 );
420 out.insert("pictures".into(), Value::Array(self.pictures));
421 out.insert("tables".into(), Value::Array(self.tables));
422 out.insert("key_value_items".into(), Value::Array(self.key_value_items));
423 out.insert("form_items".into(), Value::Array(Vec::new()));
424 if let Some((regions, items)) = self.fields {
425 out.insert("field_regions".into(), Value::Array(regions));
426 out.insert("field_items".into(), Value::Array(items));
427 }
428 out.insert("pages".into(), Value::Object(self.pages));
429 Value::Object(out)
430 }
431}
432
433impl serde::Serialize for Parts {
434 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
435 use serde::ser::SerializeMap;
436 let mut m = s.serialize_map(Some(13 + 2 * usize::from(self.fields.is_some())))?;
437 m.serialize_entry("schema_name", "DoclingDocument")?;
438 m.serialize_entry("version", SCHEMA_VERSION)?;
439 m.serialize_entry("name", &self.name)?;
440 m.serialize_entry("origin", &self.origin)?;
441 m.serialize_entry("furniture", &self.furniture)?;
442 m.serialize_entry("body", &Body(&self.body))?;
443 m.serialize_entry("groups", &self.groups)?;
444 m.serialize_entry("texts", &Texts(&self.texts))?;
445 m.serialize_entry("pictures", &self.pictures)?;
446 m.serialize_entry("tables", &self.tables)?;
447 m.serialize_entry("key_value_items", &self.key_value_items)?;
448 m.serialize_entry("form_items", &[(); 0])?;
449 if let Some((regions, items)) = &self.fields {
450 m.serialize_entry("field_regions", regions)?;
451 m.serialize_entry("field_items", items)?;
452 }
453 m.serialize_entry("pages", &self.pages)?;
454 m.end()
455 }
456}
457
458struct Body<'a>(&'a [Child]);
460
461impl serde::Serialize for Body<'_> {
462 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
463 use serde::ser::SerializeMap;
464 let mut m = s.serialize_map(Some(5))?;
465 m.serialize_entry("self_ref", "#/body")?;
466 m.serialize_entry("children", self.0)?;
467 m.serialize_entry("content_layer", "body")?;
468 m.serialize_entry("name", "_root_")?;
469 m.serialize_entry("label", "unspecified")?;
470 m.end()
471 }
472}
473
474pub fn write_json<W: std::io::Write>(
481 doc: &DoclingDocument,
482 w: W,
483 pretty: bool,
484) -> serde_json::Result<()> {
485 use serde::Serialize;
486 let parts = build_parts(doc, false);
487 if pretty {
488 parts.serialize(&mut serde_json::Serializer::pretty(w))
489 } else {
490 parts.serialize(&mut serde_json::Serializer::new(w))
491 }
492}
493
494fn classification_meta(classes: &[crate::PictureClass]) -> Value {
501 json!({
502 "classification": {
503 "predictions": classes.iter().map(|c| json!({
504 "confidence": c.confidence as f64,
505 "created_by": "DocumentPictureClassifier",
506 "class_name": c.class_name,
507 })).collect::<Vec<_>>(),
508 },
509 "annotations": [{
510 "kind": "classification",
511 "provenance": "DocumentPictureClassifier",
512 "predicted_classes": classes.iter().map(|c| json!({
513 "class_name": c.class_name,
514 "confidence": c.confidence as f64,
515 })).collect::<Vec<_>>(),
516 }],
517 })
518}
519
520fn description_meta(desc: &crate::PictureDescription) -> Value {
528 json!({
529 "description": {
530 "created_by": desc.provenance,
531 "text": desc.text,
532 },
533 "annotations": [{
534 "kind": "description",
535 "text": desc.text,
536 "provenance": desc.provenance,
537 }],
538 })
539}
540
541fn picture_meta(
547 classes: Option<&[crate::PictureClass]>,
548 desc: Option<&crate::PictureDescription>,
549) -> Option<Value> {
550 let mut meta = Map::new();
551 let mut annotations = Vec::new();
552 if let Some(desc) = desc {
553 let mut m = description_meta(desc);
554 annotations.push(m["annotations"][0].take());
555 meta.insert("description".into(), m["description"].take());
556 }
557 if let Some(classes) = classes {
558 let mut m = classification_meta(classes);
559 annotations.insert(0, m["annotations"][0].take());
560 meta.insert("classification".into(), m["classification"].take());
561 }
562 if meta.is_empty() {
563 return None;
564 }
565 meta.insert("annotations".into(), Value::Array(annotations));
566 Some(Value::Object(meta))
567}
568
569#[allow(clippy::too_many_arguments)]
580fn cell_value(
581 row_span: usize,
582 col_span: usize,
583 start_row: usize,
584 end_row: usize,
585 start_col: usize,
586 end_col: usize,
587 text: String,
588 column_header: bool,
589 row_header: bool,
590 row_section: bool,
591 bbox: Option<[f32; 4]>,
592) -> Value {
593 let mut m = serde_json::Map::with_capacity(12);
594 m.insert("row_span".into(), row_span.into());
595 m.insert("col_span".into(), col_span.into());
596 m.insert("start_row_offset_idx".into(), start_row.into());
597 m.insert("end_row_offset_idx".into(), end_row.into());
598 m.insert("start_col_offset_idx".into(), start_col.into());
599 m.insert("end_col_offset_idx".into(), end_col.into());
600 m.insert("text".into(), Value::String(text));
601 m.insert("column_header".into(), column_header.into());
602 m.insert("row_header".into(), row_header.into());
603 m.insert("row_section".into(), row_section.into());
604 m.insert("fillable".into(), false.into());
605 if let Some(b) = bbox {
606 m.insert(
607 "bbox".into(),
608 json!({
609 "l": b[0], "t": b[1], "r": b[2], "b": b[3],
610 "coord_origin": "TOPLEFT",
611 }),
612 );
613 }
614 Value::Object(m)
615}
616
617fn table_data(t: &Table) -> Value {
620 table_data_with(t, false)
621}
622
623fn table_data_with(t: &Table, raw: bool) -> Value {
626 let cell_text = |s: &str| {
627 if raw {
628 s.to_string()
629 } else {
630 unescape_text(&crate::markdown::strip_hard_breaks(s))
631 }
632 };
633 let mut num_rows = t.rows.len();
634 let mut num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
635 let ragged = t.rows.iter().any(|r| r.len() != num_cols);
643 if let Some(first_class) = t.cells.as_ref().filter(|c| ragged && !c.is_empty()) {
644 for c in first_class {
645 num_rows = num_rows.max(c.start_row + c.row_span);
646 num_cols = num_cols.max(c.start_col + c.col_span);
647 }
648 }
649 let mut grid = Vec::with_capacity(num_rows);
650 let mut cells = Vec::new();
651 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
655 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
656 for c in first_class {
657 let idx = cells.len();
658 cells.push(cell_value(
659 c.row_span,
660 c.col_span,
661 c.start_row,
662 c.start_row + c.row_span,
663 c.start_col,
664 c.start_col + c.col_span,
665 cell_text(&c.text),
666 c.column_header,
667 c.row_header,
668 c.row_section,
669 c.bbox,
670 ));
671 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
672 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
673 slot[r * num_cols + k] = Some(idx);
674 }
675 }
676 }
677 for r in 0..num_rows {
678 let mut grid_row = Vec::with_capacity(num_cols);
679 for c in 0..num_cols {
680 grid_row.push(match slot[r * num_cols + c] {
681 Some(i) => cells[i].clone(),
682 None => cell_value(
683 1,
684 1,
685 r,
686 r + 1,
687 c,
688 c + 1,
689 String::new(),
690 false,
691 false,
692 false,
693 None,
694 ),
695 });
696 }
697 grid.push(grid_row);
698 }
699 } else {
700 let s = t.structure.as_ref();
701 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
702 grid.and_then(|g| g.get(r))
703 .and_then(|row| row.get(c))
704 .copied()
705 .unwrap_or(false)
706 };
707 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
708 let (mut r0, mut c0) = (r, c);
709 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
710 c0 -= 1;
711 }
712 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
713 r0 -= 1;
714 }
715 (r0, c0)
716 };
717 let anchors: Vec<(usize, usize)> = (0..num_rows)
720 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
721 .map(|(r, c)| anchor_of(r, c))
722 .collect();
723 let mut extent: Vec<(usize, usize)> = (0..num_rows)
724 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
725 .collect();
726 for (i, &(ar, ac)) in anchors.iter().enumerate() {
727 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
728 let e = &mut extent[ar * num_cols + ac];
729 e.0 = e.0.max(r);
730 e.1 = e.1.max(c);
731 }
732 for (r, row) in t.rows.iter().enumerate() {
733 let mut grid_row = Vec::with_capacity(num_cols);
734 for c in 0..num_cols {
735 let (ar, ac) = anchors[r * num_cols + c];
736 if (ar, ac) == (r, c) {
737 let (er, ec) = extent[r * num_cols + c];
738 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
739 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
740 Some(s) => flag(Some(&s.col_header), r, c),
741 None => r == 0,
742 };
743 slot[r * num_cols + c] = Some(cells.len());
744 cells.push(cell_value(
745 er - r + 1,
746 ec - c + 1,
747 r,
748 er + 1,
749 c,
750 ec + 1,
751 text,
752 column_header,
753 flag(s.map(|s| &s.row_header), r, c),
754 false,
755 None,
756 ));
757 }
758 grid_row.push(match slot[ar * num_cols + ac] {
759 Some(i) => cells[i].clone(),
760 None => Value::Null,
761 });
762 }
763 grid.push(grid_row);
764 }
765 }
766 json!({
767 "table_cells": cells,
768 "num_rows": num_rows,
769 "num_cols": num_cols,
770 "orientation": "rot_0",
771 "grid": grid,
772 })
773}
774
775#[derive(Clone, Copy)]
779enum ProvSlot {
780 None,
781 One {
782 page_no: usize,
783 bbox: [f64; 4],
785 bottom_left: bool,
786 charspan: [usize; 2],
787 },
788}
789
790impl ProvSlot {
791 fn origin(bottom_left: bool) -> &'static str {
792 if bottom_left {
793 "BOTTOMLEFT"
794 } else {
795 "TOPLEFT"
796 }
797 }
798
799 fn to_value(self) -> Value {
802 let ProvSlot::One {
803 page_no,
804 bbox: [l, t, r, b],
805 bottom_left,
806 charspan,
807 } = self
808 else {
809 return Value::Array(Vec::new());
810 };
811 let bbox = object([
812 ("l", l.into()),
813 ("t", t.into()),
814 ("r", r.into()),
815 ("b", b.into()),
816 ("coord_origin", Self::origin(bottom_left).into()),
817 ]);
818 let span = Value::Array(vec![charspan[0].into(), charspan[1].into()]);
819 Value::Array(vec![object([
820 ("page_no", page_no.into()),
821 ("bbox", bbox),
822 ("charspan", span),
823 ])])
824 }
825
826 fn clamp(&mut self, sizes: &PageSizes) {
828 if let ProvSlot::One { page_no, bbox, .. } = self {
829 if let Some(&(w, h)) = sizes.get(page_no) {
830 for (v, hi) in bbox.iter_mut().zip([w, h, w, h]) {
831 if v.is_finite() {
834 *v = round2(v.clamp(0.0, hi.max(0.0)));
835 }
836 }
837 }
838 }
839 }
840}
841
842impl serde::Serialize for ProvSlot {
843 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
844 use serde::ser::{SerializeMap, SerializeSeq};
845 let ProvSlot::One {
846 page_no,
847 bbox: [l, t, r, b],
848 bottom_left,
849 charspan,
850 } = *self
851 else {
852 return s.serialize_seq(Some(0))?.end();
853 };
854 struct Bbox([f64; 4], &'static str);
855 impl serde::Serialize for Bbox {
856 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
857 let mut m = s.serialize_map(Some(5))?;
858 for (k, v) in ["l", "t", "r", "b"].iter().zip(self.0) {
859 m.serialize_entry(k, &v)?;
860 }
861 m.serialize_entry("coord_origin", self.1)?;
862 m.end()
863 }
864 }
865 struct Entry(usize, Bbox, [usize; 2]);
866 impl serde::Serialize for Entry {
867 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
868 let mut m = s.serialize_map(Some(3))?;
869 m.serialize_entry("page_no", &self.0)?;
870 m.serialize_entry("bbox", &self.1)?;
871 m.serialize_entry("charspan", &self.2)?;
872 m.end()
873 }
874 }
875 let mut seq = s.serialize_seq(Some(1))?;
876 seq.serialize_element(&Entry(
877 page_no,
878 Bbox([l, t, r, b], Self::origin(bottom_left)),
879 charspan,
880 ))?;
881 seq.end()
882 }
883}
884
885struct TextItem {
892 parent: Box<str>,
893 label: Box<str>,
894 prov: ProvSlot,
895 text: Box<str>,
896}
897
898impl TextItem {
899 fn into_value(self, index: usize) -> Value {
900 let text = String::from(self.text);
901 object([
902 ("self_ref", Value::String(format!("#/texts/{index}"))),
903 ("parent", ref_value(self.parent)),
904 ("children", Value::Array(Vec::new())),
905 ("content_layer", "body".into()),
906 ("label", Value::String(self.label.into())),
907 ("prov", self.prov.to_value()),
908 ("orig", Value::String(text.clone())),
909 ("text", Value::String(text)),
910 ])
911 }
912}
913
914enum Item {
918 Text(Box<TextItem>),
919 Json(Box<Value>),
920}
921
922impl Item {
923 fn json(v: Value) -> Self {
924 Item::Json(Box::new(v))
925 }
926
927 fn value_mut(&mut self, index: usize) -> &mut Value {
930 if let Item::Text(t) = self {
931 let t = std::mem::replace(
932 t,
933 Box::new(TextItem {
934 parent: "".into(),
935 label: "".into(),
936 prov: ProvSlot::None,
937 text: "".into(),
938 }),
939 );
940 *self = Item::json(t.into_value(index));
941 }
942 match self {
943 Item::Json(v) => v,
944 Item::Text(_) => unreachable!("converted above"),
945 }
946 }
947
948 fn into_value(self, index: usize) -> Value {
949 match self {
950 Item::Text(t) => t.into_value(index),
951 Item::Json(v) => *v,
952 }
953 }
954}
955
956struct Texts<'a>(&'a [Item]);
958
959impl serde::Serialize for Texts<'_> {
960 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
961 use serde::ser::{SerializeMap, SerializeSeq};
962 struct Text<'a>(usize, &'a TextItem);
963 impl serde::Serialize for Text<'_> {
964 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
965 let Text(i, t) = *self;
966 let mut m = s.serialize_map(Some(8))?;
967 m.serialize_entry("self_ref", &format_args!("#/texts/{i}"))?;
968 m.serialize_entry("parent", &Child::Ref(t.parent.clone()))?;
969 m.serialize_entry("children", &[(); 0])?;
970 m.serialize_entry("content_layer", "body")?;
971 m.serialize_entry("label", &t.label)?;
972 m.serialize_entry("prov", &t.prov)?;
973 m.serialize_entry("orig", &t.text)?;
974 m.serialize_entry("text", &t.text)?;
975 m.end()
976 }
977 }
978 let mut seq = s.serialize_seq(Some(self.0.len()))?;
979 for (i, item) in self.0.iter().enumerate() {
980 match item {
981 Item::Text(t) => seq.serialize_element(&Text(i, t))?,
982 Item::Json(v) => seq.serialize_element(v)?,
983 }
984 }
985 seq.end()
986 }
987}
988
989enum Child {
993 Ref(Box<str>),
994 Json(Box<Value>),
995}
996
997impl Child {
998 fn json(v: Value) -> Self {
999 Child::Json(Box::new(v))
1000 }
1001
1002 fn into_value(self) -> Value {
1003 match self {
1004 Child::Ref(r) => ref_value(r),
1005 Child::Json(v) => *v,
1006 }
1007 }
1008}
1009
1010impl serde::Serialize for Child {
1011 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1012 use serde::ser::SerializeMap;
1013 match self {
1014 Child::Ref(r) => {
1015 let mut m = s.serialize_map(Some(1))?;
1016 m.serialize_entry("$ref", r)?;
1017 m.end()
1018 }
1019 Child::Json(v) => v.serialize(s),
1020 }
1021 }
1022}
1023
1024fn round2(v: f64) -> f64 {
1025 (v * 100.0).round() / 100.0
1026}
1027
1028#[derive(Default)]
1029struct Builder {
1030 note_keys: bool,
1034 texts: Vec<Item>,
1035 groups: Vec<Value>,
1036 tables: Vec<Value>,
1037 pictures: Vec<Value>,
1038 field_regions: Vec<Value>,
1039 field_items: Vec<Value>,
1040 key_value_items: Vec<Value>,
1041 pages: Vec<(usize, f64, f64)>,
1045 cur_page: usize,
1047 cur_w: f64,
1048 cur_h: f64,
1049 pending_loc: Option<[u16; 4]>,
1052 pending_exact: Option<ExactProv>,
1055 pending_track: Option<(crate::tree::TreeTrack, String)>,
1058 pending_siblings: Vec<Value>,
1062 pending_after: Vec<Value>,
1066 pending_body: Vec<Value>,
1070 comment_groups: Vec<String>,
1074 last_comment_section: Option<(String, String)>,
1079 pending_comments: Vec<(String, Vec<usize>)>,
1083}
1084
1085impl Builder {
1086 fn take_prov(&mut self, char_len: usize) -> Value {
1092 self.take_prov_slot(char_len).to_value()
1093 }
1094
1095 fn take_prov_slot(&mut self, char_len: usize) -> ProvSlot {
1097 let prov = self.prov_slot(char_len, false);
1098 self.pending_exact = None;
1099 self.pending_loc = None;
1100 prov
1101 }
1102
1103 fn prov_slot(&self, char_len: usize, span_over_text: bool) -> ProvSlot {
1108 let r2 = round2;
1109 if let Some(ExactProv {
1110 page_no,
1111 bbox: [l, t, r, b],
1112 bottom_left,
1113 charspan,
1114 }) = self.pending_exact
1115 {
1116 let charspan = if span_over_text {
1117 [0, char_len]
1118 } else {
1119 charspan
1120 };
1121 return ProvSlot::One {
1122 page_no,
1123 bbox: [r2(l), r2(t), r2(r), r2(b)],
1124 bottom_left,
1125 charspan,
1126 };
1127 }
1128 let Some([x0, y0, x1, y1]) = self.pending_loc else {
1129 return ProvSlot::None;
1130 };
1131 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
1135 return ProvSlot::One {
1136 page_no: self.cur_page,
1137 bbox: [0.0; 4],
1138 bottom_left: true,
1139 charspan: [0, char_len],
1140 };
1141 }
1142 ProvSlot::One {
1143 page_no: self.cur_page,
1144 bbox: [
1145 r2(x0 as f64 * self.cur_w / 512.0),
1146 r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
1147 r2(x1 as f64 * self.cur_w / 512.0),
1148 r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
1149 ],
1150 bottom_left: true,
1151 charspan: [0, char_len],
1152 }
1153 }
1154
1155 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
1158 if self.pending_loc.is_none() && self.cur_page > 0 {
1159 self.pending_loc = loc;
1160 }
1161 }
1162
1163 fn link_comments(&mut self) {
1171 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
1172 .into_iter()
1173 .map(|(item, comments)| {
1174 let refs = comments
1175 .iter()
1176 .filter_map(|i| self.comment_groups.get(*i))
1177 .map(ref_value)
1178 .collect();
1179 (item, refs)
1180 })
1181 .collect();
1182 for (item, comment_refs) in refs {
1183 if comment_refs.is_empty() {
1184 continue;
1185 }
1186 let Some(target) = self.item_mut(&item) else {
1187 continue;
1188 };
1189 let Some(obj) = target.as_object_mut() else {
1190 continue;
1191 };
1192 let tail: Vec<(String, Value)> = obj
1193 .iter()
1194 .skip_while(|(k, _)| k.as_str() != "prov")
1195 .skip(1)
1196 .map(|(k, v)| (k.clone(), v.clone()))
1197 .collect();
1198 for (k, _) in &tail {
1199 obj.shift_remove(k);
1200 }
1201 obj.insert("comments".into(), Value::Array(comment_refs));
1202 for (k, v) in tail {
1203 obj.insert(k, v);
1204 }
1205 }
1206 }
1207
1208 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
1210 let idx = ref_index(self_ref)?;
1211 if self_ref.starts_with("#/texts/") {
1212 return self.texts.get_mut(idx).map(|item| item.value_mut(idx));
1213 }
1214 let bucket = if self_ref.starts_with("#/tables/") {
1215 &mut self.tables
1216 } else if self_ref.starts_with("#/pictures/") {
1217 &mut self.pictures
1218 } else if self_ref.starts_with("#/groups/") {
1219 &mut self.groups
1220 } else {
1221 return None;
1222 };
1223 bucket.get_mut(idx)
1224 }
1225
1226 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
1230 use crate::tree::TreeKind;
1231 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
1234 let (mut nt, mut ng, mut ntb, mut np, mut nf, mut nk) = (0, 0, 0, 0, 0, 0);
1235 for item in &tree.items {
1236 if item.deleted {
1237 refs.push(String::new());
1238 continue;
1239 }
1240 let r = match &item.kind {
1241 TreeKind::Text { .. } | TreeKind::Code { .. } => {
1242 nt += 1;
1243 format!("#/texts/{}", nt - 1)
1244 }
1245 TreeKind::Group { .. } => {
1246 ng += 1;
1247 format!("#/groups/{}", ng - 1)
1248 }
1249 TreeKind::Table { .. } => {
1250 ntb += 1;
1251 format!("#/tables/{}", ntb - 1)
1252 }
1253 TreeKind::Picture { .. } => {
1254 np += 1;
1255 format!("#/pictures/{}", np - 1)
1256 }
1257 TreeKind::FieldRegion { items } => {
1258 nt += items
1261 .iter()
1262 .map(|i| {
1263 [&i.marker, &i.key, &i.value]
1264 .iter()
1265 .filter(|p| p.is_some())
1266 .count()
1267 })
1268 .sum::<usize>();
1269 nf += 1;
1270 format!("#/field_regions/{}", nf - 1)
1271 }
1272 TreeKind::KeyValueGraph { .. } => {
1273 nk += 1;
1274 format!("#/key_value_items/{}", nk - 1)
1275 }
1276 };
1277 refs.push(r);
1278 }
1279 let ref_of = |id: usize| ref_value(refs[id].as_str());
1280 for (id, item) in tree.items.iter().enumerate() {
1281 if item.deleted {
1282 continue;
1283 }
1284 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
1285 let children: Vec<Value> = item
1289 .children
1290 .iter()
1291 .filter(|&&c| !tree.items[c].deleted)
1292 .map(|&c| ref_of(c))
1293 .collect();
1294 let layer = item.layer.map_or("body", |l| l.value());
1295 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
1298 let self_ref = match &item.kind {
1299 TreeKind::Text {
1300 label,
1301 text,
1302 orig,
1303 formatting,
1304 hyperlink,
1305 level,
1306 list,
1307 } => {
1308 let mut tail = serde_json::Map::new();
1311 if let Some(f) = formatting {
1312 tail.insert("formatting".into(), formatting_json(f));
1313 }
1314 if let Some(h) = hyperlink {
1315 tail.insert("hyperlink".into(), json!(h));
1316 }
1317 if let Some(l) = level {
1318 tail.insert("level".into(), json!(l));
1319 }
1320 if let Some(l) = list {
1321 tail.insert("enumerated".into(), json!(l.enumerated));
1322 tail.insert("marker".into(), json!(l.marker));
1323 }
1324 let r = format!("#/texts/{}", self.texts.len());
1325 let prov = self.take_prov(text.chars().count());
1326 let mut item_json = json!({
1327 "self_ref": r,
1328 "parent": { "$ref": parent },
1329 "children": children,
1330 "content_layer": layer,
1331 "label": label,
1332 "prov": prov,
1333 });
1334 if !item.comments.is_empty() {
1337 item_json["comments"] =
1338 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
1339 }
1340 if let Some(track) = &item.source {
1343 item_json["source"] = json!([track_json(track)]);
1344 }
1345 merge(
1346 &mut item_json,
1347 json!({
1348 "orig": orig.as_deref().unwrap_or(text),
1349 "text": text,
1350 }),
1351 );
1352 merge(&mut item_json, Value::Object(tail));
1353 if self.note_keys {
1354 if !item.notes.is_empty() {
1355 item_json["_notes"] = Value::Array(
1356 item.notes
1357 .iter()
1358 .map(|n| json!([n.offset, n.text]))
1359 .collect(),
1360 );
1361 }
1362 if item.note_body {
1363 item_json["_note_body"] = json!(true);
1364 }
1365 }
1366 self.texts.push(Item::json(item_json));
1367 r
1368 }
1369 TreeKind::Code {
1370 text,
1371 orig,
1372 language,
1373 formatting,
1374 hyperlink,
1375 } => {
1376 let r = format!("#/texts/{}", self.texts.len());
1377 let prov = self.take_prov(text.chars().count());
1378 let mut item_json = json!({
1379 "self_ref": r,
1380 "parent": { "$ref": parent },
1381 "children": children,
1382 "content_layer": layer,
1383 "label": "code",
1384 "prov": prov,
1385 });
1386 if !item.comments.is_empty() {
1387 item_json["comments"] =
1388 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
1389 }
1390 merge(
1391 &mut item_json,
1392 json!({
1393 "orig": orig.as_deref().unwrap_or(text),
1394 "text": text,
1395 }),
1396 );
1397 if let Some(f) = formatting {
1398 item_json["formatting"] = formatting_json(f);
1399 }
1400 if let Some(h) = hyperlink {
1401 item_json["hyperlink"] = json!(h);
1402 }
1403 merge(
1404 &mut item_json,
1405 json!({
1406 "captions": [],
1407 "references": [],
1408 "footnotes": [],
1409 "code_language": code_language(language.as_deref()),
1410 }),
1411 );
1412 self.texts.push(Item::json(item_json));
1413 r
1414 }
1415 TreeKind::Group { label, name } => {
1416 self.pending_exact = None;
1417 let r = format!("#/groups/{}", self.groups.len());
1418 self.groups.push(json!({
1419 "self_ref": r,
1420 "parent": { "$ref": parent },
1421 "children": children,
1422 "content_layer": layer,
1423 "name": name,
1424 "label": label,
1425 }));
1426 r
1427 }
1428 TreeKind::Table {
1429 table,
1430 rich_cells,
1431 captions,
1432 } => {
1433 let r = self.add_table_with(table, parent, true);
1435 let idx = ref_index(&r).expect("table ref");
1436 let t = &mut self.tables[idx];
1437 t["children"] = Value::Array(children);
1438 t["content_layer"] = json!(layer);
1439 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
1440 for &(row, col, group) in rich_cells {
1444 let cell_ref = ref_of(group);
1445 let hit = |c: &Value| {
1446 c["start_row_offset_idx"] == json!(row)
1447 && c["start_col_offset_idx"] == json!(col)
1448 };
1449 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
1450 for c in cells.iter_mut().filter(|c| hit(c)) {
1451 c["ref"] = cell_ref.clone();
1452 }
1453 }
1454 }
1455 r
1456 }
1457 TreeKind::Picture {
1458 captions,
1459 image,
1460 classification,
1461 description,
1462 confidence,
1463 chart,
1464 dpi,
1465 } => {
1466 let mut meta = classification.as_ref().map(|c| {
1471 let mut pred = serde_json::Map::new();
1472 if let Some(conf) = confidence {
1473 pred.insert("confidence".into(), json!(conf));
1474 }
1475 pred.insert("class_name".into(), json!(c));
1476 json!({ "classification": { "predictions": [pred] } })
1477 });
1478 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
1479 if !t.rows.is_empty() {
1480 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
1481 }
1482 }
1483 if let Some(desc) = description {
1487 let mut d = description_meta(desc);
1488 let mut merged = Map::new();
1489 merged.insert("description".into(), d["description"].take());
1490 if let Some(Value::Object(rest)) = meta {
1491 merged.extend(rest);
1492 }
1493 merged.insert("annotations".into(), d["annotations"].take());
1494 meta = Some(Value::Object(merged));
1495 }
1496 let prov = self.take_prov(0);
1497 let r = self.push_picture(
1498 prov,
1499 captions.iter().map(|&c| ref_of(c)).collect(),
1500 children,
1501 image.as_ref(),
1502 meta,
1503 parent,
1504 );
1505 if let Some(idx) = ref_index(&r) {
1506 self.pictures[idx]["content_layer"] = json!(layer);
1507 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
1510 img["dpi"] = json!(dpi);
1511 }
1512 }
1513 r
1514 }
1515 TreeKind::FieldRegion { items } => {
1516 self.pending_exact = None;
1517 let r = self.add_field_region(items, parent);
1518 if let Some(region) = self.field_regions.last_mut() {
1519 region["content_layer"] = json!(layer);
1520 }
1521 r
1522 }
1523 TreeKind::KeyValueGraph { cells, links } => {
1524 self.pending_exact = None;
1525 let r = self.add_key_value_graph(cells, links, parent);
1526 if let Some(item) = self.key_value_items.last_mut() {
1527 item["content_layer"] = json!(layer);
1528 }
1529 r
1530 }
1531 };
1532 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1533 }
1534 tree.body
1535 .iter()
1536 .filter(|&&c| !tree.items[c].deleted)
1537 .map(|&c| ref_of(c))
1538 .collect()
1539 }
1540
1541 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1542 match node {
1543 Node::Heading { level: 1, text } => {
1544 Some(self.add_text("title", text, parent, json!({})))
1545 }
1546 Node::Heading { level, text } => Some(self.add_text(
1547 "section_header",
1548 text,
1549 parent,
1550 json!({ "level": level.saturating_sub(1) }),
1551 )),
1552 Node::Caption { text, href } => {
1553 let extra = match href {
1554 Some(url) => json!({ "hyperlink": url }),
1555 None => json!({}),
1556 };
1557 Some(self.add_text("caption", text, parent, extra))
1558 }
1559 Node::LabeledText { label, text, href } => {
1562 let extra = match href {
1563 Some(url) => json!({ "hyperlink": url }),
1564 None => json!({}),
1565 };
1566 Some(self.add_text(label, text, parent, extra))
1567 }
1568 Node::Paragraph { text } => {
1569 let t = text.trim();
1572 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1573 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1574 _ => Some(self.add_text("text", text, parent, json!({}))),
1575 }
1576 }
1577 Node::CheckboxItem { checked, text } => {
1578 let label = if *checked {
1584 "checkbox_selected"
1585 } else {
1586 "checkbox_unselected"
1587 };
1588 Some(self.add_text(label, text, parent, json!({})))
1589 }
1590 Node::Code {
1591 language,
1592 text,
1593 orig,
1594 ..
1595 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1596 Node::Formula {
1599 latex,
1600 orig,
1601 location,
1602 } => {
1603 self.adopt_loc(*location);
1604 Some(self.add_formula_item(latex, orig, parent))
1605 }
1606 Node::CommentSection {
1612 name,
1613 text,
1614 refs_note_text,
1615 grouped,
1616 } => {
1617 if !*grouped {
1618 let child =
1622 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1623 self.comment_groups.push(child.clone());
1624 return Some(child);
1625 }
1626 if let Some((_, self_ref)) = self
1627 .last_comment_section
1628 .as_ref()
1629 .filter(|(n, _)| n == name)
1630 .cloned()
1631 {
1632 let child =
1635 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1636 if let Some(children) =
1637 self.groups[group_index(&self_ref)]["children"].as_array_mut()
1638 {
1639 children.push(ref_value(child.as_str()));
1640 }
1641 self.comment_groups.push(if *refs_note_text {
1642 child
1643 } else {
1644 self_ref.clone()
1645 });
1646 return Some(self_ref);
1647 }
1648 let self_ref = format!("#/groups/{}", self.groups.len());
1649 self.groups.push(Value::Null);
1650 let child =
1651 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1652 self.groups[group_index(&self_ref)] = json!({
1653 "self_ref": self_ref,
1654 "parent": { "$ref": parent },
1655 "children": [{ "$ref": child }],
1656 "content_layer": "notes",
1657 "name": name,
1658 "label": "comment_section",
1659 });
1660 self.last_comment_section = Some((name.clone(), self_ref.clone()));
1661 self.comment_groups.push(if *refs_note_text {
1662 child
1663 } else {
1664 self_ref.clone()
1665 });
1666 Some(self_ref)
1667 }
1668 Node::Commented { comments, inner } => {
1671 let item = self.add_node(inner, parent)?;
1672 if !comments.is_empty() {
1673 self.pending_comments.push((item.clone(), comments.clone()));
1674 }
1675 Some(item)
1676 }
1677 Node::Table(t) => Some(self.add_table(t, parent)),
1678 Node::Picture {
1679 caption,
1680 caption_href,
1681 image,
1682 classification,
1683 description,
1684 caption_parent,
1685 caption_location,
1686 } => Some(self.add_picture(
1687 caption.as_deref(),
1688 caption_href.as_deref(),
1689 image.as_ref(),
1690 picture_meta(classification.as_deref(), description.as_ref()),
1691 parent,
1692 (*caption_parent, *caption_location),
1693 )),
1694 Node::Chart {
1699 kind,
1700 table,
1701 caption,
1702 location,
1703 } => {
1704 self.adopt_loc(*location);
1705 let mut meta = json!({
1706 "classification": { "predictions": [{ "class_name": kind }] },
1707 });
1708 if !table.rows.is_empty() {
1709 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1710 }
1711 let mut captions = Vec::new();
1717 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1718 let prov = self.prov_slot(unescape_text(cap).chars().count(), true);
1719 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1720 self.pending_siblings.push(ref_value(cap_ref.as_str()));
1721 captions.push(ref_value(cap_ref));
1722 }
1723 let prov = self.take_prov(0);
1724 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1725 }
1726 Node::DoclangOnly(_) => None,
1728 Node::Group {
1729 label,
1730 name,
1731 layer,
1732 children,
1733 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1734 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1735 Node::KeyValueGraph { cells, links } => {
1736 Some(self.add_key_value_graph(cells, links, parent))
1737 }
1738 Node::InlineGroup { md_text, .. } => {
1741 Some(self.add_text("text", md_text, parent, json!({})))
1742 }
1743 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1745 Node::Furniture {
1751 layer: ContentLayer::Notes,
1752 inner,
1753 } => {
1754 let item = self.add_node(inner, parent)?;
1755 self.set_layer(&item, "notes");
1756 Some(item)
1757 }
1758 Node::Furniture { .. } => None,
1759 Node::PageFurniture {
1763 footer,
1764 location,
1765 text,
1766 } => {
1767 if self.cur_page > 0 {
1768 self.pending_loc = Some(*location);
1769 }
1770 let label = if *footer {
1771 "page_footer"
1772 } else {
1773 "page_header"
1774 };
1775 let item = self.add_text(label, text, parent, json!({}));
1776 self.pending_loc = None;
1777 self.set_layer(&item, "furniture");
1778 Some(item)
1779 }
1780 Node::FurnitureText { label, text } => {
1783 let item = self.add_text(label, text, parent, json!({}));
1784 self.set_layer(&item, "furniture");
1785 Some(item)
1786 }
1787 Node::PictureChildren(children) => {
1791 let pic = self.pictures.len().checked_sub(1)?;
1792 let pic_ref = format!("#/pictures/{pic}");
1793 let refs = self.walk_into(children, &pic_ref);
1794 if let Some(list) = self.pictures[pic]["children"].as_array_mut() {
1795 list.extend(refs.into_iter().map(Child::into_value));
1796 }
1797 None
1798 }
1799 Node::Located { location, inner } => {
1804 if self.cur_page > 0 {
1805 self.pending_loc = Some(*location);
1806 }
1807 let r = self.add_node(inner, parent);
1808 self.pending_loc = None;
1809 r
1810 }
1811 Node::Track { track, cue, inner } => {
1812 self.pending_track = Some((track.clone(), cue.clone()));
1813 let r = self.add_node(inner, parent);
1814 self.pending_track = None;
1815 r
1816 }
1817 Node::Prov {
1818 page_no,
1819 bbox,
1820 charspan,
1821 inner,
1822 ..
1823 } => {
1824 self.pending_exact = Some(ExactProv {
1825 page_no: *page_no,
1826 bbox: bbox.map(f64::from),
1827 bottom_left: false,
1828 charspan: *charspan,
1829 });
1830 let r = self.add_node(inner, parent);
1831 self.pending_exact = None;
1832 r
1833 }
1834 Node::PageBreak => None,
1836 Node::PageInfo {
1839 page_no,
1840 width,
1841 height,
1842 } => {
1843 self.cur_page = *page_no;
1844 self.cur_w = *width as f64;
1845 self.cur_h = *height as f64;
1846 if *page_no > 0 {
1847 self.pages.push((*page_no, self.cur_w, self.cur_h));
1848 }
1849 None
1850 }
1851 Node::ListItem { .. } => None,
1853 }
1854 }
1855
1856 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1860 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1861 self.field_regions.push(Value::Null);
1862 let region_index = self.field_regions.len() - 1;
1863 let mut item_refs = Vec::new();
1864 for item in items {
1865 item_refs.push(ref_value(self.add_field_item(item, &self_ref)));
1866 }
1867 self.field_regions[region_index] = json!({
1868 "self_ref": self_ref,
1869 "parent": { "$ref": parent },
1870 "children": item_refs,
1871 "content_layer": "body",
1872 "label": "field_region",
1873 "prov": [],
1874 });
1875 self_ref
1876 }
1877
1878 fn add_key_value_graph(
1882 &mut self,
1883 cells: &[crate::GraphCell],
1884 links: &[crate::GraphLink],
1885 parent: &str,
1886 ) -> String {
1887 let self_ref = format!("#/key_value_items/{}", self.key_value_items.len());
1888 let cells: Vec<Value> = cells
1889 .iter()
1890 .map(|c| {
1891 json!({
1892 "label": c.label,
1893 "cell_id": c.cell_id,
1894 "text": c.text,
1895 "orig": c.orig,
1896 })
1897 })
1898 .collect();
1899 let links: Vec<Value> = links
1900 .iter()
1901 .map(|l| {
1902 json!({
1903 "label": l.label,
1904 "source_cell_id": l.source_cell_id,
1905 "target_cell_id": l.target_cell_id,
1906 })
1907 })
1908 .collect();
1909 self.key_value_items.push(json!({
1910 "self_ref": self_ref,
1911 "parent": { "$ref": parent },
1912 "children": [],
1913 "content_layer": "body",
1914 "label": "key_value_region",
1915 "prov": [],
1916 "captions": [],
1917 "references": [],
1918 "footnotes": [],
1919 "graph": { "cells": cells, "links": links },
1920 }));
1921 self_ref
1922 }
1923
1924 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1925 let self_ref = format!("#/field_items/{}", self.field_items.len());
1926 self.field_items.push(Value::Null);
1927 let item_index = self.field_items.len() - 1;
1928 let mut child_refs = Vec::new();
1929 for (label, text) in [
1930 ("marker", &item.marker),
1931 ("field_key", &item.key),
1932 ("field_value", &item.value),
1933 ] {
1934 if let Some(text) = text {
1935 let extra = match (label, &item.value_kind) {
1938 ("field_value", Some(kind)) => json!({ "kind": kind }),
1939 _ => json!({}),
1940 };
1941 child_refs.push(ref_value(self.add_text(label, text, &self_ref, extra)));
1942 }
1943 }
1944 self.field_items[item_index] = json!({
1945 "self_ref": self_ref,
1946 "parent": { "$ref": parent },
1947 "children": child_refs,
1948 "content_layer": "body",
1949 "label": "field_item",
1950 "prov": [],
1951 });
1952 self_ref
1953 }
1954
1955 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1959 if let Some(item) = self.item_mut(self_ref) {
1960 item["content_layer"] = json!(layer);
1961 }
1962 }
1963
1964 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1965 let prov = self.take_prov_slot(unescape_text(text).chars().count());
1966 self.add_text_with(label, text, parent, extra, prov)
1967 }
1968
1969 fn add_text_with(
1972 &mut self,
1973 label: &str,
1974 text: &str,
1975 parent: &str,
1976 extra: Value,
1977 prov: ProvSlot,
1978 ) -> String {
1979 let index = self.texts.len();
1980 let self_ref = format!("#/texts/{index}");
1981 let mut item = match self.pending_track.take() {
1986 None => Item::Text(Box::new(TextItem {
1987 parent: parent.into(),
1988 label: label.into(),
1989 prov,
1990 text: unescape_text(text).into(),
1991 })),
1992 Some((track, cue)) => {
1993 let mut m = serde_json::Map::with_capacity(9);
1994 m.insert("self_ref".into(), Value::String(self_ref.clone()));
1995 m.insert("parent".into(), ref_value(parent));
1996 m.insert("children".into(), Value::Array(Vec::new()));
1997 m.insert("content_layer".into(), "body".into());
1998 m.insert("label".into(), label.into());
1999 m.insert("prov".into(), prov.to_value());
2000 m.insert("source".into(), Value::Array(vec![track_json(&track)]));
2002 m.insert("orig".into(), Value::String(cue.clone()));
2003 m.insert("text".into(), Value::String(cue));
2004 Item::json(Value::Object(m))
2005 }
2006 };
2007 if extra.as_object().is_some_and(|e| !e.is_empty()) {
2008 merge(item.value_mut(index), extra);
2009 }
2010 self.texts.push(item);
2011 self_ref
2012 }
2013
2014 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
2017 let self_ref = format!("#/texts/{}", self.texts.len());
2018 let prov = self.take_prov(latex.chars().count());
2019 self.texts.push(Item::json(json!({
2020 "self_ref": self_ref,
2021 "parent": { "$ref": parent },
2022 "children": [],
2023 "content_layer": "body",
2024 "label": "formula",
2025 "prov": prov,
2026 "orig": latex,
2027 "text": latex,
2028 })));
2029 self_ref
2030 }
2031
2032 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
2036 let self_ref = format!("#/texts/{}", self.texts.len());
2037 let prov = self.take_prov(latex.chars().count());
2038 self.texts.push(Item::json(json!({
2039 "self_ref": self_ref,
2040 "parent": { "$ref": parent },
2041 "children": [],
2042 "content_layer": "body",
2043 "label": "formula",
2044 "prov": prov,
2045 "orig": orig,
2046 "text": latex,
2047 })));
2048 self_ref
2049 }
2050
2051 fn add_code(
2052 &mut self,
2053 text: &str,
2054 language: Option<&str>,
2055 orig: Option<&str>,
2056 parent: &str,
2057 ) -> String {
2058 let self_ref = format!("#/texts/{}", self.texts.len());
2059 let raw = unescape_text(text);
2060 let prov = self.take_prov(raw.chars().count());
2061 self.texts.push(Item::json(json!({
2062 "self_ref": self_ref,
2063 "parent": { "$ref": parent },
2064 "children": [],
2065 "content_layer": "body",
2066 "label": "code",
2067 "prov": prov,
2068 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
2071 "text": raw,
2072 "captions": [],
2073 "references": [],
2074 "footnotes": [],
2075 "code_language": code_language(language),
2076 })));
2077 self_ref
2078 }
2079
2080 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
2083 let self_ref = format!("#/groups/{}", self.groups.len());
2084 self.groups.push(Value::Null);
2086 let base = level_of(&items[0]);
2087 let mut children = Vec::new();
2088 let mut i = 0;
2089 while i < items.len() {
2090 if !matches!(items[i], Node::ListItem { .. }) {
2093 i += 1;
2094 continue;
2095 }
2096 let lvl = level_of(&items[i]);
2097 if lvl > base {
2098 i += 1;
2100 continue;
2101 }
2102 let item_ref = self.add_list_item(&items[i], &self_ref);
2103 let mut j = i + 1;
2105 while j < items.len() && level_of(&items[j]) > base {
2106 j += 1;
2107 }
2108 if j > i + 1 {
2109 let mut nested = Vec::new();
2110 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
2111 if let Some(idx) = ref_index(&item_ref) {
2113 self.texts[idx].value_mut(idx)["children"]
2114 .as_array_mut()
2115 .unwrap()
2116 .extend(nested);
2117 }
2118 }
2119 children.push(ref_value(item_ref));
2120 i = j;
2121 }
2122 self.groups[group_index(&self_ref)] = json!({
2123 "self_ref": self_ref,
2124 "parent": { "$ref": parent },
2125 "children": children,
2126 "content_layer": "body",
2127 "name": "list",
2128 "label": "list",
2129 });
2130 self_ref
2131 }
2132
2133 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
2134 let Node::ListItem {
2135 ordered,
2136 number,
2137 text,
2138 location,
2139 ..
2140 } = node
2141 else {
2142 unreachable!()
2143 };
2144 self.adopt_loc(*location);
2145 let self_ref = format!("#/texts/{}", self.texts.len());
2146 let raw = unescape_text(text);
2147 let prov = self.take_prov(raw.chars().count());
2148 let marker = if *ordered {
2149 format!("{number}.")
2150 } else {
2151 "-".to_string()
2152 };
2153 self.texts.push(Item::json(json!({
2154 "self_ref": self_ref,
2155 "parent": { "$ref": parent },
2156 "children": [],
2157 "content_layer": "body",
2158 "label": "list_item",
2159 "prov": prov,
2160 "orig": raw,
2161 "text": raw,
2162 "enumerated": ordered,
2163 "marker": marker,
2164 })));
2165 self_ref
2166 }
2167
2168 fn add_table(&mut self, t: &Table, parent: &str) -> String {
2169 self.add_table_with(t, parent, false)
2170 }
2171
2172 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
2175 let self_ref = format!("#/tables/{}", self.tables.len());
2176 self.adopt_loc(t.location);
2177 let prov = self.take_prov(0);
2178 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
2182 Some(cap) => self.add_caption(
2183 cap,
2184 json!({}),
2185 &self_ref,
2186 parent,
2187 (t.caption_parent, t.caption_location),
2188 ),
2189 None => (Vec::new(), Vec::new()),
2190 };
2191 let data = table_data_with(t, raw);
2192 self.tables.push(json!({
2193 "self_ref": self_ref,
2194 "parent": { "$ref": parent },
2195 "children": children,
2196 "content_layer": "body",
2197 "label": "table",
2198 "prov": prov,
2199 "captions": captions,
2200 "references": [],
2201 "footnotes": [],
2202 "data": data,
2203 "annotations": [],
2204 }));
2205 self_ref
2206 }
2207
2208 fn add_caption(
2217 &mut self,
2218 text: &str,
2219 extra: Value,
2220 self_ref: &str,
2221 parent: &str,
2222 (choice, location): (CaptionParent, Option<[u16; 4]>),
2223 ) -> (Vec<Value>, Vec<Value>) {
2224 let cap_parent = match choice {
2229 CaptionParent::Item => self_ref,
2230 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
2231 CaptionParent::Body => "#/body",
2232 };
2233 if self.cur_page > 0 && location.is_some() {
2234 self.pending_loc = location;
2235 }
2236 let cap_ref = ref_value(self.add_text("caption", text, cap_parent, extra));
2237 match choice {
2238 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
2239 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
2242 CaptionParent::Body if parent == "#/body" => {
2243 self.pending_siblings.push(cap_ref.clone())
2244 }
2245 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
2246 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
2249 }
2250 (vec![cap_ref], Vec::new())
2251 }
2252
2253 fn add_picture(
2256 &mut self,
2257 caption: Option<&str>,
2258 caption_href: Option<&str>,
2259 image: Option<&crate::PictureImage>,
2260 meta: Option<Value>,
2261 parent: &str,
2262 caption_at: (CaptionParent, Option<[u16; 4]>),
2263 ) -> String {
2264 let self_ref = format!("#/pictures/{}", self.pictures.len());
2265 let prov = self.take_prov(0);
2268 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
2269 Some(cap) => {
2270 let extra = match caption_href {
2274 Some(href) => json!({ "hyperlink": href }),
2275 None => json!({}),
2276 };
2277 self.add_caption(cap, extra, &self_ref, parent, caption_at)
2278 }
2279 None => (Vec::new(), Vec::new()),
2280 };
2281 self.push_picture(prov, captions, children, image, meta, parent)
2282 }
2283
2284 fn push_picture(
2287 &mut self,
2288 prov: Value,
2289 captions: Vec<Value>,
2290 children: Vec<Value>,
2291 image: Option<&crate::PictureImage>,
2292 meta: Option<Value>,
2293 parent: &str,
2294 ) -> String {
2295 let self_ref = format!("#/pictures/{}", self.pictures.len());
2296 let annotations = meta
2300 .as_ref()
2301 .and_then(|m| m.get("annotations").cloned())
2302 .unwrap_or_else(|| json!([]));
2303 let meta = meta.map(|mut m| {
2304 if let Some(obj) = m.as_object_mut() {
2305 obj.remove("annotations");
2306 }
2307 m
2308 });
2309 let mut item = match meta {
2313 Some(meta) => json!({
2314 "self_ref": self_ref,
2315 "parent": { "$ref": parent },
2316 "children": children,
2317 "content_layer": "body",
2318 "meta": meta,
2319 "label": "picture",
2320 "prov": prov,
2321 "captions": captions,
2322 "references": [],
2323 "footnotes": [],
2324 "annotations": annotations,
2325 }),
2326 None => json!({
2327 "self_ref": self_ref,
2328 "parent": { "$ref": parent },
2329 "children": children,
2330 "content_layer": "body",
2331 "label": "picture",
2332 "prov": prov,
2333 "captions": captions,
2334 "references": [],
2335 "footnotes": [],
2336 "annotations": annotations,
2337 }),
2338 };
2339 if let Some(img) = image {
2344 let image = image_ref_json(img);
2345 if let Some(obj) = item.as_object_mut() {
2346 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
2347 obj.insert("image".into(), image);
2348 obj.insert("annotations".into(), annotations);
2349 }
2350 }
2351 self.pictures.push(item);
2352 self_ref
2353 }
2354
2355 fn add_group(
2356 &mut self,
2357 label: &str,
2358 name: Option<&str>,
2359 layer: Option<ContentLayer>,
2360 nodes: &[Node],
2361 parent: &str,
2362 ) -> String {
2363 let self_ref = format!("#/groups/{}", self.groups.len());
2364 self.groups.push(Value::Null);
2365 let mark = (
2369 self.texts.len(),
2370 self.tables.len(),
2371 self.pictures.len(),
2372 self.groups.len(),
2373 );
2374 let children: Vec<Value> = self
2375 .walk_into(nodes, &self_ref)
2376 .into_iter()
2377 .map(Child::into_value)
2378 .collect();
2379 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
2380 let content_layer = layer.map_or("body", |l| l.value());
2381 self.groups[group_index(&self_ref)] = json!({
2382 "self_ref": self_ref,
2383 "parent": { "$ref": parent },
2384 "children": children,
2385 "content_layer": content_layer,
2386 "name": name,
2387 "label": label,
2388 });
2389 if layer.is_some() {
2390 let (t, tb, p, g) = mark;
2391 for item in self.texts[t..]
2392 .iter_mut()
2393 .enumerate()
2394 .map(|(k, item)| item.value_mut(t + k))
2395 .chain(self.tables[tb..].iter_mut())
2396 .chain(self.pictures[p..].iter_mut())
2397 .chain(self.groups[g..].iter_mut())
2398 {
2399 if let Some(obj) = item.as_object_mut() {
2400 obj.insert("content_layer".into(), json!(content_layer));
2401 }
2402 }
2403 }
2404 self_ref
2405 }
2406
2407 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Child> {
2410 let seqs: Option<Vec<usize>> = nodes
2415 .iter()
2416 .map(|n| match n {
2417 Node::Prov { seq: Some(s), .. } => Some(*s),
2418 _ => None,
2419 })
2420 .collect();
2421 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
2422 let mut order: Vec<usize> = (0..nodes.len()).collect();
2423 order.sort_by_key(|&i| seqs[i]);
2424 let mut slots: Vec<Vec<Child>> = (0..nodes.len()).map(|_| Vec::new()).collect();
2425 for i in order {
2426 if let Some(r) = self.add_node(&nodes[i], parent) {
2427 slots[i].extend(self.pending_siblings.drain(..).map(Child::json));
2428 slots[i].push(Child::Ref(r.into()));
2429 slots[i].extend(self.pending_after.drain(..).map(Child::json));
2430 }
2431 if parent == "#/body" {
2432 slots[i].extend(self.pending_body.drain(..).map(Child::json));
2433 }
2434 }
2435 return slots.into_iter().flatten().collect();
2436 }
2437 let mut children = Vec::new();
2438 let mut i = 0;
2439 while i < nodes.len() {
2440 if matches!(nodes[i], Node::ListItem { .. }) {
2441 let start = i;
2442 i += 1;
2443 loop {
2444 match nodes.get(i) {
2445 Some(Node::ListItem { .. }) => i += 1,
2446 Some(Node::Paragraph { text })
2449 if text.is_empty()
2450 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
2451 {
2452 i += 1
2453 }
2454 _ => break,
2455 }
2456 }
2457 let mut lists = Vec::new();
2458 self.add_sibling_lists(&nodes[start..i], parent, &mut lists);
2459 children.extend(lists.into_iter().map(Child::json));
2460 } else {
2461 if let Some(r) = self.add_node(&nodes[i], parent) {
2462 children.extend(self.pending_siblings.drain(..).map(Child::json));
2463 children.push(Child::Ref(r.into()));
2464 children.extend(self.pending_after.drain(..).map(Child::json));
2465 }
2466 i += 1;
2467 }
2468 if parent == "#/body" {
2471 children.extend(self.pending_body.drain(..).map(Child::json));
2472 }
2473 }
2474 children
2475 }
2476
2477 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
2482 let base = level_of(&run[0]);
2483 let mut seg = 0;
2484 for k in 0..run.len() {
2485 let Node::ListItem {
2486 first_in_list,
2487 level,
2488 ..
2489 } = &run[k]
2490 else {
2491 continue;
2492 };
2493 if *level != base {
2494 continue; }
2496 if k > seg && *first_in_list {
2497 out.push(ref_value(self.add_list(&run[seg..k], parent)));
2498 seg = k;
2499 }
2500 }
2501 out.push(ref_value(self.add_list(&run[seg..], parent)));
2502 }
2503}
2504
2505fn level_of(node: &Node) -> u8 {
2506 match node {
2507 Node::ListItem { level, .. } => *level,
2508 _ => 0,
2509 }
2510}
2511
2512fn group_index(self_ref: &str) -> usize {
2513 self_ref.rsplit('/').next().unwrap().parse().unwrap()
2514}
2515
2516fn ref_index(self_ref: &str) -> Option<usize> {
2517 self_ref.rsplit('/').next()?.parse().ok()
2518}
2519
2520fn merge(target: &mut Value, extra: Value) {
2522 if let (Some(t), Value::Object(e)) = (target.as_object_mut(), extra) {
2523 t.extend(e);
2524 }
2525}
2526
2527fn object<const N: usize>(entries: [(&str, Value); N]) -> Value {
2533 let mut m = serde_json::Map::with_capacity(N);
2534 for (k, v) in entries {
2535 m.insert(k.to_owned(), v);
2536 }
2537 Value::Object(m)
2538}
2539
2540fn ref_value(r: impl Into<String>) -> Value {
2542 object([("$ref", Value::String(r.into()))])
2543}
2544
2545fn unescape_text(s: &str) -> String {
2547 s.replace("<", "<")
2548 .replace(">", ">")
2549 .replace("&", "&")
2550 .replace("\\_", "_")
2551}
2552
2553fn fnv1a(s: &str) -> u64 {
2556 let mut h: u64 = 0xcbf29ce484222325;
2557 for b in s.bytes() {
2558 h ^= b as u64;
2559 h = h.wrapping_mul(0x100000001b3);
2560 }
2561 h
2562}
2563
2564#[cfg(test)]
2565mod tests {
2566 use crate::{
2567 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
2568 };
2569 use serde_json::Value;
2570
2571 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
2572 crate::TableCell {
2573 text: text.into(),
2574 bbox: None,
2575 start_row: row,
2576 start_col: col,
2577 row_span: 1,
2578 col_span,
2579 column_header: row == 0,
2580 row_header: false,
2581 row_section: false,
2582 }
2583 }
2584
2585 #[test]
2591 fn ragged_rows_take_their_grid_from_first_class_cells() {
2592 let cells = vec![
2593 cell_at("h0", 0, 0, 1),
2594 cell_at("h2", 0, 2, 1),
2595 cell_at("wide", 1, 0, 3),
2596 ];
2597 let export = |rows: Vec<Vec<String>>| -> Value {
2598 let mut doc = DoclingDocument::new("t");
2599 doc.push(Node::Table(Table {
2600 rows,
2601 cells: Some(cells.clone()),
2602 ..Table::default()
2603 }));
2604 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2605 v["tables"][0]["data"].clone()
2606 };
2607 let data = export(vec![
2609 vec!["h0".into(), "h2".into()],
2610 vec!["wide".into(), "wide".into(), "wide".into()],
2611 ]);
2612 assert_eq!(data["num_rows"], 2);
2613 assert_eq!(data["num_cols"], 3);
2614 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
2615 let row0: Vec<&str> = data["grid"][0]
2616 .as_array()
2617 .unwrap()
2618 .iter()
2619 .map(|c| c["text"].as_str().unwrap())
2620 .collect();
2621 assert_eq!(row0, ["h0", "", "h2"]);
2622 assert_eq!(data["table_cells"][2]["col_span"], 3);
2623 let data = export(vec![
2626 vec!["h0".into(), "h2".into()],
2627 vec!["wide".into(), "wide".into()],
2628 ]);
2629 assert_eq!(data["num_cols"], 2);
2630 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
2631 }
2632
2633 fn doc_with_image() -> DoclingDocument {
2634 let mut doc = DoclingDocument::new("t");
2635 doc.push(Node::Picture {
2636 caption: Some("Fig 1".into()),
2637 caption_href: None,
2638 image: Some(PictureImage {
2639 dpi: crate::PictureImage::DEFAULT_DPI,
2640 mimetype: "image/png".into(),
2641 width: 4,
2642 height: 2,
2643 data: b"foobar".to_vec(),
2644 }),
2645 classification: None,
2646 description: None,
2647 caption_parent: Default::default(),
2648 caption_location: None,
2649 });
2650 doc
2651 }
2652
2653 #[test]
2658 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
2659 let mut doc = DoclingDocument::new("t");
2660 doc.push(Node::Heading {
2661 level: 1,
2662 text: "Slide One".into(),
2663 });
2664 doc.push(Node::Furniture {
2665 layer: ContentLayer::Notes,
2666 inner: Box::new(Node::Located {
2667 location: [0, 0, 0, 0],
2668 inner: Box::new(Node::Paragraph {
2669 text: "Speaker note for slide 1.".into(),
2670 }),
2671 }),
2672 });
2673 doc.push(Node::Furniture {
2674 layer: ContentLayer::Furniture,
2675 inner: Box::new(Node::Paragraph {
2676 text: "page header".into(),
2677 }),
2678 });
2679
2680 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2681 let texts = v["texts"].as_array().unwrap();
2682 assert_eq!(
2683 texts
2684 .iter()
2685 .map(|t| (
2686 t["label"].as_str().unwrap(),
2687 t["content_layer"].as_str().unwrap(),
2688 t["text"].as_str().unwrap()
2689 ))
2690 .collect::<Vec<_>>(),
2691 vec![
2692 ("title", "body", "Slide One"),
2693 ("text", "notes", "Speaker note for slide 1."),
2694 ],
2695 "the note is carried on its own layer; the furniture is not carried"
2696 );
2697 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
2699 }
2700
2701 #[test]
2707 fn continuation_flags_become_spanning_cells() {
2708 let mut doc = DoclingDocument::new("t");
2709 let rows = vec![
2711 vec!["merged".to_string(), "merged".into(), "merged".into()],
2712 vec!["merged".to_string(), "merged".into(), "merged".into()],
2713 vec!["a".to_string(), "b".into(), "c".into()],
2714 ];
2715 doc.push(Node::Table(crate::Table {
2716 rows,
2717 location: None,
2718 structure: Some(crate::TableStructure {
2719 header_row: vec![true, false, false],
2720 col_continuation: vec![
2721 vec![false, true, true],
2722 vec![false, true, true],
2723 vec![false, false, false],
2724 ],
2725 row_continuation: vec![
2726 vec![false, false, false],
2727 vec![true, true, true],
2728 vec![false, false, false],
2729 ],
2730 row_header: Vec::new(),
2731 col_header: Vec::new(),
2732 }),
2733 cell_blocks: None,
2734 cells: None,
2735 caption: None,
2736 caption_parent: Default::default(),
2737 caption_location: None,
2738 }));
2739 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2740 let data = &v["tables"][0]["data"];
2741 assert_eq!(data["num_rows"], 3);
2742 assert_eq!(data["num_cols"], 3);
2743 let cells = data["table_cells"].as_array().unwrap();
2744 assert_eq!(
2745 cells.len(),
2746 4,
2747 "one cell for the range, three for the plain row"
2748 );
2749 assert_eq!(
2750 cells[0],
2751 serde_json::json!({
2752 "row_span": 2, "col_span": 3,
2753 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2754 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2755 "text": "merged", "column_header": true, "row_header": false,
2756 "row_section": false, "fillable": false,
2757 })
2758 );
2759 assert_eq!(cells[1]["text"], "a");
2760 assert_eq!(cells[1]["row_span"], 1);
2761 assert_eq!(cells[1]["column_header"], false);
2762 let grid = data["grid"].as_array().unwrap();
2764 assert_eq!(grid.len(), 3);
2765 for (r, row) in grid.iter().take(2).enumerate() {
2766 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2767 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2768 }
2769 }
2770 assert_eq!(grid[2][2]["text"], "c");
2771 }
2772
2773 #[test]
2779 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2780 let mut doc = DoclingDocument::new("t");
2781 doc.push(Node::PageInfo {
2782 page_no: 1,
2783 width: 3.0,
2784 height: 4.0,
2785 });
2786 let table = crate::Table {
2787 rows: vec![vec!["a".to_string(), "b".into()]],
2788 ..Default::default()
2789 };
2790 doc.push(Node::Group {
2791 label: "sheet".into(),
2792 name: Some("Data".into()),
2793 layer: None,
2794 children: vec![
2795 Node::Prov {
2800 page_no: 1,
2801 bbox: [0.0, 0.0, 3.0, 4.0],
2802 charspan: [0, 0],
2803 seq: Some(1),
2804 inner: Box::new(Node::Table(table.clone())),
2805 },
2806 Node::Prov {
2807 page_no: 1,
2808 bbox: [0.0, 1.0, 1.0, 1.0],
2809 charspan: [0, 0],
2810 seq: Some(0),
2811 inner: Box::new(Node::Chart {
2812 kind: "bar_chart".into(),
2813 table,
2814 caption: Some("Sales".into()),
2815 location: Some([0, 128, 170, 128]),
2816 }),
2817 },
2818 ],
2819 });
2820 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2821 assert_eq!(
2822 v["pages"],
2823 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2824 );
2825 assert_eq!(
2826 v["tables"][0]["prov"],
2827 serde_json::json!([{
2828 "page_no": 1,
2829 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2830 "charspan": [0, 0],
2831 }])
2832 );
2833 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2834 let sheet = &v["groups"][0];
2837 assert_eq!(
2838 sheet["children"],
2839 serde_json::json!([
2840 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2841 ])
2842 );
2843 let cap = &v["texts"][0];
2844 assert_eq!(cap["label"], "caption");
2845 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2846 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2847 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2848 let pic = &v["pictures"][0];
2849 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2850 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2851 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2852 assert_eq!(
2853 pic["meta"]["classification"]["predictions"][0]["class_name"],
2854 "bar_chart"
2855 );
2856 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2857 }
2858
2859 #[test]
2863 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2864 let mut doc = DoclingDocument::new("t");
2865 doc.push(Node::PageInfo {
2866 page_no: 1,
2867 width: 12192000.0,
2868 height: 6858000.0,
2869 });
2870 doc.push(Node::Furniture {
2871 layer: ContentLayer::Notes,
2872 inner: Box::new(Node::Located {
2873 location: [0, 0, 0, 0],
2874 inner: Box::new(Node::Paragraph {
2875 text: "a note".into(),
2876 }),
2877 }),
2878 });
2879 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2880 let prov = &v["texts"][0]["prov"][0];
2881 assert_eq!(prov["page_no"], 1);
2882 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2883 assert_eq!(
2884 prov["bbox"],
2885 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2886 );
2887 assert_eq!(
2889 v["pages"]["1"]["size"],
2890 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2891 );
2892 }
2893
2894 #[test]
2904 fn captions_and_checkboxes_carry_their_own_prov() {
2905 let mut doc = DoclingDocument::new("t");
2906 doc.push(Node::PageInfo {
2907 page_no: 1,
2908 width: 512.0,
2909 height: 512.0,
2910 });
2911 doc.push(Node::Located {
2912 location: [64, 64, 448, 256],
2913 inner: Box::new(Node::Picture {
2914 caption: Some("Figure 1".into()),
2915 caption_href: None,
2916 image: None,
2917 classification: None,
2918 description: None,
2919 caption_parent: CaptionParent::Item,
2920 caption_location: Some([64, 264, 448, 280]),
2921 }),
2922 });
2923 doc.push(Node::PageInfo {
2924 page_no: 2,
2925 width: 512.0,
2926 height: 512.0,
2927 });
2928 doc.push(Node::Located {
2929 location: [64, 120, 448, 300],
2930 inner: Box::new(Node::Table(Table {
2931 rows: vec![vec!["a".into()]],
2932 caption: Some("Table 1".into()),
2933 caption_parent: CaptionParent::Item,
2934 caption_location: Some([64, 100, 300, 112]),
2935 ..Table::default()
2936 })),
2937 });
2938 doc.push(Node::Located {
2939 location: [64, 320, 200, 332],
2940 inner: Box::new(Node::CheckboxItem {
2941 checked: false,
2942 text: "First option".into(),
2943 }),
2944 });
2945 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2946 let texts = v["texts"].as_array().unwrap();
2947 let by_text = |t: &str| texts.iter().find(|x| x["text"] == t).expect(t).clone();
2948 let bbox = |p: &Value| {
2949 let b = &p["prov"][0]["bbox"];
2950 [&b["l"], &b["t"], &b["r"], &b["b"]].map(|x| x.as_f64().unwrap())
2951 };
2952 let fig = by_text("Figure 1");
2955 assert_eq!(fig["prov"][0]["page_no"], 1);
2956 assert_eq!(bbox(&fig), [64.0, 248.0, 448.0, 232.0]);
2957 assert_eq!(fig["prov"][0]["charspan"], serde_json::json!([0, 8]));
2958 assert_eq!(fig["parent"]["$ref"], "#/pictures/0");
2959 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
2960 let tab = by_text("Table 1");
2962 assert_eq!(tab["prov"][0]["page_no"], 2);
2963 assert_eq!(bbox(&tab), [64.0, 412.0, 300.0, 400.0]);
2964 assert_eq!(v["tables"][0]["prov"][0]["bbox"]["t"], 392.0);
2965 let cb = by_text("First option");
2967 assert_eq!(cb["label"], "checkbox_unselected");
2968 assert_eq!(cb["prov"][0]["page_no"], 2);
2969 assert_eq!(bbox(&cb), [64.0, 192.0, 200.0, 180.0]);
2970 assert!(texts
2971 .iter()
2972 .all(|t| !t["prov"].as_array().unwrap().is_empty()));
2973
2974 let mut plain = DoclingDocument::new("t");
2977 plain.push(Node::PageInfo {
2978 page_no: 1,
2979 width: 512.0,
2980 height: 512.0,
2981 });
2982 plain.push(Node::Located {
2983 location: [64, 64, 448, 256],
2984 inner: Box::new(Node::Picture {
2985 caption: Some("Figure 1".into()),
2986 caption_href: None,
2987 image: None,
2988 classification: None,
2989 description: None,
2990 caption_parent: CaptionParent::Item,
2991 caption_location: None,
2992 }),
2993 });
2994 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2995 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2996 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
2997 }
2998
2999 #[test]
3000 fn page_markers_produce_pages_and_prov() {
3001 let mut doc = DoclingDocument::new("t");
3002 doc.push(Node::PageInfo {
3003 page_no: 1,
3004 width: 512.0,
3005 height: 1024.0,
3006 });
3007 doc.push(Node::Located {
3008 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
3010 text: "hello".into(),
3011 }),
3012 });
3013 doc.push(Node::Table(Table {
3014 rows: vec![vec!["a".into()]],
3015 location: Some([0, 0, 512, 512]),
3016 ..Table::default()
3017 }));
3018 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3019 assert_eq!(v["pages"]["1"]["page_no"], 1);
3020 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
3021 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
3022 let prov = &v["texts"][0]["prov"][0];
3025 assert_eq!(prov["page_no"], 1);
3026 assert_eq!(prov["bbox"]["l"], 128.0);
3027 assert_eq!(prov["bbox"]["t"], 896.0);
3028 assert_eq!(prov["bbox"]["r"], 256.0);
3029 assert_eq!(prov["bbox"]["b"], 768.0);
3030 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
3031 assert_eq!(prov["charspan"][1], 5);
3032 let tprov = &v["tables"][0]["prov"][0];
3034 assert_eq!(tprov["bbox"]["t"], 1024.0);
3035 assert_eq!(tprov["bbox"]["b"], 0.0);
3036 assert_eq!(tprov["charspan"][1], 0);
3037
3038 let mut plain = DoclingDocument::new("t");
3040 plain.push(Node::Located {
3041 location: [1, 2, 3, 4],
3042 inner: Box::new(Node::Paragraph { text: "x".into() }),
3043 });
3044 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
3045 assert_eq!(v["pages"], serde_json::json!({}));
3046 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
3047 }
3048
3049 #[test]
3052 fn page_furniture_becomes_furniture_layer_text() {
3053 let mut doc = DoclingDocument::new("t");
3054 doc.push(Node::PageInfo {
3055 page_no: 1,
3056 width: 512.0,
3057 height: 512.0,
3058 });
3059 doc.push(Node::PageFurniture {
3060 footer: false,
3061 location: [10, 0, 100, 20],
3062 text: "Chapter 1".into(),
3063 });
3064 doc.push(Node::Paragraph {
3065 text: "body".into(),
3066 });
3067 doc.push(Node::PageFurniture {
3068 footer: true,
3069 location: [400, 490, 500, 512],
3070 text: "1.10.2".into(),
3071 });
3072 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3073 let texts = v["texts"].as_array().unwrap();
3074 assert_eq!(texts.len(), 3);
3075 assert_eq!(texts[0]["label"], "page_header");
3076 assert_eq!(texts[0]["content_layer"], "furniture");
3077 assert_eq!(texts[0]["parent"]["$ref"], "#/body");
3078 assert_eq!(texts[0]["prov"][0]["bbox"]["l"], 10.0);
3079 assert_eq!(texts[1]["content_layer"], "body");
3080 assert_eq!(texts[1]["prov"], serde_json::json!([]));
3081 assert_eq!(texts[2]["label"], "page_footer");
3082 assert_eq!(texts[2]["text"], "1.10.2");
3083 assert_eq!(texts[2]["content_layer"], "furniture");
3084 }
3085
3086 #[test]
3087 fn picture_image_in_markdown_modes_and_json() {
3088 let doc = doc_with_image();
3089 assert!(doc.export_to_markdown().contains("<!-- image -->"));
3091 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
3093 assert!(
3094 md.contains(""),
3095 "got:\n{md}"
3096 );
3097 assert!(files.is_empty());
3098 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
3100 assert!(
3101 md.contains(""),
3102 "got:\n{md}"
3103 );
3104 assert_eq!(
3105 files,
3106 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
3107 );
3108 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3111 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
3112 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
3113 let keys: Vec<&str> = v["pictures"][0]
3114 .as_object()
3115 .unwrap()
3116 .keys()
3117 .map(String::as_str)
3118 .collect();
3119 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
3120 assert_eq!(
3121 v["pictures"][0]["image"]["uri"],
3122 "data:image/png;base64,Zm9vYmFy"
3123 );
3124 }
3125
3126 #[test]
3127 fn exports_docling_schema() {
3128 let mut doc = DoclingDocument::new("t");
3129 doc.push(Node::Heading {
3130 level: 1,
3131 text: "Title".into(),
3132 });
3133 doc.push(Node::Heading {
3134 level: 2,
3135 text: "Sec".into(),
3136 });
3137 doc.push(Node::Paragraph {
3138 text: "Body & more".into(),
3139 }); doc.push(Node::ListItem {
3141 ordered: false,
3142 number: 0,
3143 first_in_list: true,
3144 text: "one".into(),
3145 level: 0,
3146 marker: None,
3147 location: None,
3148 dclx: None,
3149 href: None,
3150 layer: None,
3151 });
3152 doc.push(Node::ListItem {
3153 ordered: false,
3154 number: 0,
3155 first_in_list: false,
3156 text: "two".into(),
3157 level: 0,
3158 marker: None,
3159 location: None,
3160 dclx: None,
3161 href: None,
3162 layer: None,
3163 });
3164 doc.push(Node::Table(Table {
3165 rows: vec![vec!["A".into(), "B".into()]],
3166 location: None,
3167 structure: None,
3168 cell_blocks: None,
3169 cells: None,
3170 caption: None,
3171 caption_parent: Default::default(),
3172 caption_location: None,
3173 }));
3174
3175 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3176 assert_eq!(v["schema_name"], "DoclingDocument");
3177 assert_eq!(v["version"], "1.10.0");
3178 assert_eq!(v["texts"][0]["label"], "title");
3179 assert_eq!(v["texts"][1]["label"], "section_header");
3180 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
3184 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
3185 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
3186 assert_eq!(v["texts"][3]["marker"], "-");
3187 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
3189 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
3190 }
3191 #[test]
3195 fn a_layered_group_stamps_its_whole_subtree() {
3196 let doc = DoclingDocument {
3197 name: "s".into(),
3198 nodes: vec![
3199 Node::Group {
3200 label: "sheet".into(),
3201 name: Some("Sheet1".into()),
3202 layer: None,
3203 children: vec![Node::Paragraph {
3204 text: "visible".into(),
3205 }],
3206 },
3207 Node::Group {
3208 label: "sheet".into(),
3209 name: Some("Sheet2".into()),
3210 layer: Some(ContentLayer::Invisible),
3211 children: vec![Node::Paragraph {
3212 text: "hidden".into(),
3213 }],
3214 },
3215 ],
3216 ..DoclingDocument::new("s")
3217 };
3218 let v = crate::json::to_json(&doc);
3219 assert_eq!(v["groups"][0]["label"], "sheet");
3220 assert_eq!(v["groups"][0]["name"], "Sheet1");
3221 assert_eq!(v["groups"][0]["content_layer"], "body");
3222 assert_eq!(v["texts"][0]["content_layer"], "body");
3223 assert_eq!(v["groups"][1]["name"], "Sheet2");
3224 assert_eq!(v["groups"][1]["content_layer"], "invisible");
3225 assert_eq!(v["texts"][1]["content_layer"], "invisible");
3226 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
3228 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
3229 }
3230
3231 #[test]
3241 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
3242 use crate::tree::{ItemTree, TreeKind};
3243 let mut t = ItemTree::default();
3244 let text = |txt: &str| TreeKind::Text {
3245 label: "text".into(),
3246 text: txt.into(),
3247 orig: None,
3248 formatting: None,
3249 hyperlink: None,
3250 level: None,
3251 list: None,
3252 };
3253 let a = t.add(None, None, text("a"));
3254 let blank = t.add(None, None, text(""));
3255 let b = t.add(None, None, text("b"));
3256 t.delete(blank);
3257 let group = t.add(
3258 None,
3259 Some(ContentLayer::Notes),
3260 TreeKind::Group {
3261 label: "comment_section".into(),
3262 name: "comment-0".into(),
3263 },
3264 );
3265 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
3266 t.items[a].comments.push(group);
3267 t.add(
3268 None,
3269 None,
3270 TreeKind::Picture {
3271 captions: Vec::new(),
3272 image: None,
3273 classification: Some("bar_chart".into()),
3274 description: None,
3275 confidence: None,
3276 chart: Some(Table {
3277 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
3278 ..Table::default()
3279 }),
3280 dpi: None,
3281 },
3282 );
3283 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
3284 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
3285 let mut doc = DoclingDocument::new("t");
3286 doc.tree = Some(t);
3287 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3288 let texts = v["texts"].as_array().unwrap();
3289 assert_eq!(texts.len(), 3);
3290 assert_eq!(texts[1]["text"], "b");
3291 assert_eq!(texts[1]["self_ref"], "#/texts/1");
3292 assert_eq!(
3293 v["body"]["children"],
3294 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
3295 );
3296 let keys: Vec<&str> = texts[0]
3297 .as_object()
3298 .unwrap()
3299 .keys()
3300 .map(String::as_str)
3301 .collect();
3302 assert_eq!(
3303 keys,
3304 vec![
3305 "self_ref",
3306 "parent",
3307 "children",
3308 "content_layer",
3309 "label",
3310 "prov",
3311 "comments",
3312 "orig",
3313 "text"
3314 ]
3315 );
3316 assert_eq!(
3317 texts[0]["comments"],
3318 serde_json::json!([{"$ref": "#/groups/0"}])
3319 );
3320 assert!(texts[1].get("comments").is_none());
3321 let meta = &v["pictures"][0]["meta"];
3322 assert_eq!(
3323 meta["classification"]["predictions"][0]["class_name"],
3324 "bar_chart"
3325 );
3326 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
3327 }
3328
3329 #[test]
3335 fn tree_items_carry_exact_provenance_and_dpi() {
3336 use crate::tree::{ItemTree, TreeKind, TreeProv};
3337 let text = |label: &str, t: &str| TreeKind::Text {
3338 label: label.into(),
3339 text: t.into(),
3340 orig: None,
3341 formatting: None,
3342 hyperlink: None,
3343 level: None,
3344 list: None,
3345 };
3346 let mut t = ItemTree::default();
3347 let slide = t.add(
3348 None,
3349 None,
3350 TreeKind::Group {
3351 label: "chapter".into(),
3352 name: "slide-0".into(),
3353 },
3354 );
3355 t.add_with_prov(
3356 Some(slide),
3357 None,
3358 text("paragraph", "héllo"),
3359 TreeProv {
3360 page_no: 1,
3361 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
3362 bottom_left: true,
3363 charspan: [0, 5],
3364 },
3365 );
3366 t.add_with_prov(
3367 Some(slide),
3368 None,
3369 TreeKind::Picture {
3370 captions: Vec::new(),
3371 image: Some(crate::PictureImage {
3372 dpi: crate::PictureImage::DEFAULT_DPI,
3373 mimetype: "image/png".into(),
3374 width: 2,
3375 height: 2,
3376 data: vec![0],
3377 }),
3378 classification: None,
3379 description: None,
3380 confidence: None,
3381 chart: None,
3382 dpi: Some(300),
3383 },
3384 TreeProv {
3385 page_no: 1,
3386 bbox: [0.0; 4],
3387 bottom_left: false,
3388 charspan: [0, 0],
3389 },
3390 );
3391 t.add(
3392 Some(slide),
3393 Some(ContentLayer::Notes),
3394 text("text", "no geometry"),
3395 );
3396 let mut doc = DoclingDocument::new("t");
3397 doc.push(Node::PageInfo {
3398 page_no: 1,
3399 width: 9144000.0,
3400 height: 6858000.0,
3401 });
3402 doc.tree = Some(t);
3403 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3404 assert_eq!(
3405 v["texts"][0]["prov"],
3406 serde_json::json!([{
3407 "page_no": 1,
3408 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
3409 "charspan": [0, 5],
3410 }])
3411 );
3412 assert_eq!(v["texts"][0]["label"], "paragraph");
3413 assert_eq!(
3414 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
3415 "TOPLEFT"
3416 );
3417 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
3418 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
3419 assert_eq!(v["texts"][1]["content_layer"], "notes");
3420 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
3421 assert_eq!(v["pages"]["1"]["page_no"], 1);
3422 }
3423
3424 #[test]
3429 fn provenance_boxes_are_clamped_to_their_page() {
3430 let mut doc = DoclingDocument::new("t");
3431 doc.push(Node::PageInfo {
3432 page_no: 1,
3433 width: 10.0,
3434 height: 8.0,
3435 });
3436 doc.push(Node::Prov {
3437 page_no: 1,
3438 bbox: [-1.0, 2.0, 12.0, 9.5],
3439 charspan: [0, 1],
3440 seq: None,
3441 inner: Box::new(Node::Paragraph { text: "x".into() }),
3442 });
3443 let mut table = Table {
3444 rows: vec![vec!["a".into()]],
3445 ..Table::default()
3446 };
3447 table.cells = Some(vec![crate::TableCell {
3448 text: "a".into(),
3449 bbox: Some([1.0, 1.0, 11.0, 9.0]),
3450 start_row: 0,
3451 start_col: 0,
3452 row_span: 1,
3453 col_span: 1,
3454 column_header: false,
3455 row_header: false,
3456 row_section: false,
3457 }]);
3458 doc.push(Node::Prov {
3459 page_no: 1,
3460 bbox: [0.0, 0.0, 10.0, 8.0],
3461 charspan: [0, 0],
3462 seq: None,
3463 inner: Box::new(Node::Table(table)),
3464 });
3465 doc.push(Node::Prov {
3466 page_no: 7,
3467 bbox: [-5.0, 0.0, 50.0, 50.0],
3468 charspan: [0, 1],
3469 seq: None,
3470 inner: Box::new(Node::Paragraph { text: "y".into() }),
3471 });
3472 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3473 assert_eq!(
3474 v["texts"][0]["prov"][0]["bbox"],
3475 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
3476 );
3477 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
3478 assert_eq!(
3479 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
3480 (Some(1.0), Some(10.0), Some(8.0))
3481 );
3482 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
3483 assert_eq!(
3484 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
3485 "page 7 is not described"
3486 );
3487 }
3488
3489 #[test]
3496 fn misplaced_list_items_are_wrapped_like_docling_core() {
3497 use crate::tree::{ItemTree, ListMeta, TreeKind};
3498 let mut t = ItemTree::default();
3499 let li = |txt: &str| TreeKind::Text {
3500 label: "list_item".into(),
3501 text: txt.into(),
3502 orig: None,
3503 formatting: None,
3504 hyperlink: None,
3505 level: None,
3506 list: Some(ListMeta {
3507 enumerated: false,
3508 marker: "-".into(),
3509 }),
3510 };
3511 let group = |label: &str, name: &str| TreeKind::Group {
3512 label: label.into(),
3513 name: name.into(),
3514 };
3515 t.add(None, None, li("A"));
3516 t.add(None, None, li("B"));
3517 t.add(
3518 None,
3519 None,
3520 TreeKind::Text {
3521 label: "text".into(),
3522 text: "plain".into(),
3523 orig: None,
3524 formatting: None,
3525 hyperlink: None,
3526 level: None,
3527 list: None,
3528 },
3529 );
3530 let cell = t.add(None, None, group("unspecified", "cell"));
3531 t.add(Some(cell), None, li("C"));
3532 t.add(Some(cell), None, li("D"));
3533 let list = t.add(None, None, group("list", "list"));
3534 t.add(Some(list), None, li("E"));
3535 let doc = DoclingDocument {
3536 tree: Some(t),
3537 ..DoclingDocument::new("t")
3538 };
3539 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3540 let refs = |x: &Value| -> Vec<String> {
3541 x.as_array()
3542 .unwrap()
3543 .iter()
3544 .map(|r| r["$ref"].as_str().unwrap().to_string())
3545 .collect()
3546 };
3547 assert_eq!(
3548 refs(&v["body"]["children"]),
3549 ["#/groups/4", "#/texts/0", "#/groups/0", "#/groups/1"]
3550 );
3551 assert_eq!(
3552 refs(&v["groups"][0]["children"]),
3553 ["#/groups/3", "#/groups/2"]
3554 );
3555 for (g, name, kids) in [
3556 (2, "group", vec!["#/texts/2"]),
3557 (3, "group", vec!["#/texts/3"]),
3558 (4, "group", vec!["#/texts/4", "#/texts/5"]),
3559 (1, "list", vec!["#/texts/1"]),
3560 ] {
3561 assert_eq!(v["groups"][g]["label"], "list");
3562 assert_eq!(v["groups"][g]["name"], name);
3563 assert_eq!(refs(&v["groups"][g]["children"]), kids);
3564 }
3565 let texts: Vec<&str> = v["texts"]
3566 .as_array()
3567 .unwrap()
3568 .iter()
3569 .map(|t| t["text"].as_str().unwrap())
3570 .collect();
3571 assert_eq!(texts, ["plain", "E", "D", "C", "A", "B"]);
3572 assert_eq!(v["texts"][2]["parent"]["$ref"], "#/groups/2");
3573 }
3574
3575 #[test]
3578 fn a_deleted_child_is_not_written_as_an_empty_ref() {
3579 use crate::tree::{ItemTree, TreeKind};
3580 let mut t = ItemTree::default();
3581 let text = |txt: &str| TreeKind::Text {
3582 label: "text".into(),
3583 text: txt.into(),
3584 orig: None,
3585 formatting: None,
3586 hyperlink: None,
3587 level: None,
3588 list: None,
3589 };
3590 let g = t.add(
3591 None,
3592 None,
3593 TreeKind::Group {
3594 label: "unspecified".into(),
3595 name: "g".into(),
3596 },
3597 );
3598 let gone = t.add(Some(g), None, text(""));
3599 t.add(Some(g), None, text("kept"));
3600 t.items[gone].deleted = true; let doc = DoclingDocument {
3602 tree: Some(t),
3603 ..DoclingDocument::new("t")
3604 };
3605 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3606 assert_eq!(
3607 v["groups"][0]["children"],
3608 serde_json::json!([{"$ref": "#/texts/0"}])
3609 );
3610 }
3611
3612 #[test]
3613 fn a_backend_item_tree_is_written_verbatim() {
3614 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
3615 let mut t = ItemTree::default();
3616 let text = |label: &str, txt: &str| TreeKind::Text {
3617 label: label.into(),
3618 text: txt.into(),
3619 orig: None,
3620 formatting: None,
3621 hyperlink: None,
3622 level: None,
3623 list: None,
3624 };
3625 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
3626 let h = t.add(None, None, text("title", "Heading"));
3627 let group = t.add(
3628 Some(h),
3629 None,
3630 TreeKind::Group {
3631 label: "inline".into(),
3632 name: "group".into(),
3633 },
3634 );
3635 t.add(
3636 Some(group),
3637 None,
3638 TreeKind::Text {
3639 label: "text".into(),
3640 text: "bold".into(),
3641 orig: None,
3642 formatting: Some(Formatting {
3643 bold: true,
3644 ..Formatting::default()
3645 }),
3646 hyperlink: Some("https://example.com/".into()),
3647 level: None,
3648 list: None,
3649 },
3650 );
3651 t.add(
3652 Some(group),
3653 None,
3654 TreeKind::Code {
3655 text: "x = 1".into(),
3656 orig: None,
3657 language: Some("python".into()),
3658 formatting: None,
3659 hyperlink: None,
3660 },
3661 );
3662 let sub = t.add(
3663 Some(h),
3664 None,
3665 TreeKind::Text {
3666 label: "section_header".into(),
3667 text: "Sub".into(),
3668 orig: Some("Sub\u{2019}".into()),
3669 formatting: None,
3670 hyperlink: None,
3671 level: Some(1),
3672 list: None,
3673 },
3674 );
3675 let item = t.add(
3676 Some(sub),
3677 None,
3678 TreeKind::Text {
3679 label: "list_item".into(),
3680 text: "item".into(),
3681 orig: None,
3682 formatting: None,
3683 hyperlink: None,
3684 level: None,
3685 list: Some(ListMeta {
3686 enumerated: true,
3687 marker: "3.".into(),
3688 }),
3689 },
3690 );
3691 let _region = t.add(
3692 Some(sub),
3693 None,
3694 TreeKind::FieldRegion {
3695 items: vec![crate::FieldItem {
3696 marker: None,
3697 key: Some("Name".into()),
3698 value: Some("Duck".into()),
3699 value_kind: Some("read_only".into()),
3700 }],
3701 },
3702 );
3703 let table = t.add(
3704 Some(sub),
3705 None,
3706 TreeKind::Table {
3707 table: Table {
3708 rows: vec![vec!["a \n<".into(), "b".into()]],
3709 cells: Some(vec![
3710 crate::TableCell {
3711 text: "a \n<".into(),
3712 bbox: None,
3713 start_row: 0,
3714 start_col: 0,
3715 row_span: 3,
3716 col_span: 1,
3717 column_header: false,
3718 row_header: true,
3719 row_section: false,
3720 },
3721 crate::TableCell {
3722 text: "b".into(),
3723 bbox: None,
3724 start_row: 0,
3725 start_col: 1,
3726 row_span: 1,
3727 col_span: 1,
3728 column_header: false,
3729 row_header: false,
3730 row_section: false,
3731 },
3732 ]),
3733 ..Table::default()
3734 },
3735 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
3737 },
3738 );
3739 let cell_group = t.add(
3740 Some(table),
3741 None,
3742 TreeKind::Group {
3743 label: "unspecified".into(),
3744 name: "rich_cell_group_1_0_0".into(),
3745 },
3746 );
3747 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
3748 *rich_cells = vec![(0, 1, cell_group)];
3749 }
3750 let after = t.add(Some(sub), None, text("text", "after the region"));
3751 let _ = (title, after);
3752 let list = t.add(
3756 Some(sub),
3757 None,
3758 TreeKind::Group {
3759 label: "list".into(),
3760 name: "list".into(),
3761 },
3762 );
3763 t.reparent(item, Some(list));
3764
3765 let doc = DoclingDocument {
3766 tree: Some(t),
3767 ..DoclingDocument::new("t")
3768 };
3769 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3770 let texts: Vec<&str> = v["texts"]
3772 .as_array()
3773 .unwrap()
3774 .iter()
3775 .map(|t| t["text"].as_str().unwrap())
3776 .collect();
3777 assert_eq!(
3778 texts,
3779 [
3780 "Page",
3781 "Heading",
3782 "bold",
3783 "x = 1",
3784 "Sub",
3785 "item",
3786 "Name",
3787 "Duck",
3788 "after the region"
3789 ]
3790 );
3791 assert_eq!(
3792 v["body"]["children"],
3793 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
3794 );
3795 assert_eq!(v["texts"][0]["content_layer"], "furniture");
3796 assert_eq!(
3797 v["texts"][1]["children"],
3798 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
3799 );
3800 let bold = &v["texts"][2];
3801 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
3802 let keys: Vec<&str> = bold
3803 .as_object()
3804 .unwrap()
3805 .keys()
3806 .map(String::as_str)
3807 .collect();
3808 assert_eq!(
3809 keys,
3810 [
3811 "self_ref",
3812 "parent",
3813 "children",
3814 "content_layer",
3815 "label",
3816 "prov",
3817 "orig",
3818 "text",
3819 "formatting",
3820 "hyperlink"
3821 ]
3822 );
3823 assert_eq!(
3824 bold["formatting"],
3825 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
3826 );
3827 let code = &v["texts"][3];
3828 assert_eq!(code["label"], "code");
3829 assert_eq!(code["code_language"], "Python");
3830 let sub = &v["texts"][4];
3831 assert_eq!(sub["orig"], "Sub\u{2019}");
3832 assert_eq!(sub["level"], 1);
3833 let item = &v["texts"][5];
3834 let keys: Vec<&str> = item
3835 .as_object()
3836 .unwrap()
3837 .keys()
3838 .map(String::as_str)
3839 .collect();
3840 assert_eq!(
3841 keys,
3842 [
3843 "self_ref",
3844 "parent",
3845 "children",
3846 "content_layer",
3847 "label",
3848 "prov",
3849 "orig",
3850 "text",
3851 "enumerated",
3852 "marker"
3853 ]
3854 );
3855 assert_eq!(item["marker"], "3.");
3856 assert_eq!(v["texts"][7]["kind"], "read_only");
3857 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
3858 let table = &v["tables"][0];
3859 assert_eq!(
3860 table["children"],
3861 serde_json::json!([{"$ref": "#/groups/1"}])
3862 );
3863 let cells = table["data"]["table_cells"].as_array().unwrap();
3864 assert_eq!(
3865 cells[0]["text"], "a \n<",
3866 "raw cell text is written verbatim"
3867 );
3868 assert_eq!(
3869 cells[0]["end_row_offset_idx"], 3,
3870 "declared spans are not clamped"
3871 );
3872 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
3873 assert!(cells[0].get("ref").is_none());
3874 assert!(
3875 table["data"]["grid"][0][1].get("ref").is_none(),
3876 "the grid shows plain cells"
3877 );
3878 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
3879 }
3880
3881 #[test]
3885 fn a_comment_section_can_be_referenced_by_its_note_text() {
3886 let doc = DoclingDocument {
3887 name: "c".into(),
3888 nodes: vec![
3889 Node::Commented {
3890 comments: vec![0],
3891 inner: Box::new(Node::Paragraph {
3892 text: "annotated".into(),
3893 }),
3894 },
3895 Node::CommentSection {
3896 name: "comment-Sheet1-A1".into(),
3897 text: "[author: A]: note".into(),
3898 refs_note_text: true,
3899 grouped: true,
3900 },
3901 ],
3902 ..DoclingDocument::new("c")
3903 };
3904 let v = crate::json::to_json(&doc);
3905 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
3906 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
3907 }
3908
3909 #[test]
3913 fn comment_sections_link_back_to_their_items() {
3914 let doc = DoclingDocument {
3915 name: "c".into(),
3916 nodes: vec![
3917 Node::Commented {
3918 comments: vec![0],
3919 inner: Box::new(Node::Paragraph {
3920 text: "annotated".into(),
3921 }),
3922 },
3923 Node::Paragraph {
3924 text: "plain".into(),
3925 },
3926 Node::CommentSection {
3927 name: "comment-7".into(),
3928 text: "[time: t]: note".into(),
3929 refs_note_text: false,
3930 grouped: true,
3931 },
3932 ],
3933 ..DoclingDocument::new("c")
3934 };
3935 let v = crate::json::to_json(&doc);
3936 assert_eq!(v["groups"][0]["label"], "comment_section");
3938 assert_eq!(v["groups"][0]["name"], "comment-7");
3939 assert_eq!(v["groups"][0]["content_layer"], "notes");
3940 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
3941 assert_eq!(v["texts"][2]["content_layer"], "notes");
3942 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
3944 assert!(v["texts"][1].get("comments").is_none());
3945 let keys: Vec<&str> = v["texts"][0]
3947 .as_object()
3948 .unwrap()
3949 .keys()
3950 .map(String::as_str)
3951 .collect();
3952 assert_eq!(
3953 &keys[keys.len() - 4..],
3954 &["prov", "comments", "orig", "text"]
3955 );
3956 }
3957
3958 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
3959 Node::Picture {
3960 caption: Some(caption.into()),
3961 caption_href: None,
3962 image: None,
3963 classification: None,
3964 description: None,
3965 caption_parent,
3966 caption_location: None,
3967 }
3968 }
3969
3970 fn group(children: Vec<Node>) -> Node {
3971 Node::Group {
3972 label: "section".into(),
3973 name: None,
3974 layer: None,
3975 children,
3976 }
3977 }
3978
3979 fn refs(v: &Value) -> Vec<&str> {
3980 v.as_array()
3981 .unwrap()
3982 .iter()
3983 .map(|r| r["$ref"].as_str().unwrap())
3984 .collect()
3985 }
3986
3987 #[test]
3992 fn a_body_caption_follows_the_enclosing_top_level_item() {
3993 let mut doc = DoclingDocument::new("t");
3994 doc.push(picture("top", CaptionParent::Body));
3995 doc.push(group(vec![
3996 Node::Paragraph { text: "p".into() },
3997 picture("nested", CaptionParent::Body),
3998 ]));
3999 doc.push(Node::Paragraph {
4000 text: "after".into(),
4001 });
4002 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4003 assert_eq!(
4004 refs(&v["body"]["children"]),
4005 [
4006 "#/texts/0",
4007 "#/pictures/0",
4008 "#/groups/0",
4009 "#/texts/2",
4010 "#/texts/3"
4011 ]
4012 );
4013 assert_eq!(
4014 refs(&v["groups"][0]["children"]),
4015 ["#/texts/1", "#/pictures/1"]
4016 );
4017 for (cap, pic) in [(0, 0), (2, 1)] {
4018 assert_eq!(v["texts"][cap]["label"], "caption");
4019 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
4020 assert_eq!(
4021 refs(&v["pictures"][pic]["captions"]),
4022 [format!("#/texts/{cap}")]
4023 );
4024 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
4025 }
4026 }
4027
4028 #[test]
4031 fn an_item_caption_is_the_items_first_child() {
4032 let mut doc = DoclingDocument::new("t");
4033 doc.push(picture("fig", CaptionParent::Item));
4034 doc.push(Node::Table(Table {
4035 rows: vec![vec!["a".into()]],
4036 caption: Some("tab".into()),
4037 caption_parent: CaptionParent::Item,
4038 ..Table::default()
4039 }));
4040 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4041 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
4042 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
4043 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
4044 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
4045 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
4046 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
4047 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
4048 }
4049
4050 #[test]
4056 fn picture_children_hang_off_the_picture_after_its_caption() {
4057 let mut doc = DoclingDocument::new("t");
4058 doc.push(picture("fig", CaptionParent::Item));
4059 doc.push(Node::PictureChildren(vec![
4060 Node::Heading {
4061 level: 2,
4062 text: "in-figure title".into(),
4063 },
4064 Node::Paragraph {
4065 text: "axis label".into(),
4066 },
4067 Node::ListItem {
4068 ordered: false,
4069 number: 0,
4070 first_in_list: true,
4071 text: "callout".into(),
4072 level: 0,
4073 marker: None,
4074 location: None,
4075 dclx: None,
4076 href: None,
4077 layer: None,
4078 },
4079 ]));
4080 doc.push(Node::Paragraph {
4081 text: "after".into(),
4082 });
4083 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4084 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/texts/4"]);
4085 assert_eq!(
4086 refs(&v["pictures"][0]["children"]),
4087 ["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"]
4088 );
4089 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
4090 assert_eq!(v["texts"][1]["label"], "section_header");
4091 assert_eq!(v["texts"][2]["label"], "text");
4092 for t in 1..=2 {
4093 assert_eq!(v["texts"][t]["parent"]["$ref"], "#/pictures/0");
4094 assert_eq!(v["texts"][t]["content_layer"], "body");
4095 }
4096 assert_eq!(v["groups"][0]["parent"]["$ref"], "#/pictures/0");
4097 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
4098 let md = doc.export_to_markdown();
4099 assert!(
4100 !md.contains("axis label") && !md.contains("callout"),
4101 "{md}"
4102 );
4103 assert!(md.contains("after"), "{md}");
4104 }
4105
4106 #[test]
4110 fn a_container_caption_is_the_items_sibling() {
4111 let mut doc = DoclingDocument::new("t");
4112 doc.push(group(vec![
4113 picture("chart", CaptionParent::Container),
4114 Node::Table(Table {
4115 rows: vec![vec!["a".into()]],
4116 caption: Some("figcaption".into()),
4117 caption_parent: CaptionParent::ContainerAfter,
4118 ..Table::default()
4119 }),
4120 ]));
4121 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4122 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
4123 assert_eq!(
4124 refs(&v["groups"][0]["children"]),
4125 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
4126 );
4127 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
4128 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
4129 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
4130 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
4131 }
4132
4133 #[test]
4138 fn a_labeled_text_keeps_its_label_and_hyperlink_out_of_the_text() {
4139 let mut doc = DoclingDocument::new("t");
4140 doc.push(Node::LabeledText {
4141 label: "footnote".into(),
4142 text: "1 https://example.com/a\\_b".into(),
4143 href: Some("https://example.com/a_b".into()),
4144 });
4145 doc.push(Node::LabeledText {
4146 label: "footnote".into(),
4147 text: "© 2022 the authors".into(),
4148 href: None,
4149 });
4150 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4151 let t = &v["texts"][0];
4152 assert_eq!(t["label"], "footnote");
4153 assert_eq!(t["text"], "1 https://example.com/a_b");
4154 assert_eq!(t["orig"], "1 https://example.com/a_b");
4155 assert_eq!(t["hyperlink"], "https://example.com/a_b");
4156 assert_eq!(v["texts"][1]["label"], "footnote");
4157 assert!(v["texts"][1].get("hyperlink").is_none());
4158 assert_eq!(
4159 doc.export_to_markdown(),
4160 "[1 https://example.com/a\\_b](https://example.com/a_b)\n\n© 2022 the authors\n"
4161 );
4162 assert_eq!(
4163 doc.export_to_text(),
4164 "1 https://example.com/a_b\n\n© 2022 the authors"
4165 );
4166 }
4167}