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 size = |page_no: &Value| -> Option<(f64, f64)> {
133 let n = page_no.as_u64()? as usize;
134 pages
135 .iter()
136 .find(|(p, _, _)| *p == n)
137 .map(|(_, w, h)| (*w, *h))
138 };
139 let r2 = |v: f64| (v * 100.0).round() / 100.0;
140 let clamp_bbox = |bbox: &mut Value, (w, h): (f64, f64)| {
141 for (key, hi) in [("l", w), ("r", w), ("t", h), ("b", h)] {
142 if let Some(v) = bbox.get(key).and_then(Value::as_f64) {
143 bbox[key] = json!(r2(v.clamp(0.0, hi.max(0.0))));
144 }
145 }
146 };
147 for bucket in [
148 "texts",
149 "pictures",
150 "tables",
151 "key_value_items",
152 "form_items",
153 "field_regions",
154 "field_items",
155 ] {
156 let Some(items) = out.get_mut(bucket).and_then(Value::as_array_mut) else {
157 continue;
158 };
159 for item in items {
160 let mut table_page: Option<Option<(f64, f64)>> = None;
161 if let Some(provs) = item.get_mut("prov").and_then(Value::as_array_mut) {
162 let mut page_nos: Vec<u64> = Vec::new();
163 for prov in provs.iter_mut() {
164 if let Some(n) = prov.get("page_no").and_then(Value::as_u64) {
165 page_nos.push(n);
166 }
167 let Some(sz) = prov.get("page_no").and_then(size) else {
168 continue;
169 };
170 if let Some(bbox) = prov.get_mut("bbox") {
171 clamp_bbox(bbox, sz);
172 }
173 }
174 page_nos.sort_unstable();
175 page_nos.dedup();
176 if let [only] = page_nos[..] {
177 table_page = Some(size(&json!(only)));
178 }
179 }
180 if let (Some(Some(sz)), Some(data)) = (table_page, item.get_mut("data")) {
182 for key in ["table_cells", "grid"] {
183 let Some(rows) = data.get_mut(key).and_then(Value::as_array_mut) else {
184 continue;
185 };
186 for entry in rows.iter_mut() {
187 let cells: Vec<&mut Value> = match entry {
188 Value::Array(row) => row.iter_mut().collect(),
189 other => vec![other],
190 };
191 for cell in cells {
192 if let Some(bbox) = cell.get_mut("bbox").filter(|b| b.is_object()) {
193 clamp_bbox(bbox, sz);
194 }
195 }
196 }
197 }
198 }
199 }
200 }
201}
202
203fn formatting_json(f: &crate::tree::Formatting) -> Value {
205 json!({
206 "bold": f.bold,
207 "italic": f.italic,
208 "underline": f.underline,
209 "strikethrough": f.strikethrough,
210 "script": match f.script {
211 crate::Script::Baseline => "baseline",
212 crate::Script::Sub => "sub",
213 crate::Script::Super => "super",
214 },
215 })
216}
217
218fn track_json(t: &crate::tree::TreeTrack) -> Value {
221 let mut m = serde_json::Map::new();
222 m.insert("kind".into(), json!("track"));
223 m.insert("start_time".into(), json!(t.start_time));
224 m.insert("end_time".into(), json!(t.end_time));
225 if let Some(id) = &t.identifier {
226 m.insert("identifier".into(), json!(id));
227 }
228 if let Some(v) = &t.voice {
229 m.insert("voice".into(), json!(v));
230 }
231 Value::Object(m)
232}
233
234pub fn to_json(doc: &DoclingDocument) -> Value {
236 let mut b = Builder::default();
237 let body = match &doc.tree {
241 Some(tree) => {
242 for n in &doc.nodes {
245 if let Node::PageInfo {
246 page_no,
247 width,
248 height,
249 } = n
250 {
251 if *page_no > 0 {
252 b.pages.push((*page_no, *width as f64, *height as f64));
253 }
254 }
255 }
256 b.write_tree(tree)
257 }
258 None => b.walk_into(&doc.nodes, "#/body"),
259 };
260 b.link_comments();
261
262 let mut out = json!({
263 "schema_name": "DoclingDocument",
264 "version": SCHEMA_VERSION,
265 "name": doc.name,
266 "origin": {
267 "mimetype": "text/plain",
268 "binary_hash": fnv1a(&doc.name),
269 "filename": doc.name,
270 },
271 "furniture": {
272 "self_ref": "#/furniture",
273 "children": [],
274 "content_layer": "furniture",
275 "name": "_root_",
276 "label": "unspecified",
277 },
278 "body": {
279 "self_ref": "#/body",
280 "children": body,
281 "content_layer": "body",
282 "name": "_root_",
283 "label": "unspecified",
284 },
285 "groups": b.groups,
286 "texts": b.texts,
287 "pictures": b.pictures,
288 "tables": b.tables,
289 "key_value_items": [],
290 "form_items": [],
291 "pages": b.pages.iter().map(|(n, w, h)| {
292 let r2 = |v: f64| (v * 100.0).round() / 100.0;
293 (n.to_string(), json!({
294 "size": { "width": r2(*w), "height": r2(*h) },
295 "page_no": n,
296 }))
297 }).collect::<serde_json::Map<String, Value>>(),
298 });
299
300 clamp_boxes_to_pages(&mut out, &b.pages);
309
310 if !b.field_regions.is_empty() {
315 if let Some(obj) = out.as_object_mut() {
316 let pages = obj.remove("pages");
317 obj.insert("field_regions".into(), Value::Array(b.field_regions));
318 obj.insert("field_items".into(), Value::Array(b.field_items));
319 if let Some(pages) = pages {
320 obj.insert("pages".into(), pages);
321 }
322 }
323 }
324 out
325}
326
327fn classification_meta(classes: &[crate::PictureClass]) -> Value {
334 json!({
335 "classification": {
336 "predictions": classes.iter().map(|c| json!({
337 "confidence": c.confidence as f64,
338 "created_by": "DocumentPictureClassifier",
339 "class_name": c.class_name,
340 })).collect::<Vec<_>>(),
341 },
342 "annotations": [{
343 "kind": "classification",
344 "provenance": "DocumentPictureClassifier",
345 "predicted_classes": classes.iter().map(|c| json!({
346 "class_name": c.class_name,
347 "confidence": c.confidence as f64,
348 })).collect::<Vec<_>>(),
349 }],
350 })
351}
352
353#[allow(clippy::too_many_arguments)]
364fn cell_value(
365 row_span: usize,
366 col_span: usize,
367 start_row: usize,
368 end_row: usize,
369 start_col: usize,
370 end_col: usize,
371 text: String,
372 column_header: bool,
373 row_header: bool,
374 row_section: bool,
375 bbox: Option<[f32; 4]>,
376) -> Value {
377 let mut m = serde_json::Map::with_capacity(12);
378 m.insert("row_span".into(), row_span.into());
379 m.insert("col_span".into(), col_span.into());
380 m.insert("start_row_offset_idx".into(), start_row.into());
381 m.insert("end_row_offset_idx".into(), end_row.into());
382 m.insert("start_col_offset_idx".into(), start_col.into());
383 m.insert("end_col_offset_idx".into(), end_col.into());
384 m.insert("text".into(), Value::String(text));
385 m.insert("column_header".into(), column_header.into());
386 m.insert("row_header".into(), row_header.into());
387 m.insert("row_section".into(), row_section.into());
388 m.insert("fillable".into(), false.into());
389 if let Some(b) = bbox {
390 m.insert(
391 "bbox".into(),
392 json!({
393 "l": b[0], "t": b[1], "r": b[2], "b": b[3],
394 "coord_origin": "TOPLEFT",
395 }),
396 );
397 }
398 Value::Object(m)
399}
400
401fn table_data(t: &Table) -> Value {
404 table_data_with(t, false)
405}
406
407fn table_data_with(t: &Table, raw: bool) -> Value {
410 let cell_text = |s: &str| {
411 if raw {
412 s.to_string()
413 } else {
414 unescape_text(&crate::markdown::strip_hard_breaks(s))
415 }
416 };
417 let mut num_rows = t.rows.len();
418 let mut num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
419 let ragged = t.rows.iter().any(|r| r.len() != num_cols);
427 if let Some(first_class) = t.cells.as_ref().filter(|c| ragged && !c.is_empty()) {
428 for c in first_class {
429 num_rows = num_rows.max(c.start_row + c.row_span);
430 num_cols = num_cols.max(c.start_col + c.col_span);
431 }
432 }
433 let mut grid = Vec::with_capacity(num_rows);
434 let mut cells = Vec::new();
435 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
439 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
440 for c in first_class {
441 let idx = cells.len();
442 cells.push(cell_value(
443 c.row_span,
444 c.col_span,
445 c.start_row,
446 c.start_row + c.row_span,
447 c.start_col,
448 c.start_col + c.col_span,
449 cell_text(&c.text),
450 c.column_header,
451 c.row_header,
452 c.row_section,
453 c.bbox,
454 ));
455 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
456 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
457 slot[r * num_cols + k] = Some(idx);
458 }
459 }
460 }
461 for r in 0..num_rows {
462 let mut grid_row = Vec::with_capacity(num_cols);
463 for c in 0..num_cols {
464 grid_row.push(match slot[r * num_cols + c] {
465 Some(i) => cells[i].clone(),
466 None => cell_value(
467 1,
468 1,
469 r,
470 r + 1,
471 c,
472 c + 1,
473 String::new(),
474 false,
475 false,
476 false,
477 None,
478 ),
479 });
480 }
481 grid.push(grid_row);
482 }
483 } else {
484 let s = t.structure.as_ref();
485 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
486 grid.and_then(|g| g.get(r))
487 .and_then(|row| row.get(c))
488 .copied()
489 .unwrap_or(false)
490 };
491 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
492 let (mut r0, mut c0) = (r, c);
493 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
494 c0 -= 1;
495 }
496 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
497 r0 -= 1;
498 }
499 (r0, c0)
500 };
501 let anchors: Vec<(usize, usize)> = (0..num_rows)
504 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
505 .map(|(r, c)| anchor_of(r, c))
506 .collect();
507 let mut extent: Vec<(usize, usize)> = (0..num_rows)
508 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
509 .collect();
510 for (i, &(ar, ac)) in anchors.iter().enumerate() {
511 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
512 let e = &mut extent[ar * num_cols + ac];
513 e.0 = e.0.max(r);
514 e.1 = e.1.max(c);
515 }
516 for (r, row) in t.rows.iter().enumerate() {
517 let mut grid_row = Vec::with_capacity(num_cols);
518 for c in 0..num_cols {
519 let (ar, ac) = anchors[r * num_cols + c];
520 if (ar, ac) == (r, c) {
521 let (er, ec) = extent[r * num_cols + c];
522 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
523 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
524 Some(s) => flag(Some(&s.col_header), r, c),
525 None => r == 0,
526 };
527 slot[r * num_cols + c] = Some(cells.len());
528 cells.push(cell_value(
529 er - r + 1,
530 ec - c + 1,
531 r,
532 er + 1,
533 c,
534 ec + 1,
535 text,
536 column_header,
537 flag(s.map(|s| &s.row_header), r, c),
538 false,
539 None,
540 ));
541 }
542 grid_row.push(match slot[ar * num_cols + ac] {
543 Some(i) => cells[i].clone(),
544 None => Value::Null,
545 });
546 }
547 grid.push(grid_row);
548 }
549 }
550 json!({
551 "table_cells": cells,
552 "num_rows": num_rows,
553 "num_cols": num_cols,
554 "orientation": "rot_0",
555 "grid": grid,
556 })
557}
558
559#[derive(Default)]
560struct Builder {
561 texts: Vec<Value>,
562 groups: Vec<Value>,
563 tables: Vec<Value>,
564 pictures: Vec<Value>,
565 field_regions: Vec<Value>,
566 field_items: Vec<Value>,
567 pages: Vec<(usize, f64, f64)>,
571 cur_page: usize,
573 cur_w: f64,
574 cur_h: f64,
575 pending_loc: Option<[u16; 4]>,
578 pending_exact: Option<ExactProv>,
581 pending_siblings: Vec<Value>,
585 pending_after: Vec<Value>,
589 pending_body: Vec<Value>,
593 comment_groups: Vec<String>,
597 pending_comments: Vec<(String, Vec<usize>)>,
601}
602
603impl Builder {
604 fn take_prov(&mut self, char_len: usize) -> Value {
610 let prov = self.prov_json(char_len, false);
611 self.pending_exact = None;
612 self.pending_loc = None;
613 prov
614 }
615
616 fn prov_json(&self, char_len: usize, span_over_text: bool) -> Value {
621 let r2 = |v: f64| (v * 100.0).round() / 100.0;
622 if let Some(ExactProv {
623 page_no,
624 bbox: [l, t, r, b],
625 bottom_left,
626 charspan,
627 }) = self.pending_exact
628 {
629 let charspan = if span_over_text {
630 [0, char_len]
631 } else {
632 charspan
633 };
634 return json!([{
635 "page_no": page_no,
636 "bbox": {
637 "l": r2(l), "t": r2(t), "r": r2(r), "b": r2(b),
638 "coord_origin": if bottom_left { "BOTTOMLEFT" } else { "TOPLEFT" },
639 },
640 "charspan": charspan,
641 }]);
642 }
643 let Some([x0, y0, x1, y1]) = self.pending_loc else {
644 return json!([]);
645 };
646 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
650 return json!([{
651 "page_no": self.cur_page,
652 "bbox": { "l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT" },
653 "charspan": [0, char_len],
654 }]);
655 }
656 json!([{
657 "page_no": self.cur_page,
658 "bbox": {
659 "l": r2(x0 as f64 * self.cur_w / 512.0),
660 "t": r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
661 "r": r2(x1 as f64 * self.cur_w / 512.0),
662 "b": r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
663 "coord_origin": "BOTTOMLEFT",
664 },
665 "charspan": [0, char_len],
666 }])
667 }
668
669 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
672 if self.pending_loc.is_none() && self.cur_page > 0 {
673 self.pending_loc = loc;
674 }
675 }
676
677 fn link_comments(&mut self) {
685 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
686 .into_iter()
687 .map(|(item, comments)| {
688 let refs = comments
689 .iter()
690 .filter_map(|i| self.comment_groups.get(*i))
691 .map(|r| json!({ "$ref": r }))
692 .collect();
693 (item, refs)
694 })
695 .collect();
696 for (item, comment_refs) in refs {
697 if comment_refs.is_empty() {
698 continue;
699 }
700 let Some(target) = self.item_mut(&item) else {
701 continue;
702 };
703 let Some(obj) = target.as_object_mut() else {
704 continue;
705 };
706 let tail: Vec<(String, Value)> = obj
707 .iter()
708 .skip_while(|(k, _)| k.as_str() != "prov")
709 .skip(1)
710 .map(|(k, v)| (k.clone(), v.clone()))
711 .collect();
712 for (k, _) in &tail {
713 obj.shift_remove(k);
714 }
715 obj.insert("comments".into(), Value::Array(comment_refs));
716 for (k, v) in tail {
717 obj.insert(k, v);
718 }
719 }
720 }
721
722 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
724 let idx = ref_index(self_ref)?;
725 let bucket = if self_ref.starts_with("#/texts/") {
726 &mut self.texts
727 } else if self_ref.starts_with("#/tables/") {
728 &mut self.tables
729 } else if self_ref.starts_with("#/pictures/") {
730 &mut self.pictures
731 } else if self_ref.starts_with("#/groups/") {
732 &mut self.groups
733 } else {
734 return None;
735 };
736 bucket.get_mut(idx)
737 }
738
739 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
743 use crate::tree::TreeKind;
744 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
747 let (mut nt, mut ng, mut ntb, mut np, mut nf) = (0, 0, 0, 0, 0);
748 for item in &tree.items {
749 if item.deleted {
750 refs.push(String::new());
751 continue;
752 }
753 let r = match &item.kind {
754 TreeKind::Text { .. } | TreeKind::Code { .. } => {
755 nt += 1;
756 format!("#/texts/{}", nt - 1)
757 }
758 TreeKind::Group { .. } => {
759 ng += 1;
760 format!("#/groups/{}", ng - 1)
761 }
762 TreeKind::Table { .. } => {
763 ntb += 1;
764 format!("#/tables/{}", ntb - 1)
765 }
766 TreeKind::Picture { .. } => {
767 np += 1;
768 format!("#/pictures/{}", np - 1)
769 }
770 TreeKind::FieldRegion { items } => {
771 nt += items
774 .iter()
775 .map(|i| {
776 [&i.marker, &i.key, &i.value]
777 .iter()
778 .filter(|p| p.is_some())
779 .count()
780 })
781 .sum::<usize>();
782 nf += 1;
783 format!("#/field_regions/{}", nf - 1)
784 }
785 };
786 refs.push(r);
787 }
788 let ref_of = |id: usize| json!({ "$ref": refs[id] });
789 for (id, item) in tree.items.iter().enumerate() {
790 if item.deleted {
791 continue;
792 }
793 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
794 let children: Vec<Value> = item.children.iter().map(|&c| ref_of(c)).collect();
795 let layer = item.layer.map_or("body", |l| l.value());
796 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
799 let self_ref = match &item.kind {
800 TreeKind::Text {
801 label,
802 text,
803 orig,
804 formatting,
805 hyperlink,
806 level,
807 list,
808 } => {
809 let mut tail = serde_json::Map::new();
812 if let Some(f) = formatting {
813 tail.insert("formatting".into(), formatting_json(f));
814 }
815 if let Some(h) = hyperlink {
816 tail.insert("hyperlink".into(), json!(h));
817 }
818 if let Some(l) = level {
819 tail.insert("level".into(), json!(l));
820 }
821 if let Some(l) = list {
822 tail.insert("enumerated".into(), json!(l.enumerated));
823 tail.insert("marker".into(), json!(l.marker));
824 }
825 let r = format!("#/texts/{}", self.texts.len());
826 let prov = self.take_prov(text.chars().count());
827 let mut item_json = json!({
828 "self_ref": r,
829 "parent": { "$ref": parent },
830 "children": children,
831 "content_layer": layer,
832 "label": label,
833 "prov": prov,
834 });
835 if !item.comments.is_empty() {
838 item_json["comments"] =
839 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
840 }
841 if let Some(track) = &item.source {
844 item_json["source"] = json!([track_json(track)]);
845 }
846 merge(
847 &mut item_json,
848 json!({
849 "orig": orig.as_deref().unwrap_or(text),
850 "text": text,
851 }),
852 );
853 merge(&mut item_json, Value::Object(tail));
854 self.texts.push(item_json);
855 r
856 }
857 TreeKind::Code {
858 text,
859 orig,
860 language,
861 formatting,
862 hyperlink,
863 } => {
864 let r = format!("#/texts/{}", self.texts.len());
865 let prov = self.take_prov(text.chars().count());
866 let mut item_json = json!({
867 "self_ref": r,
868 "parent": { "$ref": parent },
869 "children": children,
870 "content_layer": layer,
871 "label": "code",
872 "prov": prov,
873 });
874 if !item.comments.is_empty() {
875 item_json["comments"] =
876 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
877 }
878 merge(
879 &mut item_json,
880 json!({
881 "orig": orig.as_deref().unwrap_or(text),
882 "text": text,
883 }),
884 );
885 if let Some(f) = formatting {
886 item_json["formatting"] = formatting_json(f);
887 }
888 if let Some(h) = hyperlink {
889 item_json["hyperlink"] = json!(h);
890 }
891 merge(
892 &mut item_json,
893 json!({
894 "captions": [],
895 "references": [],
896 "footnotes": [],
897 "code_language": code_language(language.as_deref()),
898 }),
899 );
900 self.texts.push(item_json);
901 r
902 }
903 TreeKind::Group { label, name } => {
904 self.pending_exact = None;
905 let r = format!("#/groups/{}", self.groups.len());
906 self.groups.push(json!({
907 "self_ref": r,
908 "parent": { "$ref": parent },
909 "children": children,
910 "content_layer": layer,
911 "name": name,
912 "label": label,
913 }));
914 r
915 }
916 TreeKind::Table {
917 table,
918 rich_cells,
919 captions,
920 } => {
921 let r = self.add_table_with(table, parent, true);
923 let idx = ref_index(&r).expect("table ref");
924 let t = &mut self.tables[idx];
925 t["children"] = Value::Array(children);
926 t["content_layer"] = json!(layer);
927 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
928 for &(row, col, group) in rich_cells {
932 let cell_ref = ref_of(group);
933 let hit = |c: &Value| {
934 c["start_row_offset_idx"] == json!(row)
935 && c["start_col_offset_idx"] == json!(col)
936 };
937 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
938 for c in cells.iter_mut().filter(|c| hit(c)) {
939 c["ref"] = cell_ref.clone();
940 }
941 }
942 }
943 r
944 }
945 TreeKind::Picture {
946 captions,
947 image,
948 classification,
949 confidence,
950 chart,
951 dpi,
952 } => {
953 let mut meta = classification.as_ref().map(|c| {
958 let mut pred = serde_json::Map::new();
959 if let Some(conf) = confidence {
960 pred.insert("confidence".into(), json!(conf));
961 }
962 pred.insert("class_name".into(), json!(c));
963 json!({ "classification": { "predictions": [pred] } })
964 });
965 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
966 if !t.rows.is_empty() {
967 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
968 }
969 }
970 let prov = self.take_prov(0);
971 let r = self.push_picture(
972 prov,
973 captions.iter().map(|&c| ref_of(c)).collect(),
974 children,
975 image.as_ref(),
976 meta,
977 parent,
978 );
979 if let Some(idx) = ref_index(&r) {
980 self.pictures[idx]["content_layer"] = json!(layer);
981 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
984 img["dpi"] = json!(dpi);
985 }
986 }
987 r
988 }
989 TreeKind::FieldRegion { items } => {
990 self.pending_exact = None;
991 let r = self.add_field_region(items, parent);
992 if let Some(region) = self.field_regions.last_mut() {
993 region["content_layer"] = json!(layer);
994 }
995 r
996 }
997 };
998 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
999 }
1000 tree.body.iter().map(|&c| ref_of(c)).collect()
1001 }
1002
1003 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1004 match node {
1005 Node::Heading { level: 1, text } => {
1006 Some(self.add_text("title", text, parent, json!({})))
1007 }
1008 Node::Heading { level, text } => Some(self.add_text(
1009 "section_header",
1010 text,
1011 parent,
1012 json!({ "level": level.saturating_sub(1) }),
1013 )),
1014 Node::Caption { text, href } => {
1015 let extra = match href {
1016 Some(url) => json!({ "hyperlink": url }),
1017 None => json!({}),
1018 };
1019 Some(self.add_text("caption", text, parent, extra))
1020 }
1021 Node::Paragraph { text } => {
1022 let t = text.trim();
1025 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1026 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1027 _ => Some(self.add_text("text", text, parent, json!({}))),
1028 }
1029 }
1030 Node::CheckboxItem { checked, text } => {
1031 let mark = if *checked { "- [x] " } else { "- [ ] " };
1034 Some(self.add_text("text", &format!("{mark}{text}"), parent, json!({})))
1035 }
1036 Node::Code {
1037 language,
1038 text,
1039 orig,
1040 ..
1041 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1042 Node::Formula {
1045 latex,
1046 orig,
1047 location,
1048 } => {
1049 self.adopt_loc(*location);
1050 Some(self.add_formula_item(latex, orig, parent))
1051 }
1052 Node::CommentSection {
1058 name,
1059 text,
1060 refs_note_text,
1061 grouped,
1062 } => {
1063 if !*grouped {
1064 let child =
1068 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1069 self.comment_groups.push(child.clone());
1070 return Some(child);
1071 }
1072 let self_ref = format!("#/groups/{}", self.groups.len());
1073 self.groups.push(Value::Null);
1074 let child =
1075 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1076 self.groups[group_index(&self_ref)] = json!({
1077 "self_ref": self_ref,
1078 "parent": { "$ref": parent },
1079 "children": [{ "$ref": child }],
1080 "content_layer": "notes",
1081 "name": name,
1082 "label": "comment_section",
1083 });
1084 self.comment_groups.push(if *refs_note_text {
1085 child
1086 } else {
1087 self_ref.clone()
1088 });
1089 Some(self_ref)
1090 }
1091 Node::Commented { comments, inner } => {
1094 let item = self.add_node(inner, parent)?;
1095 if !comments.is_empty() {
1096 self.pending_comments.push((item.clone(), comments.clone()));
1097 }
1098 Some(item)
1099 }
1100 Node::Table(t) => Some(self.add_table(t, parent)),
1101 Node::Picture {
1102 caption,
1103 caption_href,
1104 image,
1105 classification,
1106 caption_parent,
1107 } => Some(self.add_picture(
1108 caption.as_deref(),
1109 caption_href.as_deref(),
1110 image.as_ref(),
1111 classification.as_deref().map(classification_meta),
1112 parent,
1113 *caption_parent,
1114 )),
1115 Node::Chart {
1120 kind,
1121 table,
1122 caption,
1123 location,
1124 } => {
1125 self.adopt_loc(*location);
1126 let mut meta = json!({
1127 "classification": { "predictions": [{ "class_name": kind }] },
1128 });
1129 if !table.rows.is_empty() {
1130 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1131 }
1132 let mut captions = Vec::new();
1138 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1139 let prov = self.prov_json(unescape_text(cap).chars().count(), true);
1140 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1141 self.pending_siblings.push(json!({ "$ref": cap_ref }));
1142 captions.push(json!({ "$ref": cap_ref }));
1143 }
1144 let prov = self.take_prov(0);
1145 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1146 }
1147 Node::DoclangOnly(_) => None,
1149 Node::Group {
1150 label,
1151 name,
1152 layer,
1153 children,
1154 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1155 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1156 Node::InlineGroup { md_text, .. } => {
1159 Some(self.add_text("text", md_text, parent, json!({})))
1160 }
1161 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1163 Node::Furniture {
1169 layer: ContentLayer::Notes,
1170 inner,
1171 } => {
1172 let item = self.add_node(inner, parent)?;
1173 self.set_layer(&item, "notes");
1174 Some(item)
1175 }
1176 Node::Furniture { .. } => None,
1177 Node::PageFurniture { .. } => None,
1178 Node::Located { location, inner } => {
1183 if self.cur_page > 0 {
1184 self.pending_loc = Some(*location);
1185 }
1186 let r = self.add_node(inner, parent);
1187 self.pending_loc = None;
1188 r
1189 }
1190 Node::Prov {
1191 page_no,
1192 bbox,
1193 charspan,
1194 inner,
1195 ..
1196 } => {
1197 self.pending_exact = Some(ExactProv {
1198 page_no: *page_no,
1199 bbox: bbox.map(f64::from),
1200 bottom_left: false,
1201 charspan: *charspan,
1202 });
1203 let r = self.add_node(inner, parent);
1204 self.pending_exact = None;
1205 r
1206 }
1207 Node::PageBreak => None,
1209 Node::PageInfo {
1212 page_no,
1213 width,
1214 height,
1215 } => {
1216 self.cur_page = *page_no;
1217 self.cur_w = *width as f64;
1218 self.cur_h = *height as f64;
1219 if *page_no > 0 {
1220 self.pages.push((*page_no, self.cur_w, self.cur_h));
1221 }
1222 None
1223 }
1224 Node::ListItem { .. } => None,
1226 }
1227 }
1228
1229 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1233 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1234 self.field_regions.push(Value::Null);
1235 let region_index = self.field_regions.len() - 1;
1236 let mut item_refs = Vec::new();
1237 for item in items {
1238 item_refs.push(json!({ "$ref": self.add_field_item(item, &self_ref) }));
1239 }
1240 self.field_regions[region_index] = json!({
1241 "self_ref": self_ref,
1242 "parent": { "$ref": parent },
1243 "children": item_refs,
1244 "content_layer": "body",
1245 "label": "field_region",
1246 "prov": [],
1247 });
1248 self_ref
1249 }
1250
1251 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1252 let self_ref = format!("#/field_items/{}", self.field_items.len());
1253 self.field_items.push(Value::Null);
1254 let item_index = self.field_items.len() - 1;
1255 let mut child_refs = Vec::new();
1256 for (label, text) in [
1257 ("marker", &item.marker),
1258 ("field_key", &item.key),
1259 ("field_value", &item.value),
1260 ] {
1261 if let Some(text) = text {
1262 let extra = match (label, &item.value_kind) {
1265 ("field_value", Some(kind)) => json!({ "kind": kind }),
1266 _ => json!({}),
1267 };
1268 child_refs.push(json!({ "$ref": self.add_text(label, text, &self_ref, extra) }));
1269 }
1270 }
1271 self.field_items[item_index] = json!({
1272 "self_ref": self_ref,
1273 "parent": { "$ref": parent },
1274 "children": child_refs,
1275 "content_layer": "body",
1276 "label": "field_item",
1277 "prov": [],
1278 });
1279 self_ref
1280 }
1281
1282 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1286 let bucket = match self_ref.split('/').nth(1) {
1287 Some("texts") => &mut self.texts,
1288 Some("tables") => &mut self.tables,
1289 Some("pictures") => &mut self.pictures,
1290 Some("groups") => &mut self.groups,
1291 _ => return,
1292 };
1293 if let Some(item) = self_ref
1294 .rsplit('/')
1295 .next()
1296 .and_then(|i| i.parse::<usize>().ok())
1297 .and_then(|i| bucket.get_mut(i))
1298 {
1299 item["content_layer"] = json!(layer);
1300 }
1301 }
1302
1303 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1304 let prov = self.take_prov(unescape_text(text).chars().count());
1305 self.add_text_with(label, text, parent, extra, prov)
1306 }
1307
1308 fn add_text_with(
1311 &mut self,
1312 label: &str,
1313 text: &str,
1314 parent: &str,
1315 extra: Value,
1316 prov: Value,
1317 ) -> String {
1318 let self_ref = format!("#/texts/{}", self.texts.len());
1319 let raw = unescape_text(text);
1320 let mut item = json!({
1321 "self_ref": self_ref,
1322 "parent": { "$ref": parent },
1323 "children": [],
1324 "content_layer": "body",
1325 "label": label,
1326 "prov": prov,
1327 "orig": raw,
1328 "text": raw,
1329 });
1330 merge(&mut item, extra);
1331 self.texts.push(item);
1332 self_ref
1333 }
1334
1335 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
1338 let self_ref = format!("#/texts/{}", self.texts.len());
1339 let prov = self.take_prov(latex.chars().count());
1340 self.texts.push(json!({
1341 "self_ref": self_ref,
1342 "parent": { "$ref": parent },
1343 "children": [],
1344 "content_layer": "body",
1345 "label": "formula",
1346 "prov": prov,
1347 "orig": latex,
1348 "text": latex,
1349 }));
1350 self_ref
1351 }
1352
1353 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
1357 let self_ref = format!("#/texts/{}", self.texts.len());
1358 let prov = self.take_prov(latex.chars().count());
1359 self.texts.push(json!({
1360 "self_ref": self_ref,
1361 "parent": { "$ref": parent },
1362 "children": [],
1363 "content_layer": "body",
1364 "label": "formula",
1365 "prov": prov,
1366 "orig": orig,
1367 "text": latex,
1368 }));
1369 self_ref
1370 }
1371
1372 fn add_code(
1373 &mut self,
1374 text: &str,
1375 language: Option<&str>,
1376 orig: Option<&str>,
1377 parent: &str,
1378 ) -> String {
1379 let self_ref = format!("#/texts/{}", self.texts.len());
1380 let raw = unescape_text(text);
1381 let prov = self.take_prov(raw.chars().count());
1382 self.texts.push(json!({
1383 "self_ref": self_ref,
1384 "parent": { "$ref": parent },
1385 "children": [],
1386 "content_layer": "body",
1387 "label": "code",
1388 "prov": prov,
1389 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
1392 "text": raw,
1393 "captions": [],
1394 "references": [],
1395 "footnotes": [],
1396 "code_language": code_language(language),
1397 }));
1398 self_ref
1399 }
1400
1401 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
1404 let self_ref = format!("#/groups/{}", self.groups.len());
1405 self.groups.push(Value::Null);
1407 let base = level_of(&items[0]);
1408 let mut children = Vec::new();
1409 let mut i = 0;
1410 while i < items.len() {
1411 if !matches!(items[i], Node::ListItem { .. }) {
1414 i += 1;
1415 continue;
1416 }
1417 let lvl = level_of(&items[i]);
1418 if lvl > base {
1419 i += 1;
1421 continue;
1422 }
1423 let item_ref = self.add_list_item(&items[i], &self_ref);
1424 let mut j = i + 1;
1426 while j < items.len() && level_of(&items[j]) > base {
1427 j += 1;
1428 }
1429 if j > i + 1 {
1430 let mut nested = Vec::new();
1431 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
1432 if let Some(idx) = ref_index(&item_ref) {
1434 self.texts[idx]["children"]
1435 .as_array_mut()
1436 .unwrap()
1437 .extend(nested);
1438 }
1439 }
1440 children.push(json!({ "$ref": item_ref }));
1441 i = j;
1442 }
1443 self.groups[group_index(&self_ref)] = json!({
1444 "self_ref": self_ref,
1445 "parent": { "$ref": parent },
1446 "children": children,
1447 "content_layer": "body",
1448 "name": "list",
1449 "label": "list",
1450 });
1451 self_ref
1452 }
1453
1454 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
1455 let Node::ListItem {
1456 ordered,
1457 number,
1458 text,
1459 location,
1460 ..
1461 } = node
1462 else {
1463 unreachable!()
1464 };
1465 self.adopt_loc(*location);
1466 let self_ref = format!("#/texts/{}", self.texts.len());
1467 let raw = unescape_text(text);
1468 let prov = self.take_prov(raw.chars().count());
1469 let marker = if *ordered {
1470 format!("{number}.")
1471 } else {
1472 "-".to_string()
1473 };
1474 self.texts.push(json!({
1475 "self_ref": self_ref,
1476 "parent": { "$ref": parent },
1477 "children": [],
1478 "content_layer": "body",
1479 "label": "list_item",
1480 "prov": prov,
1481 "orig": raw,
1482 "text": raw,
1483 "enumerated": ordered,
1484 "marker": marker,
1485 }));
1486 self_ref
1487 }
1488
1489 fn add_table(&mut self, t: &Table, parent: &str) -> String {
1490 self.add_table_with(t, parent, false)
1491 }
1492
1493 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
1496 let self_ref = format!("#/tables/{}", self.tables.len());
1497 self.adopt_loc(t.location);
1498 let prov = self.take_prov(0);
1499 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
1503 Some(cap) => self.add_caption(cap, json!({}), &self_ref, parent, t.caption_parent),
1504 None => (Vec::new(), Vec::new()),
1505 };
1506 let data = table_data_with(t, raw);
1507 self.tables.push(json!({
1508 "self_ref": self_ref,
1509 "parent": { "$ref": parent },
1510 "children": children,
1511 "content_layer": "body",
1512 "label": "table",
1513 "prov": prov,
1514 "captions": captions,
1515 "references": [],
1516 "footnotes": [],
1517 "data": data,
1518 "annotations": [],
1519 }));
1520 self_ref
1521 }
1522
1523 fn add_caption(
1529 &mut self,
1530 text: &str,
1531 extra: Value,
1532 self_ref: &str,
1533 parent: &str,
1534 choice: CaptionParent,
1535 ) -> (Vec<Value>, Vec<Value>) {
1536 let cap_parent = match choice {
1541 CaptionParent::Item => self_ref,
1542 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
1543 CaptionParent::Body => "#/body",
1544 };
1545 let cap_ref = json!({ "$ref": self.add_text("caption", text, cap_parent, extra) });
1546 match choice {
1547 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
1548 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
1551 CaptionParent::Body if parent == "#/body" => {
1552 self.pending_siblings.push(cap_ref.clone())
1553 }
1554 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
1555 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
1558 }
1559 (vec![cap_ref], Vec::new())
1560 }
1561
1562 fn add_picture(
1565 &mut self,
1566 caption: Option<&str>,
1567 caption_href: Option<&str>,
1568 image: Option<&crate::PictureImage>,
1569 meta: Option<Value>,
1570 parent: &str,
1571 caption_parent: CaptionParent,
1572 ) -> String {
1573 let self_ref = format!("#/pictures/{}", self.pictures.len());
1574 let prov = self.take_prov(0);
1577 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
1578 Some(cap) => {
1579 let extra = match caption_href {
1583 Some(href) => json!({ "hyperlink": href }),
1584 None => json!({}),
1585 };
1586 self.add_caption(cap, extra, &self_ref, parent, caption_parent)
1587 }
1588 None => (Vec::new(), Vec::new()),
1589 };
1590 self.push_picture(prov, captions, children, image, meta, parent)
1591 }
1592
1593 fn push_picture(
1596 &mut self,
1597 prov: Value,
1598 captions: Vec<Value>,
1599 children: Vec<Value>,
1600 image: Option<&crate::PictureImage>,
1601 meta: Option<Value>,
1602 parent: &str,
1603 ) -> String {
1604 let self_ref = format!("#/pictures/{}", self.pictures.len());
1605 let annotations = meta
1609 .as_ref()
1610 .and_then(|m| m.get("annotations").cloned())
1611 .unwrap_or_else(|| json!([]));
1612 let meta = meta.map(|mut m| {
1613 if let Some(obj) = m.as_object_mut() {
1614 obj.remove("annotations");
1615 }
1616 m
1617 });
1618 let mut item = match meta {
1622 Some(meta) => json!({
1623 "self_ref": self_ref,
1624 "parent": { "$ref": parent },
1625 "children": children,
1626 "content_layer": "body",
1627 "meta": meta,
1628 "label": "picture",
1629 "prov": prov,
1630 "captions": captions,
1631 "references": [],
1632 "footnotes": [],
1633 "annotations": annotations,
1634 }),
1635 None => json!({
1636 "self_ref": self_ref,
1637 "parent": { "$ref": parent },
1638 "children": children,
1639 "content_layer": "body",
1640 "label": "picture",
1641 "prov": prov,
1642 "captions": captions,
1643 "references": [],
1644 "footnotes": [],
1645 "annotations": annotations,
1646 }),
1647 };
1648 if let Some(img) = image {
1653 let image = json!({
1654 "mimetype": img.mimetype,
1655 "dpi": 72,
1656 "size": { "width": img.width as f64, "height": img.height as f64 },
1657 "uri": img.data_uri(),
1658 });
1659 if let Some(obj) = item.as_object_mut() {
1660 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
1661 obj.insert("image".into(), image);
1662 obj.insert("annotations".into(), annotations);
1663 }
1664 }
1665 self.pictures.push(item);
1666 self_ref
1667 }
1668
1669 fn add_group(
1670 &mut self,
1671 label: &str,
1672 name: Option<&str>,
1673 layer: Option<ContentLayer>,
1674 nodes: &[Node],
1675 parent: &str,
1676 ) -> String {
1677 let self_ref = format!("#/groups/{}", self.groups.len());
1678 self.groups.push(Value::Null);
1679 let mark = (
1683 self.texts.len(),
1684 self.tables.len(),
1685 self.pictures.len(),
1686 self.groups.len(),
1687 );
1688 let children = self.walk_into(nodes, &self_ref);
1689 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
1690 let content_layer = layer.map_or("body", |l| l.value());
1691 self.groups[group_index(&self_ref)] = json!({
1692 "self_ref": self_ref,
1693 "parent": { "$ref": parent },
1694 "children": children,
1695 "content_layer": content_layer,
1696 "name": name,
1697 "label": label,
1698 });
1699 if layer.is_some() {
1700 let (t, tb, p, g) = mark;
1701 for item in self.texts[t..]
1702 .iter_mut()
1703 .chain(self.tables[tb..].iter_mut())
1704 .chain(self.pictures[p..].iter_mut())
1705 .chain(self.groups[g..].iter_mut())
1706 {
1707 if let Some(obj) = item.as_object_mut() {
1708 obj.insert("content_layer".into(), json!(content_layer));
1709 }
1710 }
1711 }
1712 self_ref
1713 }
1714
1715 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Value> {
1718 let seqs: Option<Vec<usize>> = nodes
1723 .iter()
1724 .map(|n| match n {
1725 Node::Prov { seq: Some(s), .. } => Some(*s),
1726 _ => None,
1727 })
1728 .collect();
1729 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
1730 let mut order: Vec<usize> = (0..nodes.len()).collect();
1731 order.sort_by_key(|&i| seqs[i]);
1732 let mut slots: Vec<Vec<Value>> = vec![Vec::new(); nodes.len()];
1733 for i in order {
1734 if let Some(r) = self.add_node(&nodes[i], parent) {
1735 slots[i].append(&mut self.pending_siblings);
1736 slots[i].push(json!({ "$ref": r }));
1737 slots[i].append(&mut self.pending_after);
1738 }
1739 if parent == "#/body" {
1740 slots[i].append(&mut self.pending_body);
1741 }
1742 }
1743 return slots.into_iter().flatten().collect();
1744 }
1745 let mut children = Vec::new();
1746 let mut i = 0;
1747 while i < nodes.len() {
1748 if matches!(nodes[i], Node::ListItem { .. }) {
1749 let start = i;
1750 i += 1;
1751 loop {
1752 match nodes.get(i) {
1753 Some(Node::ListItem { .. }) => i += 1,
1754 Some(Node::Paragraph { text })
1757 if text.is_empty()
1758 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
1759 {
1760 i += 1
1761 }
1762 _ => break,
1763 }
1764 }
1765 self.add_sibling_lists(&nodes[start..i], parent, &mut children);
1766 } else {
1767 if let Some(r) = self.add_node(&nodes[i], parent) {
1768 children.append(&mut self.pending_siblings);
1769 children.push(json!({ "$ref": r }));
1770 children.append(&mut self.pending_after);
1771 }
1772 i += 1;
1773 }
1774 if parent == "#/body" {
1777 children.append(&mut self.pending_body);
1778 }
1779 }
1780 children
1781 }
1782
1783 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
1788 let base = level_of(&run[0]);
1789 let mut seg = 0;
1790 for k in 0..run.len() {
1791 let Node::ListItem {
1792 first_in_list,
1793 level,
1794 ..
1795 } = &run[k]
1796 else {
1797 continue;
1798 };
1799 if *level != base {
1800 continue; }
1802 if k > seg && *first_in_list {
1803 out.push(json!({ "$ref": self.add_list(&run[seg..k], parent) }));
1804 seg = k;
1805 }
1806 }
1807 out.push(json!({ "$ref": self.add_list(&run[seg..], parent) }));
1808 }
1809}
1810
1811fn level_of(node: &Node) -> u8 {
1812 match node {
1813 Node::ListItem { level, .. } => *level,
1814 _ => 0,
1815 }
1816}
1817
1818fn group_index(self_ref: &str) -> usize {
1819 self_ref.rsplit('/').next().unwrap().parse().unwrap()
1820}
1821
1822fn ref_index(self_ref: &str) -> Option<usize> {
1823 self_ref.rsplit('/').next()?.parse().ok()
1824}
1825
1826fn merge(target: &mut Value, extra: Value) {
1828 if let (Some(t), Some(e)) = (target.as_object_mut(), extra.as_object()) {
1829 for (k, v) in e {
1830 t.insert(k.clone(), v.clone());
1831 }
1832 }
1833}
1834
1835fn unescape_text(s: &str) -> String {
1837 s.replace("<", "<")
1838 .replace(">", ">")
1839 .replace("&", "&")
1840 .replace("\\_", "_")
1841}
1842
1843fn fnv1a(s: &str) -> u64 {
1846 let mut h: u64 = 0xcbf29ce484222325;
1847 for b in s.bytes() {
1848 h ^= b as u64;
1849 h = h.wrapping_mul(0x100000001b3);
1850 }
1851 h
1852}
1853
1854#[cfg(test)]
1855mod tests {
1856 use crate::{
1857 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
1858 };
1859 use serde_json::Value;
1860
1861 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
1862 crate::TableCell {
1863 text: text.into(),
1864 bbox: None,
1865 start_row: row,
1866 start_col: col,
1867 row_span: 1,
1868 col_span,
1869 column_header: row == 0,
1870 row_header: false,
1871 row_section: false,
1872 }
1873 }
1874
1875 #[test]
1881 fn ragged_rows_take_their_grid_from_first_class_cells() {
1882 let cells = vec![
1883 cell_at("h0", 0, 0, 1),
1884 cell_at("h2", 0, 2, 1),
1885 cell_at("wide", 1, 0, 3),
1886 ];
1887 let export = |rows: Vec<Vec<String>>| -> Value {
1888 let mut doc = DoclingDocument::new("t");
1889 doc.push(Node::Table(Table {
1890 rows,
1891 cells: Some(cells.clone()),
1892 ..Table::default()
1893 }));
1894 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1895 v["tables"][0]["data"].clone()
1896 };
1897 let data = export(vec![
1899 vec!["h0".into(), "h2".into()],
1900 vec!["wide".into(), "wide".into(), "wide".into()],
1901 ]);
1902 assert_eq!(data["num_rows"], 2);
1903 assert_eq!(data["num_cols"], 3);
1904 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
1905 let row0: Vec<&str> = data["grid"][0]
1906 .as_array()
1907 .unwrap()
1908 .iter()
1909 .map(|c| c["text"].as_str().unwrap())
1910 .collect();
1911 assert_eq!(row0, ["h0", "", "h2"]);
1912 assert_eq!(data["table_cells"][2]["col_span"], 3);
1913 let data = export(vec![
1916 vec!["h0".into(), "h2".into()],
1917 vec!["wide".into(), "wide".into()],
1918 ]);
1919 assert_eq!(data["num_cols"], 2);
1920 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
1921 }
1922
1923 fn doc_with_image() -> DoclingDocument {
1924 let mut doc = DoclingDocument::new("t");
1925 doc.push(Node::Picture {
1926 caption: Some("Fig 1".into()),
1927 caption_href: None,
1928 image: Some(PictureImage {
1929 mimetype: "image/png".into(),
1930 width: 4,
1931 height: 2,
1932 data: b"foobar".to_vec(),
1933 }),
1934 classification: None,
1935 caption_parent: Default::default(),
1936 });
1937 doc
1938 }
1939
1940 #[test]
1945 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
1946 let mut doc = DoclingDocument::new("t");
1947 doc.push(Node::Heading {
1948 level: 1,
1949 text: "Slide One".into(),
1950 });
1951 doc.push(Node::Furniture {
1952 layer: ContentLayer::Notes,
1953 inner: Box::new(Node::Located {
1954 location: [0, 0, 0, 0],
1955 inner: Box::new(Node::Paragraph {
1956 text: "Speaker note for slide 1.".into(),
1957 }),
1958 }),
1959 });
1960 doc.push(Node::Furniture {
1961 layer: ContentLayer::Furniture,
1962 inner: Box::new(Node::Paragraph {
1963 text: "page header".into(),
1964 }),
1965 });
1966
1967 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1968 let texts = v["texts"].as_array().unwrap();
1969 assert_eq!(
1970 texts
1971 .iter()
1972 .map(|t| (
1973 t["label"].as_str().unwrap(),
1974 t["content_layer"].as_str().unwrap(),
1975 t["text"].as_str().unwrap()
1976 ))
1977 .collect::<Vec<_>>(),
1978 vec![
1979 ("title", "body", "Slide One"),
1980 ("text", "notes", "Speaker note for slide 1."),
1981 ],
1982 "the note is carried on its own layer; the furniture is not carried"
1983 );
1984 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
1986 }
1987
1988 #[test]
1994 fn continuation_flags_become_spanning_cells() {
1995 let mut doc = DoclingDocument::new("t");
1996 let rows = vec![
1998 vec!["merged".to_string(), "merged".into(), "merged".into()],
1999 vec!["merged".to_string(), "merged".into(), "merged".into()],
2000 vec!["a".to_string(), "b".into(), "c".into()],
2001 ];
2002 doc.push(Node::Table(crate::Table {
2003 rows,
2004 location: None,
2005 structure: Some(crate::TableStructure {
2006 header_row: vec![true, false, false],
2007 col_continuation: vec![
2008 vec![false, true, true],
2009 vec![false, true, true],
2010 vec![false, false, false],
2011 ],
2012 row_continuation: vec![
2013 vec![false, false, false],
2014 vec![true, true, true],
2015 vec![false, false, false],
2016 ],
2017 row_header: Vec::new(),
2018 col_header: Vec::new(),
2019 }),
2020 cell_blocks: None,
2021 cells: None,
2022 caption: None,
2023 caption_parent: Default::default(),
2024 }));
2025 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2026 let data = &v["tables"][0]["data"];
2027 assert_eq!(data["num_rows"], 3);
2028 assert_eq!(data["num_cols"], 3);
2029 let cells = data["table_cells"].as_array().unwrap();
2030 assert_eq!(
2031 cells.len(),
2032 4,
2033 "one cell for the range, three for the plain row"
2034 );
2035 assert_eq!(
2036 cells[0],
2037 serde_json::json!({
2038 "row_span": 2, "col_span": 3,
2039 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2040 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2041 "text": "merged", "column_header": true, "row_header": false,
2042 "row_section": false, "fillable": false,
2043 })
2044 );
2045 assert_eq!(cells[1]["text"], "a");
2046 assert_eq!(cells[1]["row_span"], 1);
2047 assert_eq!(cells[1]["column_header"], false);
2048 let grid = data["grid"].as_array().unwrap();
2050 assert_eq!(grid.len(), 3);
2051 for (r, row) in grid.iter().take(2).enumerate() {
2052 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2053 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2054 }
2055 }
2056 assert_eq!(grid[2][2]["text"], "c");
2057 }
2058
2059 #[test]
2065 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2066 let mut doc = DoclingDocument::new("t");
2067 doc.push(Node::PageInfo {
2068 page_no: 1,
2069 width: 3.0,
2070 height: 4.0,
2071 });
2072 let table = crate::Table {
2073 rows: vec![vec!["a".to_string(), "b".into()]],
2074 ..Default::default()
2075 };
2076 doc.push(Node::Group {
2077 label: "sheet".into(),
2078 name: Some("Data".into()),
2079 layer: None,
2080 children: vec![
2081 Node::Prov {
2086 page_no: 1,
2087 bbox: [0.0, 0.0, 3.0, 4.0],
2088 charspan: [0, 0],
2089 seq: Some(1),
2090 inner: Box::new(Node::Table(table.clone())),
2091 },
2092 Node::Prov {
2093 page_no: 1,
2094 bbox: [0.0, 1.0, 1.0, 1.0],
2095 charspan: [0, 0],
2096 seq: Some(0),
2097 inner: Box::new(Node::Chart {
2098 kind: "bar_chart".into(),
2099 table,
2100 caption: Some("Sales".into()),
2101 location: Some([0, 128, 170, 128]),
2102 }),
2103 },
2104 ],
2105 });
2106 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2107 assert_eq!(
2108 v["pages"],
2109 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2110 );
2111 assert_eq!(
2112 v["tables"][0]["prov"],
2113 serde_json::json!([{
2114 "page_no": 1,
2115 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2116 "charspan": [0, 0],
2117 }])
2118 );
2119 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2120 let sheet = &v["groups"][0];
2123 assert_eq!(
2124 sheet["children"],
2125 serde_json::json!([
2126 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2127 ])
2128 );
2129 let cap = &v["texts"][0];
2130 assert_eq!(cap["label"], "caption");
2131 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2132 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2133 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2134 let pic = &v["pictures"][0];
2135 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2136 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2137 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2138 assert_eq!(
2139 pic["meta"]["classification"]["predictions"][0]["class_name"],
2140 "bar_chart"
2141 );
2142 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2143 }
2144
2145 #[test]
2149 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2150 let mut doc = DoclingDocument::new("t");
2151 doc.push(Node::PageInfo {
2152 page_no: 1,
2153 width: 12192000.0,
2154 height: 6858000.0,
2155 });
2156 doc.push(Node::Furniture {
2157 layer: ContentLayer::Notes,
2158 inner: Box::new(Node::Located {
2159 location: [0, 0, 0, 0],
2160 inner: Box::new(Node::Paragraph {
2161 text: "a note".into(),
2162 }),
2163 }),
2164 });
2165 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2166 let prov = &v["texts"][0]["prov"][0];
2167 assert_eq!(prov["page_no"], 1);
2168 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2169 assert_eq!(
2170 prov["bbox"],
2171 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2172 );
2173 assert_eq!(
2175 v["pages"]["1"]["size"],
2176 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2177 );
2178 }
2179
2180 #[test]
2186 fn page_markers_produce_pages_and_prov() {
2187 let mut doc = DoclingDocument::new("t");
2188 doc.push(Node::PageInfo {
2189 page_no: 1,
2190 width: 512.0,
2191 height: 1024.0,
2192 });
2193 doc.push(Node::Located {
2194 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
2196 text: "hello".into(),
2197 }),
2198 });
2199 doc.push(Node::Table(Table {
2200 rows: vec![vec!["a".into()]],
2201 location: Some([0, 0, 512, 512]),
2202 ..Table::default()
2203 }));
2204 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2205 assert_eq!(v["pages"]["1"]["page_no"], 1);
2206 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
2207 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
2208 let prov = &v["texts"][0]["prov"][0];
2211 assert_eq!(prov["page_no"], 1);
2212 assert_eq!(prov["bbox"]["l"], 128.0);
2213 assert_eq!(prov["bbox"]["t"], 896.0);
2214 assert_eq!(prov["bbox"]["r"], 256.0);
2215 assert_eq!(prov["bbox"]["b"], 768.0);
2216 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
2217 assert_eq!(prov["charspan"][1], 5);
2218 let tprov = &v["tables"][0]["prov"][0];
2220 assert_eq!(tprov["bbox"]["t"], 1024.0);
2221 assert_eq!(tprov["bbox"]["b"], 0.0);
2222 assert_eq!(tprov["charspan"][1], 0);
2223
2224 let mut plain = DoclingDocument::new("t");
2226 plain.push(Node::Located {
2227 location: [1, 2, 3, 4],
2228 inner: Box::new(Node::Paragraph { text: "x".into() }),
2229 });
2230 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2231 assert_eq!(v["pages"], serde_json::json!({}));
2232 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2233 }
2234
2235 #[test]
2236 fn picture_image_in_markdown_modes_and_json() {
2237 let doc = doc_with_image();
2238 assert!(doc.export_to_markdown().contains("<!-- image -->"));
2240 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
2242 assert!(
2243 md.contains(""),
2244 "got:\n{md}"
2245 );
2246 assert!(files.is_empty());
2247 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
2249 assert!(
2250 md.contains(""),
2251 "got:\n{md}"
2252 );
2253 assert_eq!(
2254 files,
2255 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
2256 );
2257 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2260 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
2261 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
2262 let keys: Vec<&str> = v["pictures"][0]
2263 .as_object()
2264 .unwrap()
2265 .keys()
2266 .map(String::as_str)
2267 .collect();
2268 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
2269 assert_eq!(
2270 v["pictures"][0]["image"]["uri"],
2271 "data:image/png;base64,Zm9vYmFy"
2272 );
2273 }
2274
2275 #[test]
2276 fn exports_docling_schema() {
2277 let mut doc = DoclingDocument::new("t");
2278 doc.push(Node::Heading {
2279 level: 1,
2280 text: "Title".into(),
2281 });
2282 doc.push(Node::Heading {
2283 level: 2,
2284 text: "Sec".into(),
2285 });
2286 doc.push(Node::Paragraph {
2287 text: "Body & more".into(),
2288 }); doc.push(Node::ListItem {
2290 ordered: false,
2291 number: 0,
2292 first_in_list: true,
2293 text: "one".into(),
2294 level: 0,
2295 marker: None,
2296 location: None,
2297 dclx: None,
2298 href: None,
2299 layer: None,
2300 });
2301 doc.push(Node::ListItem {
2302 ordered: false,
2303 number: 0,
2304 first_in_list: false,
2305 text: "two".into(),
2306 level: 0,
2307 marker: None,
2308 location: None,
2309 dclx: None,
2310 href: None,
2311 layer: None,
2312 });
2313 doc.push(Node::Table(Table {
2314 rows: vec![vec!["A".into(), "B".into()]],
2315 location: None,
2316 structure: None,
2317 cell_blocks: None,
2318 cells: None,
2319 caption: None,
2320 caption_parent: Default::default(),
2321 }));
2322
2323 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2324 assert_eq!(v["schema_name"], "DoclingDocument");
2325 assert_eq!(v["version"], "1.10.0");
2326 assert_eq!(v["texts"][0]["label"], "title");
2327 assert_eq!(v["texts"][1]["label"], "section_header");
2328 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
2332 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
2333 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
2334 assert_eq!(v["texts"][3]["marker"], "-");
2335 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
2337 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
2338 }
2339 #[test]
2343 fn a_layered_group_stamps_its_whole_subtree() {
2344 let doc = DoclingDocument {
2345 name: "s".into(),
2346 nodes: vec![
2347 Node::Group {
2348 label: "sheet".into(),
2349 name: Some("Sheet1".into()),
2350 layer: None,
2351 children: vec![Node::Paragraph {
2352 text: "visible".into(),
2353 }],
2354 },
2355 Node::Group {
2356 label: "sheet".into(),
2357 name: Some("Sheet2".into()),
2358 layer: Some(ContentLayer::Invisible),
2359 children: vec![Node::Paragraph {
2360 text: "hidden".into(),
2361 }],
2362 },
2363 ],
2364 ..DoclingDocument::new("s")
2365 };
2366 let v = crate::json::to_json(&doc);
2367 assert_eq!(v["groups"][0]["label"], "sheet");
2368 assert_eq!(v["groups"][0]["name"], "Sheet1");
2369 assert_eq!(v["groups"][0]["content_layer"], "body");
2370 assert_eq!(v["texts"][0]["content_layer"], "body");
2371 assert_eq!(v["groups"][1]["name"], "Sheet2");
2372 assert_eq!(v["groups"][1]["content_layer"], "invisible");
2373 assert_eq!(v["texts"][1]["content_layer"], "invisible");
2374 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
2376 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
2377 }
2378
2379 #[test]
2389 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
2390 use crate::tree::{ItemTree, TreeKind};
2391 let mut t = ItemTree::default();
2392 let text = |txt: &str| TreeKind::Text {
2393 label: "text".into(),
2394 text: txt.into(),
2395 orig: None,
2396 formatting: None,
2397 hyperlink: None,
2398 level: None,
2399 list: None,
2400 };
2401 let a = t.add(None, None, text("a"));
2402 let blank = t.add(None, None, text(""));
2403 let b = t.add(None, None, text("b"));
2404 t.delete(blank);
2405 let group = t.add(
2406 None,
2407 Some(ContentLayer::Notes),
2408 TreeKind::Group {
2409 label: "comment_section".into(),
2410 name: "comment-0".into(),
2411 },
2412 );
2413 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
2414 t.items[a].comments.push(group);
2415 t.add(
2416 None,
2417 None,
2418 TreeKind::Picture {
2419 captions: Vec::new(),
2420 image: None,
2421 classification: Some("bar_chart".into()),
2422 confidence: None,
2423 chart: Some(Table {
2424 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
2425 ..Table::default()
2426 }),
2427 dpi: None,
2428 },
2429 );
2430 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
2431 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
2432 let mut doc = DoclingDocument::new("t");
2433 doc.tree = Some(t);
2434 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2435 let texts = v["texts"].as_array().unwrap();
2436 assert_eq!(texts.len(), 3);
2437 assert_eq!(texts[1]["text"], "b");
2438 assert_eq!(texts[1]["self_ref"], "#/texts/1");
2439 assert_eq!(
2440 v["body"]["children"],
2441 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
2442 );
2443 let keys: Vec<&str> = texts[0]
2444 .as_object()
2445 .unwrap()
2446 .keys()
2447 .map(String::as_str)
2448 .collect();
2449 assert_eq!(
2450 keys,
2451 vec![
2452 "self_ref",
2453 "parent",
2454 "children",
2455 "content_layer",
2456 "label",
2457 "prov",
2458 "comments",
2459 "orig",
2460 "text"
2461 ]
2462 );
2463 assert_eq!(
2464 texts[0]["comments"],
2465 serde_json::json!([{"$ref": "#/groups/0"}])
2466 );
2467 assert!(texts[1].get("comments").is_none());
2468 let meta = &v["pictures"][0]["meta"];
2469 assert_eq!(
2470 meta["classification"]["predictions"][0]["class_name"],
2471 "bar_chart"
2472 );
2473 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
2474 }
2475
2476 #[test]
2482 fn tree_items_carry_exact_provenance_and_dpi() {
2483 use crate::tree::{ItemTree, TreeKind, TreeProv};
2484 let text = |label: &str, t: &str| TreeKind::Text {
2485 label: label.into(),
2486 text: t.into(),
2487 orig: None,
2488 formatting: None,
2489 hyperlink: None,
2490 level: None,
2491 list: None,
2492 };
2493 let mut t = ItemTree::default();
2494 let slide = t.add(
2495 None,
2496 None,
2497 TreeKind::Group {
2498 label: "chapter".into(),
2499 name: "slide-0".into(),
2500 },
2501 );
2502 t.add_with_prov(
2503 Some(slide),
2504 None,
2505 text("paragraph", "héllo"),
2506 TreeProv {
2507 page_no: 1,
2508 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
2509 bottom_left: true,
2510 charspan: [0, 5],
2511 },
2512 );
2513 t.add_with_prov(
2514 Some(slide),
2515 None,
2516 TreeKind::Picture {
2517 captions: Vec::new(),
2518 image: Some(crate::PictureImage {
2519 mimetype: "image/png".into(),
2520 width: 2,
2521 height: 2,
2522 data: vec![0],
2523 }),
2524 classification: None,
2525 confidence: None,
2526 chart: None,
2527 dpi: Some(300),
2528 },
2529 TreeProv {
2530 page_no: 1,
2531 bbox: [0.0; 4],
2532 bottom_left: false,
2533 charspan: [0, 0],
2534 },
2535 );
2536 t.add(
2537 Some(slide),
2538 Some(ContentLayer::Notes),
2539 text("text", "no geometry"),
2540 );
2541 let mut doc = DoclingDocument::new("t");
2542 doc.push(Node::PageInfo {
2543 page_no: 1,
2544 width: 9144000.0,
2545 height: 6858000.0,
2546 });
2547 doc.tree = Some(t);
2548 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2549 assert_eq!(
2550 v["texts"][0]["prov"],
2551 serde_json::json!([{
2552 "page_no": 1,
2553 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
2554 "charspan": [0, 5],
2555 }])
2556 );
2557 assert_eq!(v["texts"][0]["label"], "paragraph");
2558 assert_eq!(
2559 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
2560 "TOPLEFT"
2561 );
2562 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
2563 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
2564 assert_eq!(v["texts"][1]["content_layer"], "notes");
2565 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
2566 assert_eq!(v["pages"]["1"]["page_no"], 1);
2567 }
2568
2569 #[test]
2574 fn provenance_boxes_are_clamped_to_their_page() {
2575 let mut doc = DoclingDocument::new("t");
2576 doc.push(Node::PageInfo {
2577 page_no: 1,
2578 width: 10.0,
2579 height: 8.0,
2580 });
2581 doc.push(Node::Prov {
2582 page_no: 1,
2583 bbox: [-1.0, 2.0, 12.0, 9.5],
2584 charspan: [0, 1],
2585 seq: None,
2586 inner: Box::new(Node::Paragraph { text: "x".into() }),
2587 });
2588 let mut table = Table {
2589 rows: vec![vec!["a".into()]],
2590 ..Table::default()
2591 };
2592 table.cells = Some(vec![crate::TableCell {
2593 text: "a".into(),
2594 bbox: Some([1.0, 1.0, 11.0, 9.0]),
2595 start_row: 0,
2596 start_col: 0,
2597 row_span: 1,
2598 col_span: 1,
2599 column_header: false,
2600 row_header: false,
2601 row_section: false,
2602 }]);
2603 doc.push(Node::Prov {
2604 page_no: 1,
2605 bbox: [0.0, 0.0, 10.0, 8.0],
2606 charspan: [0, 0],
2607 seq: None,
2608 inner: Box::new(Node::Table(table)),
2609 });
2610 doc.push(Node::Prov {
2611 page_no: 7,
2612 bbox: [-5.0, 0.0, 50.0, 50.0],
2613 charspan: [0, 1],
2614 seq: None,
2615 inner: Box::new(Node::Paragraph { text: "y".into() }),
2616 });
2617 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2618 assert_eq!(
2619 v["texts"][0]["prov"][0]["bbox"],
2620 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
2621 );
2622 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
2623 assert_eq!(
2624 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
2625 (Some(1.0), Some(10.0), Some(8.0))
2626 );
2627 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
2628 assert_eq!(
2629 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
2630 "page 7 is not described"
2631 );
2632 }
2633
2634 #[test]
2635 fn a_backend_item_tree_is_written_verbatim() {
2636 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
2637 let mut t = ItemTree::default();
2638 let text = |label: &str, txt: &str| TreeKind::Text {
2639 label: label.into(),
2640 text: txt.into(),
2641 orig: None,
2642 formatting: None,
2643 hyperlink: None,
2644 level: None,
2645 list: None,
2646 };
2647 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
2648 let h = t.add(None, None, text("title", "Heading"));
2649 let group = t.add(
2650 Some(h),
2651 None,
2652 TreeKind::Group {
2653 label: "inline".into(),
2654 name: "group".into(),
2655 },
2656 );
2657 t.add(
2658 Some(group),
2659 None,
2660 TreeKind::Text {
2661 label: "text".into(),
2662 text: "bold".into(),
2663 orig: None,
2664 formatting: Some(Formatting {
2665 bold: true,
2666 ..Formatting::default()
2667 }),
2668 hyperlink: Some("https://example.com/".into()),
2669 level: None,
2670 list: None,
2671 },
2672 );
2673 t.add(
2674 Some(group),
2675 None,
2676 TreeKind::Code {
2677 text: "x = 1".into(),
2678 orig: None,
2679 language: Some("python".into()),
2680 formatting: None,
2681 hyperlink: None,
2682 },
2683 );
2684 let sub = t.add(
2685 Some(h),
2686 None,
2687 TreeKind::Text {
2688 label: "section_header".into(),
2689 text: "Sub".into(),
2690 orig: Some("Sub\u{2019}".into()),
2691 formatting: None,
2692 hyperlink: None,
2693 level: Some(1),
2694 list: None,
2695 },
2696 );
2697 t.add(
2698 Some(sub),
2699 None,
2700 TreeKind::Text {
2701 label: "list_item".into(),
2702 text: "item".into(),
2703 orig: None,
2704 formatting: None,
2705 hyperlink: None,
2706 level: None,
2707 list: Some(ListMeta {
2708 enumerated: true,
2709 marker: "3.".into(),
2710 }),
2711 },
2712 );
2713 let _region = t.add(
2714 Some(sub),
2715 None,
2716 TreeKind::FieldRegion {
2717 items: vec![crate::FieldItem {
2718 marker: None,
2719 key: Some("Name".into()),
2720 value: Some("Duck".into()),
2721 value_kind: Some("read_only".into()),
2722 }],
2723 },
2724 );
2725 let table = t.add(
2726 Some(sub),
2727 None,
2728 TreeKind::Table {
2729 table: Table {
2730 rows: vec![vec!["a \n<".into(), "b".into()]],
2731 cells: Some(vec![
2732 crate::TableCell {
2733 text: "a \n<".into(),
2734 bbox: None,
2735 start_row: 0,
2736 start_col: 0,
2737 row_span: 3,
2738 col_span: 1,
2739 column_header: false,
2740 row_header: true,
2741 row_section: false,
2742 },
2743 crate::TableCell {
2744 text: "b".into(),
2745 bbox: None,
2746 start_row: 0,
2747 start_col: 1,
2748 row_span: 1,
2749 col_span: 1,
2750 column_header: false,
2751 row_header: false,
2752 row_section: false,
2753 },
2754 ]),
2755 ..Table::default()
2756 },
2757 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
2759 },
2760 );
2761 let cell_group = t.add(
2762 Some(table),
2763 None,
2764 TreeKind::Group {
2765 label: "unspecified".into(),
2766 name: "rich_cell_group_1_0_0".into(),
2767 },
2768 );
2769 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
2770 *rich_cells = vec![(0, 1, cell_group)];
2771 }
2772 let after = t.add(Some(sub), None, text("text", "after the region"));
2773 let _ = (title, after);
2774
2775 let doc = DoclingDocument {
2776 tree: Some(t),
2777 ..DoclingDocument::new("t")
2778 };
2779 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2780 let texts: Vec<&str> = v["texts"]
2782 .as_array()
2783 .unwrap()
2784 .iter()
2785 .map(|t| t["text"].as_str().unwrap())
2786 .collect();
2787 assert_eq!(
2788 texts,
2789 [
2790 "Page",
2791 "Heading",
2792 "bold",
2793 "x = 1",
2794 "Sub",
2795 "item",
2796 "Name",
2797 "Duck",
2798 "after the region"
2799 ]
2800 );
2801 assert_eq!(
2802 v["body"]["children"],
2803 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
2804 );
2805 assert_eq!(v["texts"][0]["content_layer"], "furniture");
2806 assert_eq!(
2807 v["texts"][1]["children"],
2808 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
2809 );
2810 let bold = &v["texts"][2];
2811 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
2812 let keys: Vec<&str> = bold
2813 .as_object()
2814 .unwrap()
2815 .keys()
2816 .map(String::as_str)
2817 .collect();
2818 assert_eq!(
2819 keys,
2820 [
2821 "self_ref",
2822 "parent",
2823 "children",
2824 "content_layer",
2825 "label",
2826 "prov",
2827 "orig",
2828 "text",
2829 "formatting",
2830 "hyperlink"
2831 ]
2832 );
2833 assert_eq!(
2834 bold["formatting"],
2835 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
2836 );
2837 let code = &v["texts"][3];
2838 assert_eq!(code["label"], "code");
2839 assert_eq!(code["code_language"], "Python");
2840 let sub = &v["texts"][4];
2841 assert_eq!(sub["orig"], "Sub\u{2019}");
2842 assert_eq!(sub["level"], 1);
2843 let item = &v["texts"][5];
2844 let keys: Vec<&str> = item
2845 .as_object()
2846 .unwrap()
2847 .keys()
2848 .map(String::as_str)
2849 .collect();
2850 assert_eq!(
2851 keys,
2852 [
2853 "self_ref",
2854 "parent",
2855 "children",
2856 "content_layer",
2857 "label",
2858 "prov",
2859 "orig",
2860 "text",
2861 "enumerated",
2862 "marker"
2863 ]
2864 );
2865 assert_eq!(item["marker"], "3.");
2866 assert_eq!(v["texts"][7]["kind"], "read_only");
2867 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
2868 let table = &v["tables"][0];
2869 assert_eq!(
2870 table["children"],
2871 serde_json::json!([{"$ref": "#/groups/1"}])
2872 );
2873 let cells = table["data"]["table_cells"].as_array().unwrap();
2874 assert_eq!(
2875 cells[0]["text"], "a \n<",
2876 "raw cell text is written verbatim"
2877 );
2878 assert_eq!(
2879 cells[0]["end_row_offset_idx"], 3,
2880 "declared spans are not clamped"
2881 );
2882 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
2883 assert!(cells[0].get("ref").is_none());
2884 assert!(
2885 table["data"]["grid"][0][1].get("ref").is_none(),
2886 "the grid shows plain cells"
2887 );
2888 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
2889 }
2890
2891 #[test]
2895 fn a_comment_section_can_be_referenced_by_its_note_text() {
2896 let doc = DoclingDocument {
2897 name: "c".into(),
2898 nodes: vec![
2899 Node::Commented {
2900 comments: vec![0],
2901 inner: Box::new(Node::Paragraph {
2902 text: "annotated".into(),
2903 }),
2904 },
2905 Node::CommentSection {
2906 name: "comment-Sheet1-A1".into(),
2907 text: "[author: A]: note".into(),
2908 refs_note_text: true,
2909 grouped: true,
2910 },
2911 ],
2912 ..DoclingDocument::new("c")
2913 };
2914 let v = crate::json::to_json(&doc);
2915 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
2916 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
2917 }
2918
2919 #[test]
2923 fn comment_sections_link_back_to_their_items() {
2924 let doc = DoclingDocument {
2925 name: "c".into(),
2926 nodes: vec![
2927 Node::Commented {
2928 comments: vec![0],
2929 inner: Box::new(Node::Paragraph {
2930 text: "annotated".into(),
2931 }),
2932 },
2933 Node::Paragraph {
2934 text: "plain".into(),
2935 },
2936 Node::CommentSection {
2937 name: "comment-7".into(),
2938 text: "[time: t]: note".into(),
2939 refs_note_text: false,
2940 grouped: true,
2941 },
2942 ],
2943 ..DoclingDocument::new("c")
2944 };
2945 let v = crate::json::to_json(&doc);
2946 assert_eq!(v["groups"][0]["label"], "comment_section");
2948 assert_eq!(v["groups"][0]["name"], "comment-7");
2949 assert_eq!(v["groups"][0]["content_layer"], "notes");
2950 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
2951 assert_eq!(v["texts"][2]["content_layer"], "notes");
2952 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
2954 assert!(v["texts"][1].get("comments").is_none());
2955 let keys: Vec<&str> = v["texts"][0]
2957 .as_object()
2958 .unwrap()
2959 .keys()
2960 .map(String::as_str)
2961 .collect();
2962 assert_eq!(
2963 &keys[keys.len() - 4..],
2964 &["prov", "comments", "orig", "text"]
2965 );
2966 }
2967
2968 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
2969 Node::Picture {
2970 caption: Some(caption.into()),
2971 caption_href: None,
2972 image: None,
2973 classification: None,
2974 caption_parent,
2975 }
2976 }
2977
2978 fn group(children: Vec<Node>) -> Node {
2979 Node::Group {
2980 label: "section".into(),
2981 name: None,
2982 layer: None,
2983 children,
2984 }
2985 }
2986
2987 fn refs(v: &Value) -> Vec<&str> {
2988 v.as_array()
2989 .unwrap()
2990 .iter()
2991 .map(|r| r["$ref"].as_str().unwrap())
2992 .collect()
2993 }
2994
2995 #[test]
3000 fn a_body_caption_follows_the_enclosing_top_level_item() {
3001 let mut doc = DoclingDocument::new("t");
3002 doc.push(picture("top", CaptionParent::Body));
3003 doc.push(group(vec![
3004 Node::Paragraph { text: "p".into() },
3005 picture("nested", CaptionParent::Body),
3006 ]));
3007 doc.push(Node::Paragraph {
3008 text: "after".into(),
3009 });
3010 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3011 assert_eq!(
3012 refs(&v["body"]["children"]),
3013 [
3014 "#/texts/0",
3015 "#/pictures/0",
3016 "#/groups/0",
3017 "#/texts/2",
3018 "#/texts/3"
3019 ]
3020 );
3021 assert_eq!(
3022 refs(&v["groups"][0]["children"]),
3023 ["#/texts/1", "#/pictures/1"]
3024 );
3025 for (cap, pic) in [(0, 0), (2, 1)] {
3026 assert_eq!(v["texts"][cap]["label"], "caption");
3027 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
3028 assert_eq!(
3029 refs(&v["pictures"][pic]["captions"]),
3030 [format!("#/texts/{cap}")]
3031 );
3032 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
3033 }
3034 }
3035
3036 #[test]
3039 fn an_item_caption_is_the_items_first_child() {
3040 let mut doc = DoclingDocument::new("t");
3041 doc.push(picture("fig", CaptionParent::Item));
3042 doc.push(Node::Table(Table {
3043 rows: vec![vec!["a".into()]],
3044 caption: Some("tab".into()),
3045 caption_parent: CaptionParent::Item,
3046 ..Table::default()
3047 }));
3048 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3049 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
3050 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
3051 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
3052 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3053 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
3054 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
3055 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
3056 }
3057
3058 #[test]
3062 fn a_container_caption_is_the_items_sibling() {
3063 let mut doc = DoclingDocument::new("t");
3064 doc.push(group(vec![
3065 picture("chart", CaptionParent::Container),
3066 Node::Table(Table {
3067 rows: vec![vec!["a".into()]],
3068 caption: Some("figcaption".into()),
3069 caption_parent: CaptionParent::ContainerAfter,
3070 ..Table::default()
3071 }),
3072 ]));
3073 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3074 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
3075 assert_eq!(
3076 refs(&v["groups"][0]["children"]),
3077 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
3078 );
3079 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
3080 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
3081 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
3082 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
3083 }
3084}