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
124fn clamp_boxes_to_pages(out: &mut Value, pages: &[(usize, f64, f64)]) {
129 if pages.is_empty() {
130 return;
131 }
132 let mut sizes: std::collections::HashMap<usize, (f64, f64)> =
135 std::collections::HashMap::with_capacity(pages.len());
136 for &(p, w, h) in pages {
137 sizes.entry(p).or_insert((w, h));
138 }
139 let size = |page_no: &Value| -> Option<(f64, f64)> {
140 let n = page_no.as_u64()? as usize;
141 sizes.get(&n).copied()
142 };
143 let r2 = |v: f64| (v * 100.0).round() / 100.0;
144 let clamp_bbox = |bbox: &mut Value, (w, h): (f64, f64)| {
145 for (key, hi) in [("l", w), ("r", w), ("t", h), ("b", h)] {
146 if let Some(v) = bbox.get(key).and_then(Value::as_f64) {
147 bbox[key] = json!(r2(v.clamp(0.0, hi.max(0.0))));
148 }
149 }
150 };
151 for bucket in [
152 "texts",
153 "pictures",
154 "tables",
155 "key_value_items",
156 "form_items",
157 "field_regions",
158 "field_items",
159 ] {
160 let Some(items) = out.get_mut(bucket).and_then(Value::as_array_mut) else {
161 continue;
162 };
163 for item in items {
164 let mut table_page: Option<Option<(f64, f64)>> = None;
165 if let Some(provs) = item.get_mut("prov").and_then(Value::as_array_mut) {
166 let mut page_nos: Vec<u64> = Vec::new();
167 for prov in provs.iter_mut() {
168 if let Some(n) = prov.get("page_no").and_then(Value::as_u64) {
169 page_nos.push(n);
170 }
171 let Some(sz) = prov.get("page_no").and_then(size) else {
172 continue;
173 };
174 if let Some(bbox) = prov.get_mut("bbox") {
175 clamp_bbox(bbox, sz);
176 }
177 }
178 page_nos.sort_unstable();
179 page_nos.dedup();
180 if let [only] = page_nos[..] {
181 table_page = Some(size(&json!(only)));
182 }
183 }
184 if let (Some(Some(sz)), Some(data)) = (table_page, item.get_mut("data")) {
186 for key in ["table_cells", "grid"] {
187 let Some(rows) = data.get_mut(key).and_then(Value::as_array_mut) else {
188 continue;
189 };
190 for entry in rows.iter_mut() {
191 let cells: Vec<&mut Value> = match entry {
192 Value::Array(row) => row.iter_mut().collect(),
193 other => vec![other],
194 };
195 for cell in cells {
196 if let Some(bbox) = cell.get_mut("bbox").filter(|b| b.is_object()) {
197 clamp_bbox(bbox, sz);
198 }
199 }
200 }
201 }
202 }
203 }
204 }
205}
206
207fn formatting_json(f: &crate::tree::Formatting) -> Value {
209 json!({
210 "bold": f.bold,
211 "italic": f.italic,
212 "underline": f.underline,
213 "strikethrough": f.strikethrough,
214 "script": match f.script {
215 crate::Script::Baseline => "baseline",
216 crate::Script::Sub => "sub",
217 crate::Script::Super => "super",
218 },
219 })
220}
221
222fn track_json(t: &crate::tree::TreeTrack) -> Value {
225 let mut m = serde_json::Map::new();
226 m.insert("kind".into(), json!("track"));
227 m.insert("start_time".into(), json!(t.start_time));
228 m.insert("end_time".into(), json!(t.end_time));
229 if let Some(id) = &t.identifier {
230 m.insert("identifier".into(), json!(id));
231 }
232 if let Some(v) = &t.voice {
233 m.insert("voice".into(), json!(v));
234 }
235 Value::Object(m)
236}
237
238fn image_ref_json(img: &crate::PictureImage) -> Value {
242 let (mimetype, uri) = crate::pixel_digest::docling_data_uri(img);
243 json!({
244 "mimetype": mimetype,
245 "dpi": img.dpi,
246 "size": { "width": img.width as f64, "height": img.height as f64 },
247 "uri": uri,
248 })
249}
250
251pub fn to_json(doc: &DoclingDocument) -> Value {
253 build_json(doc, false)
254}
255
256pub(crate) fn to_json_with_notes(doc: &DoclingDocument) -> Value {
261 build_json(doc, true)
262}
263
264fn build_json(doc: &DoclingDocument, note_keys: bool) -> Value {
265 let mut b = Builder {
266 note_keys,
267 ..Builder::default()
268 };
269 let body = match &doc.tree {
273 Some(tree) => {
274 for n in &doc.nodes {
277 if let Node::PageInfo {
278 page_no,
279 width,
280 height,
281 } = n
282 {
283 if *page_no > 0 {
284 b.pages.push((*page_no, *width as f64, *height as f64));
285 }
286 }
287 }
288 let mut tree = tree.clone();
292 tree.wrap_misplaced_list_items();
293 b.write_tree(&tree)
294 }
295 None => b.walk_into(&doc.nodes, "#/body"),
296 };
297 b.link_comments();
298
299 let pages = b
300 .pages
301 .iter()
302 .map(|(n, w, h)| {
303 let r2 = |v: f64| (v * 100.0).round() / 100.0;
304 let mut page = serde_json::Map::new();
307 page.insert("size".into(), json!({ "width": r2(*w), "height": r2(*h) }));
308 if let Some(img) = doc.page_images.get(n) {
309 page.insert("image".into(), image_ref_json(img));
310 }
311 page.insert("page_no".into(), json!(n));
312 (n.to_string(), Value::Object(page))
313 })
314 .collect::<serde_json::Map<String, Value>>();
315 let mut out = object([
319 ("schema_name", "DoclingDocument".into()),
320 ("version", SCHEMA_VERSION.into()),
321 ("name", doc.name.as_str().into()),
322 (
323 "origin",
324 json!({
325 "mimetype": "text/plain",
326 "binary_hash": fnv1a(&doc.name),
327 "filename": doc.name,
328 }),
329 ),
330 (
331 "furniture",
332 json!({
333 "self_ref": "#/furniture",
334 "children": [],
335 "content_layer": "furniture",
336 "name": "_root_",
337 "label": "unspecified",
338 }),
339 ),
340 (
341 "body",
342 object([
343 ("self_ref", "#/body".into()),
344 ("children", Value::Array(body)),
345 ("content_layer", "body".into()),
346 ("name", "_root_".into()),
347 ("label", "unspecified".into()),
348 ]),
349 ),
350 ("groups", Value::Array(std::mem::take(&mut b.groups))),
351 ("texts", Value::Array(std::mem::take(&mut b.texts))),
352 ("pictures", Value::Array(std::mem::take(&mut b.pictures))),
353 ("tables", Value::Array(std::mem::take(&mut b.tables))),
354 (
355 "key_value_items",
356 Value::Array(std::mem::take(&mut b.key_value_items)),
357 ),
358 ("form_items", Value::Array(Vec::new())),
359 ("pages", Value::Object(pages)),
360 ]);
361
362 clamp_boxes_to_pages(&mut out, &b.pages);
371
372 if !b.field_regions.is_empty() {
377 if let Some(obj) = out.as_object_mut() {
378 let pages = obj.remove("pages");
379 obj.insert("field_regions".into(), Value::Array(b.field_regions));
380 obj.insert("field_items".into(), Value::Array(b.field_items));
381 if let Some(pages) = pages {
382 obj.insert("pages".into(), pages);
383 }
384 }
385 }
386 out
387}
388
389fn classification_meta(classes: &[crate::PictureClass]) -> Value {
396 json!({
397 "classification": {
398 "predictions": classes.iter().map(|c| json!({
399 "confidence": c.confidence as f64,
400 "created_by": "DocumentPictureClassifier",
401 "class_name": c.class_name,
402 })).collect::<Vec<_>>(),
403 },
404 "annotations": [{
405 "kind": "classification",
406 "provenance": "DocumentPictureClassifier",
407 "predicted_classes": classes.iter().map(|c| json!({
408 "class_name": c.class_name,
409 "confidence": c.confidence as f64,
410 })).collect::<Vec<_>>(),
411 }],
412 })
413}
414
415#[allow(clippy::too_many_arguments)]
426fn cell_value(
427 row_span: usize,
428 col_span: usize,
429 start_row: usize,
430 end_row: usize,
431 start_col: usize,
432 end_col: usize,
433 text: String,
434 column_header: bool,
435 row_header: bool,
436 row_section: bool,
437 bbox: Option<[f32; 4]>,
438) -> Value {
439 let mut m = serde_json::Map::with_capacity(12);
440 m.insert("row_span".into(), row_span.into());
441 m.insert("col_span".into(), col_span.into());
442 m.insert("start_row_offset_idx".into(), start_row.into());
443 m.insert("end_row_offset_idx".into(), end_row.into());
444 m.insert("start_col_offset_idx".into(), start_col.into());
445 m.insert("end_col_offset_idx".into(), end_col.into());
446 m.insert("text".into(), Value::String(text));
447 m.insert("column_header".into(), column_header.into());
448 m.insert("row_header".into(), row_header.into());
449 m.insert("row_section".into(), row_section.into());
450 m.insert("fillable".into(), false.into());
451 if let Some(b) = bbox {
452 m.insert(
453 "bbox".into(),
454 json!({
455 "l": b[0], "t": b[1], "r": b[2], "b": b[3],
456 "coord_origin": "TOPLEFT",
457 }),
458 );
459 }
460 Value::Object(m)
461}
462
463fn table_data(t: &Table) -> Value {
466 table_data_with(t, false)
467}
468
469fn table_data_with(t: &Table, raw: bool) -> Value {
472 let cell_text = |s: &str| {
473 if raw {
474 s.to_string()
475 } else {
476 unescape_text(&crate::markdown::strip_hard_breaks(s))
477 }
478 };
479 let mut num_rows = t.rows.len();
480 let mut num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
481 let ragged = t.rows.iter().any(|r| r.len() != num_cols);
489 if let Some(first_class) = t.cells.as_ref().filter(|c| ragged && !c.is_empty()) {
490 for c in first_class {
491 num_rows = num_rows.max(c.start_row + c.row_span);
492 num_cols = num_cols.max(c.start_col + c.col_span);
493 }
494 }
495 let mut grid = Vec::with_capacity(num_rows);
496 let mut cells = Vec::new();
497 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
501 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
502 for c in first_class {
503 let idx = cells.len();
504 cells.push(cell_value(
505 c.row_span,
506 c.col_span,
507 c.start_row,
508 c.start_row + c.row_span,
509 c.start_col,
510 c.start_col + c.col_span,
511 cell_text(&c.text),
512 c.column_header,
513 c.row_header,
514 c.row_section,
515 c.bbox,
516 ));
517 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
518 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
519 slot[r * num_cols + k] = Some(idx);
520 }
521 }
522 }
523 for r in 0..num_rows {
524 let mut grid_row = Vec::with_capacity(num_cols);
525 for c in 0..num_cols {
526 grid_row.push(match slot[r * num_cols + c] {
527 Some(i) => cells[i].clone(),
528 None => cell_value(
529 1,
530 1,
531 r,
532 r + 1,
533 c,
534 c + 1,
535 String::new(),
536 false,
537 false,
538 false,
539 None,
540 ),
541 });
542 }
543 grid.push(grid_row);
544 }
545 } else {
546 let s = t.structure.as_ref();
547 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
548 grid.and_then(|g| g.get(r))
549 .and_then(|row| row.get(c))
550 .copied()
551 .unwrap_or(false)
552 };
553 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
554 let (mut r0, mut c0) = (r, c);
555 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
556 c0 -= 1;
557 }
558 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
559 r0 -= 1;
560 }
561 (r0, c0)
562 };
563 let anchors: Vec<(usize, usize)> = (0..num_rows)
566 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
567 .map(|(r, c)| anchor_of(r, c))
568 .collect();
569 let mut extent: Vec<(usize, usize)> = (0..num_rows)
570 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
571 .collect();
572 for (i, &(ar, ac)) in anchors.iter().enumerate() {
573 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
574 let e = &mut extent[ar * num_cols + ac];
575 e.0 = e.0.max(r);
576 e.1 = e.1.max(c);
577 }
578 for (r, row) in t.rows.iter().enumerate() {
579 let mut grid_row = Vec::with_capacity(num_cols);
580 for c in 0..num_cols {
581 let (ar, ac) = anchors[r * num_cols + c];
582 if (ar, ac) == (r, c) {
583 let (er, ec) = extent[r * num_cols + c];
584 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
585 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
586 Some(s) => flag(Some(&s.col_header), r, c),
587 None => r == 0,
588 };
589 slot[r * num_cols + c] = Some(cells.len());
590 cells.push(cell_value(
591 er - r + 1,
592 ec - c + 1,
593 r,
594 er + 1,
595 c,
596 ec + 1,
597 text,
598 column_header,
599 flag(s.map(|s| &s.row_header), r, c),
600 false,
601 None,
602 ));
603 }
604 grid_row.push(match slot[ar * num_cols + ac] {
605 Some(i) => cells[i].clone(),
606 None => Value::Null,
607 });
608 }
609 grid.push(grid_row);
610 }
611 }
612 json!({
613 "table_cells": cells,
614 "num_rows": num_rows,
615 "num_cols": num_cols,
616 "orientation": "rot_0",
617 "grid": grid,
618 })
619}
620
621fn prov_value(page_no: usize, [l, t, r, b]: [f64; 4], origin: &str, charspan: [usize; 2]) -> Value {
624 let bbox = object([
625 ("l", l.into()),
626 ("t", t.into()),
627 ("r", r.into()),
628 ("b", b.into()),
629 ("coord_origin", origin.into()),
630 ]);
631 let span = Value::Array(vec![charspan[0].into(), charspan[1].into()]);
632 Value::Array(vec![object([
633 ("page_no", page_no.into()),
634 ("bbox", bbox),
635 ("charspan", span),
636 ])])
637}
638
639#[derive(Default)]
640struct Builder {
641 note_keys: bool,
645 texts: Vec<Value>,
646 groups: Vec<Value>,
647 tables: Vec<Value>,
648 pictures: Vec<Value>,
649 field_regions: Vec<Value>,
650 field_items: Vec<Value>,
651 key_value_items: Vec<Value>,
652 pages: Vec<(usize, f64, f64)>,
656 cur_page: usize,
658 cur_w: f64,
659 cur_h: f64,
660 pending_loc: Option<[u16; 4]>,
663 pending_exact: Option<ExactProv>,
666 pending_track: Option<(crate::tree::TreeTrack, String)>,
669 pending_siblings: Vec<Value>,
673 pending_after: Vec<Value>,
677 pending_body: Vec<Value>,
681 comment_groups: Vec<String>,
685 last_comment_section: Option<(String, String)>,
690 pending_comments: Vec<(String, Vec<usize>)>,
694}
695
696impl Builder {
697 fn take_prov(&mut self, char_len: usize) -> Value {
703 let prov = self.prov_json(char_len, false);
704 self.pending_exact = None;
705 self.pending_loc = None;
706 prov
707 }
708
709 fn prov_json(&self, char_len: usize, span_over_text: bool) -> Value {
714 let r2 = |v: f64| (v * 100.0).round() / 100.0;
715 if let Some(ExactProv {
716 page_no,
717 bbox: [l, t, r, b],
718 bottom_left,
719 charspan,
720 }) = self.pending_exact
721 {
722 let charspan = if span_over_text {
723 [0, char_len]
724 } else {
725 charspan
726 };
727 return prov_value(
728 page_no,
729 [r2(l), r2(t), r2(r), r2(b)],
730 if bottom_left { "BOTTOMLEFT" } else { "TOPLEFT" },
731 charspan,
732 );
733 }
734 let Some([x0, y0, x1, y1]) = self.pending_loc else {
735 return json!([]);
736 };
737 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
741 return prov_value(self.cur_page, [0.0; 4], "BOTTOMLEFT", [0, char_len]);
742 }
743 prov_value(
744 self.cur_page,
745 [
746 r2(x0 as f64 * self.cur_w / 512.0),
747 r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
748 r2(x1 as f64 * self.cur_w / 512.0),
749 r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
750 ],
751 "BOTTOMLEFT",
752 [0, char_len],
753 )
754 }
755
756 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
759 if self.pending_loc.is_none() && self.cur_page > 0 {
760 self.pending_loc = loc;
761 }
762 }
763
764 fn link_comments(&mut self) {
772 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
773 .into_iter()
774 .map(|(item, comments)| {
775 let refs = comments
776 .iter()
777 .filter_map(|i| self.comment_groups.get(*i))
778 .map(ref_value)
779 .collect();
780 (item, refs)
781 })
782 .collect();
783 for (item, comment_refs) in refs {
784 if comment_refs.is_empty() {
785 continue;
786 }
787 let Some(target) = self.item_mut(&item) else {
788 continue;
789 };
790 let Some(obj) = target.as_object_mut() else {
791 continue;
792 };
793 let tail: Vec<(String, Value)> = obj
794 .iter()
795 .skip_while(|(k, _)| k.as_str() != "prov")
796 .skip(1)
797 .map(|(k, v)| (k.clone(), v.clone()))
798 .collect();
799 for (k, _) in &tail {
800 obj.shift_remove(k);
801 }
802 obj.insert("comments".into(), Value::Array(comment_refs));
803 for (k, v) in tail {
804 obj.insert(k, v);
805 }
806 }
807 }
808
809 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
811 let idx = ref_index(self_ref)?;
812 let bucket = if self_ref.starts_with("#/texts/") {
813 &mut self.texts
814 } else if self_ref.starts_with("#/tables/") {
815 &mut self.tables
816 } else if self_ref.starts_with("#/pictures/") {
817 &mut self.pictures
818 } else if self_ref.starts_with("#/groups/") {
819 &mut self.groups
820 } else {
821 return None;
822 };
823 bucket.get_mut(idx)
824 }
825
826 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
830 use crate::tree::TreeKind;
831 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
834 let (mut nt, mut ng, mut ntb, mut np, mut nf, mut nk) = (0, 0, 0, 0, 0, 0);
835 for item in &tree.items {
836 if item.deleted {
837 refs.push(String::new());
838 continue;
839 }
840 let r = match &item.kind {
841 TreeKind::Text { .. } | TreeKind::Code { .. } => {
842 nt += 1;
843 format!("#/texts/{}", nt - 1)
844 }
845 TreeKind::Group { .. } => {
846 ng += 1;
847 format!("#/groups/{}", ng - 1)
848 }
849 TreeKind::Table { .. } => {
850 ntb += 1;
851 format!("#/tables/{}", ntb - 1)
852 }
853 TreeKind::Picture { .. } => {
854 np += 1;
855 format!("#/pictures/{}", np - 1)
856 }
857 TreeKind::FieldRegion { items } => {
858 nt += items
861 .iter()
862 .map(|i| {
863 [&i.marker, &i.key, &i.value]
864 .iter()
865 .filter(|p| p.is_some())
866 .count()
867 })
868 .sum::<usize>();
869 nf += 1;
870 format!("#/field_regions/{}", nf - 1)
871 }
872 TreeKind::KeyValueGraph { .. } => {
873 nk += 1;
874 format!("#/key_value_items/{}", nk - 1)
875 }
876 };
877 refs.push(r);
878 }
879 let ref_of = |id: usize| ref_value(refs[id].as_str());
880 for (id, item) in tree.items.iter().enumerate() {
881 if item.deleted {
882 continue;
883 }
884 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
885 let children: Vec<Value> = item
889 .children
890 .iter()
891 .filter(|&&c| !tree.items[c].deleted)
892 .map(|&c| ref_of(c))
893 .collect();
894 let layer = item.layer.map_or("body", |l| l.value());
895 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
898 let self_ref = match &item.kind {
899 TreeKind::Text {
900 label,
901 text,
902 orig,
903 formatting,
904 hyperlink,
905 level,
906 list,
907 } => {
908 let mut tail = serde_json::Map::new();
911 if let Some(f) = formatting {
912 tail.insert("formatting".into(), formatting_json(f));
913 }
914 if let Some(h) = hyperlink {
915 tail.insert("hyperlink".into(), json!(h));
916 }
917 if let Some(l) = level {
918 tail.insert("level".into(), json!(l));
919 }
920 if let Some(l) = list {
921 tail.insert("enumerated".into(), json!(l.enumerated));
922 tail.insert("marker".into(), json!(l.marker));
923 }
924 let r = format!("#/texts/{}", self.texts.len());
925 let prov = self.take_prov(text.chars().count());
926 let mut item_json = json!({
927 "self_ref": r,
928 "parent": { "$ref": parent },
929 "children": children,
930 "content_layer": layer,
931 "label": label,
932 "prov": prov,
933 });
934 if !item.comments.is_empty() {
937 item_json["comments"] =
938 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
939 }
940 if let Some(track) = &item.source {
943 item_json["source"] = json!([track_json(track)]);
944 }
945 merge(
946 &mut item_json,
947 json!({
948 "orig": orig.as_deref().unwrap_or(text),
949 "text": text,
950 }),
951 );
952 merge(&mut item_json, Value::Object(tail));
953 if self.note_keys {
954 if !item.notes.is_empty() {
955 item_json["_notes"] = Value::Array(
956 item.notes
957 .iter()
958 .map(|n| json!([n.offset, n.text]))
959 .collect(),
960 );
961 }
962 if item.note_body {
963 item_json["_note_body"] = json!(true);
964 }
965 }
966 self.texts.push(item_json);
967 r
968 }
969 TreeKind::Code {
970 text,
971 orig,
972 language,
973 formatting,
974 hyperlink,
975 } => {
976 let r = format!("#/texts/{}", self.texts.len());
977 let prov = self.take_prov(text.chars().count());
978 let mut item_json = json!({
979 "self_ref": r,
980 "parent": { "$ref": parent },
981 "children": children,
982 "content_layer": layer,
983 "label": "code",
984 "prov": prov,
985 });
986 if !item.comments.is_empty() {
987 item_json["comments"] =
988 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
989 }
990 merge(
991 &mut item_json,
992 json!({
993 "orig": orig.as_deref().unwrap_or(text),
994 "text": text,
995 }),
996 );
997 if let Some(f) = formatting {
998 item_json["formatting"] = formatting_json(f);
999 }
1000 if let Some(h) = hyperlink {
1001 item_json["hyperlink"] = json!(h);
1002 }
1003 merge(
1004 &mut item_json,
1005 json!({
1006 "captions": [],
1007 "references": [],
1008 "footnotes": [],
1009 "code_language": code_language(language.as_deref()),
1010 }),
1011 );
1012 self.texts.push(item_json);
1013 r
1014 }
1015 TreeKind::Group { label, name } => {
1016 self.pending_exact = None;
1017 let r = format!("#/groups/{}", self.groups.len());
1018 self.groups.push(json!({
1019 "self_ref": r,
1020 "parent": { "$ref": parent },
1021 "children": children,
1022 "content_layer": layer,
1023 "name": name,
1024 "label": label,
1025 }));
1026 r
1027 }
1028 TreeKind::Table {
1029 table,
1030 rich_cells,
1031 captions,
1032 } => {
1033 let r = self.add_table_with(table, parent, true);
1035 let idx = ref_index(&r).expect("table ref");
1036 let t = &mut self.tables[idx];
1037 t["children"] = Value::Array(children);
1038 t["content_layer"] = json!(layer);
1039 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
1040 for &(row, col, group) in rich_cells {
1044 let cell_ref = ref_of(group);
1045 let hit = |c: &Value| {
1046 c["start_row_offset_idx"] == json!(row)
1047 && c["start_col_offset_idx"] == json!(col)
1048 };
1049 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
1050 for c in cells.iter_mut().filter(|c| hit(c)) {
1051 c["ref"] = cell_ref.clone();
1052 }
1053 }
1054 }
1055 r
1056 }
1057 TreeKind::Picture {
1058 captions,
1059 image,
1060 classification,
1061 confidence,
1062 chart,
1063 dpi,
1064 } => {
1065 let mut meta = classification.as_ref().map(|c| {
1070 let mut pred = serde_json::Map::new();
1071 if let Some(conf) = confidence {
1072 pred.insert("confidence".into(), json!(conf));
1073 }
1074 pred.insert("class_name".into(), json!(c));
1075 json!({ "classification": { "predictions": [pred] } })
1076 });
1077 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
1078 if !t.rows.is_empty() {
1079 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
1080 }
1081 }
1082 let prov = self.take_prov(0);
1083 let r = self.push_picture(
1084 prov,
1085 captions.iter().map(|&c| ref_of(c)).collect(),
1086 children,
1087 image.as_ref(),
1088 meta,
1089 parent,
1090 );
1091 if let Some(idx) = ref_index(&r) {
1092 self.pictures[idx]["content_layer"] = json!(layer);
1093 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
1096 img["dpi"] = json!(dpi);
1097 }
1098 }
1099 r
1100 }
1101 TreeKind::FieldRegion { items } => {
1102 self.pending_exact = None;
1103 let r = self.add_field_region(items, parent);
1104 if let Some(region) = self.field_regions.last_mut() {
1105 region["content_layer"] = json!(layer);
1106 }
1107 r
1108 }
1109 TreeKind::KeyValueGraph { cells, links } => {
1110 self.pending_exact = None;
1111 let r = self.add_key_value_graph(cells, links, parent);
1112 if let Some(item) = self.key_value_items.last_mut() {
1113 item["content_layer"] = json!(layer);
1114 }
1115 r
1116 }
1117 };
1118 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1119 }
1120 tree.body
1121 .iter()
1122 .filter(|&&c| !tree.items[c].deleted)
1123 .map(|&c| ref_of(c))
1124 .collect()
1125 }
1126
1127 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1128 match node {
1129 Node::Heading { level: 1, text } => {
1130 Some(self.add_text("title", text, parent, json!({})))
1131 }
1132 Node::Heading { level, text } => Some(self.add_text(
1133 "section_header",
1134 text,
1135 parent,
1136 json!({ "level": level.saturating_sub(1) }),
1137 )),
1138 Node::Caption { text, href } => {
1139 let extra = match href {
1140 Some(url) => json!({ "hyperlink": url }),
1141 None => json!({}),
1142 };
1143 Some(self.add_text("caption", text, parent, extra))
1144 }
1145 Node::LabeledText { label, text, href } => {
1148 let extra = match href {
1149 Some(url) => json!({ "hyperlink": url }),
1150 None => json!({}),
1151 };
1152 Some(self.add_text(label, text, parent, extra))
1153 }
1154 Node::Paragraph { text } => {
1155 let t = text.trim();
1158 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1159 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1160 _ => Some(self.add_text("text", text, parent, json!({}))),
1161 }
1162 }
1163 Node::CheckboxItem { checked, text } => {
1164 let label = if *checked {
1170 "checkbox_selected"
1171 } else {
1172 "checkbox_unselected"
1173 };
1174 Some(self.add_text(label, text, parent, json!({})))
1175 }
1176 Node::Code {
1177 language,
1178 text,
1179 orig,
1180 ..
1181 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1182 Node::Formula {
1185 latex,
1186 orig,
1187 location,
1188 } => {
1189 self.adopt_loc(*location);
1190 Some(self.add_formula_item(latex, orig, parent))
1191 }
1192 Node::CommentSection {
1198 name,
1199 text,
1200 refs_note_text,
1201 grouped,
1202 } => {
1203 if !*grouped {
1204 let child =
1208 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1209 self.comment_groups.push(child.clone());
1210 return Some(child);
1211 }
1212 if let Some((_, self_ref)) = self
1213 .last_comment_section
1214 .as_ref()
1215 .filter(|(n, _)| n == name)
1216 .cloned()
1217 {
1218 let child =
1221 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1222 if let Some(children) =
1223 self.groups[group_index(&self_ref)]["children"].as_array_mut()
1224 {
1225 children.push(ref_value(child.as_str()));
1226 }
1227 self.comment_groups.push(if *refs_note_text {
1228 child
1229 } else {
1230 self_ref.clone()
1231 });
1232 return Some(self_ref);
1233 }
1234 let self_ref = format!("#/groups/{}", self.groups.len());
1235 self.groups.push(Value::Null);
1236 let child =
1237 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1238 self.groups[group_index(&self_ref)] = json!({
1239 "self_ref": self_ref,
1240 "parent": { "$ref": parent },
1241 "children": [{ "$ref": child }],
1242 "content_layer": "notes",
1243 "name": name,
1244 "label": "comment_section",
1245 });
1246 self.last_comment_section = Some((name.clone(), self_ref.clone()));
1247 self.comment_groups.push(if *refs_note_text {
1248 child
1249 } else {
1250 self_ref.clone()
1251 });
1252 Some(self_ref)
1253 }
1254 Node::Commented { comments, inner } => {
1257 let item = self.add_node(inner, parent)?;
1258 if !comments.is_empty() {
1259 self.pending_comments.push((item.clone(), comments.clone()));
1260 }
1261 Some(item)
1262 }
1263 Node::Table(t) => Some(self.add_table(t, parent)),
1264 Node::Picture {
1265 caption,
1266 caption_href,
1267 image,
1268 classification,
1269 caption_parent,
1270 caption_location,
1271 } => Some(self.add_picture(
1272 caption.as_deref(),
1273 caption_href.as_deref(),
1274 image.as_ref(),
1275 classification.as_deref().map(classification_meta),
1276 parent,
1277 (*caption_parent, *caption_location),
1278 )),
1279 Node::Chart {
1284 kind,
1285 table,
1286 caption,
1287 location,
1288 } => {
1289 self.adopt_loc(*location);
1290 let mut meta = json!({
1291 "classification": { "predictions": [{ "class_name": kind }] },
1292 });
1293 if !table.rows.is_empty() {
1294 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1295 }
1296 let mut captions = Vec::new();
1302 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1303 let prov = self.prov_json(unescape_text(cap).chars().count(), true);
1304 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1305 self.pending_siblings.push(ref_value(cap_ref.as_str()));
1306 captions.push(ref_value(cap_ref));
1307 }
1308 let prov = self.take_prov(0);
1309 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1310 }
1311 Node::DoclangOnly(_) => None,
1313 Node::Group {
1314 label,
1315 name,
1316 layer,
1317 children,
1318 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1319 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1320 Node::KeyValueGraph { cells, links } => {
1321 Some(self.add_key_value_graph(cells, links, parent))
1322 }
1323 Node::InlineGroup { md_text, .. } => {
1326 Some(self.add_text("text", md_text, parent, json!({})))
1327 }
1328 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1330 Node::Furniture {
1336 layer: ContentLayer::Notes,
1337 inner,
1338 } => {
1339 let item = self.add_node(inner, parent)?;
1340 self.set_layer(&item, "notes");
1341 Some(item)
1342 }
1343 Node::Furniture { .. } => None,
1344 Node::PageFurniture {
1348 footer,
1349 location,
1350 text,
1351 } => {
1352 if self.cur_page > 0 {
1353 self.pending_loc = Some(*location);
1354 }
1355 let label = if *footer {
1356 "page_footer"
1357 } else {
1358 "page_header"
1359 };
1360 let item = self.add_text(label, text, parent, json!({}));
1361 self.pending_loc = None;
1362 self.set_layer(&item, "furniture");
1363 Some(item)
1364 }
1365 Node::FurnitureText { label, text } => {
1368 let item = self.add_text(label, text, parent, json!({}));
1369 self.set_layer(&item, "furniture");
1370 Some(item)
1371 }
1372 Node::PictureChildren(children) => {
1376 let pic = self.pictures.len().checked_sub(1)?;
1377 let pic_ref = format!("#/pictures/{pic}");
1378 let refs = self.walk_into(children, &pic_ref);
1379 if let Some(list) = self.pictures[pic]["children"].as_array_mut() {
1380 list.extend(refs);
1381 }
1382 None
1383 }
1384 Node::Located { location, inner } => {
1389 if self.cur_page > 0 {
1390 self.pending_loc = Some(*location);
1391 }
1392 let r = self.add_node(inner, parent);
1393 self.pending_loc = None;
1394 r
1395 }
1396 Node::Track { track, cue, inner } => {
1397 self.pending_track = Some((track.clone(), cue.clone()));
1398 let r = self.add_node(inner, parent);
1399 self.pending_track = None;
1400 r
1401 }
1402 Node::Prov {
1403 page_no,
1404 bbox,
1405 charspan,
1406 inner,
1407 ..
1408 } => {
1409 self.pending_exact = Some(ExactProv {
1410 page_no: *page_no,
1411 bbox: bbox.map(f64::from),
1412 bottom_left: false,
1413 charspan: *charspan,
1414 });
1415 let r = self.add_node(inner, parent);
1416 self.pending_exact = None;
1417 r
1418 }
1419 Node::PageBreak => None,
1421 Node::PageInfo {
1424 page_no,
1425 width,
1426 height,
1427 } => {
1428 self.cur_page = *page_no;
1429 self.cur_w = *width as f64;
1430 self.cur_h = *height as f64;
1431 if *page_no > 0 {
1432 self.pages.push((*page_no, self.cur_w, self.cur_h));
1433 }
1434 None
1435 }
1436 Node::ListItem { .. } => None,
1438 }
1439 }
1440
1441 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1445 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1446 self.field_regions.push(Value::Null);
1447 let region_index = self.field_regions.len() - 1;
1448 let mut item_refs = Vec::new();
1449 for item in items {
1450 item_refs.push(ref_value(self.add_field_item(item, &self_ref)));
1451 }
1452 self.field_regions[region_index] = json!({
1453 "self_ref": self_ref,
1454 "parent": { "$ref": parent },
1455 "children": item_refs,
1456 "content_layer": "body",
1457 "label": "field_region",
1458 "prov": [],
1459 });
1460 self_ref
1461 }
1462
1463 fn add_key_value_graph(
1467 &mut self,
1468 cells: &[crate::GraphCell],
1469 links: &[crate::GraphLink],
1470 parent: &str,
1471 ) -> String {
1472 let self_ref = format!("#/key_value_items/{}", self.key_value_items.len());
1473 let cells: Vec<Value> = cells
1474 .iter()
1475 .map(|c| {
1476 json!({
1477 "label": c.label,
1478 "cell_id": c.cell_id,
1479 "text": c.text,
1480 "orig": c.orig,
1481 })
1482 })
1483 .collect();
1484 let links: Vec<Value> = links
1485 .iter()
1486 .map(|l| {
1487 json!({
1488 "label": l.label,
1489 "source_cell_id": l.source_cell_id,
1490 "target_cell_id": l.target_cell_id,
1491 })
1492 })
1493 .collect();
1494 self.key_value_items.push(json!({
1495 "self_ref": self_ref,
1496 "parent": { "$ref": parent },
1497 "children": [],
1498 "content_layer": "body",
1499 "label": "key_value_region",
1500 "prov": [],
1501 "captions": [],
1502 "references": [],
1503 "footnotes": [],
1504 "graph": { "cells": cells, "links": links },
1505 }));
1506 self_ref
1507 }
1508
1509 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1510 let self_ref = format!("#/field_items/{}", self.field_items.len());
1511 self.field_items.push(Value::Null);
1512 let item_index = self.field_items.len() - 1;
1513 let mut child_refs = Vec::new();
1514 for (label, text) in [
1515 ("marker", &item.marker),
1516 ("field_key", &item.key),
1517 ("field_value", &item.value),
1518 ] {
1519 if let Some(text) = text {
1520 let extra = match (label, &item.value_kind) {
1523 ("field_value", Some(kind)) => json!({ "kind": kind }),
1524 _ => json!({}),
1525 };
1526 child_refs.push(ref_value(self.add_text(label, text, &self_ref, extra)));
1527 }
1528 }
1529 self.field_items[item_index] = json!({
1530 "self_ref": self_ref,
1531 "parent": { "$ref": parent },
1532 "children": child_refs,
1533 "content_layer": "body",
1534 "label": "field_item",
1535 "prov": [],
1536 });
1537 self_ref
1538 }
1539
1540 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1544 let bucket = match self_ref.split('/').nth(1) {
1545 Some("texts") => &mut self.texts,
1546 Some("tables") => &mut self.tables,
1547 Some("pictures") => &mut self.pictures,
1548 Some("groups") => &mut self.groups,
1549 _ => return,
1550 };
1551 if let Some(item) = self_ref
1552 .rsplit('/')
1553 .next()
1554 .and_then(|i| i.parse::<usize>().ok())
1555 .and_then(|i| bucket.get_mut(i))
1556 {
1557 item["content_layer"] = json!(layer);
1558 }
1559 }
1560
1561 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1562 let prov = self.take_prov(unescape_text(text).chars().count());
1563 self.add_text_with(label, text, parent, extra, prov)
1564 }
1565
1566 fn add_text_with(
1569 &mut self,
1570 label: &str,
1571 text: &str,
1572 parent: &str,
1573 extra: Value,
1574 prov: Value,
1575 ) -> String {
1576 let self_ref = format!("#/texts/{}", self.texts.len());
1577 let (raw, track) = match self.pending_track.take() {
1581 Some((track, cue)) => (cue, Some(track)),
1582 None => (unescape_text(text), None),
1583 };
1584 let mut m = serde_json::Map::with_capacity(8 + usize::from(track.is_some()));
1585 m.insert("self_ref".into(), Value::String(self_ref.clone()));
1586 m.insert("parent".into(), ref_value(parent));
1587 m.insert("children".into(), Value::Array(Vec::new()));
1588 m.insert("content_layer".into(), "body".into());
1589 m.insert("label".into(), label.into());
1590 m.insert("prov".into(), prov);
1591 if let Some(track) = track {
1594 m.insert("source".into(), Value::Array(vec![track_json(&track)]));
1595 }
1596 m.insert("orig".into(), Value::String(raw.clone()));
1597 m.insert("text".into(), Value::String(raw));
1598 let mut item = Value::Object(m);
1599 if extra.as_object().is_some_and(|e| !e.is_empty()) {
1600 merge(&mut item, extra);
1601 }
1602 self.texts.push(item);
1603 self_ref
1604 }
1605
1606 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
1609 let self_ref = format!("#/texts/{}", self.texts.len());
1610 let prov = self.take_prov(latex.chars().count());
1611 self.texts.push(json!({
1612 "self_ref": self_ref,
1613 "parent": { "$ref": parent },
1614 "children": [],
1615 "content_layer": "body",
1616 "label": "formula",
1617 "prov": prov,
1618 "orig": latex,
1619 "text": latex,
1620 }));
1621 self_ref
1622 }
1623
1624 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
1628 let self_ref = format!("#/texts/{}", self.texts.len());
1629 let prov = self.take_prov(latex.chars().count());
1630 self.texts.push(json!({
1631 "self_ref": self_ref,
1632 "parent": { "$ref": parent },
1633 "children": [],
1634 "content_layer": "body",
1635 "label": "formula",
1636 "prov": prov,
1637 "orig": orig,
1638 "text": latex,
1639 }));
1640 self_ref
1641 }
1642
1643 fn add_code(
1644 &mut self,
1645 text: &str,
1646 language: Option<&str>,
1647 orig: Option<&str>,
1648 parent: &str,
1649 ) -> String {
1650 let self_ref = format!("#/texts/{}", self.texts.len());
1651 let raw = unescape_text(text);
1652 let prov = self.take_prov(raw.chars().count());
1653 self.texts.push(json!({
1654 "self_ref": self_ref,
1655 "parent": { "$ref": parent },
1656 "children": [],
1657 "content_layer": "body",
1658 "label": "code",
1659 "prov": prov,
1660 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
1663 "text": raw,
1664 "captions": [],
1665 "references": [],
1666 "footnotes": [],
1667 "code_language": code_language(language),
1668 }));
1669 self_ref
1670 }
1671
1672 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
1675 let self_ref = format!("#/groups/{}", self.groups.len());
1676 self.groups.push(Value::Null);
1678 let base = level_of(&items[0]);
1679 let mut children = Vec::new();
1680 let mut i = 0;
1681 while i < items.len() {
1682 if !matches!(items[i], Node::ListItem { .. }) {
1685 i += 1;
1686 continue;
1687 }
1688 let lvl = level_of(&items[i]);
1689 if lvl > base {
1690 i += 1;
1692 continue;
1693 }
1694 let item_ref = self.add_list_item(&items[i], &self_ref);
1695 let mut j = i + 1;
1697 while j < items.len() && level_of(&items[j]) > base {
1698 j += 1;
1699 }
1700 if j > i + 1 {
1701 let mut nested = Vec::new();
1702 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
1703 if let Some(idx) = ref_index(&item_ref) {
1705 self.texts[idx]["children"]
1706 .as_array_mut()
1707 .unwrap()
1708 .extend(nested);
1709 }
1710 }
1711 children.push(ref_value(item_ref));
1712 i = j;
1713 }
1714 self.groups[group_index(&self_ref)] = json!({
1715 "self_ref": self_ref,
1716 "parent": { "$ref": parent },
1717 "children": children,
1718 "content_layer": "body",
1719 "name": "list",
1720 "label": "list",
1721 });
1722 self_ref
1723 }
1724
1725 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
1726 let Node::ListItem {
1727 ordered,
1728 number,
1729 text,
1730 location,
1731 ..
1732 } = node
1733 else {
1734 unreachable!()
1735 };
1736 self.adopt_loc(*location);
1737 let self_ref = format!("#/texts/{}", self.texts.len());
1738 let raw = unescape_text(text);
1739 let prov = self.take_prov(raw.chars().count());
1740 let marker = if *ordered {
1741 format!("{number}.")
1742 } else {
1743 "-".to_string()
1744 };
1745 self.texts.push(json!({
1746 "self_ref": self_ref,
1747 "parent": { "$ref": parent },
1748 "children": [],
1749 "content_layer": "body",
1750 "label": "list_item",
1751 "prov": prov,
1752 "orig": raw,
1753 "text": raw,
1754 "enumerated": ordered,
1755 "marker": marker,
1756 }));
1757 self_ref
1758 }
1759
1760 fn add_table(&mut self, t: &Table, parent: &str) -> String {
1761 self.add_table_with(t, parent, false)
1762 }
1763
1764 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
1767 let self_ref = format!("#/tables/{}", self.tables.len());
1768 self.adopt_loc(t.location);
1769 let prov = self.take_prov(0);
1770 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
1774 Some(cap) => self.add_caption(
1775 cap,
1776 json!({}),
1777 &self_ref,
1778 parent,
1779 (t.caption_parent, t.caption_location),
1780 ),
1781 None => (Vec::new(), Vec::new()),
1782 };
1783 let data = table_data_with(t, raw);
1784 self.tables.push(json!({
1785 "self_ref": self_ref,
1786 "parent": { "$ref": parent },
1787 "children": children,
1788 "content_layer": "body",
1789 "label": "table",
1790 "prov": prov,
1791 "captions": captions,
1792 "references": [],
1793 "footnotes": [],
1794 "data": data,
1795 "annotations": [],
1796 }));
1797 self_ref
1798 }
1799
1800 fn add_caption(
1809 &mut self,
1810 text: &str,
1811 extra: Value,
1812 self_ref: &str,
1813 parent: &str,
1814 (choice, location): (CaptionParent, Option<[u16; 4]>),
1815 ) -> (Vec<Value>, Vec<Value>) {
1816 let cap_parent = match choice {
1821 CaptionParent::Item => self_ref,
1822 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
1823 CaptionParent::Body => "#/body",
1824 };
1825 if self.cur_page > 0 && location.is_some() {
1826 self.pending_loc = location;
1827 }
1828 let cap_ref = ref_value(self.add_text("caption", text, cap_parent, extra));
1829 match choice {
1830 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
1831 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
1834 CaptionParent::Body if parent == "#/body" => {
1835 self.pending_siblings.push(cap_ref.clone())
1836 }
1837 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
1838 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
1841 }
1842 (vec![cap_ref], Vec::new())
1843 }
1844
1845 fn add_picture(
1848 &mut self,
1849 caption: Option<&str>,
1850 caption_href: Option<&str>,
1851 image: Option<&crate::PictureImage>,
1852 meta: Option<Value>,
1853 parent: &str,
1854 caption_at: (CaptionParent, Option<[u16; 4]>),
1855 ) -> String {
1856 let self_ref = format!("#/pictures/{}", self.pictures.len());
1857 let prov = self.take_prov(0);
1860 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
1861 Some(cap) => {
1862 let extra = match caption_href {
1866 Some(href) => json!({ "hyperlink": href }),
1867 None => json!({}),
1868 };
1869 self.add_caption(cap, extra, &self_ref, parent, caption_at)
1870 }
1871 None => (Vec::new(), Vec::new()),
1872 };
1873 self.push_picture(prov, captions, children, image, meta, parent)
1874 }
1875
1876 fn push_picture(
1879 &mut self,
1880 prov: Value,
1881 captions: Vec<Value>,
1882 children: Vec<Value>,
1883 image: Option<&crate::PictureImage>,
1884 meta: Option<Value>,
1885 parent: &str,
1886 ) -> String {
1887 let self_ref = format!("#/pictures/{}", self.pictures.len());
1888 let annotations = meta
1892 .as_ref()
1893 .and_then(|m| m.get("annotations").cloned())
1894 .unwrap_or_else(|| json!([]));
1895 let meta = meta.map(|mut m| {
1896 if let Some(obj) = m.as_object_mut() {
1897 obj.remove("annotations");
1898 }
1899 m
1900 });
1901 let mut item = match meta {
1905 Some(meta) => json!({
1906 "self_ref": self_ref,
1907 "parent": { "$ref": parent },
1908 "children": children,
1909 "content_layer": "body",
1910 "meta": meta,
1911 "label": "picture",
1912 "prov": prov,
1913 "captions": captions,
1914 "references": [],
1915 "footnotes": [],
1916 "annotations": annotations,
1917 }),
1918 None => json!({
1919 "self_ref": self_ref,
1920 "parent": { "$ref": parent },
1921 "children": children,
1922 "content_layer": "body",
1923 "label": "picture",
1924 "prov": prov,
1925 "captions": captions,
1926 "references": [],
1927 "footnotes": [],
1928 "annotations": annotations,
1929 }),
1930 };
1931 if let Some(img) = image {
1936 let image = image_ref_json(img);
1937 if let Some(obj) = item.as_object_mut() {
1938 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
1939 obj.insert("image".into(), image);
1940 obj.insert("annotations".into(), annotations);
1941 }
1942 }
1943 self.pictures.push(item);
1944 self_ref
1945 }
1946
1947 fn add_group(
1948 &mut self,
1949 label: &str,
1950 name: Option<&str>,
1951 layer: Option<ContentLayer>,
1952 nodes: &[Node],
1953 parent: &str,
1954 ) -> String {
1955 let self_ref = format!("#/groups/{}", self.groups.len());
1956 self.groups.push(Value::Null);
1957 let mark = (
1961 self.texts.len(),
1962 self.tables.len(),
1963 self.pictures.len(),
1964 self.groups.len(),
1965 );
1966 let children = self.walk_into(nodes, &self_ref);
1967 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
1968 let content_layer = layer.map_or("body", |l| l.value());
1969 self.groups[group_index(&self_ref)] = json!({
1970 "self_ref": self_ref,
1971 "parent": { "$ref": parent },
1972 "children": children,
1973 "content_layer": content_layer,
1974 "name": name,
1975 "label": label,
1976 });
1977 if layer.is_some() {
1978 let (t, tb, p, g) = mark;
1979 for item in self.texts[t..]
1980 .iter_mut()
1981 .chain(self.tables[tb..].iter_mut())
1982 .chain(self.pictures[p..].iter_mut())
1983 .chain(self.groups[g..].iter_mut())
1984 {
1985 if let Some(obj) = item.as_object_mut() {
1986 obj.insert("content_layer".into(), json!(content_layer));
1987 }
1988 }
1989 }
1990 self_ref
1991 }
1992
1993 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Value> {
1996 let seqs: Option<Vec<usize>> = nodes
2001 .iter()
2002 .map(|n| match n {
2003 Node::Prov { seq: Some(s), .. } => Some(*s),
2004 _ => None,
2005 })
2006 .collect();
2007 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
2008 let mut order: Vec<usize> = (0..nodes.len()).collect();
2009 order.sort_by_key(|&i| seqs[i]);
2010 let mut slots: Vec<Vec<Value>> = vec![Vec::new(); nodes.len()];
2011 for i in order {
2012 if let Some(r) = self.add_node(&nodes[i], parent) {
2013 slots[i].append(&mut self.pending_siblings);
2014 slots[i].push(ref_value(r));
2015 slots[i].append(&mut self.pending_after);
2016 }
2017 if parent == "#/body" {
2018 slots[i].append(&mut self.pending_body);
2019 }
2020 }
2021 return slots.into_iter().flatten().collect();
2022 }
2023 let mut children = Vec::new();
2024 let mut i = 0;
2025 while i < nodes.len() {
2026 if matches!(nodes[i], Node::ListItem { .. }) {
2027 let start = i;
2028 i += 1;
2029 loop {
2030 match nodes.get(i) {
2031 Some(Node::ListItem { .. }) => i += 1,
2032 Some(Node::Paragraph { text })
2035 if text.is_empty()
2036 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
2037 {
2038 i += 1
2039 }
2040 _ => break,
2041 }
2042 }
2043 self.add_sibling_lists(&nodes[start..i], parent, &mut children);
2044 } else {
2045 if let Some(r) = self.add_node(&nodes[i], parent) {
2046 children.append(&mut self.pending_siblings);
2047 children.push(ref_value(r));
2048 children.append(&mut self.pending_after);
2049 }
2050 i += 1;
2051 }
2052 if parent == "#/body" {
2055 children.append(&mut self.pending_body);
2056 }
2057 }
2058 children
2059 }
2060
2061 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
2066 let base = level_of(&run[0]);
2067 let mut seg = 0;
2068 for k in 0..run.len() {
2069 let Node::ListItem {
2070 first_in_list,
2071 level,
2072 ..
2073 } = &run[k]
2074 else {
2075 continue;
2076 };
2077 if *level != base {
2078 continue; }
2080 if k > seg && *first_in_list {
2081 out.push(ref_value(self.add_list(&run[seg..k], parent)));
2082 seg = k;
2083 }
2084 }
2085 out.push(ref_value(self.add_list(&run[seg..], parent)));
2086 }
2087}
2088
2089fn level_of(node: &Node) -> u8 {
2090 match node {
2091 Node::ListItem { level, .. } => *level,
2092 _ => 0,
2093 }
2094}
2095
2096fn group_index(self_ref: &str) -> usize {
2097 self_ref.rsplit('/').next().unwrap().parse().unwrap()
2098}
2099
2100fn ref_index(self_ref: &str) -> Option<usize> {
2101 self_ref.rsplit('/').next()?.parse().ok()
2102}
2103
2104fn merge(target: &mut Value, extra: Value) {
2106 if let (Some(t), Value::Object(e)) = (target.as_object_mut(), extra) {
2107 t.extend(e);
2108 }
2109}
2110
2111fn object<const N: usize>(entries: [(&str, Value); N]) -> Value {
2117 let mut m = serde_json::Map::with_capacity(N);
2118 for (k, v) in entries {
2119 m.insert(k.to_owned(), v);
2120 }
2121 Value::Object(m)
2122}
2123
2124fn ref_value(r: impl Into<String>) -> Value {
2126 object([("$ref", Value::String(r.into()))])
2127}
2128
2129fn unescape_text(s: &str) -> String {
2131 s.replace("<", "<")
2132 .replace(">", ">")
2133 .replace("&", "&")
2134 .replace("\\_", "_")
2135}
2136
2137fn fnv1a(s: &str) -> u64 {
2140 let mut h: u64 = 0xcbf29ce484222325;
2141 for b in s.bytes() {
2142 h ^= b as u64;
2143 h = h.wrapping_mul(0x100000001b3);
2144 }
2145 h
2146}
2147
2148#[cfg(test)]
2149mod tests {
2150 use crate::{
2151 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
2152 };
2153 use serde_json::Value;
2154
2155 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
2156 crate::TableCell {
2157 text: text.into(),
2158 bbox: None,
2159 start_row: row,
2160 start_col: col,
2161 row_span: 1,
2162 col_span,
2163 column_header: row == 0,
2164 row_header: false,
2165 row_section: false,
2166 }
2167 }
2168
2169 #[test]
2175 fn ragged_rows_take_their_grid_from_first_class_cells() {
2176 let cells = vec![
2177 cell_at("h0", 0, 0, 1),
2178 cell_at("h2", 0, 2, 1),
2179 cell_at("wide", 1, 0, 3),
2180 ];
2181 let export = |rows: Vec<Vec<String>>| -> Value {
2182 let mut doc = DoclingDocument::new("t");
2183 doc.push(Node::Table(Table {
2184 rows,
2185 cells: Some(cells.clone()),
2186 ..Table::default()
2187 }));
2188 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2189 v["tables"][0]["data"].clone()
2190 };
2191 let data = export(vec![
2193 vec!["h0".into(), "h2".into()],
2194 vec!["wide".into(), "wide".into(), "wide".into()],
2195 ]);
2196 assert_eq!(data["num_rows"], 2);
2197 assert_eq!(data["num_cols"], 3);
2198 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
2199 let row0: Vec<&str> = data["grid"][0]
2200 .as_array()
2201 .unwrap()
2202 .iter()
2203 .map(|c| c["text"].as_str().unwrap())
2204 .collect();
2205 assert_eq!(row0, ["h0", "", "h2"]);
2206 assert_eq!(data["table_cells"][2]["col_span"], 3);
2207 let data = export(vec![
2210 vec!["h0".into(), "h2".into()],
2211 vec!["wide".into(), "wide".into()],
2212 ]);
2213 assert_eq!(data["num_cols"], 2);
2214 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
2215 }
2216
2217 fn doc_with_image() -> DoclingDocument {
2218 let mut doc = DoclingDocument::new("t");
2219 doc.push(Node::Picture {
2220 caption: Some("Fig 1".into()),
2221 caption_href: None,
2222 image: Some(PictureImage {
2223 dpi: crate::PictureImage::DEFAULT_DPI,
2224 mimetype: "image/png".into(),
2225 width: 4,
2226 height: 2,
2227 data: b"foobar".to_vec(),
2228 }),
2229 classification: None,
2230 caption_parent: Default::default(),
2231 caption_location: None,
2232 });
2233 doc
2234 }
2235
2236 #[test]
2241 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
2242 let mut doc = DoclingDocument::new("t");
2243 doc.push(Node::Heading {
2244 level: 1,
2245 text: "Slide One".into(),
2246 });
2247 doc.push(Node::Furniture {
2248 layer: ContentLayer::Notes,
2249 inner: Box::new(Node::Located {
2250 location: [0, 0, 0, 0],
2251 inner: Box::new(Node::Paragraph {
2252 text: "Speaker note for slide 1.".into(),
2253 }),
2254 }),
2255 });
2256 doc.push(Node::Furniture {
2257 layer: ContentLayer::Furniture,
2258 inner: Box::new(Node::Paragraph {
2259 text: "page header".into(),
2260 }),
2261 });
2262
2263 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2264 let texts = v["texts"].as_array().unwrap();
2265 assert_eq!(
2266 texts
2267 .iter()
2268 .map(|t| (
2269 t["label"].as_str().unwrap(),
2270 t["content_layer"].as_str().unwrap(),
2271 t["text"].as_str().unwrap()
2272 ))
2273 .collect::<Vec<_>>(),
2274 vec![
2275 ("title", "body", "Slide One"),
2276 ("text", "notes", "Speaker note for slide 1."),
2277 ],
2278 "the note is carried on its own layer; the furniture is not carried"
2279 );
2280 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
2282 }
2283
2284 #[test]
2290 fn continuation_flags_become_spanning_cells() {
2291 let mut doc = DoclingDocument::new("t");
2292 let rows = vec![
2294 vec!["merged".to_string(), "merged".into(), "merged".into()],
2295 vec!["merged".to_string(), "merged".into(), "merged".into()],
2296 vec!["a".to_string(), "b".into(), "c".into()],
2297 ];
2298 doc.push(Node::Table(crate::Table {
2299 rows,
2300 location: None,
2301 structure: Some(crate::TableStructure {
2302 header_row: vec![true, false, false],
2303 col_continuation: vec![
2304 vec![false, true, true],
2305 vec![false, true, true],
2306 vec![false, false, false],
2307 ],
2308 row_continuation: vec![
2309 vec![false, false, false],
2310 vec![true, true, true],
2311 vec![false, false, false],
2312 ],
2313 row_header: Vec::new(),
2314 col_header: Vec::new(),
2315 }),
2316 cell_blocks: None,
2317 cells: None,
2318 caption: None,
2319 caption_parent: Default::default(),
2320 caption_location: None,
2321 }));
2322 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2323 let data = &v["tables"][0]["data"];
2324 assert_eq!(data["num_rows"], 3);
2325 assert_eq!(data["num_cols"], 3);
2326 let cells = data["table_cells"].as_array().unwrap();
2327 assert_eq!(
2328 cells.len(),
2329 4,
2330 "one cell for the range, three for the plain row"
2331 );
2332 assert_eq!(
2333 cells[0],
2334 serde_json::json!({
2335 "row_span": 2, "col_span": 3,
2336 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2337 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2338 "text": "merged", "column_header": true, "row_header": false,
2339 "row_section": false, "fillable": false,
2340 })
2341 );
2342 assert_eq!(cells[1]["text"], "a");
2343 assert_eq!(cells[1]["row_span"], 1);
2344 assert_eq!(cells[1]["column_header"], false);
2345 let grid = data["grid"].as_array().unwrap();
2347 assert_eq!(grid.len(), 3);
2348 for (r, row) in grid.iter().take(2).enumerate() {
2349 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2350 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2351 }
2352 }
2353 assert_eq!(grid[2][2]["text"], "c");
2354 }
2355
2356 #[test]
2362 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2363 let mut doc = DoclingDocument::new("t");
2364 doc.push(Node::PageInfo {
2365 page_no: 1,
2366 width: 3.0,
2367 height: 4.0,
2368 });
2369 let table = crate::Table {
2370 rows: vec![vec!["a".to_string(), "b".into()]],
2371 ..Default::default()
2372 };
2373 doc.push(Node::Group {
2374 label: "sheet".into(),
2375 name: Some("Data".into()),
2376 layer: None,
2377 children: vec![
2378 Node::Prov {
2383 page_no: 1,
2384 bbox: [0.0, 0.0, 3.0, 4.0],
2385 charspan: [0, 0],
2386 seq: Some(1),
2387 inner: Box::new(Node::Table(table.clone())),
2388 },
2389 Node::Prov {
2390 page_no: 1,
2391 bbox: [0.0, 1.0, 1.0, 1.0],
2392 charspan: [0, 0],
2393 seq: Some(0),
2394 inner: Box::new(Node::Chart {
2395 kind: "bar_chart".into(),
2396 table,
2397 caption: Some("Sales".into()),
2398 location: Some([0, 128, 170, 128]),
2399 }),
2400 },
2401 ],
2402 });
2403 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2404 assert_eq!(
2405 v["pages"],
2406 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2407 );
2408 assert_eq!(
2409 v["tables"][0]["prov"],
2410 serde_json::json!([{
2411 "page_no": 1,
2412 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2413 "charspan": [0, 0],
2414 }])
2415 );
2416 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2417 let sheet = &v["groups"][0];
2420 assert_eq!(
2421 sheet["children"],
2422 serde_json::json!([
2423 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2424 ])
2425 );
2426 let cap = &v["texts"][0];
2427 assert_eq!(cap["label"], "caption");
2428 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2429 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2430 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2431 let pic = &v["pictures"][0];
2432 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2433 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2434 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2435 assert_eq!(
2436 pic["meta"]["classification"]["predictions"][0]["class_name"],
2437 "bar_chart"
2438 );
2439 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2440 }
2441
2442 #[test]
2446 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2447 let mut doc = DoclingDocument::new("t");
2448 doc.push(Node::PageInfo {
2449 page_no: 1,
2450 width: 12192000.0,
2451 height: 6858000.0,
2452 });
2453 doc.push(Node::Furniture {
2454 layer: ContentLayer::Notes,
2455 inner: Box::new(Node::Located {
2456 location: [0, 0, 0, 0],
2457 inner: Box::new(Node::Paragraph {
2458 text: "a note".into(),
2459 }),
2460 }),
2461 });
2462 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2463 let prov = &v["texts"][0]["prov"][0];
2464 assert_eq!(prov["page_no"], 1);
2465 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2466 assert_eq!(
2467 prov["bbox"],
2468 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2469 );
2470 assert_eq!(
2472 v["pages"]["1"]["size"],
2473 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2474 );
2475 }
2476
2477 #[test]
2487 fn captions_and_checkboxes_carry_their_own_prov() {
2488 let mut doc = DoclingDocument::new("t");
2489 doc.push(Node::PageInfo {
2490 page_no: 1,
2491 width: 512.0,
2492 height: 512.0,
2493 });
2494 doc.push(Node::Located {
2495 location: [64, 64, 448, 256],
2496 inner: Box::new(Node::Picture {
2497 caption: Some("Figure 1".into()),
2498 caption_href: None,
2499 image: None,
2500 classification: None,
2501 caption_parent: CaptionParent::Item,
2502 caption_location: Some([64, 264, 448, 280]),
2503 }),
2504 });
2505 doc.push(Node::PageInfo {
2506 page_no: 2,
2507 width: 512.0,
2508 height: 512.0,
2509 });
2510 doc.push(Node::Located {
2511 location: [64, 120, 448, 300],
2512 inner: Box::new(Node::Table(Table {
2513 rows: vec![vec!["a".into()]],
2514 caption: Some("Table 1".into()),
2515 caption_parent: CaptionParent::Item,
2516 caption_location: Some([64, 100, 300, 112]),
2517 ..Table::default()
2518 })),
2519 });
2520 doc.push(Node::Located {
2521 location: [64, 320, 200, 332],
2522 inner: Box::new(Node::CheckboxItem {
2523 checked: false,
2524 text: "First option".into(),
2525 }),
2526 });
2527 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2528 let texts = v["texts"].as_array().unwrap();
2529 let by_text = |t: &str| texts.iter().find(|x| x["text"] == t).expect(t).clone();
2530 let bbox = |p: &Value| {
2531 let b = &p["prov"][0]["bbox"];
2532 [&b["l"], &b["t"], &b["r"], &b["b"]].map(|x| x.as_f64().unwrap())
2533 };
2534 let fig = by_text("Figure 1");
2537 assert_eq!(fig["prov"][0]["page_no"], 1);
2538 assert_eq!(bbox(&fig), [64.0, 248.0, 448.0, 232.0]);
2539 assert_eq!(fig["prov"][0]["charspan"], serde_json::json!([0, 8]));
2540 assert_eq!(fig["parent"]["$ref"], "#/pictures/0");
2541 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
2542 let tab = by_text("Table 1");
2544 assert_eq!(tab["prov"][0]["page_no"], 2);
2545 assert_eq!(bbox(&tab), [64.0, 412.0, 300.0, 400.0]);
2546 assert_eq!(v["tables"][0]["prov"][0]["bbox"]["t"], 392.0);
2547 let cb = by_text("First option");
2549 assert_eq!(cb["label"], "checkbox_unselected");
2550 assert_eq!(cb["prov"][0]["page_no"], 2);
2551 assert_eq!(bbox(&cb), [64.0, 192.0, 200.0, 180.0]);
2552 assert!(texts
2553 .iter()
2554 .all(|t| !t["prov"].as_array().unwrap().is_empty()));
2555
2556 let mut plain = DoclingDocument::new("t");
2559 plain.push(Node::PageInfo {
2560 page_no: 1,
2561 width: 512.0,
2562 height: 512.0,
2563 });
2564 plain.push(Node::Located {
2565 location: [64, 64, 448, 256],
2566 inner: Box::new(Node::Picture {
2567 caption: Some("Figure 1".into()),
2568 caption_href: None,
2569 image: None,
2570 classification: None,
2571 caption_parent: CaptionParent::Item,
2572 caption_location: None,
2573 }),
2574 });
2575 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2576 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2577 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
2578 }
2579
2580 #[test]
2581 fn page_markers_produce_pages_and_prov() {
2582 let mut doc = DoclingDocument::new("t");
2583 doc.push(Node::PageInfo {
2584 page_no: 1,
2585 width: 512.0,
2586 height: 1024.0,
2587 });
2588 doc.push(Node::Located {
2589 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
2591 text: "hello".into(),
2592 }),
2593 });
2594 doc.push(Node::Table(Table {
2595 rows: vec![vec!["a".into()]],
2596 location: Some([0, 0, 512, 512]),
2597 ..Table::default()
2598 }));
2599 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2600 assert_eq!(v["pages"]["1"]["page_no"], 1);
2601 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
2602 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
2603 let prov = &v["texts"][0]["prov"][0];
2606 assert_eq!(prov["page_no"], 1);
2607 assert_eq!(prov["bbox"]["l"], 128.0);
2608 assert_eq!(prov["bbox"]["t"], 896.0);
2609 assert_eq!(prov["bbox"]["r"], 256.0);
2610 assert_eq!(prov["bbox"]["b"], 768.0);
2611 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
2612 assert_eq!(prov["charspan"][1], 5);
2613 let tprov = &v["tables"][0]["prov"][0];
2615 assert_eq!(tprov["bbox"]["t"], 1024.0);
2616 assert_eq!(tprov["bbox"]["b"], 0.0);
2617 assert_eq!(tprov["charspan"][1], 0);
2618
2619 let mut plain = DoclingDocument::new("t");
2621 plain.push(Node::Located {
2622 location: [1, 2, 3, 4],
2623 inner: Box::new(Node::Paragraph { text: "x".into() }),
2624 });
2625 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2626 assert_eq!(v["pages"], serde_json::json!({}));
2627 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2628 }
2629
2630 #[test]
2633 fn page_furniture_becomes_furniture_layer_text() {
2634 let mut doc = DoclingDocument::new("t");
2635 doc.push(Node::PageInfo {
2636 page_no: 1,
2637 width: 512.0,
2638 height: 512.0,
2639 });
2640 doc.push(Node::PageFurniture {
2641 footer: false,
2642 location: [10, 0, 100, 20],
2643 text: "Chapter 1".into(),
2644 });
2645 doc.push(Node::Paragraph {
2646 text: "body".into(),
2647 });
2648 doc.push(Node::PageFurniture {
2649 footer: true,
2650 location: [400, 490, 500, 512],
2651 text: "1.10.2".into(),
2652 });
2653 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2654 let texts = v["texts"].as_array().unwrap();
2655 assert_eq!(texts.len(), 3);
2656 assert_eq!(texts[0]["label"], "page_header");
2657 assert_eq!(texts[0]["content_layer"], "furniture");
2658 assert_eq!(texts[0]["parent"]["$ref"], "#/body");
2659 assert_eq!(texts[0]["prov"][0]["bbox"]["l"], 10.0);
2660 assert_eq!(texts[1]["content_layer"], "body");
2661 assert_eq!(texts[1]["prov"], serde_json::json!([]));
2662 assert_eq!(texts[2]["label"], "page_footer");
2663 assert_eq!(texts[2]["text"], "1.10.2");
2664 assert_eq!(texts[2]["content_layer"], "furniture");
2665 }
2666
2667 #[test]
2668 fn picture_image_in_markdown_modes_and_json() {
2669 let doc = doc_with_image();
2670 assert!(doc.export_to_markdown().contains("<!-- image -->"));
2672 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
2674 assert!(
2675 md.contains(""),
2676 "got:\n{md}"
2677 );
2678 assert!(files.is_empty());
2679 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
2681 assert!(
2682 md.contains(""),
2683 "got:\n{md}"
2684 );
2685 assert_eq!(
2686 files,
2687 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
2688 );
2689 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2692 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
2693 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
2694 let keys: Vec<&str> = v["pictures"][0]
2695 .as_object()
2696 .unwrap()
2697 .keys()
2698 .map(String::as_str)
2699 .collect();
2700 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
2701 assert_eq!(
2702 v["pictures"][0]["image"]["uri"],
2703 "data:image/png;base64,Zm9vYmFy"
2704 );
2705 }
2706
2707 #[test]
2708 fn exports_docling_schema() {
2709 let mut doc = DoclingDocument::new("t");
2710 doc.push(Node::Heading {
2711 level: 1,
2712 text: "Title".into(),
2713 });
2714 doc.push(Node::Heading {
2715 level: 2,
2716 text: "Sec".into(),
2717 });
2718 doc.push(Node::Paragraph {
2719 text: "Body & more".into(),
2720 }); doc.push(Node::ListItem {
2722 ordered: false,
2723 number: 0,
2724 first_in_list: true,
2725 text: "one".into(),
2726 level: 0,
2727 marker: None,
2728 location: None,
2729 dclx: None,
2730 href: None,
2731 layer: None,
2732 });
2733 doc.push(Node::ListItem {
2734 ordered: false,
2735 number: 0,
2736 first_in_list: false,
2737 text: "two".into(),
2738 level: 0,
2739 marker: None,
2740 location: None,
2741 dclx: None,
2742 href: None,
2743 layer: None,
2744 });
2745 doc.push(Node::Table(Table {
2746 rows: vec![vec!["A".into(), "B".into()]],
2747 location: None,
2748 structure: None,
2749 cell_blocks: None,
2750 cells: None,
2751 caption: None,
2752 caption_parent: Default::default(),
2753 caption_location: None,
2754 }));
2755
2756 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2757 assert_eq!(v["schema_name"], "DoclingDocument");
2758 assert_eq!(v["version"], "1.10.0");
2759 assert_eq!(v["texts"][0]["label"], "title");
2760 assert_eq!(v["texts"][1]["label"], "section_header");
2761 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
2765 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
2766 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
2767 assert_eq!(v["texts"][3]["marker"], "-");
2768 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
2770 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
2771 }
2772 #[test]
2776 fn a_layered_group_stamps_its_whole_subtree() {
2777 let doc = DoclingDocument {
2778 name: "s".into(),
2779 nodes: vec![
2780 Node::Group {
2781 label: "sheet".into(),
2782 name: Some("Sheet1".into()),
2783 layer: None,
2784 children: vec![Node::Paragraph {
2785 text: "visible".into(),
2786 }],
2787 },
2788 Node::Group {
2789 label: "sheet".into(),
2790 name: Some("Sheet2".into()),
2791 layer: Some(ContentLayer::Invisible),
2792 children: vec![Node::Paragraph {
2793 text: "hidden".into(),
2794 }],
2795 },
2796 ],
2797 ..DoclingDocument::new("s")
2798 };
2799 let v = crate::json::to_json(&doc);
2800 assert_eq!(v["groups"][0]["label"], "sheet");
2801 assert_eq!(v["groups"][0]["name"], "Sheet1");
2802 assert_eq!(v["groups"][0]["content_layer"], "body");
2803 assert_eq!(v["texts"][0]["content_layer"], "body");
2804 assert_eq!(v["groups"][1]["name"], "Sheet2");
2805 assert_eq!(v["groups"][1]["content_layer"], "invisible");
2806 assert_eq!(v["texts"][1]["content_layer"], "invisible");
2807 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
2809 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
2810 }
2811
2812 #[test]
2822 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
2823 use crate::tree::{ItemTree, TreeKind};
2824 let mut t = ItemTree::default();
2825 let text = |txt: &str| TreeKind::Text {
2826 label: "text".into(),
2827 text: txt.into(),
2828 orig: None,
2829 formatting: None,
2830 hyperlink: None,
2831 level: None,
2832 list: None,
2833 };
2834 let a = t.add(None, None, text("a"));
2835 let blank = t.add(None, None, text(""));
2836 let b = t.add(None, None, text("b"));
2837 t.delete(blank);
2838 let group = t.add(
2839 None,
2840 Some(ContentLayer::Notes),
2841 TreeKind::Group {
2842 label: "comment_section".into(),
2843 name: "comment-0".into(),
2844 },
2845 );
2846 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
2847 t.items[a].comments.push(group);
2848 t.add(
2849 None,
2850 None,
2851 TreeKind::Picture {
2852 captions: Vec::new(),
2853 image: None,
2854 classification: Some("bar_chart".into()),
2855 confidence: None,
2856 chart: Some(Table {
2857 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
2858 ..Table::default()
2859 }),
2860 dpi: None,
2861 },
2862 );
2863 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
2864 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
2865 let mut doc = DoclingDocument::new("t");
2866 doc.tree = Some(t);
2867 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2868 let texts = v["texts"].as_array().unwrap();
2869 assert_eq!(texts.len(), 3);
2870 assert_eq!(texts[1]["text"], "b");
2871 assert_eq!(texts[1]["self_ref"], "#/texts/1");
2872 assert_eq!(
2873 v["body"]["children"],
2874 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
2875 );
2876 let keys: Vec<&str> = texts[0]
2877 .as_object()
2878 .unwrap()
2879 .keys()
2880 .map(String::as_str)
2881 .collect();
2882 assert_eq!(
2883 keys,
2884 vec![
2885 "self_ref",
2886 "parent",
2887 "children",
2888 "content_layer",
2889 "label",
2890 "prov",
2891 "comments",
2892 "orig",
2893 "text"
2894 ]
2895 );
2896 assert_eq!(
2897 texts[0]["comments"],
2898 serde_json::json!([{"$ref": "#/groups/0"}])
2899 );
2900 assert!(texts[1].get("comments").is_none());
2901 let meta = &v["pictures"][0]["meta"];
2902 assert_eq!(
2903 meta["classification"]["predictions"][0]["class_name"],
2904 "bar_chart"
2905 );
2906 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
2907 }
2908
2909 #[test]
2915 fn tree_items_carry_exact_provenance_and_dpi() {
2916 use crate::tree::{ItemTree, TreeKind, TreeProv};
2917 let text = |label: &str, t: &str| TreeKind::Text {
2918 label: label.into(),
2919 text: t.into(),
2920 orig: None,
2921 formatting: None,
2922 hyperlink: None,
2923 level: None,
2924 list: None,
2925 };
2926 let mut t = ItemTree::default();
2927 let slide = t.add(
2928 None,
2929 None,
2930 TreeKind::Group {
2931 label: "chapter".into(),
2932 name: "slide-0".into(),
2933 },
2934 );
2935 t.add_with_prov(
2936 Some(slide),
2937 None,
2938 text("paragraph", "héllo"),
2939 TreeProv {
2940 page_no: 1,
2941 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
2942 bottom_left: true,
2943 charspan: [0, 5],
2944 },
2945 );
2946 t.add_with_prov(
2947 Some(slide),
2948 None,
2949 TreeKind::Picture {
2950 captions: Vec::new(),
2951 image: Some(crate::PictureImage {
2952 dpi: crate::PictureImage::DEFAULT_DPI,
2953 mimetype: "image/png".into(),
2954 width: 2,
2955 height: 2,
2956 data: vec![0],
2957 }),
2958 classification: None,
2959 confidence: None,
2960 chart: None,
2961 dpi: Some(300),
2962 },
2963 TreeProv {
2964 page_no: 1,
2965 bbox: [0.0; 4],
2966 bottom_left: false,
2967 charspan: [0, 0],
2968 },
2969 );
2970 t.add(
2971 Some(slide),
2972 Some(ContentLayer::Notes),
2973 text("text", "no geometry"),
2974 );
2975 let mut doc = DoclingDocument::new("t");
2976 doc.push(Node::PageInfo {
2977 page_no: 1,
2978 width: 9144000.0,
2979 height: 6858000.0,
2980 });
2981 doc.tree = Some(t);
2982 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2983 assert_eq!(
2984 v["texts"][0]["prov"],
2985 serde_json::json!([{
2986 "page_no": 1,
2987 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
2988 "charspan": [0, 5],
2989 }])
2990 );
2991 assert_eq!(v["texts"][0]["label"], "paragraph");
2992 assert_eq!(
2993 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
2994 "TOPLEFT"
2995 );
2996 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
2997 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
2998 assert_eq!(v["texts"][1]["content_layer"], "notes");
2999 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
3000 assert_eq!(v["pages"]["1"]["page_no"], 1);
3001 }
3002
3003 #[test]
3008 fn provenance_boxes_are_clamped_to_their_page() {
3009 let mut doc = DoclingDocument::new("t");
3010 doc.push(Node::PageInfo {
3011 page_no: 1,
3012 width: 10.0,
3013 height: 8.0,
3014 });
3015 doc.push(Node::Prov {
3016 page_no: 1,
3017 bbox: [-1.0, 2.0, 12.0, 9.5],
3018 charspan: [0, 1],
3019 seq: None,
3020 inner: Box::new(Node::Paragraph { text: "x".into() }),
3021 });
3022 let mut table = Table {
3023 rows: vec![vec!["a".into()]],
3024 ..Table::default()
3025 };
3026 table.cells = Some(vec![crate::TableCell {
3027 text: "a".into(),
3028 bbox: Some([1.0, 1.0, 11.0, 9.0]),
3029 start_row: 0,
3030 start_col: 0,
3031 row_span: 1,
3032 col_span: 1,
3033 column_header: false,
3034 row_header: false,
3035 row_section: false,
3036 }]);
3037 doc.push(Node::Prov {
3038 page_no: 1,
3039 bbox: [0.0, 0.0, 10.0, 8.0],
3040 charspan: [0, 0],
3041 seq: None,
3042 inner: Box::new(Node::Table(table)),
3043 });
3044 doc.push(Node::Prov {
3045 page_no: 7,
3046 bbox: [-5.0, 0.0, 50.0, 50.0],
3047 charspan: [0, 1],
3048 seq: None,
3049 inner: Box::new(Node::Paragraph { text: "y".into() }),
3050 });
3051 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3052 assert_eq!(
3053 v["texts"][0]["prov"][0]["bbox"],
3054 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
3055 );
3056 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
3057 assert_eq!(
3058 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
3059 (Some(1.0), Some(10.0), Some(8.0))
3060 );
3061 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
3062 assert_eq!(
3063 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
3064 "page 7 is not described"
3065 );
3066 }
3067
3068 #[test]
3075 fn misplaced_list_items_are_wrapped_like_docling_core() {
3076 use crate::tree::{ItemTree, ListMeta, TreeKind};
3077 let mut t = ItemTree::default();
3078 let li = |txt: &str| TreeKind::Text {
3079 label: "list_item".into(),
3080 text: txt.into(),
3081 orig: None,
3082 formatting: None,
3083 hyperlink: None,
3084 level: None,
3085 list: Some(ListMeta {
3086 enumerated: false,
3087 marker: "-".into(),
3088 }),
3089 };
3090 let group = |label: &str, name: &str| TreeKind::Group {
3091 label: label.into(),
3092 name: name.into(),
3093 };
3094 t.add(None, None, li("A"));
3095 t.add(None, None, li("B"));
3096 t.add(
3097 None,
3098 None,
3099 TreeKind::Text {
3100 label: "text".into(),
3101 text: "plain".into(),
3102 orig: None,
3103 formatting: None,
3104 hyperlink: None,
3105 level: None,
3106 list: None,
3107 },
3108 );
3109 let cell = t.add(None, None, group("unspecified", "cell"));
3110 t.add(Some(cell), None, li("C"));
3111 t.add(Some(cell), None, li("D"));
3112 let list = t.add(None, None, group("list", "list"));
3113 t.add(Some(list), None, li("E"));
3114 let doc = DoclingDocument {
3115 tree: Some(t),
3116 ..DoclingDocument::new("t")
3117 };
3118 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3119 let refs = |x: &Value| -> Vec<String> {
3120 x.as_array()
3121 .unwrap()
3122 .iter()
3123 .map(|r| r["$ref"].as_str().unwrap().to_string())
3124 .collect()
3125 };
3126 assert_eq!(
3127 refs(&v["body"]["children"]),
3128 ["#/groups/4", "#/texts/0", "#/groups/0", "#/groups/1"]
3129 );
3130 assert_eq!(
3131 refs(&v["groups"][0]["children"]),
3132 ["#/groups/3", "#/groups/2"]
3133 );
3134 for (g, name, kids) in [
3135 (2, "group", vec!["#/texts/2"]),
3136 (3, "group", vec!["#/texts/3"]),
3137 (4, "group", vec!["#/texts/4", "#/texts/5"]),
3138 (1, "list", vec!["#/texts/1"]),
3139 ] {
3140 assert_eq!(v["groups"][g]["label"], "list");
3141 assert_eq!(v["groups"][g]["name"], name);
3142 assert_eq!(refs(&v["groups"][g]["children"]), kids);
3143 }
3144 let texts: Vec<&str> = v["texts"]
3145 .as_array()
3146 .unwrap()
3147 .iter()
3148 .map(|t| t["text"].as_str().unwrap())
3149 .collect();
3150 assert_eq!(texts, ["plain", "E", "D", "C", "A", "B"]);
3151 assert_eq!(v["texts"][2]["parent"]["$ref"], "#/groups/2");
3152 }
3153
3154 #[test]
3157 fn a_deleted_child_is_not_written_as_an_empty_ref() {
3158 use crate::tree::{ItemTree, TreeKind};
3159 let mut t = ItemTree::default();
3160 let text = |txt: &str| TreeKind::Text {
3161 label: "text".into(),
3162 text: txt.into(),
3163 orig: None,
3164 formatting: None,
3165 hyperlink: None,
3166 level: None,
3167 list: None,
3168 };
3169 let g = t.add(
3170 None,
3171 None,
3172 TreeKind::Group {
3173 label: "unspecified".into(),
3174 name: "g".into(),
3175 },
3176 );
3177 let gone = t.add(Some(g), None, text(""));
3178 t.add(Some(g), None, text("kept"));
3179 t.items[gone].deleted = true; let doc = DoclingDocument {
3181 tree: Some(t),
3182 ..DoclingDocument::new("t")
3183 };
3184 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3185 assert_eq!(
3186 v["groups"][0]["children"],
3187 serde_json::json!([{"$ref": "#/texts/0"}])
3188 );
3189 }
3190
3191 #[test]
3192 fn a_backend_item_tree_is_written_verbatim() {
3193 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
3194 let mut t = ItemTree::default();
3195 let text = |label: &str, txt: &str| TreeKind::Text {
3196 label: label.into(),
3197 text: txt.into(),
3198 orig: None,
3199 formatting: None,
3200 hyperlink: None,
3201 level: None,
3202 list: None,
3203 };
3204 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
3205 let h = t.add(None, None, text("title", "Heading"));
3206 let group = t.add(
3207 Some(h),
3208 None,
3209 TreeKind::Group {
3210 label: "inline".into(),
3211 name: "group".into(),
3212 },
3213 );
3214 t.add(
3215 Some(group),
3216 None,
3217 TreeKind::Text {
3218 label: "text".into(),
3219 text: "bold".into(),
3220 orig: None,
3221 formatting: Some(Formatting {
3222 bold: true,
3223 ..Formatting::default()
3224 }),
3225 hyperlink: Some("https://example.com/".into()),
3226 level: None,
3227 list: None,
3228 },
3229 );
3230 t.add(
3231 Some(group),
3232 None,
3233 TreeKind::Code {
3234 text: "x = 1".into(),
3235 orig: None,
3236 language: Some("python".into()),
3237 formatting: None,
3238 hyperlink: None,
3239 },
3240 );
3241 let sub = t.add(
3242 Some(h),
3243 None,
3244 TreeKind::Text {
3245 label: "section_header".into(),
3246 text: "Sub".into(),
3247 orig: Some("Sub\u{2019}".into()),
3248 formatting: None,
3249 hyperlink: None,
3250 level: Some(1),
3251 list: None,
3252 },
3253 );
3254 let item = t.add(
3255 Some(sub),
3256 None,
3257 TreeKind::Text {
3258 label: "list_item".into(),
3259 text: "item".into(),
3260 orig: None,
3261 formatting: None,
3262 hyperlink: None,
3263 level: None,
3264 list: Some(ListMeta {
3265 enumerated: true,
3266 marker: "3.".into(),
3267 }),
3268 },
3269 );
3270 let _region = t.add(
3271 Some(sub),
3272 None,
3273 TreeKind::FieldRegion {
3274 items: vec![crate::FieldItem {
3275 marker: None,
3276 key: Some("Name".into()),
3277 value: Some("Duck".into()),
3278 value_kind: Some("read_only".into()),
3279 }],
3280 },
3281 );
3282 let table = t.add(
3283 Some(sub),
3284 None,
3285 TreeKind::Table {
3286 table: Table {
3287 rows: vec![vec!["a \n<".into(), "b".into()]],
3288 cells: Some(vec![
3289 crate::TableCell {
3290 text: "a \n<".into(),
3291 bbox: None,
3292 start_row: 0,
3293 start_col: 0,
3294 row_span: 3,
3295 col_span: 1,
3296 column_header: false,
3297 row_header: true,
3298 row_section: false,
3299 },
3300 crate::TableCell {
3301 text: "b".into(),
3302 bbox: None,
3303 start_row: 0,
3304 start_col: 1,
3305 row_span: 1,
3306 col_span: 1,
3307 column_header: false,
3308 row_header: false,
3309 row_section: false,
3310 },
3311 ]),
3312 ..Table::default()
3313 },
3314 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
3316 },
3317 );
3318 let cell_group = t.add(
3319 Some(table),
3320 None,
3321 TreeKind::Group {
3322 label: "unspecified".into(),
3323 name: "rich_cell_group_1_0_0".into(),
3324 },
3325 );
3326 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
3327 *rich_cells = vec![(0, 1, cell_group)];
3328 }
3329 let after = t.add(Some(sub), None, text("text", "after the region"));
3330 let _ = (title, after);
3331 let list = t.add(
3335 Some(sub),
3336 None,
3337 TreeKind::Group {
3338 label: "list".into(),
3339 name: "list".into(),
3340 },
3341 );
3342 t.reparent(item, Some(list));
3343
3344 let doc = DoclingDocument {
3345 tree: Some(t),
3346 ..DoclingDocument::new("t")
3347 };
3348 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3349 let texts: Vec<&str> = v["texts"]
3351 .as_array()
3352 .unwrap()
3353 .iter()
3354 .map(|t| t["text"].as_str().unwrap())
3355 .collect();
3356 assert_eq!(
3357 texts,
3358 [
3359 "Page",
3360 "Heading",
3361 "bold",
3362 "x = 1",
3363 "Sub",
3364 "item",
3365 "Name",
3366 "Duck",
3367 "after the region"
3368 ]
3369 );
3370 assert_eq!(
3371 v["body"]["children"],
3372 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
3373 );
3374 assert_eq!(v["texts"][0]["content_layer"], "furniture");
3375 assert_eq!(
3376 v["texts"][1]["children"],
3377 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
3378 );
3379 let bold = &v["texts"][2];
3380 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
3381 let keys: Vec<&str> = bold
3382 .as_object()
3383 .unwrap()
3384 .keys()
3385 .map(String::as_str)
3386 .collect();
3387 assert_eq!(
3388 keys,
3389 [
3390 "self_ref",
3391 "parent",
3392 "children",
3393 "content_layer",
3394 "label",
3395 "prov",
3396 "orig",
3397 "text",
3398 "formatting",
3399 "hyperlink"
3400 ]
3401 );
3402 assert_eq!(
3403 bold["formatting"],
3404 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
3405 );
3406 let code = &v["texts"][3];
3407 assert_eq!(code["label"], "code");
3408 assert_eq!(code["code_language"], "Python");
3409 let sub = &v["texts"][4];
3410 assert_eq!(sub["orig"], "Sub\u{2019}");
3411 assert_eq!(sub["level"], 1);
3412 let item = &v["texts"][5];
3413 let keys: Vec<&str> = item
3414 .as_object()
3415 .unwrap()
3416 .keys()
3417 .map(String::as_str)
3418 .collect();
3419 assert_eq!(
3420 keys,
3421 [
3422 "self_ref",
3423 "parent",
3424 "children",
3425 "content_layer",
3426 "label",
3427 "prov",
3428 "orig",
3429 "text",
3430 "enumerated",
3431 "marker"
3432 ]
3433 );
3434 assert_eq!(item["marker"], "3.");
3435 assert_eq!(v["texts"][7]["kind"], "read_only");
3436 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
3437 let table = &v["tables"][0];
3438 assert_eq!(
3439 table["children"],
3440 serde_json::json!([{"$ref": "#/groups/1"}])
3441 );
3442 let cells = table["data"]["table_cells"].as_array().unwrap();
3443 assert_eq!(
3444 cells[0]["text"], "a \n<",
3445 "raw cell text is written verbatim"
3446 );
3447 assert_eq!(
3448 cells[0]["end_row_offset_idx"], 3,
3449 "declared spans are not clamped"
3450 );
3451 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
3452 assert!(cells[0].get("ref").is_none());
3453 assert!(
3454 table["data"]["grid"][0][1].get("ref").is_none(),
3455 "the grid shows plain cells"
3456 );
3457 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
3458 }
3459
3460 #[test]
3464 fn a_comment_section_can_be_referenced_by_its_note_text() {
3465 let doc = DoclingDocument {
3466 name: "c".into(),
3467 nodes: vec![
3468 Node::Commented {
3469 comments: vec![0],
3470 inner: Box::new(Node::Paragraph {
3471 text: "annotated".into(),
3472 }),
3473 },
3474 Node::CommentSection {
3475 name: "comment-Sheet1-A1".into(),
3476 text: "[author: A]: note".into(),
3477 refs_note_text: true,
3478 grouped: true,
3479 },
3480 ],
3481 ..DoclingDocument::new("c")
3482 };
3483 let v = crate::json::to_json(&doc);
3484 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
3485 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
3486 }
3487
3488 #[test]
3492 fn comment_sections_link_back_to_their_items() {
3493 let doc = DoclingDocument {
3494 name: "c".into(),
3495 nodes: vec![
3496 Node::Commented {
3497 comments: vec![0],
3498 inner: Box::new(Node::Paragraph {
3499 text: "annotated".into(),
3500 }),
3501 },
3502 Node::Paragraph {
3503 text: "plain".into(),
3504 },
3505 Node::CommentSection {
3506 name: "comment-7".into(),
3507 text: "[time: t]: note".into(),
3508 refs_note_text: false,
3509 grouped: true,
3510 },
3511 ],
3512 ..DoclingDocument::new("c")
3513 };
3514 let v = crate::json::to_json(&doc);
3515 assert_eq!(v["groups"][0]["label"], "comment_section");
3517 assert_eq!(v["groups"][0]["name"], "comment-7");
3518 assert_eq!(v["groups"][0]["content_layer"], "notes");
3519 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
3520 assert_eq!(v["texts"][2]["content_layer"], "notes");
3521 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
3523 assert!(v["texts"][1].get("comments").is_none());
3524 let keys: Vec<&str> = v["texts"][0]
3526 .as_object()
3527 .unwrap()
3528 .keys()
3529 .map(String::as_str)
3530 .collect();
3531 assert_eq!(
3532 &keys[keys.len() - 4..],
3533 &["prov", "comments", "orig", "text"]
3534 );
3535 }
3536
3537 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
3538 Node::Picture {
3539 caption: Some(caption.into()),
3540 caption_href: None,
3541 image: None,
3542 classification: None,
3543 caption_parent,
3544 caption_location: None,
3545 }
3546 }
3547
3548 fn group(children: Vec<Node>) -> Node {
3549 Node::Group {
3550 label: "section".into(),
3551 name: None,
3552 layer: None,
3553 children,
3554 }
3555 }
3556
3557 fn refs(v: &Value) -> Vec<&str> {
3558 v.as_array()
3559 .unwrap()
3560 .iter()
3561 .map(|r| r["$ref"].as_str().unwrap())
3562 .collect()
3563 }
3564
3565 #[test]
3570 fn a_body_caption_follows_the_enclosing_top_level_item() {
3571 let mut doc = DoclingDocument::new("t");
3572 doc.push(picture("top", CaptionParent::Body));
3573 doc.push(group(vec![
3574 Node::Paragraph { text: "p".into() },
3575 picture("nested", CaptionParent::Body),
3576 ]));
3577 doc.push(Node::Paragraph {
3578 text: "after".into(),
3579 });
3580 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3581 assert_eq!(
3582 refs(&v["body"]["children"]),
3583 [
3584 "#/texts/0",
3585 "#/pictures/0",
3586 "#/groups/0",
3587 "#/texts/2",
3588 "#/texts/3"
3589 ]
3590 );
3591 assert_eq!(
3592 refs(&v["groups"][0]["children"]),
3593 ["#/texts/1", "#/pictures/1"]
3594 );
3595 for (cap, pic) in [(0, 0), (2, 1)] {
3596 assert_eq!(v["texts"][cap]["label"], "caption");
3597 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
3598 assert_eq!(
3599 refs(&v["pictures"][pic]["captions"]),
3600 [format!("#/texts/{cap}")]
3601 );
3602 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
3603 }
3604 }
3605
3606 #[test]
3609 fn an_item_caption_is_the_items_first_child() {
3610 let mut doc = DoclingDocument::new("t");
3611 doc.push(picture("fig", CaptionParent::Item));
3612 doc.push(Node::Table(Table {
3613 rows: vec![vec!["a".into()]],
3614 caption: Some("tab".into()),
3615 caption_parent: CaptionParent::Item,
3616 ..Table::default()
3617 }));
3618 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3619 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
3620 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
3621 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
3622 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3623 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
3624 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
3625 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
3626 }
3627
3628 #[test]
3634 fn picture_children_hang_off_the_picture_after_its_caption() {
3635 let mut doc = DoclingDocument::new("t");
3636 doc.push(picture("fig", CaptionParent::Item));
3637 doc.push(Node::PictureChildren(vec![
3638 Node::Heading {
3639 level: 2,
3640 text: "in-figure title".into(),
3641 },
3642 Node::Paragraph {
3643 text: "axis label".into(),
3644 },
3645 Node::ListItem {
3646 ordered: false,
3647 number: 0,
3648 first_in_list: true,
3649 text: "callout".into(),
3650 level: 0,
3651 marker: None,
3652 location: None,
3653 dclx: None,
3654 href: None,
3655 layer: None,
3656 },
3657 ]));
3658 doc.push(Node::Paragraph {
3659 text: "after".into(),
3660 });
3661 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3662 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/texts/4"]);
3663 assert_eq!(
3664 refs(&v["pictures"][0]["children"]),
3665 ["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"]
3666 );
3667 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3668 assert_eq!(v["texts"][1]["label"], "section_header");
3669 assert_eq!(v["texts"][2]["label"], "text");
3670 for t in 1..=2 {
3671 assert_eq!(v["texts"][t]["parent"]["$ref"], "#/pictures/0");
3672 assert_eq!(v["texts"][t]["content_layer"], "body");
3673 }
3674 assert_eq!(v["groups"][0]["parent"]["$ref"], "#/pictures/0");
3675 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
3676 let md = doc.export_to_markdown();
3677 assert!(
3678 !md.contains("axis label") && !md.contains("callout"),
3679 "{md}"
3680 );
3681 assert!(md.contains("after"), "{md}");
3682 }
3683
3684 #[test]
3688 fn a_container_caption_is_the_items_sibling() {
3689 let mut doc = DoclingDocument::new("t");
3690 doc.push(group(vec![
3691 picture("chart", CaptionParent::Container),
3692 Node::Table(Table {
3693 rows: vec![vec!["a".into()]],
3694 caption: Some("figcaption".into()),
3695 caption_parent: CaptionParent::ContainerAfter,
3696 ..Table::default()
3697 }),
3698 ]));
3699 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3700 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
3701 assert_eq!(
3702 refs(&v["groups"][0]["children"]),
3703 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
3704 );
3705 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
3706 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
3707 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
3708 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
3709 }
3710
3711 #[test]
3716 fn a_labeled_text_keeps_its_label_and_hyperlink_out_of_the_text() {
3717 let mut doc = DoclingDocument::new("t");
3718 doc.push(Node::LabeledText {
3719 label: "footnote".into(),
3720 text: "1 https://example.com/a\\_b".into(),
3721 href: Some("https://example.com/a_b".into()),
3722 });
3723 doc.push(Node::LabeledText {
3724 label: "footnote".into(),
3725 text: "© 2022 the authors".into(),
3726 href: None,
3727 });
3728 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3729 let t = &v["texts"][0];
3730 assert_eq!(t["label"], "footnote");
3731 assert_eq!(t["text"], "1 https://example.com/a_b");
3732 assert_eq!(t["orig"], "1 https://example.com/a_b");
3733 assert_eq!(t["hyperlink"], "https://example.com/a_b");
3734 assert_eq!(v["texts"][1]["label"], "footnote");
3735 assert!(v["texts"][1].get("hyperlink").is_none());
3736 assert_eq!(
3737 doc.export_to_markdown(),
3738 "[1 https://example.com/a\\_b](https://example.com/a_b)\n\n© 2022 the authors\n"
3739 );
3740 assert_eq!(
3741 doc.export_to_text(),
3742 "1 https://example.com/a_b\n\n© 2022 the authors"
3743 );
3744 }
3745}