1use serde_json::{json, Value};
12
13use crate::document::{CaptionParent, ContentLayer, DoclingDocument, Node, Table};
14
15const SCHEMA_VERSION: &str = "1.10.0";
16
17const CODE_LANGUAGES: &[&str] = &[
20 "Ada",
21 "Awk",
22 "Bash",
23 "bc",
24 "C",
25 "C#",
26 "C++",
27 "CMake",
28 "COBOL",
29 "CSS",
30 "Ceylon",
31 "Clojure",
32 "Crystal",
33 "Cuda",
34 "Cython",
35 "D",
36 "Dart",
37 "dc",
38 "Dockerfile",
39 "DocLang",
40 "Elixir",
41 "Erlang",
42 "FORTRAN",
43 "Forth",
44 "Go",
45 "HTML",
46 "Haskell",
47 "Haxe",
48 "Java",
49 "JavaScript",
50 "JSON",
51 "Julia",
52 "Kotlin",
53 "Latex",
54 "Lisp",
55 "Lua",
56 "Matlab",
57 "MoonScript",
58 "Nim",
59 "OCaml",
60 "ObjectiveC",
61 "Octave",
62 "PHP",
63 "Pascal",
64 "Perl",
65 "Prolog",
66 "Python",
67 "Racket",
68 "Ruby",
69 "Rust",
70 "SML",
71 "SQL",
72 "Scala",
73 "Scheme",
74 "Swift",
75 "Tikz",
76 "TypeScript",
77 "VisualBasic",
78 "XML",
79 "YAML",
80];
81
82pub fn code_language_label(lang: &str) -> &'static str {
88 code_language(Some(lang))
89}
90
91pub(crate) fn code_language(lang: Option<&str>) -> &'static str {
92 match lang {
93 Some(l) => CODE_LANGUAGES
94 .iter()
95 .find(|c| c.eq_ignore_ascii_case(l))
96 .copied()
97 .unwrap_or("unknown"),
98 None => "unknown",
99 }
100}
101
102#[derive(Clone, Copy)]
106struct ExactProv {
107 page_no: usize,
108 bbox: [f64; 4],
109 bottom_left: bool,
110 charspan: [usize; 2],
111}
112
113impl From<&crate::tree::TreeProv> for ExactProv {
114 fn from(p: &crate::tree::TreeProv) -> Self {
115 ExactProv {
116 page_no: p.page_no,
117 bbox: p.bbox,
118 bottom_left: p.bottom_left,
119 charspan: p.charspan,
120 }
121 }
122}
123
124type PageSizes = std::collections::HashMap<usize, (f64, f64)>;
126
127fn page_sizes(pages: &[(usize, f64, f64)]) -> PageSizes {
128 let mut sizes = PageSizes::with_capacity(pages.len());
129 for &(p, w, h) in pages {
130 sizes.entry(p).or_insert((w, h));
131 }
132 sizes
133}
134
135fn clamp_boxes_to_pages(b: &mut Builder) {
142 if b.pages.is_empty() {
143 return;
144 }
145 let sizes = page_sizes(&b.pages);
147 for item in &mut b.texts {
148 match item {
149 Item::Text(t) => t.prov.clamp(&sizes),
150 Item::Json(v) => clamp_item(v, &sizes),
151 }
152 }
153 for item in b
154 .pictures
155 .iter_mut()
156 .chain(b.tables.iter_mut())
157 .chain(b.key_value_items.iter_mut())
158 {
159 clamp_item(item, &sizes);
160 }
161}
162
163fn clamp_item(item: &mut Value, sizes: &PageSizes) {
164 let size = |page_no: &Value| -> Option<(f64, f64)> {
165 let n = page_no.as_u64()? as usize;
166 sizes.get(&n).copied()
167 };
168 let clamp_bbox = |bbox: &mut Value, (w, h): (f64, f64)| {
169 for (key, hi) in [("l", w), ("r", w), ("t", h), ("b", h)] {
170 if let Some(v) = bbox.get(key).and_then(Value::as_f64) {
171 bbox[key] = json!(round2(v.clamp(0.0, hi.max(0.0))));
172 }
173 }
174 };
175 let mut table_page: Option<Option<(f64, f64)>> = None;
176 if let Some(provs) = item.get_mut("prov").and_then(Value::as_array_mut) {
177 let mut page_nos: Vec<u64> = Vec::new();
178 for prov in provs.iter_mut() {
179 if let Some(n) = prov.get("page_no").and_then(Value::as_u64) {
180 page_nos.push(n);
181 }
182 let Some(sz) = prov.get("page_no").and_then(size) else {
183 continue;
184 };
185 if let Some(bbox) = prov.get_mut("bbox") {
186 clamp_bbox(bbox, sz);
187 }
188 }
189 page_nos.sort_unstable();
190 page_nos.dedup();
191 if let [only] = page_nos[..] {
192 table_page = Some(size(&json!(only)));
193 }
194 }
195 if let (Some(Some(sz)), Some(data)) = (table_page, item.get_mut("data")) {
197 for key in ["table_cells", "grid"] {
198 let Some(rows) = data.get_mut(key).and_then(Value::as_array_mut) else {
199 continue;
200 };
201 for entry in rows.iter_mut() {
202 let cells: Vec<&mut Value> = match entry {
203 Value::Array(row) => row.iter_mut().collect(),
204 other => vec![other],
205 };
206 for cell in cells {
207 if let Some(bbox) = cell.get_mut("bbox").filter(|b| b.is_object()) {
208 clamp_bbox(bbox, sz);
209 }
210 }
211 }
212 }
213 }
214}
215
216fn formatting_json(f: &crate::tree::Formatting) -> Value {
218 json!({
219 "bold": f.bold,
220 "italic": f.italic,
221 "underline": f.underline,
222 "strikethrough": f.strikethrough,
223 "script": match f.script {
224 crate::Script::Baseline => "baseline",
225 crate::Script::Sub => "sub",
226 crate::Script::Super => "super",
227 },
228 })
229}
230
231fn track_json(t: &crate::tree::TreeTrack) -> Value {
234 let mut m = serde_json::Map::new();
235 m.insert("kind".into(), json!("track"));
236 m.insert("start_time".into(), json!(t.start_time));
237 m.insert("end_time".into(), json!(t.end_time));
238 if let Some(id) = &t.identifier {
239 m.insert("identifier".into(), json!(id));
240 }
241 if let Some(v) = &t.voice {
242 m.insert("voice".into(), json!(v));
243 }
244 Value::Object(m)
245}
246
247fn image_ref_json(img: &crate::PictureImage) -> Value {
251 let (mimetype, uri) = crate::pixel_digest::docling_data_uri(img);
252 json!({
253 "mimetype": mimetype,
254 "dpi": img.dpi,
255 "size": { "width": img.width as f64, "height": img.height as f64 },
256 "uri": uri,
257 })
258}
259
260pub fn to_json(doc: &DoclingDocument) -> Value {
262 build_json(doc, false)
263}
264
265pub(crate) fn to_json_with_notes(doc: &DoclingDocument) -> Value {
270 build_json(doc, true)
271}
272
273fn build_parts(doc: &DoclingDocument, note_keys: bool) -> Parts {
274 let mut b = Builder {
275 note_keys,
276 ..Builder::default()
277 };
278 let body = match &doc.tree {
282 Some(tree) => {
283 for n in &doc.nodes {
286 if let Node::PageInfo {
287 page_no,
288 width,
289 height,
290 } = n
291 {
292 if *page_no > 0 {
293 b.pages.push((*page_no, *width as f64, *height as f64));
294 }
295 }
296 }
297 let mut tree = tree.clone();
301 tree.wrap_misplaced_list_items();
302 b.write_tree(&tree).into_iter().map(Child::json).collect()
303 }
304 None => b.walk_into(&doc.nodes, "#/body"),
305 };
306 b.link_comments();
307
308 clamp_boxes_to_pages(&mut b);
317
318 let pages = b
319 .pages
320 .iter()
321 .map(|(n, w, h)| {
322 let r2 = |v: f64| (v * 100.0).round() / 100.0;
323 let mut page = serde_json::Map::new();
326 page.insert("size".into(), json!({ "width": r2(*w), "height": r2(*h) }));
327 if let Some(img) = doc.page_images.get(n) {
328 page.insert("image".into(), image_ref_json(img));
329 }
330 page.insert("page_no".into(), json!(n));
331 (n.to_string(), Value::Object(page))
332 })
333 .collect::<serde_json::Map<String, Value>>();
334 let fields = (!b.field_regions.is_empty()).then_some((b.field_regions, b.field_items));
337 Parts {
338 name: doc.name.clone(),
339 origin: json!({
340 "mimetype": "text/plain",
341 "binary_hash": fnv1a(&doc.name),
342 "filename": doc.name,
343 }),
344 furniture: json!({
345 "self_ref": "#/furniture",
346 "children": [],
347 "content_layer": "furniture",
348 "name": "_root_",
349 "label": "unspecified",
350 }),
351 body,
352 groups: b.groups,
353 texts: b.texts,
354 pictures: b.pictures,
355 tables: b.tables,
356 key_value_items: b.key_value_items,
357 fields,
358 pages,
359 }
360}
361
362fn build_json(doc: &DoclingDocument, note_keys: bool) -> Value {
363 build_parts(doc, note_keys).into_value()
364}
365
366struct Parts {
371 name: String,
372 origin: Value,
373 furniture: Value,
374 body: Vec<Child>,
376 groups: Vec<Value>,
377 texts: Vec<Item>,
378 pictures: Vec<Value>,
379 tables: Vec<Value>,
380 key_value_items: Vec<Value>,
381 fields: Option<(Vec<Value>, Vec<Value>)>,
384 pages: serde_json::Map<String, Value>,
385}
386
387impl Parts {
388 fn into_value(self) -> Value {
389 let mut out = serde_json::Map::with_capacity(15);
391 out.insert("schema_name".into(), "DoclingDocument".into());
392 out.insert("version".into(), SCHEMA_VERSION.into());
393 out.insert("name".into(), Value::String(self.name));
394 out.insert("origin".into(), self.origin);
395 out.insert("furniture".into(), self.furniture);
396 out.insert(
397 "body".into(),
398 object([
399 ("self_ref", "#/body".into()),
400 (
401 "children",
402 Value::Array(self.body.into_iter().map(Child::into_value).collect()),
403 ),
404 ("content_layer", "body".into()),
405 ("name", "_root_".into()),
406 ("label", "unspecified".into()),
407 ]),
408 );
409 out.insert("groups".into(), Value::Array(self.groups));
410 out.insert(
411 "texts".into(),
412 Value::Array(
413 self.texts
414 .into_iter()
415 .enumerate()
416 .map(|(i, item)| item.into_value(i))
417 .collect(),
418 ),
419 );
420 out.insert("pictures".into(), Value::Array(self.pictures));
421 out.insert("tables".into(), Value::Array(self.tables));
422 out.insert("key_value_items".into(), Value::Array(self.key_value_items));
423 out.insert("form_items".into(), Value::Array(Vec::new()));
424 if let Some((regions, items)) = self.fields {
425 out.insert("field_regions".into(), Value::Array(regions));
426 out.insert("field_items".into(), Value::Array(items));
427 }
428 out.insert("pages".into(), Value::Object(self.pages));
429 Value::Object(out)
430 }
431}
432
433impl serde::Serialize for Parts {
434 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
435 use serde::ser::SerializeMap;
436 let mut m = s.serialize_map(Some(13 + 2 * usize::from(self.fields.is_some())))?;
437 m.serialize_entry("schema_name", "DoclingDocument")?;
438 m.serialize_entry("version", SCHEMA_VERSION)?;
439 m.serialize_entry("name", &self.name)?;
440 m.serialize_entry("origin", &self.origin)?;
441 m.serialize_entry("furniture", &self.furniture)?;
442 m.serialize_entry("body", &Body(&self.body))?;
443 m.serialize_entry("groups", &self.groups)?;
444 m.serialize_entry("texts", &Texts(&self.texts))?;
445 m.serialize_entry("pictures", &self.pictures)?;
446 m.serialize_entry("tables", &self.tables)?;
447 m.serialize_entry("key_value_items", &self.key_value_items)?;
448 m.serialize_entry("form_items", &[(); 0])?;
449 if let Some((regions, items)) = &self.fields {
450 m.serialize_entry("field_regions", regions)?;
451 m.serialize_entry("field_items", items)?;
452 }
453 m.serialize_entry("pages", &self.pages)?;
454 m.end()
455 }
456}
457
458struct Body<'a>(&'a [Child]);
460
461impl serde::Serialize for Body<'_> {
462 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
463 use serde::ser::SerializeMap;
464 let mut m = s.serialize_map(Some(5))?;
465 m.serialize_entry("self_ref", "#/body")?;
466 m.serialize_entry("children", self.0)?;
467 m.serialize_entry("content_layer", "body")?;
468 m.serialize_entry("name", "_root_")?;
469 m.serialize_entry("label", "unspecified")?;
470 m.end()
471 }
472}
473
474pub fn write_json<W: std::io::Write>(
481 doc: &DoclingDocument,
482 w: W,
483 pretty: bool,
484) -> serde_json::Result<()> {
485 use serde::Serialize;
486 let parts = build_parts(doc, false);
487 if pretty {
488 parts.serialize(&mut serde_json::Serializer::pretty(w))
489 } else {
490 parts.serialize(&mut serde_json::Serializer::new(w))
491 }
492}
493
494fn classification_meta(classes: &[crate::PictureClass]) -> Value {
501 json!({
502 "classification": {
503 "predictions": classes.iter().map(|c| json!({
504 "confidence": c.confidence as f64,
505 "created_by": "DocumentPictureClassifier",
506 "class_name": c.class_name,
507 })).collect::<Vec<_>>(),
508 },
509 "annotations": [{
510 "kind": "classification",
511 "provenance": "DocumentPictureClassifier",
512 "predicted_classes": classes.iter().map(|c| json!({
513 "class_name": c.class_name,
514 "confidence": c.confidence as f64,
515 })).collect::<Vec<_>>(),
516 }],
517 })
518}
519
520#[allow(clippy::too_many_arguments)]
531fn cell_value(
532 row_span: usize,
533 col_span: usize,
534 start_row: usize,
535 end_row: usize,
536 start_col: usize,
537 end_col: usize,
538 text: String,
539 column_header: bool,
540 row_header: bool,
541 row_section: bool,
542 bbox: Option<[f32; 4]>,
543) -> Value {
544 let mut m = serde_json::Map::with_capacity(12);
545 m.insert("row_span".into(), row_span.into());
546 m.insert("col_span".into(), col_span.into());
547 m.insert("start_row_offset_idx".into(), start_row.into());
548 m.insert("end_row_offset_idx".into(), end_row.into());
549 m.insert("start_col_offset_idx".into(), start_col.into());
550 m.insert("end_col_offset_idx".into(), end_col.into());
551 m.insert("text".into(), Value::String(text));
552 m.insert("column_header".into(), column_header.into());
553 m.insert("row_header".into(), row_header.into());
554 m.insert("row_section".into(), row_section.into());
555 m.insert("fillable".into(), false.into());
556 if let Some(b) = bbox {
557 m.insert(
558 "bbox".into(),
559 json!({
560 "l": b[0], "t": b[1], "r": b[2], "b": b[3],
561 "coord_origin": "TOPLEFT",
562 }),
563 );
564 }
565 Value::Object(m)
566}
567
568fn table_data(t: &Table) -> Value {
571 table_data_with(t, false)
572}
573
574fn table_data_with(t: &Table, raw: bool) -> Value {
577 let cell_text = |s: &str| {
578 if raw {
579 s.to_string()
580 } else {
581 unescape_text(&crate::markdown::strip_hard_breaks(s))
582 }
583 };
584 let mut num_rows = t.rows.len();
585 let mut num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
586 let ragged = t.rows.iter().any(|r| r.len() != num_cols);
594 if let Some(first_class) = t.cells.as_ref().filter(|c| ragged && !c.is_empty()) {
595 for c in first_class {
596 num_rows = num_rows.max(c.start_row + c.row_span);
597 num_cols = num_cols.max(c.start_col + c.col_span);
598 }
599 }
600 let mut grid = Vec::with_capacity(num_rows);
601 let mut cells = Vec::new();
602 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
606 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
607 for c in first_class {
608 let idx = cells.len();
609 cells.push(cell_value(
610 c.row_span,
611 c.col_span,
612 c.start_row,
613 c.start_row + c.row_span,
614 c.start_col,
615 c.start_col + c.col_span,
616 cell_text(&c.text),
617 c.column_header,
618 c.row_header,
619 c.row_section,
620 c.bbox,
621 ));
622 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
623 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
624 slot[r * num_cols + k] = Some(idx);
625 }
626 }
627 }
628 for r in 0..num_rows {
629 let mut grid_row = Vec::with_capacity(num_cols);
630 for c in 0..num_cols {
631 grid_row.push(match slot[r * num_cols + c] {
632 Some(i) => cells[i].clone(),
633 None => cell_value(
634 1,
635 1,
636 r,
637 r + 1,
638 c,
639 c + 1,
640 String::new(),
641 false,
642 false,
643 false,
644 None,
645 ),
646 });
647 }
648 grid.push(grid_row);
649 }
650 } else {
651 let s = t.structure.as_ref();
652 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
653 grid.and_then(|g| g.get(r))
654 .and_then(|row| row.get(c))
655 .copied()
656 .unwrap_or(false)
657 };
658 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
659 let (mut r0, mut c0) = (r, c);
660 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
661 c0 -= 1;
662 }
663 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
664 r0 -= 1;
665 }
666 (r0, c0)
667 };
668 let anchors: Vec<(usize, usize)> = (0..num_rows)
671 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
672 .map(|(r, c)| anchor_of(r, c))
673 .collect();
674 let mut extent: Vec<(usize, usize)> = (0..num_rows)
675 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
676 .collect();
677 for (i, &(ar, ac)) in anchors.iter().enumerate() {
678 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
679 let e = &mut extent[ar * num_cols + ac];
680 e.0 = e.0.max(r);
681 e.1 = e.1.max(c);
682 }
683 for (r, row) in t.rows.iter().enumerate() {
684 let mut grid_row = Vec::with_capacity(num_cols);
685 for c in 0..num_cols {
686 let (ar, ac) = anchors[r * num_cols + c];
687 if (ar, ac) == (r, c) {
688 let (er, ec) = extent[r * num_cols + c];
689 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
690 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
691 Some(s) => flag(Some(&s.col_header), r, c),
692 None => r == 0,
693 };
694 slot[r * num_cols + c] = Some(cells.len());
695 cells.push(cell_value(
696 er - r + 1,
697 ec - c + 1,
698 r,
699 er + 1,
700 c,
701 ec + 1,
702 text,
703 column_header,
704 flag(s.map(|s| &s.row_header), r, c),
705 false,
706 None,
707 ));
708 }
709 grid_row.push(match slot[ar * num_cols + ac] {
710 Some(i) => cells[i].clone(),
711 None => Value::Null,
712 });
713 }
714 grid.push(grid_row);
715 }
716 }
717 json!({
718 "table_cells": cells,
719 "num_rows": num_rows,
720 "num_cols": num_cols,
721 "orientation": "rot_0",
722 "grid": grid,
723 })
724}
725
726#[derive(Clone, Copy)]
730enum ProvSlot {
731 None,
732 One {
733 page_no: usize,
734 bbox: [f64; 4],
736 bottom_left: bool,
737 charspan: [usize; 2],
738 },
739}
740
741impl ProvSlot {
742 fn origin(bottom_left: bool) -> &'static str {
743 if bottom_left {
744 "BOTTOMLEFT"
745 } else {
746 "TOPLEFT"
747 }
748 }
749
750 fn to_value(self) -> Value {
753 let ProvSlot::One {
754 page_no,
755 bbox: [l, t, r, b],
756 bottom_left,
757 charspan,
758 } = self
759 else {
760 return Value::Array(Vec::new());
761 };
762 let bbox = object([
763 ("l", l.into()),
764 ("t", t.into()),
765 ("r", r.into()),
766 ("b", b.into()),
767 ("coord_origin", Self::origin(bottom_left).into()),
768 ]);
769 let span = Value::Array(vec![charspan[0].into(), charspan[1].into()]);
770 Value::Array(vec![object([
771 ("page_no", page_no.into()),
772 ("bbox", bbox),
773 ("charspan", span),
774 ])])
775 }
776
777 fn clamp(&mut self, sizes: &PageSizes) {
779 if let ProvSlot::One { page_no, bbox, .. } = self {
780 if let Some(&(w, h)) = sizes.get(page_no) {
781 for (v, hi) in bbox.iter_mut().zip([w, h, w, h]) {
782 if v.is_finite() {
785 *v = round2(v.clamp(0.0, hi.max(0.0)));
786 }
787 }
788 }
789 }
790 }
791}
792
793impl serde::Serialize for ProvSlot {
794 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
795 use serde::ser::{SerializeMap, SerializeSeq};
796 let ProvSlot::One {
797 page_no,
798 bbox: [l, t, r, b],
799 bottom_left,
800 charspan,
801 } = *self
802 else {
803 return s.serialize_seq(Some(0))?.end();
804 };
805 struct Bbox([f64; 4], &'static str);
806 impl serde::Serialize for Bbox {
807 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
808 let mut m = s.serialize_map(Some(5))?;
809 for (k, v) in ["l", "t", "r", "b"].iter().zip(self.0) {
810 m.serialize_entry(k, &v)?;
811 }
812 m.serialize_entry("coord_origin", self.1)?;
813 m.end()
814 }
815 }
816 struct Entry(usize, Bbox, [usize; 2]);
817 impl serde::Serialize for Entry {
818 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
819 let mut m = s.serialize_map(Some(3))?;
820 m.serialize_entry("page_no", &self.0)?;
821 m.serialize_entry("bbox", &self.1)?;
822 m.serialize_entry("charspan", &self.2)?;
823 m.end()
824 }
825 }
826 let mut seq = s.serialize_seq(Some(1))?;
827 seq.serialize_element(&Entry(
828 page_no,
829 Bbox([l, t, r, b], Self::origin(bottom_left)),
830 charspan,
831 ))?;
832 seq.end()
833 }
834}
835
836struct TextItem {
843 parent: Box<str>,
844 label: Box<str>,
845 prov: ProvSlot,
846 text: Box<str>,
847}
848
849impl TextItem {
850 fn into_value(self, index: usize) -> Value {
851 let text = String::from(self.text);
852 object([
853 ("self_ref", Value::String(format!("#/texts/{index}"))),
854 ("parent", ref_value(self.parent)),
855 ("children", Value::Array(Vec::new())),
856 ("content_layer", "body".into()),
857 ("label", Value::String(self.label.into())),
858 ("prov", self.prov.to_value()),
859 ("orig", Value::String(text.clone())),
860 ("text", Value::String(text)),
861 ])
862 }
863}
864
865enum Item {
869 Text(Box<TextItem>),
870 Json(Box<Value>),
871}
872
873impl Item {
874 fn json(v: Value) -> Self {
875 Item::Json(Box::new(v))
876 }
877
878 fn value_mut(&mut self, index: usize) -> &mut Value {
881 if let Item::Text(t) = self {
882 let t = std::mem::replace(
883 t,
884 Box::new(TextItem {
885 parent: "".into(),
886 label: "".into(),
887 prov: ProvSlot::None,
888 text: "".into(),
889 }),
890 );
891 *self = Item::json(t.into_value(index));
892 }
893 match self {
894 Item::Json(v) => v,
895 Item::Text(_) => unreachable!("converted above"),
896 }
897 }
898
899 fn into_value(self, index: usize) -> Value {
900 match self {
901 Item::Text(t) => t.into_value(index),
902 Item::Json(v) => *v,
903 }
904 }
905}
906
907struct Texts<'a>(&'a [Item]);
909
910impl serde::Serialize for Texts<'_> {
911 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
912 use serde::ser::{SerializeMap, SerializeSeq};
913 struct Text<'a>(usize, &'a TextItem);
914 impl serde::Serialize for Text<'_> {
915 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
916 let Text(i, t) = *self;
917 let mut m = s.serialize_map(Some(8))?;
918 m.serialize_entry("self_ref", &format_args!("#/texts/{i}"))?;
919 m.serialize_entry("parent", &Child::Ref(t.parent.clone()))?;
920 m.serialize_entry("children", &[(); 0])?;
921 m.serialize_entry("content_layer", "body")?;
922 m.serialize_entry("label", &t.label)?;
923 m.serialize_entry("prov", &t.prov)?;
924 m.serialize_entry("orig", &t.text)?;
925 m.serialize_entry("text", &t.text)?;
926 m.end()
927 }
928 }
929 let mut seq = s.serialize_seq(Some(self.0.len()))?;
930 for (i, item) in self.0.iter().enumerate() {
931 match item {
932 Item::Text(t) => seq.serialize_element(&Text(i, t))?,
933 Item::Json(v) => seq.serialize_element(v)?,
934 }
935 }
936 seq.end()
937 }
938}
939
940enum Child {
944 Ref(Box<str>),
945 Json(Box<Value>),
946}
947
948impl Child {
949 fn json(v: Value) -> Self {
950 Child::Json(Box::new(v))
951 }
952
953 fn into_value(self) -> Value {
954 match self {
955 Child::Ref(r) => ref_value(r),
956 Child::Json(v) => *v,
957 }
958 }
959}
960
961impl serde::Serialize for Child {
962 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
963 use serde::ser::SerializeMap;
964 match self {
965 Child::Ref(r) => {
966 let mut m = s.serialize_map(Some(1))?;
967 m.serialize_entry("$ref", r)?;
968 m.end()
969 }
970 Child::Json(v) => v.serialize(s),
971 }
972 }
973}
974
975fn round2(v: f64) -> f64 {
976 (v * 100.0).round() / 100.0
977}
978
979#[derive(Default)]
980struct Builder {
981 note_keys: bool,
985 texts: Vec<Item>,
986 groups: Vec<Value>,
987 tables: Vec<Value>,
988 pictures: Vec<Value>,
989 field_regions: Vec<Value>,
990 field_items: Vec<Value>,
991 key_value_items: Vec<Value>,
992 pages: Vec<(usize, f64, f64)>,
996 cur_page: usize,
998 cur_w: f64,
999 cur_h: f64,
1000 pending_loc: Option<[u16; 4]>,
1003 pending_exact: Option<ExactProv>,
1006 pending_track: Option<(crate::tree::TreeTrack, String)>,
1009 pending_siblings: Vec<Value>,
1013 pending_after: Vec<Value>,
1017 pending_body: Vec<Value>,
1021 comment_groups: Vec<String>,
1025 last_comment_section: Option<(String, String)>,
1030 pending_comments: Vec<(String, Vec<usize>)>,
1034}
1035
1036impl Builder {
1037 fn take_prov(&mut self, char_len: usize) -> Value {
1043 self.take_prov_slot(char_len).to_value()
1044 }
1045
1046 fn take_prov_slot(&mut self, char_len: usize) -> ProvSlot {
1048 let prov = self.prov_slot(char_len, false);
1049 self.pending_exact = None;
1050 self.pending_loc = None;
1051 prov
1052 }
1053
1054 fn prov_slot(&self, char_len: usize, span_over_text: bool) -> ProvSlot {
1059 let r2 = round2;
1060 if let Some(ExactProv {
1061 page_no,
1062 bbox: [l, t, r, b],
1063 bottom_left,
1064 charspan,
1065 }) = self.pending_exact
1066 {
1067 let charspan = if span_over_text {
1068 [0, char_len]
1069 } else {
1070 charspan
1071 };
1072 return ProvSlot::One {
1073 page_no,
1074 bbox: [r2(l), r2(t), r2(r), r2(b)],
1075 bottom_left,
1076 charspan,
1077 };
1078 }
1079 let Some([x0, y0, x1, y1]) = self.pending_loc else {
1080 return ProvSlot::None;
1081 };
1082 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
1086 return ProvSlot::One {
1087 page_no: self.cur_page,
1088 bbox: [0.0; 4],
1089 bottom_left: true,
1090 charspan: [0, char_len],
1091 };
1092 }
1093 ProvSlot::One {
1094 page_no: self.cur_page,
1095 bbox: [
1096 r2(x0 as f64 * self.cur_w / 512.0),
1097 r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
1098 r2(x1 as f64 * self.cur_w / 512.0),
1099 r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
1100 ],
1101 bottom_left: true,
1102 charspan: [0, char_len],
1103 }
1104 }
1105
1106 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
1109 if self.pending_loc.is_none() && self.cur_page > 0 {
1110 self.pending_loc = loc;
1111 }
1112 }
1113
1114 fn link_comments(&mut self) {
1122 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
1123 .into_iter()
1124 .map(|(item, comments)| {
1125 let refs = comments
1126 .iter()
1127 .filter_map(|i| self.comment_groups.get(*i))
1128 .map(ref_value)
1129 .collect();
1130 (item, refs)
1131 })
1132 .collect();
1133 for (item, comment_refs) in refs {
1134 if comment_refs.is_empty() {
1135 continue;
1136 }
1137 let Some(target) = self.item_mut(&item) else {
1138 continue;
1139 };
1140 let Some(obj) = target.as_object_mut() else {
1141 continue;
1142 };
1143 let tail: Vec<(String, Value)> = obj
1144 .iter()
1145 .skip_while(|(k, _)| k.as_str() != "prov")
1146 .skip(1)
1147 .map(|(k, v)| (k.clone(), v.clone()))
1148 .collect();
1149 for (k, _) in &tail {
1150 obj.shift_remove(k);
1151 }
1152 obj.insert("comments".into(), Value::Array(comment_refs));
1153 for (k, v) in tail {
1154 obj.insert(k, v);
1155 }
1156 }
1157 }
1158
1159 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
1161 let idx = ref_index(self_ref)?;
1162 if self_ref.starts_with("#/texts/") {
1163 return self.texts.get_mut(idx).map(|item| item.value_mut(idx));
1164 }
1165 let bucket = if self_ref.starts_with("#/tables/") {
1166 &mut self.tables
1167 } else if self_ref.starts_with("#/pictures/") {
1168 &mut self.pictures
1169 } else if self_ref.starts_with("#/groups/") {
1170 &mut self.groups
1171 } else {
1172 return None;
1173 };
1174 bucket.get_mut(idx)
1175 }
1176
1177 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
1181 use crate::tree::TreeKind;
1182 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
1185 let (mut nt, mut ng, mut ntb, mut np, mut nf, mut nk) = (0, 0, 0, 0, 0, 0);
1186 for item in &tree.items {
1187 if item.deleted {
1188 refs.push(String::new());
1189 continue;
1190 }
1191 let r = match &item.kind {
1192 TreeKind::Text { .. } | TreeKind::Code { .. } => {
1193 nt += 1;
1194 format!("#/texts/{}", nt - 1)
1195 }
1196 TreeKind::Group { .. } => {
1197 ng += 1;
1198 format!("#/groups/{}", ng - 1)
1199 }
1200 TreeKind::Table { .. } => {
1201 ntb += 1;
1202 format!("#/tables/{}", ntb - 1)
1203 }
1204 TreeKind::Picture { .. } => {
1205 np += 1;
1206 format!("#/pictures/{}", np - 1)
1207 }
1208 TreeKind::FieldRegion { items } => {
1209 nt += items
1212 .iter()
1213 .map(|i| {
1214 [&i.marker, &i.key, &i.value]
1215 .iter()
1216 .filter(|p| p.is_some())
1217 .count()
1218 })
1219 .sum::<usize>();
1220 nf += 1;
1221 format!("#/field_regions/{}", nf - 1)
1222 }
1223 TreeKind::KeyValueGraph { .. } => {
1224 nk += 1;
1225 format!("#/key_value_items/{}", nk - 1)
1226 }
1227 };
1228 refs.push(r);
1229 }
1230 let ref_of = |id: usize| ref_value(refs[id].as_str());
1231 for (id, item) in tree.items.iter().enumerate() {
1232 if item.deleted {
1233 continue;
1234 }
1235 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
1236 let children: Vec<Value> = item
1240 .children
1241 .iter()
1242 .filter(|&&c| !tree.items[c].deleted)
1243 .map(|&c| ref_of(c))
1244 .collect();
1245 let layer = item.layer.map_or("body", |l| l.value());
1246 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
1249 let self_ref = match &item.kind {
1250 TreeKind::Text {
1251 label,
1252 text,
1253 orig,
1254 formatting,
1255 hyperlink,
1256 level,
1257 list,
1258 } => {
1259 let mut tail = serde_json::Map::new();
1262 if let Some(f) = formatting {
1263 tail.insert("formatting".into(), formatting_json(f));
1264 }
1265 if let Some(h) = hyperlink {
1266 tail.insert("hyperlink".into(), json!(h));
1267 }
1268 if let Some(l) = level {
1269 tail.insert("level".into(), json!(l));
1270 }
1271 if let Some(l) = list {
1272 tail.insert("enumerated".into(), json!(l.enumerated));
1273 tail.insert("marker".into(), json!(l.marker));
1274 }
1275 let r = format!("#/texts/{}", self.texts.len());
1276 let prov = self.take_prov(text.chars().count());
1277 let mut item_json = json!({
1278 "self_ref": r,
1279 "parent": { "$ref": parent },
1280 "children": children,
1281 "content_layer": layer,
1282 "label": label,
1283 "prov": prov,
1284 });
1285 if !item.comments.is_empty() {
1288 item_json["comments"] =
1289 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
1290 }
1291 if let Some(track) = &item.source {
1294 item_json["source"] = json!([track_json(track)]);
1295 }
1296 merge(
1297 &mut item_json,
1298 json!({
1299 "orig": orig.as_deref().unwrap_or(text),
1300 "text": text,
1301 }),
1302 );
1303 merge(&mut item_json, Value::Object(tail));
1304 if self.note_keys {
1305 if !item.notes.is_empty() {
1306 item_json["_notes"] = Value::Array(
1307 item.notes
1308 .iter()
1309 .map(|n| json!([n.offset, n.text]))
1310 .collect(),
1311 );
1312 }
1313 if item.note_body {
1314 item_json["_note_body"] = json!(true);
1315 }
1316 }
1317 self.texts.push(Item::json(item_json));
1318 r
1319 }
1320 TreeKind::Code {
1321 text,
1322 orig,
1323 language,
1324 formatting,
1325 hyperlink,
1326 } => {
1327 let r = format!("#/texts/{}", self.texts.len());
1328 let prov = self.take_prov(text.chars().count());
1329 let mut item_json = json!({
1330 "self_ref": r,
1331 "parent": { "$ref": parent },
1332 "children": children,
1333 "content_layer": layer,
1334 "label": "code",
1335 "prov": prov,
1336 });
1337 if !item.comments.is_empty() {
1338 item_json["comments"] =
1339 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
1340 }
1341 merge(
1342 &mut item_json,
1343 json!({
1344 "orig": orig.as_deref().unwrap_or(text),
1345 "text": text,
1346 }),
1347 );
1348 if let Some(f) = formatting {
1349 item_json["formatting"] = formatting_json(f);
1350 }
1351 if let Some(h) = hyperlink {
1352 item_json["hyperlink"] = json!(h);
1353 }
1354 merge(
1355 &mut item_json,
1356 json!({
1357 "captions": [],
1358 "references": [],
1359 "footnotes": [],
1360 "code_language": code_language(language.as_deref()),
1361 }),
1362 );
1363 self.texts.push(Item::json(item_json));
1364 r
1365 }
1366 TreeKind::Group { label, name } => {
1367 self.pending_exact = None;
1368 let r = format!("#/groups/{}", self.groups.len());
1369 self.groups.push(json!({
1370 "self_ref": r,
1371 "parent": { "$ref": parent },
1372 "children": children,
1373 "content_layer": layer,
1374 "name": name,
1375 "label": label,
1376 }));
1377 r
1378 }
1379 TreeKind::Table {
1380 table,
1381 rich_cells,
1382 captions,
1383 } => {
1384 let r = self.add_table_with(table, parent, true);
1386 let idx = ref_index(&r).expect("table ref");
1387 let t = &mut self.tables[idx];
1388 t["children"] = Value::Array(children);
1389 t["content_layer"] = json!(layer);
1390 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
1391 for &(row, col, group) in rich_cells {
1395 let cell_ref = ref_of(group);
1396 let hit = |c: &Value| {
1397 c["start_row_offset_idx"] == json!(row)
1398 && c["start_col_offset_idx"] == json!(col)
1399 };
1400 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
1401 for c in cells.iter_mut().filter(|c| hit(c)) {
1402 c["ref"] = cell_ref.clone();
1403 }
1404 }
1405 }
1406 r
1407 }
1408 TreeKind::Picture {
1409 captions,
1410 image,
1411 classification,
1412 confidence,
1413 chart,
1414 dpi,
1415 } => {
1416 let mut meta = classification.as_ref().map(|c| {
1421 let mut pred = serde_json::Map::new();
1422 if let Some(conf) = confidence {
1423 pred.insert("confidence".into(), json!(conf));
1424 }
1425 pred.insert("class_name".into(), json!(c));
1426 json!({ "classification": { "predictions": [pred] } })
1427 });
1428 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
1429 if !t.rows.is_empty() {
1430 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
1431 }
1432 }
1433 let prov = self.take_prov(0);
1434 let r = self.push_picture(
1435 prov,
1436 captions.iter().map(|&c| ref_of(c)).collect(),
1437 children,
1438 image.as_ref(),
1439 meta,
1440 parent,
1441 );
1442 if let Some(idx) = ref_index(&r) {
1443 self.pictures[idx]["content_layer"] = json!(layer);
1444 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
1447 img["dpi"] = json!(dpi);
1448 }
1449 }
1450 r
1451 }
1452 TreeKind::FieldRegion { items } => {
1453 self.pending_exact = None;
1454 let r = self.add_field_region(items, parent);
1455 if let Some(region) = self.field_regions.last_mut() {
1456 region["content_layer"] = json!(layer);
1457 }
1458 r
1459 }
1460 TreeKind::KeyValueGraph { cells, links } => {
1461 self.pending_exact = None;
1462 let r = self.add_key_value_graph(cells, links, parent);
1463 if let Some(item) = self.key_value_items.last_mut() {
1464 item["content_layer"] = json!(layer);
1465 }
1466 r
1467 }
1468 };
1469 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1470 }
1471 tree.body
1472 .iter()
1473 .filter(|&&c| !tree.items[c].deleted)
1474 .map(|&c| ref_of(c))
1475 .collect()
1476 }
1477
1478 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1479 match node {
1480 Node::Heading { level: 1, text } => {
1481 Some(self.add_text("title", text, parent, json!({})))
1482 }
1483 Node::Heading { level, text } => Some(self.add_text(
1484 "section_header",
1485 text,
1486 parent,
1487 json!({ "level": level.saturating_sub(1) }),
1488 )),
1489 Node::Caption { text, href } => {
1490 let extra = match href {
1491 Some(url) => json!({ "hyperlink": url }),
1492 None => json!({}),
1493 };
1494 Some(self.add_text("caption", text, parent, extra))
1495 }
1496 Node::LabeledText { label, text, href } => {
1499 let extra = match href {
1500 Some(url) => json!({ "hyperlink": url }),
1501 None => json!({}),
1502 };
1503 Some(self.add_text(label, text, parent, extra))
1504 }
1505 Node::Paragraph { text } => {
1506 let t = text.trim();
1509 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1510 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1511 _ => Some(self.add_text("text", text, parent, json!({}))),
1512 }
1513 }
1514 Node::CheckboxItem { checked, text } => {
1515 let label = if *checked {
1521 "checkbox_selected"
1522 } else {
1523 "checkbox_unselected"
1524 };
1525 Some(self.add_text(label, text, parent, json!({})))
1526 }
1527 Node::Code {
1528 language,
1529 text,
1530 orig,
1531 ..
1532 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1533 Node::Formula {
1536 latex,
1537 orig,
1538 location,
1539 } => {
1540 self.adopt_loc(*location);
1541 Some(self.add_formula_item(latex, orig, parent))
1542 }
1543 Node::CommentSection {
1549 name,
1550 text,
1551 refs_note_text,
1552 grouped,
1553 } => {
1554 if !*grouped {
1555 let child =
1559 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1560 self.comment_groups.push(child.clone());
1561 return Some(child);
1562 }
1563 if let Some((_, self_ref)) = self
1564 .last_comment_section
1565 .as_ref()
1566 .filter(|(n, _)| n == name)
1567 .cloned()
1568 {
1569 let child =
1572 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1573 if let Some(children) =
1574 self.groups[group_index(&self_ref)]["children"].as_array_mut()
1575 {
1576 children.push(ref_value(child.as_str()));
1577 }
1578 self.comment_groups.push(if *refs_note_text {
1579 child
1580 } else {
1581 self_ref.clone()
1582 });
1583 return Some(self_ref);
1584 }
1585 let self_ref = format!("#/groups/{}", self.groups.len());
1586 self.groups.push(Value::Null);
1587 let child =
1588 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1589 self.groups[group_index(&self_ref)] = json!({
1590 "self_ref": self_ref,
1591 "parent": { "$ref": parent },
1592 "children": [{ "$ref": child }],
1593 "content_layer": "notes",
1594 "name": name,
1595 "label": "comment_section",
1596 });
1597 self.last_comment_section = Some((name.clone(), self_ref.clone()));
1598 self.comment_groups.push(if *refs_note_text {
1599 child
1600 } else {
1601 self_ref.clone()
1602 });
1603 Some(self_ref)
1604 }
1605 Node::Commented { comments, inner } => {
1608 let item = self.add_node(inner, parent)?;
1609 if !comments.is_empty() {
1610 self.pending_comments.push((item.clone(), comments.clone()));
1611 }
1612 Some(item)
1613 }
1614 Node::Table(t) => Some(self.add_table(t, parent)),
1615 Node::Picture {
1616 caption,
1617 caption_href,
1618 image,
1619 classification,
1620 caption_parent,
1621 caption_location,
1622 } => Some(self.add_picture(
1623 caption.as_deref(),
1624 caption_href.as_deref(),
1625 image.as_ref(),
1626 classification.as_deref().map(classification_meta),
1627 parent,
1628 (*caption_parent, *caption_location),
1629 )),
1630 Node::Chart {
1635 kind,
1636 table,
1637 caption,
1638 location,
1639 } => {
1640 self.adopt_loc(*location);
1641 let mut meta = json!({
1642 "classification": { "predictions": [{ "class_name": kind }] },
1643 });
1644 if !table.rows.is_empty() {
1645 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1646 }
1647 let mut captions = Vec::new();
1653 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1654 let prov = self.prov_slot(unescape_text(cap).chars().count(), true);
1655 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1656 self.pending_siblings.push(ref_value(cap_ref.as_str()));
1657 captions.push(ref_value(cap_ref));
1658 }
1659 let prov = self.take_prov(0);
1660 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1661 }
1662 Node::DoclangOnly(_) => None,
1664 Node::Group {
1665 label,
1666 name,
1667 layer,
1668 children,
1669 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1670 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1671 Node::KeyValueGraph { cells, links } => {
1672 Some(self.add_key_value_graph(cells, links, parent))
1673 }
1674 Node::InlineGroup { md_text, .. } => {
1677 Some(self.add_text("text", md_text, parent, json!({})))
1678 }
1679 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1681 Node::Furniture {
1687 layer: ContentLayer::Notes,
1688 inner,
1689 } => {
1690 let item = self.add_node(inner, parent)?;
1691 self.set_layer(&item, "notes");
1692 Some(item)
1693 }
1694 Node::Furniture { .. } => None,
1695 Node::PageFurniture {
1699 footer,
1700 location,
1701 text,
1702 } => {
1703 if self.cur_page > 0 {
1704 self.pending_loc = Some(*location);
1705 }
1706 let label = if *footer {
1707 "page_footer"
1708 } else {
1709 "page_header"
1710 };
1711 let item = self.add_text(label, text, parent, json!({}));
1712 self.pending_loc = None;
1713 self.set_layer(&item, "furniture");
1714 Some(item)
1715 }
1716 Node::FurnitureText { label, text } => {
1719 let item = self.add_text(label, text, parent, json!({}));
1720 self.set_layer(&item, "furniture");
1721 Some(item)
1722 }
1723 Node::PictureChildren(children) => {
1727 let pic = self.pictures.len().checked_sub(1)?;
1728 let pic_ref = format!("#/pictures/{pic}");
1729 let refs = self.walk_into(children, &pic_ref);
1730 if let Some(list) = self.pictures[pic]["children"].as_array_mut() {
1731 list.extend(refs.into_iter().map(Child::into_value));
1732 }
1733 None
1734 }
1735 Node::Located { location, inner } => {
1740 if self.cur_page > 0 {
1741 self.pending_loc = Some(*location);
1742 }
1743 let r = self.add_node(inner, parent);
1744 self.pending_loc = None;
1745 r
1746 }
1747 Node::Track { track, cue, inner } => {
1748 self.pending_track = Some((track.clone(), cue.clone()));
1749 let r = self.add_node(inner, parent);
1750 self.pending_track = None;
1751 r
1752 }
1753 Node::Prov {
1754 page_no,
1755 bbox,
1756 charspan,
1757 inner,
1758 ..
1759 } => {
1760 self.pending_exact = Some(ExactProv {
1761 page_no: *page_no,
1762 bbox: bbox.map(f64::from),
1763 bottom_left: false,
1764 charspan: *charspan,
1765 });
1766 let r = self.add_node(inner, parent);
1767 self.pending_exact = None;
1768 r
1769 }
1770 Node::PageBreak => None,
1772 Node::PageInfo {
1775 page_no,
1776 width,
1777 height,
1778 } => {
1779 self.cur_page = *page_no;
1780 self.cur_w = *width as f64;
1781 self.cur_h = *height as f64;
1782 if *page_no > 0 {
1783 self.pages.push((*page_no, self.cur_w, self.cur_h));
1784 }
1785 None
1786 }
1787 Node::ListItem { .. } => None,
1789 }
1790 }
1791
1792 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1796 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1797 self.field_regions.push(Value::Null);
1798 let region_index = self.field_regions.len() - 1;
1799 let mut item_refs = Vec::new();
1800 for item in items {
1801 item_refs.push(ref_value(self.add_field_item(item, &self_ref)));
1802 }
1803 self.field_regions[region_index] = json!({
1804 "self_ref": self_ref,
1805 "parent": { "$ref": parent },
1806 "children": item_refs,
1807 "content_layer": "body",
1808 "label": "field_region",
1809 "prov": [],
1810 });
1811 self_ref
1812 }
1813
1814 fn add_key_value_graph(
1818 &mut self,
1819 cells: &[crate::GraphCell],
1820 links: &[crate::GraphLink],
1821 parent: &str,
1822 ) -> String {
1823 let self_ref = format!("#/key_value_items/{}", self.key_value_items.len());
1824 let cells: Vec<Value> = cells
1825 .iter()
1826 .map(|c| {
1827 json!({
1828 "label": c.label,
1829 "cell_id": c.cell_id,
1830 "text": c.text,
1831 "orig": c.orig,
1832 })
1833 })
1834 .collect();
1835 let links: Vec<Value> = links
1836 .iter()
1837 .map(|l| {
1838 json!({
1839 "label": l.label,
1840 "source_cell_id": l.source_cell_id,
1841 "target_cell_id": l.target_cell_id,
1842 })
1843 })
1844 .collect();
1845 self.key_value_items.push(json!({
1846 "self_ref": self_ref,
1847 "parent": { "$ref": parent },
1848 "children": [],
1849 "content_layer": "body",
1850 "label": "key_value_region",
1851 "prov": [],
1852 "captions": [],
1853 "references": [],
1854 "footnotes": [],
1855 "graph": { "cells": cells, "links": links },
1856 }));
1857 self_ref
1858 }
1859
1860 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1861 let self_ref = format!("#/field_items/{}", self.field_items.len());
1862 self.field_items.push(Value::Null);
1863 let item_index = self.field_items.len() - 1;
1864 let mut child_refs = Vec::new();
1865 for (label, text) in [
1866 ("marker", &item.marker),
1867 ("field_key", &item.key),
1868 ("field_value", &item.value),
1869 ] {
1870 if let Some(text) = text {
1871 let extra = match (label, &item.value_kind) {
1874 ("field_value", Some(kind)) => json!({ "kind": kind }),
1875 _ => json!({}),
1876 };
1877 child_refs.push(ref_value(self.add_text(label, text, &self_ref, extra)));
1878 }
1879 }
1880 self.field_items[item_index] = json!({
1881 "self_ref": self_ref,
1882 "parent": { "$ref": parent },
1883 "children": child_refs,
1884 "content_layer": "body",
1885 "label": "field_item",
1886 "prov": [],
1887 });
1888 self_ref
1889 }
1890
1891 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1895 if let Some(item) = self.item_mut(self_ref) {
1896 item["content_layer"] = json!(layer);
1897 }
1898 }
1899
1900 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1901 let prov = self.take_prov_slot(unescape_text(text).chars().count());
1902 self.add_text_with(label, text, parent, extra, prov)
1903 }
1904
1905 fn add_text_with(
1908 &mut self,
1909 label: &str,
1910 text: &str,
1911 parent: &str,
1912 extra: Value,
1913 prov: ProvSlot,
1914 ) -> String {
1915 let index = self.texts.len();
1916 let self_ref = format!("#/texts/{index}");
1917 let mut item = match self.pending_track.take() {
1922 None => Item::Text(Box::new(TextItem {
1923 parent: parent.into(),
1924 label: label.into(),
1925 prov,
1926 text: unescape_text(text).into(),
1927 })),
1928 Some((track, cue)) => {
1929 let mut m = serde_json::Map::with_capacity(9);
1930 m.insert("self_ref".into(), Value::String(self_ref.clone()));
1931 m.insert("parent".into(), ref_value(parent));
1932 m.insert("children".into(), Value::Array(Vec::new()));
1933 m.insert("content_layer".into(), "body".into());
1934 m.insert("label".into(), label.into());
1935 m.insert("prov".into(), prov.to_value());
1936 m.insert("source".into(), Value::Array(vec![track_json(&track)]));
1938 m.insert("orig".into(), Value::String(cue.clone()));
1939 m.insert("text".into(), Value::String(cue));
1940 Item::json(Value::Object(m))
1941 }
1942 };
1943 if extra.as_object().is_some_and(|e| !e.is_empty()) {
1944 merge(item.value_mut(index), extra);
1945 }
1946 self.texts.push(item);
1947 self_ref
1948 }
1949
1950 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
1953 let self_ref = format!("#/texts/{}", self.texts.len());
1954 let prov = self.take_prov(latex.chars().count());
1955 self.texts.push(Item::json(json!({
1956 "self_ref": self_ref,
1957 "parent": { "$ref": parent },
1958 "children": [],
1959 "content_layer": "body",
1960 "label": "formula",
1961 "prov": prov,
1962 "orig": latex,
1963 "text": latex,
1964 })));
1965 self_ref
1966 }
1967
1968 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
1972 let self_ref = format!("#/texts/{}", self.texts.len());
1973 let prov = self.take_prov(latex.chars().count());
1974 self.texts.push(Item::json(json!({
1975 "self_ref": self_ref,
1976 "parent": { "$ref": parent },
1977 "children": [],
1978 "content_layer": "body",
1979 "label": "formula",
1980 "prov": prov,
1981 "orig": orig,
1982 "text": latex,
1983 })));
1984 self_ref
1985 }
1986
1987 fn add_code(
1988 &mut self,
1989 text: &str,
1990 language: Option<&str>,
1991 orig: Option<&str>,
1992 parent: &str,
1993 ) -> String {
1994 let self_ref = format!("#/texts/{}", self.texts.len());
1995 let raw = unescape_text(text);
1996 let prov = self.take_prov(raw.chars().count());
1997 self.texts.push(Item::json(json!({
1998 "self_ref": self_ref,
1999 "parent": { "$ref": parent },
2000 "children": [],
2001 "content_layer": "body",
2002 "label": "code",
2003 "prov": prov,
2004 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
2007 "text": raw,
2008 "captions": [],
2009 "references": [],
2010 "footnotes": [],
2011 "code_language": code_language(language),
2012 })));
2013 self_ref
2014 }
2015
2016 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
2019 let self_ref = format!("#/groups/{}", self.groups.len());
2020 self.groups.push(Value::Null);
2022 let base = level_of(&items[0]);
2023 let mut children = Vec::new();
2024 let mut i = 0;
2025 while i < items.len() {
2026 if !matches!(items[i], Node::ListItem { .. }) {
2029 i += 1;
2030 continue;
2031 }
2032 let lvl = level_of(&items[i]);
2033 if lvl > base {
2034 i += 1;
2036 continue;
2037 }
2038 let item_ref = self.add_list_item(&items[i], &self_ref);
2039 let mut j = i + 1;
2041 while j < items.len() && level_of(&items[j]) > base {
2042 j += 1;
2043 }
2044 if j > i + 1 {
2045 let mut nested = Vec::new();
2046 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
2047 if let Some(idx) = ref_index(&item_ref) {
2049 self.texts[idx].value_mut(idx)["children"]
2050 .as_array_mut()
2051 .unwrap()
2052 .extend(nested);
2053 }
2054 }
2055 children.push(ref_value(item_ref));
2056 i = j;
2057 }
2058 self.groups[group_index(&self_ref)] = json!({
2059 "self_ref": self_ref,
2060 "parent": { "$ref": parent },
2061 "children": children,
2062 "content_layer": "body",
2063 "name": "list",
2064 "label": "list",
2065 });
2066 self_ref
2067 }
2068
2069 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
2070 let Node::ListItem {
2071 ordered,
2072 number,
2073 text,
2074 location,
2075 ..
2076 } = node
2077 else {
2078 unreachable!()
2079 };
2080 self.adopt_loc(*location);
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 let marker = if *ordered {
2085 format!("{number}.")
2086 } else {
2087 "-".to_string()
2088 };
2089 self.texts.push(Item::json(json!({
2090 "self_ref": self_ref,
2091 "parent": { "$ref": parent },
2092 "children": [],
2093 "content_layer": "body",
2094 "label": "list_item",
2095 "prov": prov,
2096 "orig": raw,
2097 "text": raw,
2098 "enumerated": ordered,
2099 "marker": marker,
2100 })));
2101 self_ref
2102 }
2103
2104 fn add_table(&mut self, t: &Table, parent: &str) -> String {
2105 self.add_table_with(t, parent, false)
2106 }
2107
2108 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
2111 let self_ref = format!("#/tables/{}", self.tables.len());
2112 self.adopt_loc(t.location);
2113 let prov = self.take_prov(0);
2114 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
2118 Some(cap) => self.add_caption(
2119 cap,
2120 json!({}),
2121 &self_ref,
2122 parent,
2123 (t.caption_parent, t.caption_location),
2124 ),
2125 None => (Vec::new(), Vec::new()),
2126 };
2127 let data = table_data_with(t, raw);
2128 self.tables.push(json!({
2129 "self_ref": self_ref,
2130 "parent": { "$ref": parent },
2131 "children": children,
2132 "content_layer": "body",
2133 "label": "table",
2134 "prov": prov,
2135 "captions": captions,
2136 "references": [],
2137 "footnotes": [],
2138 "data": data,
2139 "annotations": [],
2140 }));
2141 self_ref
2142 }
2143
2144 fn add_caption(
2153 &mut self,
2154 text: &str,
2155 extra: Value,
2156 self_ref: &str,
2157 parent: &str,
2158 (choice, location): (CaptionParent, Option<[u16; 4]>),
2159 ) -> (Vec<Value>, Vec<Value>) {
2160 let cap_parent = match choice {
2165 CaptionParent::Item => self_ref,
2166 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
2167 CaptionParent::Body => "#/body",
2168 };
2169 if self.cur_page > 0 && location.is_some() {
2170 self.pending_loc = location;
2171 }
2172 let cap_ref = ref_value(self.add_text("caption", text, cap_parent, extra));
2173 match choice {
2174 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
2175 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
2178 CaptionParent::Body if parent == "#/body" => {
2179 self.pending_siblings.push(cap_ref.clone())
2180 }
2181 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
2182 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
2185 }
2186 (vec![cap_ref], Vec::new())
2187 }
2188
2189 fn add_picture(
2192 &mut self,
2193 caption: Option<&str>,
2194 caption_href: Option<&str>,
2195 image: Option<&crate::PictureImage>,
2196 meta: Option<Value>,
2197 parent: &str,
2198 caption_at: (CaptionParent, Option<[u16; 4]>),
2199 ) -> String {
2200 let self_ref = format!("#/pictures/{}", self.pictures.len());
2201 let prov = self.take_prov(0);
2204 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
2205 Some(cap) => {
2206 let extra = match caption_href {
2210 Some(href) => json!({ "hyperlink": href }),
2211 None => json!({}),
2212 };
2213 self.add_caption(cap, extra, &self_ref, parent, caption_at)
2214 }
2215 None => (Vec::new(), Vec::new()),
2216 };
2217 self.push_picture(prov, captions, children, image, meta, parent)
2218 }
2219
2220 fn push_picture(
2223 &mut self,
2224 prov: Value,
2225 captions: Vec<Value>,
2226 children: Vec<Value>,
2227 image: Option<&crate::PictureImage>,
2228 meta: Option<Value>,
2229 parent: &str,
2230 ) -> String {
2231 let self_ref = format!("#/pictures/{}", self.pictures.len());
2232 let annotations = meta
2236 .as_ref()
2237 .and_then(|m| m.get("annotations").cloned())
2238 .unwrap_or_else(|| json!([]));
2239 let meta = meta.map(|mut m| {
2240 if let Some(obj) = m.as_object_mut() {
2241 obj.remove("annotations");
2242 }
2243 m
2244 });
2245 let mut item = match meta {
2249 Some(meta) => json!({
2250 "self_ref": self_ref,
2251 "parent": { "$ref": parent },
2252 "children": children,
2253 "content_layer": "body",
2254 "meta": meta,
2255 "label": "picture",
2256 "prov": prov,
2257 "captions": captions,
2258 "references": [],
2259 "footnotes": [],
2260 "annotations": annotations,
2261 }),
2262 None => json!({
2263 "self_ref": self_ref,
2264 "parent": { "$ref": parent },
2265 "children": children,
2266 "content_layer": "body",
2267 "label": "picture",
2268 "prov": prov,
2269 "captions": captions,
2270 "references": [],
2271 "footnotes": [],
2272 "annotations": annotations,
2273 }),
2274 };
2275 if let Some(img) = image {
2280 let image = image_ref_json(img);
2281 if let Some(obj) = item.as_object_mut() {
2282 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
2283 obj.insert("image".into(), image);
2284 obj.insert("annotations".into(), annotations);
2285 }
2286 }
2287 self.pictures.push(item);
2288 self_ref
2289 }
2290
2291 fn add_group(
2292 &mut self,
2293 label: &str,
2294 name: Option<&str>,
2295 layer: Option<ContentLayer>,
2296 nodes: &[Node],
2297 parent: &str,
2298 ) -> String {
2299 let self_ref = format!("#/groups/{}", self.groups.len());
2300 self.groups.push(Value::Null);
2301 let mark = (
2305 self.texts.len(),
2306 self.tables.len(),
2307 self.pictures.len(),
2308 self.groups.len(),
2309 );
2310 let children: Vec<Value> = self
2311 .walk_into(nodes, &self_ref)
2312 .into_iter()
2313 .map(Child::into_value)
2314 .collect();
2315 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
2316 let content_layer = layer.map_or("body", |l| l.value());
2317 self.groups[group_index(&self_ref)] = json!({
2318 "self_ref": self_ref,
2319 "parent": { "$ref": parent },
2320 "children": children,
2321 "content_layer": content_layer,
2322 "name": name,
2323 "label": label,
2324 });
2325 if layer.is_some() {
2326 let (t, tb, p, g) = mark;
2327 for item in self.texts[t..]
2328 .iter_mut()
2329 .enumerate()
2330 .map(|(k, item)| item.value_mut(t + k))
2331 .chain(self.tables[tb..].iter_mut())
2332 .chain(self.pictures[p..].iter_mut())
2333 .chain(self.groups[g..].iter_mut())
2334 {
2335 if let Some(obj) = item.as_object_mut() {
2336 obj.insert("content_layer".into(), json!(content_layer));
2337 }
2338 }
2339 }
2340 self_ref
2341 }
2342
2343 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Child> {
2346 let seqs: Option<Vec<usize>> = nodes
2351 .iter()
2352 .map(|n| match n {
2353 Node::Prov { seq: Some(s), .. } => Some(*s),
2354 _ => None,
2355 })
2356 .collect();
2357 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
2358 let mut order: Vec<usize> = (0..nodes.len()).collect();
2359 order.sort_by_key(|&i| seqs[i]);
2360 let mut slots: Vec<Vec<Child>> = (0..nodes.len()).map(|_| Vec::new()).collect();
2361 for i in order {
2362 if let Some(r) = self.add_node(&nodes[i], parent) {
2363 slots[i].extend(self.pending_siblings.drain(..).map(Child::json));
2364 slots[i].push(Child::Ref(r.into()));
2365 slots[i].extend(self.pending_after.drain(..).map(Child::json));
2366 }
2367 if parent == "#/body" {
2368 slots[i].extend(self.pending_body.drain(..).map(Child::json));
2369 }
2370 }
2371 return slots.into_iter().flatten().collect();
2372 }
2373 let mut children = Vec::new();
2374 let mut i = 0;
2375 while i < nodes.len() {
2376 if matches!(nodes[i], Node::ListItem { .. }) {
2377 let start = i;
2378 i += 1;
2379 loop {
2380 match nodes.get(i) {
2381 Some(Node::ListItem { .. }) => i += 1,
2382 Some(Node::Paragraph { text })
2385 if text.is_empty()
2386 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
2387 {
2388 i += 1
2389 }
2390 _ => break,
2391 }
2392 }
2393 let mut lists = Vec::new();
2394 self.add_sibling_lists(&nodes[start..i], parent, &mut lists);
2395 children.extend(lists.into_iter().map(Child::json));
2396 } else {
2397 if let Some(r) = self.add_node(&nodes[i], parent) {
2398 children.extend(self.pending_siblings.drain(..).map(Child::json));
2399 children.push(Child::Ref(r.into()));
2400 children.extend(self.pending_after.drain(..).map(Child::json));
2401 }
2402 i += 1;
2403 }
2404 if parent == "#/body" {
2407 children.extend(self.pending_body.drain(..).map(Child::json));
2408 }
2409 }
2410 children
2411 }
2412
2413 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
2418 let base = level_of(&run[0]);
2419 let mut seg = 0;
2420 for k in 0..run.len() {
2421 let Node::ListItem {
2422 first_in_list,
2423 level,
2424 ..
2425 } = &run[k]
2426 else {
2427 continue;
2428 };
2429 if *level != base {
2430 continue; }
2432 if k > seg && *first_in_list {
2433 out.push(ref_value(self.add_list(&run[seg..k], parent)));
2434 seg = k;
2435 }
2436 }
2437 out.push(ref_value(self.add_list(&run[seg..], parent)));
2438 }
2439}
2440
2441fn level_of(node: &Node) -> u8 {
2442 match node {
2443 Node::ListItem { level, .. } => *level,
2444 _ => 0,
2445 }
2446}
2447
2448fn group_index(self_ref: &str) -> usize {
2449 self_ref.rsplit('/').next().unwrap().parse().unwrap()
2450}
2451
2452fn ref_index(self_ref: &str) -> Option<usize> {
2453 self_ref.rsplit('/').next()?.parse().ok()
2454}
2455
2456fn merge(target: &mut Value, extra: Value) {
2458 if let (Some(t), Value::Object(e)) = (target.as_object_mut(), extra) {
2459 t.extend(e);
2460 }
2461}
2462
2463fn object<const N: usize>(entries: [(&str, Value); N]) -> Value {
2469 let mut m = serde_json::Map::with_capacity(N);
2470 for (k, v) in entries {
2471 m.insert(k.to_owned(), v);
2472 }
2473 Value::Object(m)
2474}
2475
2476fn ref_value(r: impl Into<String>) -> Value {
2478 object([("$ref", Value::String(r.into()))])
2479}
2480
2481fn unescape_text(s: &str) -> String {
2483 s.replace("<", "<")
2484 .replace(">", ">")
2485 .replace("&", "&")
2486 .replace("\\_", "_")
2487}
2488
2489fn fnv1a(s: &str) -> u64 {
2492 let mut h: u64 = 0xcbf29ce484222325;
2493 for b in s.bytes() {
2494 h ^= b as u64;
2495 h = h.wrapping_mul(0x100000001b3);
2496 }
2497 h
2498}
2499
2500#[cfg(test)]
2501mod tests {
2502 use crate::{
2503 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
2504 };
2505 use serde_json::Value;
2506
2507 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
2508 crate::TableCell {
2509 text: text.into(),
2510 bbox: None,
2511 start_row: row,
2512 start_col: col,
2513 row_span: 1,
2514 col_span,
2515 column_header: row == 0,
2516 row_header: false,
2517 row_section: false,
2518 }
2519 }
2520
2521 #[test]
2527 fn ragged_rows_take_their_grid_from_first_class_cells() {
2528 let cells = vec![
2529 cell_at("h0", 0, 0, 1),
2530 cell_at("h2", 0, 2, 1),
2531 cell_at("wide", 1, 0, 3),
2532 ];
2533 let export = |rows: Vec<Vec<String>>| -> Value {
2534 let mut doc = DoclingDocument::new("t");
2535 doc.push(Node::Table(Table {
2536 rows,
2537 cells: Some(cells.clone()),
2538 ..Table::default()
2539 }));
2540 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2541 v["tables"][0]["data"].clone()
2542 };
2543 let data = export(vec![
2545 vec!["h0".into(), "h2".into()],
2546 vec!["wide".into(), "wide".into(), "wide".into()],
2547 ]);
2548 assert_eq!(data["num_rows"], 2);
2549 assert_eq!(data["num_cols"], 3);
2550 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
2551 let row0: Vec<&str> = data["grid"][0]
2552 .as_array()
2553 .unwrap()
2554 .iter()
2555 .map(|c| c["text"].as_str().unwrap())
2556 .collect();
2557 assert_eq!(row0, ["h0", "", "h2"]);
2558 assert_eq!(data["table_cells"][2]["col_span"], 3);
2559 let data = export(vec![
2562 vec!["h0".into(), "h2".into()],
2563 vec!["wide".into(), "wide".into()],
2564 ]);
2565 assert_eq!(data["num_cols"], 2);
2566 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
2567 }
2568
2569 fn doc_with_image() -> DoclingDocument {
2570 let mut doc = DoclingDocument::new("t");
2571 doc.push(Node::Picture {
2572 caption: Some("Fig 1".into()),
2573 caption_href: None,
2574 image: Some(PictureImage {
2575 dpi: crate::PictureImage::DEFAULT_DPI,
2576 mimetype: "image/png".into(),
2577 width: 4,
2578 height: 2,
2579 data: b"foobar".to_vec(),
2580 }),
2581 classification: None,
2582 caption_parent: Default::default(),
2583 caption_location: None,
2584 });
2585 doc
2586 }
2587
2588 #[test]
2593 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
2594 let mut doc = DoclingDocument::new("t");
2595 doc.push(Node::Heading {
2596 level: 1,
2597 text: "Slide One".into(),
2598 });
2599 doc.push(Node::Furniture {
2600 layer: ContentLayer::Notes,
2601 inner: Box::new(Node::Located {
2602 location: [0, 0, 0, 0],
2603 inner: Box::new(Node::Paragraph {
2604 text: "Speaker note for slide 1.".into(),
2605 }),
2606 }),
2607 });
2608 doc.push(Node::Furniture {
2609 layer: ContentLayer::Furniture,
2610 inner: Box::new(Node::Paragraph {
2611 text: "page header".into(),
2612 }),
2613 });
2614
2615 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2616 let texts = v["texts"].as_array().unwrap();
2617 assert_eq!(
2618 texts
2619 .iter()
2620 .map(|t| (
2621 t["label"].as_str().unwrap(),
2622 t["content_layer"].as_str().unwrap(),
2623 t["text"].as_str().unwrap()
2624 ))
2625 .collect::<Vec<_>>(),
2626 vec![
2627 ("title", "body", "Slide One"),
2628 ("text", "notes", "Speaker note for slide 1."),
2629 ],
2630 "the note is carried on its own layer; the furniture is not carried"
2631 );
2632 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
2634 }
2635
2636 #[test]
2642 fn continuation_flags_become_spanning_cells() {
2643 let mut doc = DoclingDocument::new("t");
2644 let rows = vec![
2646 vec!["merged".to_string(), "merged".into(), "merged".into()],
2647 vec!["merged".to_string(), "merged".into(), "merged".into()],
2648 vec!["a".to_string(), "b".into(), "c".into()],
2649 ];
2650 doc.push(Node::Table(crate::Table {
2651 rows,
2652 location: None,
2653 structure: Some(crate::TableStructure {
2654 header_row: vec![true, false, false],
2655 col_continuation: vec![
2656 vec![false, true, true],
2657 vec![false, true, true],
2658 vec![false, false, false],
2659 ],
2660 row_continuation: vec![
2661 vec![false, false, false],
2662 vec![true, true, true],
2663 vec![false, false, false],
2664 ],
2665 row_header: Vec::new(),
2666 col_header: Vec::new(),
2667 }),
2668 cell_blocks: None,
2669 cells: None,
2670 caption: None,
2671 caption_parent: Default::default(),
2672 caption_location: None,
2673 }));
2674 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2675 let data = &v["tables"][0]["data"];
2676 assert_eq!(data["num_rows"], 3);
2677 assert_eq!(data["num_cols"], 3);
2678 let cells = data["table_cells"].as_array().unwrap();
2679 assert_eq!(
2680 cells.len(),
2681 4,
2682 "one cell for the range, three for the plain row"
2683 );
2684 assert_eq!(
2685 cells[0],
2686 serde_json::json!({
2687 "row_span": 2, "col_span": 3,
2688 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2689 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2690 "text": "merged", "column_header": true, "row_header": false,
2691 "row_section": false, "fillable": false,
2692 })
2693 );
2694 assert_eq!(cells[1]["text"], "a");
2695 assert_eq!(cells[1]["row_span"], 1);
2696 assert_eq!(cells[1]["column_header"], false);
2697 let grid = data["grid"].as_array().unwrap();
2699 assert_eq!(grid.len(), 3);
2700 for (r, row) in grid.iter().take(2).enumerate() {
2701 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2702 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2703 }
2704 }
2705 assert_eq!(grid[2][2]["text"], "c");
2706 }
2707
2708 #[test]
2714 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2715 let mut doc = DoclingDocument::new("t");
2716 doc.push(Node::PageInfo {
2717 page_no: 1,
2718 width: 3.0,
2719 height: 4.0,
2720 });
2721 let table = crate::Table {
2722 rows: vec![vec!["a".to_string(), "b".into()]],
2723 ..Default::default()
2724 };
2725 doc.push(Node::Group {
2726 label: "sheet".into(),
2727 name: Some("Data".into()),
2728 layer: None,
2729 children: vec![
2730 Node::Prov {
2735 page_no: 1,
2736 bbox: [0.0, 0.0, 3.0, 4.0],
2737 charspan: [0, 0],
2738 seq: Some(1),
2739 inner: Box::new(Node::Table(table.clone())),
2740 },
2741 Node::Prov {
2742 page_no: 1,
2743 bbox: [0.0, 1.0, 1.0, 1.0],
2744 charspan: [0, 0],
2745 seq: Some(0),
2746 inner: Box::new(Node::Chart {
2747 kind: "bar_chart".into(),
2748 table,
2749 caption: Some("Sales".into()),
2750 location: Some([0, 128, 170, 128]),
2751 }),
2752 },
2753 ],
2754 });
2755 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2756 assert_eq!(
2757 v["pages"],
2758 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2759 );
2760 assert_eq!(
2761 v["tables"][0]["prov"],
2762 serde_json::json!([{
2763 "page_no": 1,
2764 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2765 "charspan": [0, 0],
2766 }])
2767 );
2768 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2769 let sheet = &v["groups"][0];
2772 assert_eq!(
2773 sheet["children"],
2774 serde_json::json!([
2775 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2776 ])
2777 );
2778 let cap = &v["texts"][0];
2779 assert_eq!(cap["label"], "caption");
2780 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2781 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2782 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2783 let pic = &v["pictures"][0];
2784 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2785 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2786 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2787 assert_eq!(
2788 pic["meta"]["classification"]["predictions"][0]["class_name"],
2789 "bar_chart"
2790 );
2791 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2792 }
2793
2794 #[test]
2798 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2799 let mut doc = DoclingDocument::new("t");
2800 doc.push(Node::PageInfo {
2801 page_no: 1,
2802 width: 12192000.0,
2803 height: 6858000.0,
2804 });
2805 doc.push(Node::Furniture {
2806 layer: ContentLayer::Notes,
2807 inner: Box::new(Node::Located {
2808 location: [0, 0, 0, 0],
2809 inner: Box::new(Node::Paragraph {
2810 text: "a note".into(),
2811 }),
2812 }),
2813 });
2814 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2815 let prov = &v["texts"][0]["prov"][0];
2816 assert_eq!(prov["page_no"], 1);
2817 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2818 assert_eq!(
2819 prov["bbox"],
2820 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2821 );
2822 assert_eq!(
2824 v["pages"]["1"]["size"],
2825 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2826 );
2827 }
2828
2829 #[test]
2839 fn captions_and_checkboxes_carry_their_own_prov() {
2840 let mut doc = DoclingDocument::new("t");
2841 doc.push(Node::PageInfo {
2842 page_no: 1,
2843 width: 512.0,
2844 height: 512.0,
2845 });
2846 doc.push(Node::Located {
2847 location: [64, 64, 448, 256],
2848 inner: Box::new(Node::Picture {
2849 caption: Some("Figure 1".into()),
2850 caption_href: None,
2851 image: None,
2852 classification: None,
2853 caption_parent: CaptionParent::Item,
2854 caption_location: Some([64, 264, 448, 280]),
2855 }),
2856 });
2857 doc.push(Node::PageInfo {
2858 page_no: 2,
2859 width: 512.0,
2860 height: 512.0,
2861 });
2862 doc.push(Node::Located {
2863 location: [64, 120, 448, 300],
2864 inner: Box::new(Node::Table(Table {
2865 rows: vec![vec!["a".into()]],
2866 caption: Some("Table 1".into()),
2867 caption_parent: CaptionParent::Item,
2868 caption_location: Some([64, 100, 300, 112]),
2869 ..Table::default()
2870 })),
2871 });
2872 doc.push(Node::Located {
2873 location: [64, 320, 200, 332],
2874 inner: Box::new(Node::CheckboxItem {
2875 checked: false,
2876 text: "First option".into(),
2877 }),
2878 });
2879 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2880 let texts = v["texts"].as_array().unwrap();
2881 let by_text = |t: &str| texts.iter().find(|x| x["text"] == t).expect(t).clone();
2882 let bbox = |p: &Value| {
2883 let b = &p["prov"][0]["bbox"];
2884 [&b["l"], &b["t"], &b["r"], &b["b"]].map(|x| x.as_f64().unwrap())
2885 };
2886 let fig = by_text("Figure 1");
2889 assert_eq!(fig["prov"][0]["page_no"], 1);
2890 assert_eq!(bbox(&fig), [64.0, 248.0, 448.0, 232.0]);
2891 assert_eq!(fig["prov"][0]["charspan"], serde_json::json!([0, 8]));
2892 assert_eq!(fig["parent"]["$ref"], "#/pictures/0");
2893 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
2894 let tab = by_text("Table 1");
2896 assert_eq!(tab["prov"][0]["page_no"], 2);
2897 assert_eq!(bbox(&tab), [64.0, 412.0, 300.0, 400.0]);
2898 assert_eq!(v["tables"][0]["prov"][0]["bbox"]["t"], 392.0);
2899 let cb = by_text("First option");
2901 assert_eq!(cb["label"], "checkbox_unselected");
2902 assert_eq!(cb["prov"][0]["page_no"], 2);
2903 assert_eq!(bbox(&cb), [64.0, 192.0, 200.0, 180.0]);
2904 assert!(texts
2905 .iter()
2906 .all(|t| !t["prov"].as_array().unwrap().is_empty()));
2907
2908 let mut plain = DoclingDocument::new("t");
2911 plain.push(Node::PageInfo {
2912 page_no: 1,
2913 width: 512.0,
2914 height: 512.0,
2915 });
2916 plain.push(Node::Located {
2917 location: [64, 64, 448, 256],
2918 inner: Box::new(Node::Picture {
2919 caption: Some("Figure 1".into()),
2920 caption_href: None,
2921 image: None,
2922 classification: None,
2923 caption_parent: CaptionParent::Item,
2924 caption_location: None,
2925 }),
2926 });
2927 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2928 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2929 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
2930 }
2931
2932 #[test]
2933 fn page_markers_produce_pages_and_prov() {
2934 let mut doc = DoclingDocument::new("t");
2935 doc.push(Node::PageInfo {
2936 page_no: 1,
2937 width: 512.0,
2938 height: 1024.0,
2939 });
2940 doc.push(Node::Located {
2941 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
2943 text: "hello".into(),
2944 }),
2945 });
2946 doc.push(Node::Table(Table {
2947 rows: vec![vec!["a".into()]],
2948 location: Some([0, 0, 512, 512]),
2949 ..Table::default()
2950 }));
2951 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2952 assert_eq!(v["pages"]["1"]["page_no"], 1);
2953 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
2954 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
2955 let prov = &v["texts"][0]["prov"][0];
2958 assert_eq!(prov["page_no"], 1);
2959 assert_eq!(prov["bbox"]["l"], 128.0);
2960 assert_eq!(prov["bbox"]["t"], 896.0);
2961 assert_eq!(prov["bbox"]["r"], 256.0);
2962 assert_eq!(prov["bbox"]["b"], 768.0);
2963 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
2964 assert_eq!(prov["charspan"][1], 5);
2965 let tprov = &v["tables"][0]["prov"][0];
2967 assert_eq!(tprov["bbox"]["t"], 1024.0);
2968 assert_eq!(tprov["bbox"]["b"], 0.0);
2969 assert_eq!(tprov["charspan"][1], 0);
2970
2971 let mut plain = DoclingDocument::new("t");
2973 plain.push(Node::Located {
2974 location: [1, 2, 3, 4],
2975 inner: Box::new(Node::Paragraph { text: "x".into() }),
2976 });
2977 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2978 assert_eq!(v["pages"], serde_json::json!({}));
2979 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2980 }
2981
2982 #[test]
2985 fn page_furniture_becomes_furniture_layer_text() {
2986 let mut doc = DoclingDocument::new("t");
2987 doc.push(Node::PageInfo {
2988 page_no: 1,
2989 width: 512.0,
2990 height: 512.0,
2991 });
2992 doc.push(Node::PageFurniture {
2993 footer: false,
2994 location: [10, 0, 100, 20],
2995 text: "Chapter 1".into(),
2996 });
2997 doc.push(Node::Paragraph {
2998 text: "body".into(),
2999 });
3000 doc.push(Node::PageFurniture {
3001 footer: true,
3002 location: [400, 490, 500, 512],
3003 text: "1.10.2".into(),
3004 });
3005 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3006 let texts = v["texts"].as_array().unwrap();
3007 assert_eq!(texts.len(), 3);
3008 assert_eq!(texts[0]["label"], "page_header");
3009 assert_eq!(texts[0]["content_layer"], "furniture");
3010 assert_eq!(texts[0]["parent"]["$ref"], "#/body");
3011 assert_eq!(texts[0]["prov"][0]["bbox"]["l"], 10.0);
3012 assert_eq!(texts[1]["content_layer"], "body");
3013 assert_eq!(texts[1]["prov"], serde_json::json!([]));
3014 assert_eq!(texts[2]["label"], "page_footer");
3015 assert_eq!(texts[2]["text"], "1.10.2");
3016 assert_eq!(texts[2]["content_layer"], "furniture");
3017 }
3018
3019 #[test]
3020 fn picture_image_in_markdown_modes_and_json() {
3021 let doc = doc_with_image();
3022 assert!(doc.export_to_markdown().contains("<!-- image -->"));
3024 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
3026 assert!(
3027 md.contains(""),
3028 "got:\n{md}"
3029 );
3030 assert!(files.is_empty());
3031 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
3033 assert!(
3034 md.contains(""),
3035 "got:\n{md}"
3036 );
3037 assert_eq!(
3038 files,
3039 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
3040 );
3041 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3044 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
3045 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
3046 let keys: Vec<&str> = v["pictures"][0]
3047 .as_object()
3048 .unwrap()
3049 .keys()
3050 .map(String::as_str)
3051 .collect();
3052 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
3053 assert_eq!(
3054 v["pictures"][0]["image"]["uri"],
3055 "data:image/png;base64,Zm9vYmFy"
3056 );
3057 }
3058
3059 #[test]
3060 fn exports_docling_schema() {
3061 let mut doc = DoclingDocument::new("t");
3062 doc.push(Node::Heading {
3063 level: 1,
3064 text: "Title".into(),
3065 });
3066 doc.push(Node::Heading {
3067 level: 2,
3068 text: "Sec".into(),
3069 });
3070 doc.push(Node::Paragraph {
3071 text: "Body & more".into(),
3072 }); doc.push(Node::ListItem {
3074 ordered: false,
3075 number: 0,
3076 first_in_list: true,
3077 text: "one".into(),
3078 level: 0,
3079 marker: None,
3080 location: None,
3081 dclx: None,
3082 href: None,
3083 layer: None,
3084 });
3085 doc.push(Node::ListItem {
3086 ordered: false,
3087 number: 0,
3088 first_in_list: false,
3089 text: "two".into(),
3090 level: 0,
3091 marker: None,
3092 location: None,
3093 dclx: None,
3094 href: None,
3095 layer: None,
3096 });
3097 doc.push(Node::Table(Table {
3098 rows: vec![vec!["A".into(), "B".into()]],
3099 location: None,
3100 structure: None,
3101 cell_blocks: None,
3102 cells: None,
3103 caption: None,
3104 caption_parent: Default::default(),
3105 caption_location: None,
3106 }));
3107
3108 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3109 assert_eq!(v["schema_name"], "DoclingDocument");
3110 assert_eq!(v["version"], "1.10.0");
3111 assert_eq!(v["texts"][0]["label"], "title");
3112 assert_eq!(v["texts"][1]["label"], "section_header");
3113 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
3117 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
3118 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
3119 assert_eq!(v["texts"][3]["marker"], "-");
3120 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
3122 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
3123 }
3124 #[test]
3128 fn a_layered_group_stamps_its_whole_subtree() {
3129 let doc = DoclingDocument {
3130 name: "s".into(),
3131 nodes: vec![
3132 Node::Group {
3133 label: "sheet".into(),
3134 name: Some("Sheet1".into()),
3135 layer: None,
3136 children: vec![Node::Paragraph {
3137 text: "visible".into(),
3138 }],
3139 },
3140 Node::Group {
3141 label: "sheet".into(),
3142 name: Some("Sheet2".into()),
3143 layer: Some(ContentLayer::Invisible),
3144 children: vec![Node::Paragraph {
3145 text: "hidden".into(),
3146 }],
3147 },
3148 ],
3149 ..DoclingDocument::new("s")
3150 };
3151 let v = crate::json::to_json(&doc);
3152 assert_eq!(v["groups"][0]["label"], "sheet");
3153 assert_eq!(v["groups"][0]["name"], "Sheet1");
3154 assert_eq!(v["groups"][0]["content_layer"], "body");
3155 assert_eq!(v["texts"][0]["content_layer"], "body");
3156 assert_eq!(v["groups"][1]["name"], "Sheet2");
3157 assert_eq!(v["groups"][1]["content_layer"], "invisible");
3158 assert_eq!(v["texts"][1]["content_layer"], "invisible");
3159 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
3161 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
3162 }
3163
3164 #[test]
3174 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
3175 use crate::tree::{ItemTree, TreeKind};
3176 let mut t = ItemTree::default();
3177 let text = |txt: &str| TreeKind::Text {
3178 label: "text".into(),
3179 text: txt.into(),
3180 orig: None,
3181 formatting: None,
3182 hyperlink: None,
3183 level: None,
3184 list: None,
3185 };
3186 let a = t.add(None, None, text("a"));
3187 let blank = t.add(None, None, text(""));
3188 let b = t.add(None, None, text("b"));
3189 t.delete(blank);
3190 let group = t.add(
3191 None,
3192 Some(ContentLayer::Notes),
3193 TreeKind::Group {
3194 label: "comment_section".into(),
3195 name: "comment-0".into(),
3196 },
3197 );
3198 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
3199 t.items[a].comments.push(group);
3200 t.add(
3201 None,
3202 None,
3203 TreeKind::Picture {
3204 captions: Vec::new(),
3205 image: None,
3206 classification: Some("bar_chart".into()),
3207 confidence: None,
3208 chart: Some(Table {
3209 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
3210 ..Table::default()
3211 }),
3212 dpi: None,
3213 },
3214 );
3215 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
3216 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
3217 let mut doc = DoclingDocument::new("t");
3218 doc.tree = Some(t);
3219 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3220 let texts = v["texts"].as_array().unwrap();
3221 assert_eq!(texts.len(), 3);
3222 assert_eq!(texts[1]["text"], "b");
3223 assert_eq!(texts[1]["self_ref"], "#/texts/1");
3224 assert_eq!(
3225 v["body"]["children"],
3226 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
3227 );
3228 let keys: Vec<&str> = texts[0]
3229 .as_object()
3230 .unwrap()
3231 .keys()
3232 .map(String::as_str)
3233 .collect();
3234 assert_eq!(
3235 keys,
3236 vec![
3237 "self_ref",
3238 "parent",
3239 "children",
3240 "content_layer",
3241 "label",
3242 "prov",
3243 "comments",
3244 "orig",
3245 "text"
3246 ]
3247 );
3248 assert_eq!(
3249 texts[0]["comments"],
3250 serde_json::json!([{"$ref": "#/groups/0"}])
3251 );
3252 assert!(texts[1].get("comments").is_none());
3253 let meta = &v["pictures"][0]["meta"];
3254 assert_eq!(
3255 meta["classification"]["predictions"][0]["class_name"],
3256 "bar_chart"
3257 );
3258 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
3259 }
3260
3261 #[test]
3267 fn tree_items_carry_exact_provenance_and_dpi() {
3268 use crate::tree::{ItemTree, TreeKind, TreeProv};
3269 let text = |label: &str, t: &str| TreeKind::Text {
3270 label: label.into(),
3271 text: t.into(),
3272 orig: None,
3273 formatting: None,
3274 hyperlink: None,
3275 level: None,
3276 list: None,
3277 };
3278 let mut t = ItemTree::default();
3279 let slide = t.add(
3280 None,
3281 None,
3282 TreeKind::Group {
3283 label: "chapter".into(),
3284 name: "slide-0".into(),
3285 },
3286 );
3287 t.add_with_prov(
3288 Some(slide),
3289 None,
3290 text("paragraph", "héllo"),
3291 TreeProv {
3292 page_no: 1,
3293 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
3294 bottom_left: true,
3295 charspan: [0, 5],
3296 },
3297 );
3298 t.add_with_prov(
3299 Some(slide),
3300 None,
3301 TreeKind::Picture {
3302 captions: Vec::new(),
3303 image: Some(crate::PictureImage {
3304 dpi: crate::PictureImage::DEFAULT_DPI,
3305 mimetype: "image/png".into(),
3306 width: 2,
3307 height: 2,
3308 data: vec![0],
3309 }),
3310 classification: None,
3311 confidence: None,
3312 chart: None,
3313 dpi: Some(300),
3314 },
3315 TreeProv {
3316 page_no: 1,
3317 bbox: [0.0; 4],
3318 bottom_left: false,
3319 charspan: [0, 0],
3320 },
3321 );
3322 t.add(
3323 Some(slide),
3324 Some(ContentLayer::Notes),
3325 text("text", "no geometry"),
3326 );
3327 let mut doc = DoclingDocument::new("t");
3328 doc.push(Node::PageInfo {
3329 page_no: 1,
3330 width: 9144000.0,
3331 height: 6858000.0,
3332 });
3333 doc.tree = Some(t);
3334 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3335 assert_eq!(
3336 v["texts"][0]["prov"],
3337 serde_json::json!([{
3338 "page_no": 1,
3339 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
3340 "charspan": [0, 5],
3341 }])
3342 );
3343 assert_eq!(v["texts"][0]["label"], "paragraph");
3344 assert_eq!(
3345 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
3346 "TOPLEFT"
3347 );
3348 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
3349 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
3350 assert_eq!(v["texts"][1]["content_layer"], "notes");
3351 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
3352 assert_eq!(v["pages"]["1"]["page_no"], 1);
3353 }
3354
3355 #[test]
3360 fn provenance_boxes_are_clamped_to_their_page() {
3361 let mut doc = DoclingDocument::new("t");
3362 doc.push(Node::PageInfo {
3363 page_no: 1,
3364 width: 10.0,
3365 height: 8.0,
3366 });
3367 doc.push(Node::Prov {
3368 page_no: 1,
3369 bbox: [-1.0, 2.0, 12.0, 9.5],
3370 charspan: [0, 1],
3371 seq: None,
3372 inner: Box::new(Node::Paragraph { text: "x".into() }),
3373 });
3374 let mut table = Table {
3375 rows: vec![vec!["a".into()]],
3376 ..Table::default()
3377 };
3378 table.cells = Some(vec![crate::TableCell {
3379 text: "a".into(),
3380 bbox: Some([1.0, 1.0, 11.0, 9.0]),
3381 start_row: 0,
3382 start_col: 0,
3383 row_span: 1,
3384 col_span: 1,
3385 column_header: false,
3386 row_header: false,
3387 row_section: false,
3388 }]);
3389 doc.push(Node::Prov {
3390 page_no: 1,
3391 bbox: [0.0, 0.0, 10.0, 8.0],
3392 charspan: [0, 0],
3393 seq: None,
3394 inner: Box::new(Node::Table(table)),
3395 });
3396 doc.push(Node::Prov {
3397 page_no: 7,
3398 bbox: [-5.0, 0.0, 50.0, 50.0],
3399 charspan: [0, 1],
3400 seq: None,
3401 inner: Box::new(Node::Paragraph { text: "y".into() }),
3402 });
3403 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3404 assert_eq!(
3405 v["texts"][0]["prov"][0]["bbox"],
3406 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
3407 );
3408 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
3409 assert_eq!(
3410 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
3411 (Some(1.0), Some(10.0), Some(8.0))
3412 );
3413 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
3414 assert_eq!(
3415 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
3416 "page 7 is not described"
3417 );
3418 }
3419
3420 #[test]
3427 fn misplaced_list_items_are_wrapped_like_docling_core() {
3428 use crate::tree::{ItemTree, ListMeta, TreeKind};
3429 let mut t = ItemTree::default();
3430 let li = |txt: &str| TreeKind::Text {
3431 label: "list_item".into(),
3432 text: txt.into(),
3433 orig: None,
3434 formatting: None,
3435 hyperlink: None,
3436 level: None,
3437 list: Some(ListMeta {
3438 enumerated: false,
3439 marker: "-".into(),
3440 }),
3441 };
3442 let group = |label: &str, name: &str| TreeKind::Group {
3443 label: label.into(),
3444 name: name.into(),
3445 };
3446 t.add(None, None, li("A"));
3447 t.add(None, None, li("B"));
3448 t.add(
3449 None,
3450 None,
3451 TreeKind::Text {
3452 label: "text".into(),
3453 text: "plain".into(),
3454 orig: None,
3455 formatting: None,
3456 hyperlink: None,
3457 level: None,
3458 list: None,
3459 },
3460 );
3461 let cell = t.add(None, None, group("unspecified", "cell"));
3462 t.add(Some(cell), None, li("C"));
3463 t.add(Some(cell), None, li("D"));
3464 let list = t.add(None, None, group("list", "list"));
3465 t.add(Some(list), None, li("E"));
3466 let doc = DoclingDocument {
3467 tree: Some(t),
3468 ..DoclingDocument::new("t")
3469 };
3470 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3471 let refs = |x: &Value| -> Vec<String> {
3472 x.as_array()
3473 .unwrap()
3474 .iter()
3475 .map(|r| r["$ref"].as_str().unwrap().to_string())
3476 .collect()
3477 };
3478 assert_eq!(
3479 refs(&v["body"]["children"]),
3480 ["#/groups/4", "#/texts/0", "#/groups/0", "#/groups/1"]
3481 );
3482 assert_eq!(
3483 refs(&v["groups"][0]["children"]),
3484 ["#/groups/3", "#/groups/2"]
3485 );
3486 for (g, name, kids) in [
3487 (2, "group", vec!["#/texts/2"]),
3488 (3, "group", vec!["#/texts/3"]),
3489 (4, "group", vec!["#/texts/4", "#/texts/5"]),
3490 (1, "list", vec!["#/texts/1"]),
3491 ] {
3492 assert_eq!(v["groups"][g]["label"], "list");
3493 assert_eq!(v["groups"][g]["name"], name);
3494 assert_eq!(refs(&v["groups"][g]["children"]), kids);
3495 }
3496 let texts: Vec<&str> = v["texts"]
3497 .as_array()
3498 .unwrap()
3499 .iter()
3500 .map(|t| t["text"].as_str().unwrap())
3501 .collect();
3502 assert_eq!(texts, ["plain", "E", "D", "C", "A", "B"]);
3503 assert_eq!(v["texts"][2]["parent"]["$ref"], "#/groups/2");
3504 }
3505
3506 #[test]
3509 fn a_deleted_child_is_not_written_as_an_empty_ref() {
3510 use crate::tree::{ItemTree, TreeKind};
3511 let mut t = ItemTree::default();
3512 let text = |txt: &str| TreeKind::Text {
3513 label: "text".into(),
3514 text: txt.into(),
3515 orig: None,
3516 formatting: None,
3517 hyperlink: None,
3518 level: None,
3519 list: None,
3520 };
3521 let g = t.add(
3522 None,
3523 None,
3524 TreeKind::Group {
3525 label: "unspecified".into(),
3526 name: "g".into(),
3527 },
3528 );
3529 let gone = t.add(Some(g), None, text(""));
3530 t.add(Some(g), None, text("kept"));
3531 t.items[gone].deleted = true; let doc = DoclingDocument {
3533 tree: Some(t),
3534 ..DoclingDocument::new("t")
3535 };
3536 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3537 assert_eq!(
3538 v["groups"][0]["children"],
3539 serde_json::json!([{"$ref": "#/texts/0"}])
3540 );
3541 }
3542
3543 #[test]
3544 fn a_backend_item_tree_is_written_verbatim() {
3545 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
3546 let mut t = ItemTree::default();
3547 let text = |label: &str, txt: &str| TreeKind::Text {
3548 label: label.into(),
3549 text: txt.into(),
3550 orig: None,
3551 formatting: None,
3552 hyperlink: None,
3553 level: None,
3554 list: None,
3555 };
3556 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
3557 let h = t.add(None, None, text("title", "Heading"));
3558 let group = t.add(
3559 Some(h),
3560 None,
3561 TreeKind::Group {
3562 label: "inline".into(),
3563 name: "group".into(),
3564 },
3565 );
3566 t.add(
3567 Some(group),
3568 None,
3569 TreeKind::Text {
3570 label: "text".into(),
3571 text: "bold".into(),
3572 orig: None,
3573 formatting: Some(Formatting {
3574 bold: true,
3575 ..Formatting::default()
3576 }),
3577 hyperlink: Some("https://example.com/".into()),
3578 level: None,
3579 list: None,
3580 },
3581 );
3582 t.add(
3583 Some(group),
3584 None,
3585 TreeKind::Code {
3586 text: "x = 1".into(),
3587 orig: None,
3588 language: Some("python".into()),
3589 formatting: None,
3590 hyperlink: None,
3591 },
3592 );
3593 let sub = t.add(
3594 Some(h),
3595 None,
3596 TreeKind::Text {
3597 label: "section_header".into(),
3598 text: "Sub".into(),
3599 orig: Some("Sub\u{2019}".into()),
3600 formatting: None,
3601 hyperlink: None,
3602 level: Some(1),
3603 list: None,
3604 },
3605 );
3606 let item = t.add(
3607 Some(sub),
3608 None,
3609 TreeKind::Text {
3610 label: "list_item".into(),
3611 text: "item".into(),
3612 orig: None,
3613 formatting: None,
3614 hyperlink: None,
3615 level: None,
3616 list: Some(ListMeta {
3617 enumerated: true,
3618 marker: "3.".into(),
3619 }),
3620 },
3621 );
3622 let _region = t.add(
3623 Some(sub),
3624 None,
3625 TreeKind::FieldRegion {
3626 items: vec![crate::FieldItem {
3627 marker: None,
3628 key: Some("Name".into()),
3629 value: Some("Duck".into()),
3630 value_kind: Some("read_only".into()),
3631 }],
3632 },
3633 );
3634 let table = t.add(
3635 Some(sub),
3636 None,
3637 TreeKind::Table {
3638 table: Table {
3639 rows: vec![vec!["a \n<".into(), "b".into()]],
3640 cells: Some(vec![
3641 crate::TableCell {
3642 text: "a \n<".into(),
3643 bbox: None,
3644 start_row: 0,
3645 start_col: 0,
3646 row_span: 3,
3647 col_span: 1,
3648 column_header: false,
3649 row_header: true,
3650 row_section: false,
3651 },
3652 crate::TableCell {
3653 text: "b".into(),
3654 bbox: None,
3655 start_row: 0,
3656 start_col: 1,
3657 row_span: 1,
3658 col_span: 1,
3659 column_header: false,
3660 row_header: false,
3661 row_section: false,
3662 },
3663 ]),
3664 ..Table::default()
3665 },
3666 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
3668 },
3669 );
3670 let cell_group = t.add(
3671 Some(table),
3672 None,
3673 TreeKind::Group {
3674 label: "unspecified".into(),
3675 name: "rich_cell_group_1_0_0".into(),
3676 },
3677 );
3678 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
3679 *rich_cells = vec![(0, 1, cell_group)];
3680 }
3681 let after = t.add(Some(sub), None, text("text", "after the region"));
3682 let _ = (title, after);
3683 let list = t.add(
3687 Some(sub),
3688 None,
3689 TreeKind::Group {
3690 label: "list".into(),
3691 name: "list".into(),
3692 },
3693 );
3694 t.reparent(item, Some(list));
3695
3696 let doc = DoclingDocument {
3697 tree: Some(t),
3698 ..DoclingDocument::new("t")
3699 };
3700 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3701 let texts: Vec<&str> = v["texts"]
3703 .as_array()
3704 .unwrap()
3705 .iter()
3706 .map(|t| t["text"].as_str().unwrap())
3707 .collect();
3708 assert_eq!(
3709 texts,
3710 [
3711 "Page",
3712 "Heading",
3713 "bold",
3714 "x = 1",
3715 "Sub",
3716 "item",
3717 "Name",
3718 "Duck",
3719 "after the region"
3720 ]
3721 );
3722 assert_eq!(
3723 v["body"]["children"],
3724 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
3725 );
3726 assert_eq!(v["texts"][0]["content_layer"], "furniture");
3727 assert_eq!(
3728 v["texts"][1]["children"],
3729 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
3730 );
3731 let bold = &v["texts"][2];
3732 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
3733 let keys: Vec<&str> = bold
3734 .as_object()
3735 .unwrap()
3736 .keys()
3737 .map(String::as_str)
3738 .collect();
3739 assert_eq!(
3740 keys,
3741 [
3742 "self_ref",
3743 "parent",
3744 "children",
3745 "content_layer",
3746 "label",
3747 "prov",
3748 "orig",
3749 "text",
3750 "formatting",
3751 "hyperlink"
3752 ]
3753 );
3754 assert_eq!(
3755 bold["formatting"],
3756 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
3757 );
3758 let code = &v["texts"][3];
3759 assert_eq!(code["label"], "code");
3760 assert_eq!(code["code_language"], "Python");
3761 let sub = &v["texts"][4];
3762 assert_eq!(sub["orig"], "Sub\u{2019}");
3763 assert_eq!(sub["level"], 1);
3764 let item = &v["texts"][5];
3765 let keys: Vec<&str> = item
3766 .as_object()
3767 .unwrap()
3768 .keys()
3769 .map(String::as_str)
3770 .collect();
3771 assert_eq!(
3772 keys,
3773 [
3774 "self_ref",
3775 "parent",
3776 "children",
3777 "content_layer",
3778 "label",
3779 "prov",
3780 "orig",
3781 "text",
3782 "enumerated",
3783 "marker"
3784 ]
3785 );
3786 assert_eq!(item["marker"], "3.");
3787 assert_eq!(v["texts"][7]["kind"], "read_only");
3788 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
3789 let table = &v["tables"][0];
3790 assert_eq!(
3791 table["children"],
3792 serde_json::json!([{"$ref": "#/groups/1"}])
3793 );
3794 let cells = table["data"]["table_cells"].as_array().unwrap();
3795 assert_eq!(
3796 cells[0]["text"], "a \n<",
3797 "raw cell text is written verbatim"
3798 );
3799 assert_eq!(
3800 cells[0]["end_row_offset_idx"], 3,
3801 "declared spans are not clamped"
3802 );
3803 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
3804 assert!(cells[0].get("ref").is_none());
3805 assert!(
3806 table["data"]["grid"][0][1].get("ref").is_none(),
3807 "the grid shows plain cells"
3808 );
3809 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
3810 }
3811
3812 #[test]
3816 fn a_comment_section_can_be_referenced_by_its_note_text() {
3817 let doc = DoclingDocument {
3818 name: "c".into(),
3819 nodes: vec![
3820 Node::Commented {
3821 comments: vec![0],
3822 inner: Box::new(Node::Paragraph {
3823 text: "annotated".into(),
3824 }),
3825 },
3826 Node::CommentSection {
3827 name: "comment-Sheet1-A1".into(),
3828 text: "[author: A]: note".into(),
3829 refs_note_text: true,
3830 grouped: true,
3831 },
3832 ],
3833 ..DoclingDocument::new("c")
3834 };
3835 let v = crate::json::to_json(&doc);
3836 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
3837 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
3838 }
3839
3840 #[test]
3844 fn comment_sections_link_back_to_their_items() {
3845 let doc = DoclingDocument {
3846 name: "c".into(),
3847 nodes: vec![
3848 Node::Commented {
3849 comments: vec![0],
3850 inner: Box::new(Node::Paragraph {
3851 text: "annotated".into(),
3852 }),
3853 },
3854 Node::Paragraph {
3855 text: "plain".into(),
3856 },
3857 Node::CommentSection {
3858 name: "comment-7".into(),
3859 text: "[time: t]: note".into(),
3860 refs_note_text: false,
3861 grouped: true,
3862 },
3863 ],
3864 ..DoclingDocument::new("c")
3865 };
3866 let v = crate::json::to_json(&doc);
3867 assert_eq!(v["groups"][0]["label"], "comment_section");
3869 assert_eq!(v["groups"][0]["name"], "comment-7");
3870 assert_eq!(v["groups"][0]["content_layer"], "notes");
3871 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
3872 assert_eq!(v["texts"][2]["content_layer"], "notes");
3873 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
3875 assert!(v["texts"][1].get("comments").is_none());
3876 let keys: Vec<&str> = v["texts"][0]
3878 .as_object()
3879 .unwrap()
3880 .keys()
3881 .map(String::as_str)
3882 .collect();
3883 assert_eq!(
3884 &keys[keys.len() - 4..],
3885 &["prov", "comments", "orig", "text"]
3886 );
3887 }
3888
3889 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
3890 Node::Picture {
3891 caption: Some(caption.into()),
3892 caption_href: None,
3893 image: None,
3894 classification: None,
3895 caption_parent,
3896 caption_location: None,
3897 }
3898 }
3899
3900 fn group(children: Vec<Node>) -> Node {
3901 Node::Group {
3902 label: "section".into(),
3903 name: None,
3904 layer: None,
3905 children,
3906 }
3907 }
3908
3909 fn refs(v: &Value) -> Vec<&str> {
3910 v.as_array()
3911 .unwrap()
3912 .iter()
3913 .map(|r| r["$ref"].as_str().unwrap())
3914 .collect()
3915 }
3916
3917 #[test]
3922 fn a_body_caption_follows_the_enclosing_top_level_item() {
3923 let mut doc = DoclingDocument::new("t");
3924 doc.push(picture("top", CaptionParent::Body));
3925 doc.push(group(vec![
3926 Node::Paragraph { text: "p".into() },
3927 picture("nested", CaptionParent::Body),
3928 ]));
3929 doc.push(Node::Paragraph {
3930 text: "after".into(),
3931 });
3932 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3933 assert_eq!(
3934 refs(&v["body"]["children"]),
3935 [
3936 "#/texts/0",
3937 "#/pictures/0",
3938 "#/groups/0",
3939 "#/texts/2",
3940 "#/texts/3"
3941 ]
3942 );
3943 assert_eq!(
3944 refs(&v["groups"][0]["children"]),
3945 ["#/texts/1", "#/pictures/1"]
3946 );
3947 for (cap, pic) in [(0, 0), (2, 1)] {
3948 assert_eq!(v["texts"][cap]["label"], "caption");
3949 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
3950 assert_eq!(
3951 refs(&v["pictures"][pic]["captions"]),
3952 [format!("#/texts/{cap}")]
3953 );
3954 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
3955 }
3956 }
3957
3958 #[test]
3961 fn an_item_caption_is_the_items_first_child() {
3962 let mut doc = DoclingDocument::new("t");
3963 doc.push(picture("fig", CaptionParent::Item));
3964 doc.push(Node::Table(Table {
3965 rows: vec![vec!["a".into()]],
3966 caption: Some("tab".into()),
3967 caption_parent: CaptionParent::Item,
3968 ..Table::default()
3969 }));
3970 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3971 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
3972 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
3973 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
3974 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3975 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
3976 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
3977 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
3978 }
3979
3980 #[test]
3986 fn picture_children_hang_off_the_picture_after_its_caption() {
3987 let mut doc = DoclingDocument::new("t");
3988 doc.push(picture("fig", CaptionParent::Item));
3989 doc.push(Node::PictureChildren(vec![
3990 Node::Heading {
3991 level: 2,
3992 text: "in-figure title".into(),
3993 },
3994 Node::Paragraph {
3995 text: "axis label".into(),
3996 },
3997 Node::ListItem {
3998 ordered: false,
3999 number: 0,
4000 first_in_list: true,
4001 text: "callout".into(),
4002 level: 0,
4003 marker: None,
4004 location: None,
4005 dclx: None,
4006 href: None,
4007 layer: None,
4008 },
4009 ]));
4010 doc.push(Node::Paragraph {
4011 text: "after".into(),
4012 });
4013 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4014 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/texts/4"]);
4015 assert_eq!(
4016 refs(&v["pictures"][0]["children"]),
4017 ["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"]
4018 );
4019 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
4020 assert_eq!(v["texts"][1]["label"], "section_header");
4021 assert_eq!(v["texts"][2]["label"], "text");
4022 for t in 1..=2 {
4023 assert_eq!(v["texts"][t]["parent"]["$ref"], "#/pictures/0");
4024 assert_eq!(v["texts"][t]["content_layer"], "body");
4025 }
4026 assert_eq!(v["groups"][0]["parent"]["$ref"], "#/pictures/0");
4027 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
4028 let md = doc.export_to_markdown();
4029 assert!(
4030 !md.contains("axis label") && !md.contains("callout"),
4031 "{md}"
4032 );
4033 assert!(md.contains("after"), "{md}");
4034 }
4035
4036 #[test]
4040 fn a_container_caption_is_the_items_sibling() {
4041 let mut doc = DoclingDocument::new("t");
4042 doc.push(group(vec![
4043 picture("chart", CaptionParent::Container),
4044 Node::Table(Table {
4045 rows: vec![vec!["a".into()]],
4046 caption: Some("figcaption".into()),
4047 caption_parent: CaptionParent::ContainerAfter,
4048 ..Table::default()
4049 }),
4050 ]));
4051 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4052 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
4053 assert_eq!(
4054 refs(&v["groups"][0]["children"]),
4055 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
4056 );
4057 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
4058 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
4059 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
4060 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
4061 }
4062
4063 #[test]
4068 fn a_labeled_text_keeps_its_label_and_hyperlink_out_of_the_text() {
4069 let mut doc = DoclingDocument::new("t");
4070 doc.push(Node::LabeledText {
4071 label: "footnote".into(),
4072 text: "1 https://example.com/a\\_b".into(),
4073 href: Some("https://example.com/a_b".into()),
4074 });
4075 doc.push(Node::LabeledText {
4076 label: "footnote".into(),
4077 text: "© 2022 the authors".into(),
4078 href: None,
4079 });
4080 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4081 let t = &v["texts"][0];
4082 assert_eq!(t["label"], "footnote");
4083 assert_eq!(t["text"], "1 https://example.com/a_b");
4084 assert_eq!(t["orig"], "1 https://example.com/a_b");
4085 assert_eq!(t["hyperlink"], "https://example.com/a_b");
4086 assert_eq!(v["texts"][1]["label"], "footnote");
4087 assert!(v["texts"][1].get("hyperlink").is_none());
4088 assert_eq!(
4089 doc.export_to_markdown(),
4090 "[1 https://example.com/a\\_b](https://example.com/a_b)\n\n© 2022 the authors\n"
4091 );
4092 assert_eq!(
4093 doc.export_to_text(),
4094 "1 https://example.com/a_b\n\n© 2022 the authors"
4095 );
4096 }
4097}