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