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 last_comment_section: Option<(String, String)>,
602 pending_comments: Vec<(String, Vec<usize>)>,
606}
607
608impl Builder {
609 fn take_prov(&mut self, char_len: usize) -> Value {
615 let prov = self.prov_json(char_len, false);
616 self.pending_exact = None;
617 self.pending_loc = None;
618 prov
619 }
620
621 fn prov_json(&self, char_len: usize, span_over_text: bool) -> Value {
626 let r2 = |v: f64| (v * 100.0).round() / 100.0;
627 if let Some(ExactProv {
628 page_no,
629 bbox: [l, t, r, b],
630 bottom_left,
631 charspan,
632 }) = self.pending_exact
633 {
634 let charspan = if span_over_text {
635 [0, char_len]
636 } else {
637 charspan
638 };
639 return json!([{
640 "page_no": page_no,
641 "bbox": {
642 "l": r2(l), "t": r2(t), "r": r2(r), "b": r2(b),
643 "coord_origin": if bottom_left { "BOTTOMLEFT" } else { "TOPLEFT" },
644 },
645 "charspan": charspan,
646 }]);
647 }
648 let Some([x0, y0, x1, y1]) = self.pending_loc else {
649 return json!([]);
650 };
651 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
655 return json!([{
656 "page_no": self.cur_page,
657 "bbox": { "l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT" },
658 "charspan": [0, char_len],
659 }]);
660 }
661 json!([{
662 "page_no": self.cur_page,
663 "bbox": {
664 "l": r2(x0 as f64 * self.cur_w / 512.0),
665 "t": r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
666 "r": r2(x1 as f64 * self.cur_w / 512.0),
667 "b": r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
668 "coord_origin": "BOTTOMLEFT",
669 },
670 "charspan": [0, char_len],
671 }])
672 }
673
674 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
677 if self.pending_loc.is_none() && self.cur_page > 0 {
678 self.pending_loc = loc;
679 }
680 }
681
682 fn link_comments(&mut self) {
690 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
691 .into_iter()
692 .map(|(item, comments)| {
693 let refs = comments
694 .iter()
695 .filter_map(|i| self.comment_groups.get(*i))
696 .map(|r| json!({ "$ref": r }))
697 .collect();
698 (item, refs)
699 })
700 .collect();
701 for (item, comment_refs) in refs {
702 if comment_refs.is_empty() {
703 continue;
704 }
705 let Some(target) = self.item_mut(&item) else {
706 continue;
707 };
708 let Some(obj) = target.as_object_mut() else {
709 continue;
710 };
711 let tail: Vec<(String, Value)> = obj
712 .iter()
713 .skip_while(|(k, _)| k.as_str() != "prov")
714 .skip(1)
715 .map(|(k, v)| (k.clone(), v.clone()))
716 .collect();
717 for (k, _) in &tail {
718 obj.shift_remove(k);
719 }
720 obj.insert("comments".into(), Value::Array(comment_refs));
721 for (k, v) in tail {
722 obj.insert(k, v);
723 }
724 }
725 }
726
727 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
729 let idx = ref_index(self_ref)?;
730 let bucket = if self_ref.starts_with("#/texts/") {
731 &mut self.texts
732 } else if self_ref.starts_with("#/tables/") {
733 &mut self.tables
734 } else if self_ref.starts_with("#/pictures/") {
735 &mut self.pictures
736 } else if self_ref.starts_with("#/groups/") {
737 &mut self.groups
738 } else {
739 return None;
740 };
741 bucket.get_mut(idx)
742 }
743
744 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
748 use crate::tree::TreeKind;
749 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
752 let (mut nt, mut ng, mut ntb, mut np, mut nf) = (0, 0, 0, 0, 0);
753 for item in &tree.items {
754 if item.deleted {
755 refs.push(String::new());
756 continue;
757 }
758 let r = match &item.kind {
759 TreeKind::Text { .. } | TreeKind::Code { .. } => {
760 nt += 1;
761 format!("#/texts/{}", nt - 1)
762 }
763 TreeKind::Group { .. } => {
764 ng += 1;
765 format!("#/groups/{}", ng - 1)
766 }
767 TreeKind::Table { .. } => {
768 ntb += 1;
769 format!("#/tables/{}", ntb - 1)
770 }
771 TreeKind::Picture { .. } => {
772 np += 1;
773 format!("#/pictures/{}", np - 1)
774 }
775 TreeKind::FieldRegion { items } => {
776 nt += items
779 .iter()
780 .map(|i| {
781 [&i.marker, &i.key, &i.value]
782 .iter()
783 .filter(|p| p.is_some())
784 .count()
785 })
786 .sum::<usize>();
787 nf += 1;
788 format!("#/field_regions/{}", nf - 1)
789 }
790 };
791 refs.push(r);
792 }
793 let ref_of = |id: usize| json!({ "$ref": refs[id] });
794 for (id, item) in tree.items.iter().enumerate() {
795 if item.deleted {
796 continue;
797 }
798 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
799 let children: Vec<Value> = item.children.iter().map(|&c| ref_of(c)).collect();
800 let layer = item.layer.map_or("body", |l| l.value());
801 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
804 let self_ref = match &item.kind {
805 TreeKind::Text {
806 label,
807 text,
808 orig,
809 formatting,
810 hyperlink,
811 level,
812 list,
813 } => {
814 let mut tail = serde_json::Map::new();
817 if let Some(f) = formatting {
818 tail.insert("formatting".into(), formatting_json(f));
819 }
820 if let Some(h) = hyperlink {
821 tail.insert("hyperlink".into(), json!(h));
822 }
823 if let Some(l) = level {
824 tail.insert("level".into(), json!(l));
825 }
826 if let Some(l) = list {
827 tail.insert("enumerated".into(), json!(l.enumerated));
828 tail.insert("marker".into(), json!(l.marker));
829 }
830 let r = format!("#/texts/{}", self.texts.len());
831 let prov = self.take_prov(text.chars().count());
832 let mut item_json = json!({
833 "self_ref": r,
834 "parent": { "$ref": parent },
835 "children": children,
836 "content_layer": layer,
837 "label": label,
838 "prov": prov,
839 });
840 if !item.comments.is_empty() {
843 item_json["comments"] =
844 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
845 }
846 if let Some(track) = &item.source {
849 item_json["source"] = json!([track_json(track)]);
850 }
851 merge(
852 &mut item_json,
853 json!({
854 "orig": orig.as_deref().unwrap_or(text),
855 "text": text,
856 }),
857 );
858 merge(&mut item_json, Value::Object(tail));
859 self.texts.push(item_json);
860 r
861 }
862 TreeKind::Code {
863 text,
864 orig,
865 language,
866 formatting,
867 hyperlink,
868 } => {
869 let r = format!("#/texts/{}", self.texts.len());
870 let prov = self.take_prov(text.chars().count());
871 let mut item_json = json!({
872 "self_ref": r,
873 "parent": { "$ref": parent },
874 "children": children,
875 "content_layer": layer,
876 "label": "code",
877 "prov": prov,
878 });
879 if !item.comments.is_empty() {
880 item_json["comments"] =
881 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
882 }
883 merge(
884 &mut item_json,
885 json!({
886 "orig": orig.as_deref().unwrap_or(text),
887 "text": text,
888 }),
889 );
890 if let Some(f) = formatting {
891 item_json["formatting"] = formatting_json(f);
892 }
893 if let Some(h) = hyperlink {
894 item_json["hyperlink"] = json!(h);
895 }
896 merge(
897 &mut item_json,
898 json!({
899 "captions": [],
900 "references": [],
901 "footnotes": [],
902 "code_language": code_language(language.as_deref()),
903 }),
904 );
905 self.texts.push(item_json);
906 r
907 }
908 TreeKind::Group { label, name } => {
909 self.pending_exact = None;
910 let r = format!("#/groups/{}", self.groups.len());
911 self.groups.push(json!({
912 "self_ref": r,
913 "parent": { "$ref": parent },
914 "children": children,
915 "content_layer": layer,
916 "name": name,
917 "label": label,
918 }));
919 r
920 }
921 TreeKind::Table {
922 table,
923 rich_cells,
924 captions,
925 } => {
926 let r = self.add_table_with(table, parent, true);
928 let idx = ref_index(&r).expect("table ref");
929 let t = &mut self.tables[idx];
930 t["children"] = Value::Array(children);
931 t["content_layer"] = json!(layer);
932 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
933 for &(row, col, group) in rich_cells {
937 let cell_ref = ref_of(group);
938 let hit = |c: &Value| {
939 c["start_row_offset_idx"] == json!(row)
940 && c["start_col_offset_idx"] == json!(col)
941 };
942 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
943 for c in cells.iter_mut().filter(|c| hit(c)) {
944 c["ref"] = cell_ref.clone();
945 }
946 }
947 }
948 r
949 }
950 TreeKind::Picture {
951 captions,
952 image,
953 classification,
954 confidence,
955 chart,
956 dpi,
957 } => {
958 let mut meta = classification.as_ref().map(|c| {
963 let mut pred = serde_json::Map::new();
964 if let Some(conf) = confidence {
965 pred.insert("confidence".into(), json!(conf));
966 }
967 pred.insert("class_name".into(), json!(c));
968 json!({ "classification": { "predictions": [pred] } })
969 });
970 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
971 if !t.rows.is_empty() {
972 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
973 }
974 }
975 let prov = self.take_prov(0);
976 let r = self.push_picture(
977 prov,
978 captions.iter().map(|&c| ref_of(c)).collect(),
979 children,
980 image.as_ref(),
981 meta,
982 parent,
983 );
984 if let Some(idx) = ref_index(&r) {
985 self.pictures[idx]["content_layer"] = json!(layer);
986 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
989 img["dpi"] = json!(dpi);
990 }
991 }
992 r
993 }
994 TreeKind::FieldRegion { items } => {
995 self.pending_exact = None;
996 let r = self.add_field_region(items, parent);
997 if let Some(region) = self.field_regions.last_mut() {
998 region["content_layer"] = json!(layer);
999 }
1000 r
1001 }
1002 };
1003 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1004 }
1005 tree.body.iter().map(|&c| ref_of(c)).collect()
1006 }
1007
1008 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1009 match node {
1010 Node::Heading { level: 1, text } => {
1011 Some(self.add_text("title", text, parent, json!({})))
1012 }
1013 Node::Heading { level, text } => Some(self.add_text(
1014 "section_header",
1015 text,
1016 parent,
1017 json!({ "level": level.saturating_sub(1) }),
1018 )),
1019 Node::Caption { text, href } => {
1020 let extra = match href {
1021 Some(url) => json!({ "hyperlink": url }),
1022 None => json!({}),
1023 };
1024 Some(self.add_text("caption", text, parent, extra))
1025 }
1026 Node::Paragraph { text } => {
1027 let t = text.trim();
1030 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1031 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1032 _ => Some(self.add_text("text", text, parent, json!({}))),
1033 }
1034 }
1035 Node::CheckboxItem { checked, text } => {
1036 let mark = if *checked { "- [x] " } else { "- [ ] " };
1039 Some(self.add_text("text", &format!("{mark}{text}"), parent, json!({})))
1040 }
1041 Node::Code {
1042 language,
1043 text,
1044 orig,
1045 ..
1046 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1047 Node::Formula {
1050 latex,
1051 orig,
1052 location,
1053 } => {
1054 self.adopt_loc(*location);
1055 Some(self.add_formula_item(latex, orig, parent))
1056 }
1057 Node::CommentSection {
1063 name,
1064 text,
1065 refs_note_text,
1066 grouped,
1067 } => {
1068 if !*grouped {
1069 let child =
1073 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1074 self.comment_groups.push(child.clone());
1075 return Some(child);
1076 }
1077 if let Some((_, self_ref)) = self
1078 .last_comment_section
1079 .as_ref()
1080 .filter(|(n, _)| n == name)
1081 .cloned()
1082 {
1083 let child =
1086 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1087 if let Some(children) =
1088 self.groups[group_index(&self_ref)]["children"].as_array_mut()
1089 {
1090 children.push(json!({ "$ref": child }));
1091 }
1092 self.comment_groups.push(if *refs_note_text {
1093 child
1094 } else {
1095 self_ref.clone()
1096 });
1097 return Some(self_ref);
1098 }
1099 let self_ref = format!("#/groups/{}", self.groups.len());
1100 self.groups.push(Value::Null);
1101 let child =
1102 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1103 self.groups[group_index(&self_ref)] = json!({
1104 "self_ref": self_ref,
1105 "parent": { "$ref": parent },
1106 "children": [{ "$ref": child }],
1107 "content_layer": "notes",
1108 "name": name,
1109 "label": "comment_section",
1110 });
1111 self.last_comment_section = Some((name.clone(), self_ref.clone()));
1112 self.comment_groups.push(if *refs_note_text {
1113 child
1114 } else {
1115 self_ref.clone()
1116 });
1117 Some(self_ref)
1118 }
1119 Node::Commented { comments, inner } => {
1122 let item = self.add_node(inner, parent)?;
1123 if !comments.is_empty() {
1124 self.pending_comments.push((item.clone(), comments.clone()));
1125 }
1126 Some(item)
1127 }
1128 Node::Table(t) => Some(self.add_table(t, parent)),
1129 Node::Picture {
1130 caption,
1131 caption_href,
1132 image,
1133 classification,
1134 caption_parent,
1135 } => Some(self.add_picture(
1136 caption.as_deref(),
1137 caption_href.as_deref(),
1138 image.as_ref(),
1139 classification.as_deref().map(classification_meta),
1140 parent,
1141 *caption_parent,
1142 )),
1143 Node::Chart {
1148 kind,
1149 table,
1150 caption,
1151 location,
1152 } => {
1153 self.adopt_loc(*location);
1154 let mut meta = json!({
1155 "classification": { "predictions": [{ "class_name": kind }] },
1156 });
1157 if !table.rows.is_empty() {
1158 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1159 }
1160 let mut captions = Vec::new();
1166 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1167 let prov = self.prov_json(unescape_text(cap).chars().count(), true);
1168 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1169 self.pending_siblings.push(json!({ "$ref": cap_ref }));
1170 captions.push(json!({ "$ref": cap_ref }));
1171 }
1172 let prov = self.take_prov(0);
1173 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1174 }
1175 Node::DoclangOnly(_) => None,
1177 Node::Group {
1178 label,
1179 name,
1180 layer,
1181 children,
1182 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1183 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1184 Node::InlineGroup { md_text, .. } => {
1187 Some(self.add_text("text", md_text, parent, json!({})))
1188 }
1189 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1191 Node::Furniture {
1197 layer: ContentLayer::Notes,
1198 inner,
1199 } => {
1200 let item = self.add_node(inner, parent)?;
1201 self.set_layer(&item, "notes");
1202 Some(item)
1203 }
1204 Node::Furniture { .. } => None,
1205 Node::PageFurniture { .. } => None,
1206 Node::Located { location, inner } => {
1211 if self.cur_page > 0 {
1212 self.pending_loc = Some(*location);
1213 }
1214 let r = self.add_node(inner, parent);
1215 self.pending_loc = None;
1216 r
1217 }
1218 Node::Prov {
1219 page_no,
1220 bbox,
1221 charspan,
1222 inner,
1223 ..
1224 } => {
1225 self.pending_exact = Some(ExactProv {
1226 page_no: *page_no,
1227 bbox: bbox.map(f64::from),
1228 bottom_left: false,
1229 charspan: *charspan,
1230 });
1231 let r = self.add_node(inner, parent);
1232 self.pending_exact = None;
1233 r
1234 }
1235 Node::PageBreak => None,
1237 Node::PageInfo {
1240 page_no,
1241 width,
1242 height,
1243 } => {
1244 self.cur_page = *page_no;
1245 self.cur_w = *width as f64;
1246 self.cur_h = *height as f64;
1247 if *page_no > 0 {
1248 self.pages.push((*page_no, self.cur_w, self.cur_h));
1249 }
1250 None
1251 }
1252 Node::ListItem { .. } => None,
1254 }
1255 }
1256
1257 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1261 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1262 self.field_regions.push(Value::Null);
1263 let region_index = self.field_regions.len() - 1;
1264 let mut item_refs = Vec::new();
1265 for item in items {
1266 item_refs.push(json!({ "$ref": self.add_field_item(item, &self_ref) }));
1267 }
1268 self.field_regions[region_index] = json!({
1269 "self_ref": self_ref,
1270 "parent": { "$ref": parent },
1271 "children": item_refs,
1272 "content_layer": "body",
1273 "label": "field_region",
1274 "prov": [],
1275 });
1276 self_ref
1277 }
1278
1279 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1280 let self_ref = format!("#/field_items/{}", self.field_items.len());
1281 self.field_items.push(Value::Null);
1282 let item_index = self.field_items.len() - 1;
1283 let mut child_refs = Vec::new();
1284 for (label, text) in [
1285 ("marker", &item.marker),
1286 ("field_key", &item.key),
1287 ("field_value", &item.value),
1288 ] {
1289 if let Some(text) = text {
1290 let extra = match (label, &item.value_kind) {
1293 ("field_value", Some(kind)) => json!({ "kind": kind }),
1294 _ => json!({}),
1295 };
1296 child_refs.push(json!({ "$ref": self.add_text(label, text, &self_ref, extra) }));
1297 }
1298 }
1299 self.field_items[item_index] = json!({
1300 "self_ref": self_ref,
1301 "parent": { "$ref": parent },
1302 "children": child_refs,
1303 "content_layer": "body",
1304 "label": "field_item",
1305 "prov": [],
1306 });
1307 self_ref
1308 }
1309
1310 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1314 let bucket = match self_ref.split('/').nth(1) {
1315 Some("texts") => &mut self.texts,
1316 Some("tables") => &mut self.tables,
1317 Some("pictures") => &mut self.pictures,
1318 Some("groups") => &mut self.groups,
1319 _ => return,
1320 };
1321 if let Some(item) = self_ref
1322 .rsplit('/')
1323 .next()
1324 .and_then(|i| i.parse::<usize>().ok())
1325 .and_then(|i| bucket.get_mut(i))
1326 {
1327 item["content_layer"] = json!(layer);
1328 }
1329 }
1330
1331 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1332 let prov = self.take_prov(unescape_text(text).chars().count());
1333 self.add_text_with(label, text, parent, extra, prov)
1334 }
1335
1336 fn add_text_with(
1339 &mut self,
1340 label: &str,
1341 text: &str,
1342 parent: &str,
1343 extra: Value,
1344 prov: Value,
1345 ) -> String {
1346 let self_ref = format!("#/texts/{}", self.texts.len());
1347 let raw = unescape_text(text);
1348 let mut item = json!({
1349 "self_ref": self_ref,
1350 "parent": { "$ref": parent },
1351 "children": [],
1352 "content_layer": "body",
1353 "label": label,
1354 "prov": prov,
1355 "orig": raw,
1356 "text": raw,
1357 });
1358 merge(&mut item, extra);
1359 self.texts.push(item);
1360 self_ref
1361 }
1362
1363 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
1366 let self_ref = format!("#/texts/{}", self.texts.len());
1367 let prov = self.take_prov(latex.chars().count());
1368 self.texts.push(json!({
1369 "self_ref": self_ref,
1370 "parent": { "$ref": parent },
1371 "children": [],
1372 "content_layer": "body",
1373 "label": "formula",
1374 "prov": prov,
1375 "orig": latex,
1376 "text": latex,
1377 }));
1378 self_ref
1379 }
1380
1381 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
1385 let self_ref = format!("#/texts/{}", self.texts.len());
1386 let prov = self.take_prov(latex.chars().count());
1387 self.texts.push(json!({
1388 "self_ref": self_ref,
1389 "parent": { "$ref": parent },
1390 "children": [],
1391 "content_layer": "body",
1392 "label": "formula",
1393 "prov": prov,
1394 "orig": orig,
1395 "text": latex,
1396 }));
1397 self_ref
1398 }
1399
1400 fn add_code(
1401 &mut self,
1402 text: &str,
1403 language: Option<&str>,
1404 orig: Option<&str>,
1405 parent: &str,
1406 ) -> String {
1407 let self_ref = format!("#/texts/{}", self.texts.len());
1408 let raw = unescape_text(text);
1409 let prov = self.take_prov(raw.chars().count());
1410 self.texts.push(json!({
1411 "self_ref": self_ref,
1412 "parent": { "$ref": parent },
1413 "children": [],
1414 "content_layer": "body",
1415 "label": "code",
1416 "prov": prov,
1417 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
1420 "text": raw,
1421 "captions": [],
1422 "references": [],
1423 "footnotes": [],
1424 "code_language": code_language(language),
1425 }));
1426 self_ref
1427 }
1428
1429 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
1432 let self_ref = format!("#/groups/{}", self.groups.len());
1433 self.groups.push(Value::Null);
1435 let base = level_of(&items[0]);
1436 let mut children = Vec::new();
1437 let mut i = 0;
1438 while i < items.len() {
1439 if !matches!(items[i], Node::ListItem { .. }) {
1442 i += 1;
1443 continue;
1444 }
1445 let lvl = level_of(&items[i]);
1446 if lvl > base {
1447 i += 1;
1449 continue;
1450 }
1451 let item_ref = self.add_list_item(&items[i], &self_ref);
1452 let mut j = i + 1;
1454 while j < items.len() && level_of(&items[j]) > base {
1455 j += 1;
1456 }
1457 if j > i + 1 {
1458 let mut nested = Vec::new();
1459 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
1460 if let Some(idx) = ref_index(&item_ref) {
1462 self.texts[idx]["children"]
1463 .as_array_mut()
1464 .unwrap()
1465 .extend(nested);
1466 }
1467 }
1468 children.push(json!({ "$ref": item_ref }));
1469 i = j;
1470 }
1471 self.groups[group_index(&self_ref)] = json!({
1472 "self_ref": self_ref,
1473 "parent": { "$ref": parent },
1474 "children": children,
1475 "content_layer": "body",
1476 "name": "list",
1477 "label": "list",
1478 });
1479 self_ref
1480 }
1481
1482 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
1483 let Node::ListItem {
1484 ordered,
1485 number,
1486 text,
1487 location,
1488 ..
1489 } = node
1490 else {
1491 unreachable!()
1492 };
1493 self.adopt_loc(*location);
1494 let self_ref = format!("#/texts/{}", self.texts.len());
1495 let raw = unescape_text(text);
1496 let prov = self.take_prov(raw.chars().count());
1497 let marker = if *ordered {
1498 format!("{number}.")
1499 } else {
1500 "-".to_string()
1501 };
1502 self.texts.push(json!({
1503 "self_ref": self_ref,
1504 "parent": { "$ref": parent },
1505 "children": [],
1506 "content_layer": "body",
1507 "label": "list_item",
1508 "prov": prov,
1509 "orig": raw,
1510 "text": raw,
1511 "enumerated": ordered,
1512 "marker": marker,
1513 }));
1514 self_ref
1515 }
1516
1517 fn add_table(&mut self, t: &Table, parent: &str) -> String {
1518 self.add_table_with(t, parent, false)
1519 }
1520
1521 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
1524 let self_ref = format!("#/tables/{}", self.tables.len());
1525 self.adopt_loc(t.location);
1526 let prov = self.take_prov(0);
1527 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
1531 Some(cap) => self.add_caption(cap, json!({}), &self_ref, parent, t.caption_parent),
1532 None => (Vec::new(), Vec::new()),
1533 };
1534 let data = table_data_with(t, raw);
1535 self.tables.push(json!({
1536 "self_ref": self_ref,
1537 "parent": { "$ref": parent },
1538 "children": children,
1539 "content_layer": "body",
1540 "label": "table",
1541 "prov": prov,
1542 "captions": captions,
1543 "references": [],
1544 "footnotes": [],
1545 "data": data,
1546 "annotations": [],
1547 }));
1548 self_ref
1549 }
1550
1551 fn add_caption(
1557 &mut self,
1558 text: &str,
1559 extra: Value,
1560 self_ref: &str,
1561 parent: &str,
1562 choice: CaptionParent,
1563 ) -> (Vec<Value>, Vec<Value>) {
1564 let cap_parent = match choice {
1569 CaptionParent::Item => self_ref,
1570 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
1571 CaptionParent::Body => "#/body",
1572 };
1573 let cap_ref = json!({ "$ref": self.add_text("caption", text, cap_parent, extra) });
1574 match choice {
1575 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
1576 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
1579 CaptionParent::Body if parent == "#/body" => {
1580 self.pending_siblings.push(cap_ref.clone())
1581 }
1582 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
1583 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
1586 }
1587 (vec![cap_ref], Vec::new())
1588 }
1589
1590 fn add_picture(
1593 &mut self,
1594 caption: Option<&str>,
1595 caption_href: Option<&str>,
1596 image: Option<&crate::PictureImage>,
1597 meta: Option<Value>,
1598 parent: &str,
1599 caption_parent: CaptionParent,
1600 ) -> String {
1601 let self_ref = format!("#/pictures/{}", self.pictures.len());
1602 let prov = self.take_prov(0);
1605 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
1606 Some(cap) => {
1607 let extra = match caption_href {
1611 Some(href) => json!({ "hyperlink": href }),
1612 None => json!({}),
1613 };
1614 self.add_caption(cap, extra, &self_ref, parent, caption_parent)
1615 }
1616 None => (Vec::new(), Vec::new()),
1617 };
1618 self.push_picture(prov, captions, children, image, meta, parent)
1619 }
1620
1621 fn push_picture(
1624 &mut self,
1625 prov: Value,
1626 captions: Vec<Value>,
1627 children: Vec<Value>,
1628 image: Option<&crate::PictureImage>,
1629 meta: Option<Value>,
1630 parent: &str,
1631 ) -> String {
1632 let self_ref = format!("#/pictures/{}", self.pictures.len());
1633 let annotations = meta
1637 .as_ref()
1638 .and_then(|m| m.get("annotations").cloned())
1639 .unwrap_or_else(|| json!([]));
1640 let meta = meta.map(|mut m| {
1641 if let Some(obj) = m.as_object_mut() {
1642 obj.remove("annotations");
1643 }
1644 m
1645 });
1646 let mut item = match meta {
1650 Some(meta) => json!({
1651 "self_ref": self_ref,
1652 "parent": { "$ref": parent },
1653 "children": children,
1654 "content_layer": "body",
1655 "meta": meta,
1656 "label": "picture",
1657 "prov": prov,
1658 "captions": captions,
1659 "references": [],
1660 "footnotes": [],
1661 "annotations": annotations,
1662 }),
1663 None => json!({
1664 "self_ref": self_ref,
1665 "parent": { "$ref": parent },
1666 "children": children,
1667 "content_layer": "body",
1668 "label": "picture",
1669 "prov": prov,
1670 "captions": captions,
1671 "references": [],
1672 "footnotes": [],
1673 "annotations": annotations,
1674 }),
1675 };
1676 if let Some(img) = image {
1681 let image = json!({
1682 "mimetype": img.mimetype,
1683 "dpi": 72,
1684 "size": { "width": img.width as f64, "height": img.height as f64 },
1685 "uri": img.data_uri(),
1686 });
1687 if let Some(obj) = item.as_object_mut() {
1688 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
1689 obj.insert("image".into(), image);
1690 obj.insert("annotations".into(), annotations);
1691 }
1692 }
1693 self.pictures.push(item);
1694 self_ref
1695 }
1696
1697 fn add_group(
1698 &mut self,
1699 label: &str,
1700 name: Option<&str>,
1701 layer: Option<ContentLayer>,
1702 nodes: &[Node],
1703 parent: &str,
1704 ) -> String {
1705 let self_ref = format!("#/groups/{}", self.groups.len());
1706 self.groups.push(Value::Null);
1707 let mark = (
1711 self.texts.len(),
1712 self.tables.len(),
1713 self.pictures.len(),
1714 self.groups.len(),
1715 );
1716 let children = self.walk_into(nodes, &self_ref);
1717 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
1718 let content_layer = layer.map_or("body", |l| l.value());
1719 self.groups[group_index(&self_ref)] = json!({
1720 "self_ref": self_ref,
1721 "parent": { "$ref": parent },
1722 "children": children,
1723 "content_layer": content_layer,
1724 "name": name,
1725 "label": label,
1726 });
1727 if layer.is_some() {
1728 let (t, tb, p, g) = mark;
1729 for item in self.texts[t..]
1730 .iter_mut()
1731 .chain(self.tables[tb..].iter_mut())
1732 .chain(self.pictures[p..].iter_mut())
1733 .chain(self.groups[g..].iter_mut())
1734 {
1735 if let Some(obj) = item.as_object_mut() {
1736 obj.insert("content_layer".into(), json!(content_layer));
1737 }
1738 }
1739 }
1740 self_ref
1741 }
1742
1743 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Value> {
1746 let seqs: Option<Vec<usize>> = nodes
1751 .iter()
1752 .map(|n| match n {
1753 Node::Prov { seq: Some(s), .. } => Some(*s),
1754 _ => None,
1755 })
1756 .collect();
1757 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
1758 let mut order: Vec<usize> = (0..nodes.len()).collect();
1759 order.sort_by_key(|&i| seqs[i]);
1760 let mut slots: Vec<Vec<Value>> = vec![Vec::new(); nodes.len()];
1761 for i in order {
1762 if let Some(r) = self.add_node(&nodes[i], parent) {
1763 slots[i].append(&mut self.pending_siblings);
1764 slots[i].push(json!({ "$ref": r }));
1765 slots[i].append(&mut self.pending_after);
1766 }
1767 if parent == "#/body" {
1768 slots[i].append(&mut self.pending_body);
1769 }
1770 }
1771 return slots.into_iter().flatten().collect();
1772 }
1773 let mut children = Vec::new();
1774 let mut i = 0;
1775 while i < nodes.len() {
1776 if matches!(nodes[i], Node::ListItem { .. }) {
1777 let start = i;
1778 i += 1;
1779 loop {
1780 match nodes.get(i) {
1781 Some(Node::ListItem { .. }) => i += 1,
1782 Some(Node::Paragraph { text })
1785 if text.is_empty()
1786 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
1787 {
1788 i += 1
1789 }
1790 _ => break,
1791 }
1792 }
1793 self.add_sibling_lists(&nodes[start..i], parent, &mut children);
1794 } else {
1795 if let Some(r) = self.add_node(&nodes[i], parent) {
1796 children.append(&mut self.pending_siblings);
1797 children.push(json!({ "$ref": r }));
1798 children.append(&mut self.pending_after);
1799 }
1800 i += 1;
1801 }
1802 if parent == "#/body" {
1805 children.append(&mut self.pending_body);
1806 }
1807 }
1808 children
1809 }
1810
1811 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
1816 let base = level_of(&run[0]);
1817 let mut seg = 0;
1818 for k in 0..run.len() {
1819 let Node::ListItem {
1820 first_in_list,
1821 level,
1822 ..
1823 } = &run[k]
1824 else {
1825 continue;
1826 };
1827 if *level != base {
1828 continue; }
1830 if k > seg && *first_in_list {
1831 out.push(json!({ "$ref": self.add_list(&run[seg..k], parent) }));
1832 seg = k;
1833 }
1834 }
1835 out.push(json!({ "$ref": self.add_list(&run[seg..], parent) }));
1836 }
1837}
1838
1839fn level_of(node: &Node) -> u8 {
1840 match node {
1841 Node::ListItem { level, .. } => *level,
1842 _ => 0,
1843 }
1844}
1845
1846fn group_index(self_ref: &str) -> usize {
1847 self_ref.rsplit('/').next().unwrap().parse().unwrap()
1848}
1849
1850fn ref_index(self_ref: &str) -> Option<usize> {
1851 self_ref.rsplit('/').next()?.parse().ok()
1852}
1853
1854fn merge(target: &mut Value, extra: Value) {
1856 if let (Some(t), Some(e)) = (target.as_object_mut(), extra.as_object()) {
1857 for (k, v) in e {
1858 t.insert(k.clone(), v.clone());
1859 }
1860 }
1861}
1862
1863fn unescape_text(s: &str) -> String {
1865 s.replace("<", "<")
1866 .replace(">", ">")
1867 .replace("&", "&")
1868 .replace("\\_", "_")
1869}
1870
1871fn fnv1a(s: &str) -> u64 {
1874 let mut h: u64 = 0xcbf29ce484222325;
1875 for b in s.bytes() {
1876 h ^= b as u64;
1877 h = h.wrapping_mul(0x100000001b3);
1878 }
1879 h
1880}
1881
1882#[cfg(test)]
1883mod tests {
1884 use crate::{
1885 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
1886 };
1887 use serde_json::Value;
1888
1889 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
1890 crate::TableCell {
1891 text: text.into(),
1892 bbox: None,
1893 start_row: row,
1894 start_col: col,
1895 row_span: 1,
1896 col_span,
1897 column_header: row == 0,
1898 row_header: false,
1899 row_section: false,
1900 }
1901 }
1902
1903 #[test]
1909 fn ragged_rows_take_their_grid_from_first_class_cells() {
1910 let cells = vec![
1911 cell_at("h0", 0, 0, 1),
1912 cell_at("h2", 0, 2, 1),
1913 cell_at("wide", 1, 0, 3),
1914 ];
1915 let export = |rows: Vec<Vec<String>>| -> Value {
1916 let mut doc = DoclingDocument::new("t");
1917 doc.push(Node::Table(Table {
1918 rows,
1919 cells: Some(cells.clone()),
1920 ..Table::default()
1921 }));
1922 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1923 v["tables"][0]["data"].clone()
1924 };
1925 let data = export(vec![
1927 vec!["h0".into(), "h2".into()],
1928 vec!["wide".into(), "wide".into(), "wide".into()],
1929 ]);
1930 assert_eq!(data["num_rows"], 2);
1931 assert_eq!(data["num_cols"], 3);
1932 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
1933 let row0: Vec<&str> = data["grid"][0]
1934 .as_array()
1935 .unwrap()
1936 .iter()
1937 .map(|c| c["text"].as_str().unwrap())
1938 .collect();
1939 assert_eq!(row0, ["h0", "", "h2"]);
1940 assert_eq!(data["table_cells"][2]["col_span"], 3);
1941 let data = export(vec![
1944 vec!["h0".into(), "h2".into()],
1945 vec!["wide".into(), "wide".into()],
1946 ]);
1947 assert_eq!(data["num_cols"], 2);
1948 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
1949 }
1950
1951 fn doc_with_image() -> DoclingDocument {
1952 let mut doc = DoclingDocument::new("t");
1953 doc.push(Node::Picture {
1954 caption: Some("Fig 1".into()),
1955 caption_href: None,
1956 image: Some(PictureImage {
1957 mimetype: "image/png".into(),
1958 width: 4,
1959 height: 2,
1960 data: b"foobar".to_vec(),
1961 }),
1962 classification: None,
1963 caption_parent: Default::default(),
1964 });
1965 doc
1966 }
1967
1968 #[test]
1973 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
1974 let mut doc = DoclingDocument::new("t");
1975 doc.push(Node::Heading {
1976 level: 1,
1977 text: "Slide One".into(),
1978 });
1979 doc.push(Node::Furniture {
1980 layer: ContentLayer::Notes,
1981 inner: Box::new(Node::Located {
1982 location: [0, 0, 0, 0],
1983 inner: Box::new(Node::Paragraph {
1984 text: "Speaker note for slide 1.".into(),
1985 }),
1986 }),
1987 });
1988 doc.push(Node::Furniture {
1989 layer: ContentLayer::Furniture,
1990 inner: Box::new(Node::Paragraph {
1991 text: "page header".into(),
1992 }),
1993 });
1994
1995 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1996 let texts = v["texts"].as_array().unwrap();
1997 assert_eq!(
1998 texts
1999 .iter()
2000 .map(|t| (
2001 t["label"].as_str().unwrap(),
2002 t["content_layer"].as_str().unwrap(),
2003 t["text"].as_str().unwrap()
2004 ))
2005 .collect::<Vec<_>>(),
2006 vec![
2007 ("title", "body", "Slide One"),
2008 ("text", "notes", "Speaker note for slide 1."),
2009 ],
2010 "the note is carried on its own layer; the furniture is not carried"
2011 );
2012 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
2014 }
2015
2016 #[test]
2022 fn continuation_flags_become_spanning_cells() {
2023 let mut doc = DoclingDocument::new("t");
2024 let rows = vec![
2026 vec!["merged".to_string(), "merged".into(), "merged".into()],
2027 vec!["merged".to_string(), "merged".into(), "merged".into()],
2028 vec!["a".to_string(), "b".into(), "c".into()],
2029 ];
2030 doc.push(Node::Table(crate::Table {
2031 rows,
2032 location: None,
2033 structure: Some(crate::TableStructure {
2034 header_row: vec![true, false, false],
2035 col_continuation: vec![
2036 vec![false, true, true],
2037 vec![false, true, true],
2038 vec![false, false, false],
2039 ],
2040 row_continuation: vec![
2041 vec![false, false, false],
2042 vec![true, true, true],
2043 vec![false, false, false],
2044 ],
2045 row_header: Vec::new(),
2046 col_header: Vec::new(),
2047 }),
2048 cell_blocks: None,
2049 cells: None,
2050 caption: None,
2051 caption_parent: Default::default(),
2052 }));
2053 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2054 let data = &v["tables"][0]["data"];
2055 assert_eq!(data["num_rows"], 3);
2056 assert_eq!(data["num_cols"], 3);
2057 let cells = data["table_cells"].as_array().unwrap();
2058 assert_eq!(
2059 cells.len(),
2060 4,
2061 "one cell for the range, three for the plain row"
2062 );
2063 assert_eq!(
2064 cells[0],
2065 serde_json::json!({
2066 "row_span": 2, "col_span": 3,
2067 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2068 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2069 "text": "merged", "column_header": true, "row_header": false,
2070 "row_section": false, "fillable": false,
2071 })
2072 );
2073 assert_eq!(cells[1]["text"], "a");
2074 assert_eq!(cells[1]["row_span"], 1);
2075 assert_eq!(cells[1]["column_header"], false);
2076 let grid = data["grid"].as_array().unwrap();
2078 assert_eq!(grid.len(), 3);
2079 for (r, row) in grid.iter().take(2).enumerate() {
2080 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2081 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2082 }
2083 }
2084 assert_eq!(grid[2][2]["text"], "c");
2085 }
2086
2087 #[test]
2093 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2094 let mut doc = DoclingDocument::new("t");
2095 doc.push(Node::PageInfo {
2096 page_no: 1,
2097 width: 3.0,
2098 height: 4.0,
2099 });
2100 let table = crate::Table {
2101 rows: vec![vec!["a".to_string(), "b".into()]],
2102 ..Default::default()
2103 };
2104 doc.push(Node::Group {
2105 label: "sheet".into(),
2106 name: Some("Data".into()),
2107 layer: None,
2108 children: vec![
2109 Node::Prov {
2114 page_no: 1,
2115 bbox: [0.0, 0.0, 3.0, 4.0],
2116 charspan: [0, 0],
2117 seq: Some(1),
2118 inner: Box::new(Node::Table(table.clone())),
2119 },
2120 Node::Prov {
2121 page_no: 1,
2122 bbox: [0.0, 1.0, 1.0, 1.0],
2123 charspan: [0, 0],
2124 seq: Some(0),
2125 inner: Box::new(Node::Chart {
2126 kind: "bar_chart".into(),
2127 table,
2128 caption: Some("Sales".into()),
2129 location: Some([0, 128, 170, 128]),
2130 }),
2131 },
2132 ],
2133 });
2134 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2135 assert_eq!(
2136 v["pages"],
2137 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2138 );
2139 assert_eq!(
2140 v["tables"][0]["prov"],
2141 serde_json::json!([{
2142 "page_no": 1,
2143 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2144 "charspan": [0, 0],
2145 }])
2146 );
2147 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2148 let sheet = &v["groups"][0];
2151 assert_eq!(
2152 sheet["children"],
2153 serde_json::json!([
2154 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2155 ])
2156 );
2157 let cap = &v["texts"][0];
2158 assert_eq!(cap["label"], "caption");
2159 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2160 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2161 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2162 let pic = &v["pictures"][0];
2163 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2164 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2165 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2166 assert_eq!(
2167 pic["meta"]["classification"]["predictions"][0]["class_name"],
2168 "bar_chart"
2169 );
2170 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2171 }
2172
2173 #[test]
2177 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2178 let mut doc = DoclingDocument::new("t");
2179 doc.push(Node::PageInfo {
2180 page_no: 1,
2181 width: 12192000.0,
2182 height: 6858000.0,
2183 });
2184 doc.push(Node::Furniture {
2185 layer: ContentLayer::Notes,
2186 inner: Box::new(Node::Located {
2187 location: [0, 0, 0, 0],
2188 inner: Box::new(Node::Paragraph {
2189 text: "a note".into(),
2190 }),
2191 }),
2192 });
2193 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2194 let prov = &v["texts"][0]["prov"][0];
2195 assert_eq!(prov["page_no"], 1);
2196 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2197 assert_eq!(
2198 prov["bbox"],
2199 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2200 );
2201 assert_eq!(
2203 v["pages"]["1"]["size"],
2204 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2205 );
2206 }
2207
2208 #[test]
2214 fn page_markers_produce_pages_and_prov() {
2215 let mut doc = DoclingDocument::new("t");
2216 doc.push(Node::PageInfo {
2217 page_no: 1,
2218 width: 512.0,
2219 height: 1024.0,
2220 });
2221 doc.push(Node::Located {
2222 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
2224 text: "hello".into(),
2225 }),
2226 });
2227 doc.push(Node::Table(Table {
2228 rows: vec![vec!["a".into()]],
2229 location: Some([0, 0, 512, 512]),
2230 ..Table::default()
2231 }));
2232 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2233 assert_eq!(v["pages"]["1"]["page_no"], 1);
2234 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
2235 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
2236 let prov = &v["texts"][0]["prov"][0];
2239 assert_eq!(prov["page_no"], 1);
2240 assert_eq!(prov["bbox"]["l"], 128.0);
2241 assert_eq!(prov["bbox"]["t"], 896.0);
2242 assert_eq!(prov["bbox"]["r"], 256.0);
2243 assert_eq!(prov["bbox"]["b"], 768.0);
2244 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
2245 assert_eq!(prov["charspan"][1], 5);
2246 let tprov = &v["tables"][0]["prov"][0];
2248 assert_eq!(tprov["bbox"]["t"], 1024.0);
2249 assert_eq!(tprov["bbox"]["b"], 0.0);
2250 assert_eq!(tprov["charspan"][1], 0);
2251
2252 let mut plain = DoclingDocument::new("t");
2254 plain.push(Node::Located {
2255 location: [1, 2, 3, 4],
2256 inner: Box::new(Node::Paragraph { text: "x".into() }),
2257 });
2258 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2259 assert_eq!(v["pages"], serde_json::json!({}));
2260 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2261 }
2262
2263 #[test]
2264 fn picture_image_in_markdown_modes_and_json() {
2265 let doc = doc_with_image();
2266 assert!(doc.export_to_markdown().contains("<!-- image -->"));
2268 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
2270 assert!(
2271 md.contains(""),
2272 "got:\n{md}"
2273 );
2274 assert!(files.is_empty());
2275 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
2277 assert!(
2278 md.contains(""),
2279 "got:\n{md}"
2280 );
2281 assert_eq!(
2282 files,
2283 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
2284 );
2285 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2288 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
2289 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
2290 let keys: Vec<&str> = v["pictures"][0]
2291 .as_object()
2292 .unwrap()
2293 .keys()
2294 .map(String::as_str)
2295 .collect();
2296 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
2297 assert_eq!(
2298 v["pictures"][0]["image"]["uri"],
2299 "data:image/png;base64,Zm9vYmFy"
2300 );
2301 }
2302
2303 #[test]
2304 fn exports_docling_schema() {
2305 let mut doc = DoclingDocument::new("t");
2306 doc.push(Node::Heading {
2307 level: 1,
2308 text: "Title".into(),
2309 });
2310 doc.push(Node::Heading {
2311 level: 2,
2312 text: "Sec".into(),
2313 });
2314 doc.push(Node::Paragraph {
2315 text: "Body & more".into(),
2316 }); doc.push(Node::ListItem {
2318 ordered: false,
2319 number: 0,
2320 first_in_list: true,
2321 text: "one".into(),
2322 level: 0,
2323 marker: None,
2324 location: None,
2325 dclx: None,
2326 href: None,
2327 layer: None,
2328 });
2329 doc.push(Node::ListItem {
2330 ordered: false,
2331 number: 0,
2332 first_in_list: false,
2333 text: "two".into(),
2334 level: 0,
2335 marker: None,
2336 location: None,
2337 dclx: None,
2338 href: None,
2339 layer: None,
2340 });
2341 doc.push(Node::Table(Table {
2342 rows: vec![vec!["A".into(), "B".into()]],
2343 location: None,
2344 structure: None,
2345 cell_blocks: None,
2346 cells: None,
2347 caption: None,
2348 caption_parent: Default::default(),
2349 }));
2350
2351 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2352 assert_eq!(v["schema_name"], "DoclingDocument");
2353 assert_eq!(v["version"], "1.10.0");
2354 assert_eq!(v["texts"][0]["label"], "title");
2355 assert_eq!(v["texts"][1]["label"], "section_header");
2356 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
2360 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
2361 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
2362 assert_eq!(v["texts"][3]["marker"], "-");
2363 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
2365 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
2366 }
2367 #[test]
2371 fn a_layered_group_stamps_its_whole_subtree() {
2372 let doc = DoclingDocument {
2373 name: "s".into(),
2374 nodes: vec![
2375 Node::Group {
2376 label: "sheet".into(),
2377 name: Some("Sheet1".into()),
2378 layer: None,
2379 children: vec![Node::Paragraph {
2380 text: "visible".into(),
2381 }],
2382 },
2383 Node::Group {
2384 label: "sheet".into(),
2385 name: Some("Sheet2".into()),
2386 layer: Some(ContentLayer::Invisible),
2387 children: vec![Node::Paragraph {
2388 text: "hidden".into(),
2389 }],
2390 },
2391 ],
2392 ..DoclingDocument::new("s")
2393 };
2394 let v = crate::json::to_json(&doc);
2395 assert_eq!(v["groups"][0]["label"], "sheet");
2396 assert_eq!(v["groups"][0]["name"], "Sheet1");
2397 assert_eq!(v["groups"][0]["content_layer"], "body");
2398 assert_eq!(v["texts"][0]["content_layer"], "body");
2399 assert_eq!(v["groups"][1]["name"], "Sheet2");
2400 assert_eq!(v["groups"][1]["content_layer"], "invisible");
2401 assert_eq!(v["texts"][1]["content_layer"], "invisible");
2402 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
2404 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
2405 }
2406
2407 #[test]
2417 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
2418 use crate::tree::{ItemTree, TreeKind};
2419 let mut t = ItemTree::default();
2420 let text = |txt: &str| TreeKind::Text {
2421 label: "text".into(),
2422 text: txt.into(),
2423 orig: None,
2424 formatting: None,
2425 hyperlink: None,
2426 level: None,
2427 list: None,
2428 };
2429 let a = t.add(None, None, text("a"));
2430 let blank = t.add(None, None, text(""));
2431 let b = t.add(None, None, text("b"));
2432 t.delete(blank);
2433 let group = t.add(
2434 None,
2435 Some(ContentLayer::Notes),
2436 TreeKind::Group {
2437 label: "comment_section".into(),
2438 name: "comment-0".into(),
2439 },
2440 );
2441 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
2442 t.items[a].comments.push(group);
2443 t.add(
2444 None,
2445 None,
2446 TreeKind::Picture {
2447 captions: Vec::new(),
2448 image: None,
2449 classification: Some("bar_chart".into()),
2450 confidence: None,
2451 chart: Some(Table {
2452 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
2453 ..Table::default()
2454 }),
2455 dpi: None,
2456 },
2457 );
2458 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
2459 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
2460 let mut doc = DoclingDocument::new("t");
2461 doc.tree = Some(t);
2462 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2463 let texts = v["texts"].as_array().unwrap();
2464 assert_eq!(texts.len(), 3);
2465 assert_eq!(texts[1]["text"], "b");
2466 assert_eq!(texts[1]["self_ref"], "#/texts/1");
2467 assert_eq!(
2468 v["body"]["children"],
2469 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
2470 );
2471 let keys: Vec<&str> = texts[0]
2472 .as_object()
2473 .unwrap()
2474 .keys()
2475 .map(String::as_str)
2476 .collect();
2477 assert_eq!(
2478 keys,
2479 vec![
2480 "self_ref",
2481 "parent",
2482 "children",
2483 "content_layer",
2484 "label",
2485 "prov",
2486 "comments",
2487 "orig",
2488 "text"
2489 ]
2490 );
2491 assert_eq!(
2492 texts[0]["comments"],
2493 serde_json::json!([{"$ref": "#/groups/0"}])
2494 );
2495 assert!(texts[1].get("comments").is_none());
2496 let meta = &v["pictures"][0]["meta"];
2497 assert_eq!(
2498 meta["classification"]["predictions"][0]["class_name"],
2499 "bar_chart"
2500 );
2501 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
2502 }
2503
2504 #[test]
2510 fn tree_items_carry_exact_provenance_and_dpi() {
2511 use crate::tree::{ItemTree, TreeKind, TreeProv};
2512 let text = |label: &str, t: &str| TreeKind::Text {
2513 label: label.into(),
2514 text: t.into(),
2515 orig: None,
2516 formatting: None,
2517 hyperlink: None,
2518 level: None,
2519 list: None,
2520 };
2521 let mut t = ItemTree::default();
2522 let slide = t.add(
2523 None,
2524 None,
2525 TreeKind::Group {
2526 label: "chapter".into(),
2527 name: "slide-0".into(),
2528 },
2529 );
2530 t.add_with_prov(
2531 Some(slide),
2532 None,
2533 text("paragraph", "héllo"),
2534 TreeProv {
2535 page_no: 1,
2536 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
2537 bottom_left: true,
2538 charspan: [0, 5],
2539 },
2540 );
2541 t.add_with_prov(
2542 Some(slide),
2543 None,
2544 TreeKind::Picture {
2545 captions: Vec::new(),
2546 image: Some(crate::PictureImage {
2547 mimetype: "image/png".into(),
2548 width: 2,
2549 height: 2,
2550 data: vec![0],
2551 }),
2552 classification: None,
2553 confidence: None,
2554 chart: None,
2555 dpi: Some(300),
2556 },
2557 TreeProv {
2558 page_no: 1,
2559 bbox: [0.0; 4],
2560 bottom_left: false,
2561 charspan: [0, 0],
2562 },
2563 );
2564 t.add(
2565 Some(slide),
2566 Some(ContentLayer::Notes),
2567 text("text", "no geometry"),
2568 );
2569 let mut doc = DoclingDocument::new("t");
2570 doc.push(Node::PageInfo {
2571 page_no: 1,
2572 width: 9144000.0,
2573 height: 6858000.0,
2574 });
2575 doc.tree = Some(t);
2576 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2577 assert_eq!(
2578 v["texts"][0]["prov"],
2579 serde_json::json!([{
2580 "page_no": 1,
2581 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
2582 "charspan": [0, 5],
2583 }])
2584 );
2585 assert_eq!(v["texts"][0]["label"], "paragraph");
2586 assert_eq!(
2587 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
2588 "TOPLEFT"
2589 );
2590 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
2591 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
2592 assert_eq!(v["texts"][1]["content_layer"], "notes");
2593 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
2594 assert_eq!(v["pages"]["1"]["page_no"], 1);
2595 }
2596
2597 #[test]
2602 fn provenance_boxes_are_clamped_to_their_page() {
2603 let mut doc = DoclingDocument::new("t");
2604 doc.push(Node::PageInfo {
2605 page_no: 1,
2606 width: 10.0,
2607 height: 8.0,
2608 });
2609 doc.push(Node::Prov {
2610 page_no: 1,
2611 bbox: [-1.0, 2.0, 12.0, 9.5],
2612 charspan: [0, 1],
2613 seq: None,
2614 inner: Box::new(Node::Paragraph { text: "x".into() }),
2615 });
2616 let mut table = Table {
2617 rows: vec![vec!["a".into()]],
2618 ..Table::default()
2619 };
2620 table.cells = Some(vec![crate::TableCell {
2621 text: "a".into(),
2622 bbox: Some([1.0, 1.0, 11.0, 9.0]),
2623 start_row: 0,
2624 start_col: 0,
2625 row_span: 1,
2626 col_span: 1,
2627 column_header: false,
2628 row_header: false,
2629 row_section: false,
2630 }]);
2631 doc.push(Node::Prov {
2632 page_no: 1,
2633 bbox: [0.0, 0.0, 10.0, 8.0],
2634 charspan: [0, 0],
2635 seq: None,
2636 inner: Box::new(Node::Table(table)),
2637 });
2638 doc.push(Node::Prov {
2639 page_no: 7,
2640 bbox: [-5.0, 0.0, 50.0, 50.0],
2641 charspan: [0, 1],
2642 seq: None,
2643 inner: Box::new(Node::Paragraph { text: "y".into() }),
2644 });
2645 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2646 assert_eq!(
2647 v["texts"][0]["prov"][0]["bbox"],
2648 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
2649 );
2650 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
2651 assert_eq!(
2652 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
2653 (Some(1.0), Some(10.0), Some(8.0))
2654 );
2655 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
2656 assert_eq!(
2657 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
2658 "page 7 is not described"
2659 );
2660 }
2661
2662 #[test]
2663 fn a_backend_item_tree_is_written_verbatim() {
2664 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
2665 let mut t = ItemTree::default();
2666 let text = |label: &str, txt: &str| TreeKind::Text {
2667 label: label.into(),
2668 text: txt.into(),
2669 orig: None,
2670 formatting: None,
2671 hyperlink: None,
2672 level: None,
2673 list: None,
2674 };
2675 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
2676 let h = t.add(None, None, text("title", "Heading"));
2677 let group = t.add(
2678 Some(h),
2679 None,
2680 TreeKind::Group {
2681 label: "inline".into(),
2682 name: "group".into(),
2683 },
2684 );
2685 t.add(
2686 Some(group),
2687 None,
2688 TreeKind::Text {
2689 label: "text".into(),
2690 text: "bold".into(),
2691 orig: None,
2692 formatting: Some(Formatting {
2693 bold: true,
2694 ..Formatting::default()
2695 }),
2696 hyperlink: Some("https://example.com/".into()),
2697 level: None,
2698 list: None,
2699 },
2700 );
2701 t.add(
2702 Some(group),
2703 None,
2704 TreeKind::Code {
2705 text: "x = 1".into(),
2706 orig: None,
2707 language: Some("python".into()),
2708 formatting: None,
2709 hyperlink: None,
2710 },
2711 );
2712 let sub = t.add(
2713 Some(h),
2714 None,
2715 TreeKind::Text {
2716 label: "section_header".into(),
2717 text: "Sub".into(),
2718 orig: Some("Sub\u{2019}".into()),
2719 formatting: None,
2720 hyperlink: None,
2721 level: Some(1),
2722 list: None,
2723 },
2724 );
2725 t.add(
2726 Some(sub),
2727 None,
2728 TreeKind::Text {
2729 label: "list_item".into(),
2730 text: "item".into(),
2731 orig: None,
2732 formatting: None,
2733 hyperlink: None,
2734 level: None,
2735 list: Some(ListMeta {
2736 enumerated: true,
2737 marker: "3.".into(),
2738 }),
2739 },
2740 );
2741 let _region = t.add(
2742 Some(sub),
2743 None,
2744 TreeKind::FieldRegion {
2745 items: vec![crate::FieldItem {
2746 marker: None,
2747 key: Some("Name".into()),
2748 value: Some("Duck".into()),
2749 value_kind: Some("read_only".into()),
2750 }],
2751 },
2752 );
2753 let table = t.add(
2754 Some(sub),
2755 None,
2756 TreeKind::Table {
2757 table: Table {
2758 rows: vec![vec!["a \n<".into(), "b".into()]],
2759 cells: Some(vec![
2760 crate::TableCell {
2761 text: "a \n<".into(),
2762 bbox: None,
2763 start_row: 0,
2764 start_col: 0,
2765 row_span: 3,
2766 col_span: 1,
2767 column_header: false,
2768 row_header: true,
2769 row_section: false,
2770 },
2771 crate::TableCell {
2772 text: "b".into(),
2773 bbox: None,
2774 start_row: 0,
2775 start_col: 1,
2776 row_span: 1,
2777 col_span: 1,
2778 column_header: false,
2779 row_header: false,
2780 row_section: false,
2781 },
2782 ]),
2783 ..Table::default()
2784 },
2785 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
2787 },
2788 );
2789 let cell_group = t.add(
2790 Some(table),
2791 None,
2792 TreeKind::Group {
2793 label: "unspecified".into(),
2794 name: "rich_cell_group_1_0_0".into(),
2795 },
2796 );
2797 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
2798 *rich_cells = vec![(0, 1, cell_group)];
2799 }
2800 let after = t.add(Some(sub), None, text("text", "after the region"));
2801 let _ = (title, after);
2802
2803 let doc = DoclingDocument {
2804 tree: Some(t),
2805 ..DoclingDocument::new("t")
2806 };
2807 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2808 let texts: Vec<&str> = v["texts"]
2810 .as_array()
2811 .unwrap()
2812 .iter()
2813 .map(|t| t["text"].as_str().unwrap())
2814 .collect();
2815 assert_eq!(
2816 texts,
2817 [
2818 "Page",
2819 "Heading",
2820 "bold",
2821 "x = 1",
2822 "Sub",
2823 "item",
2824 "Name",
2825 "Duck",
2826 "after the region"
2827 ]
2828 );
2829 assert_eq!(
2830 v["body"]["children"],
2831 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
2832 );
2833 assert_eq!(v["texts"][0]["content_layer"], "furniture");
2834 assert_eq!(
2835 v["texts"][1]["children"],
2836 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
2837 );
2838 let bold = &v["texts"][2];
2839 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
2840 let keys: Vec<&str> = bold
2841 .as_object()
2842 .unwrap()
2843 .keys()
2844 .map(String::as_str)
2845 .collect();
2846 assert_eq!(
2847 keys,
2848 [
2849 "self_ref",
2850 "parent",
2851 "children",
2852 "content_layer",
2853 "label",
2854 "prov",
2855 "orig",
2856 "text",
2857 "formatting",
2858 "hyperlink"
2859 ]
2860 );
2861 assert_eq!(
2862 bold["formatting"],
2863 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
2864 );
2865 let code = &v["texts"][3];
2866 assert_eq!(code["label"], "code");
2867 assert_eq!(code["code_language"], "Python");
2868 let sub = &v["texts"][4];
2869 assert_eq!(sub["orig"], "Sub\u{2019}");
2870 assert_eq!(sub["level"], 1);
2871 let item = &v["texts"][5];
2872 let keys: Vec<&str> = item
2873 .as_object()
2874 .unwrap()
2875 .keys()
2876 .map(String::as_str)
2877 .collect();
2878 assert_eq!(
2879 keys,
2880 [
2881 "self_ref",
2882 "parent",
2883 "children",
2884 "content_layer",
2885 "label",
2886 "prov",
2887 "orig",
2888 "text",
2889 "enumerated",
2890 "marker"
2891 ]
2892 );
2893 assert_eq!(item["marker"], "3.");
2894 assert_eq!(v["texts"][7]["kind"], "read_only");
2895 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
2896 let table = &v["tables"][0];
2897 assert_eq!(
2898 table["children"],
2899 serde_json::json!([{"$ref": "#/groups/1"}])
2900 );
2901 let cells = table["data"]["table_cells"].as_array().unwrap();
2902 assert_eq!(
2903 cells[0]["text"], "a \n<",
2904 "raw cell text is written verbatim"
2905 );
2906 assert_eq!(
2907 cells[0]["end_row_offset_idx"], 3,
2908 "declared spans are not clamped"
2909 );
2910 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
2911 assert!(cells[0].get("ref").is_none());
2912 assert!(
2913 table["data"]["grid"][0][1].get("ref").is_none(),
2914 "the grid shows plain cells"
2915 );
2916 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
2917 }
2918
2919 #[test]
2923 fn a_comment_section_can_be_referenced_by_its_note_text() {
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::CommentSection {
2934 name: "comment-Sheet1-A1".into(),
2935 text: "[author: A]: note".into(),
2936 refs_note_text: true,
2937 grouped: true,
2938 },
2939 ],
2940 ..DoclingDocument::new("c")
2941 };
2942 let v = crate::json::to_json(&doc);
2943 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
2944 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
2945 }
2946
2947 #[test]
2951 fn comment_sections_link_back_to_their_items() {
2952 let doc = DoclingDocument {
2953 name: "c".into(),
2954 nodes: vec![
2955 Node::Commented {
2956 comments: vec![0],
2957 inner: Box::new(Node::Paragraph {
2958 text: "annotated".into(),
2959 }),
2960 },
2961 Node::Paragraph {
2962 text: "plain".into(),
2963 },
2964 Node::CommentSection {
2965 name: "comment-7".into(),
2966 text: "[time: t]: note".into(),
2967 refs_note_text: false,
2968 grouped: true,
2969 },
2970 ],
2971 ..DoclingDocument::new("c")
2972 };
2973 let v = crate::json::to_json(&doc);
2974 assert_eq!(v["groups"][0]["label"], "comment_section");
2976 assert_eq!(v["groups"][0]["name"], "comment-7");
2977 assert_eq!(v["groups"][0]["content_layer"], "notes");
2978 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
2979 assert_eq!(v["texts"][2]["content_layer"], "notes");
2980 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
2982 assert!(v["texts"][1].get("comments").is_none());
2983 let keys: Vec<&str> = v["texts"][0]
2985 .as_object()
2986 .unwrap()
2987 .keys()
2988 .map(String::as_str)
2989 .collect();
2990 assert_eq!(
2991 &keys[keys.len() - 4..],
2992 &["prov", "comments", "orig", "text"]
2993 );
2994 }
2995
2996 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
2997 Node::Picture {
2998 caption: Some(caption.into()),
2999 caption_href: None,
3000 image: None,
3001 classification: None,
3002 caption_parent,
3003 }
3004 }
3005
3006 fn group(children: Vec<Node>) -> Node {
3007 Node::Group {
3008 label: "section".into(),
3009 name: None,
3010 layer: None,
3011 children,
3012 }
3013 }
3014
3015 fn refs(v: &Value) -> Vec<&str> {
3016 v.as_array()
3017 .unwrap()
3018 .iter()
3019 .map(|r| r["$ref"].as_str().unwrap())
3020 .collect()
3021 }
3022
3023 #[test]
3028 fn a_body_caption_follows_the_enclosing_top_level_item() {
3029 let mut doc = DoclingDocument::new("t");
3030 doc.push(picture("top", CaptionParent::Body));
3031 doc.push(group(vec![
3032 Node::Paragraph { text: "p".into() },
3033 picture("nested", CaptionParent::Body),
3034 ]));
3035 doc.push(Node::Paragraph {
3036 text: "after".into(),
3037 });
3038 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3039 assert_eq!(
3040 refs(&v["body"]["children"]),
3041 [
3042 "#/texts/0",
3043 "#/pictures/0",
3044 "#/groups/0",
3045 "#/texts/2",
3046 "#/texts/3"
3047 ]
3048 );
3049 assert_eq!(
3050 refs(&v["groups"][0]["children"]),
3051 ["#/texts/1", "#/pictures/1"]
3052 );
3053 for (cap, pic) in [(0, 0), (2, 1)] {
3054 assert_eq!(v["texts"][cap]["label"], "caption");
3055 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
3056 assert_eq!(
3057 refs(&v["pictures"][pic]["captions"]),
3058 [format!("#/texts/{cap}")]
3059 );
3060 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
3061 }
3062 }
3063
3064 #[test]
3067 fn an_item_caption_is_the_items_first_child() {
3068 let mut doc = DoclingDocument::new("t");
3069 doc.push(picture("fig", CaptionParent::Item));
3070 doc.push(Node::Table(Table {
3071 rows: vec![vec!["a".into()]],
3072 caption: Some("tab".into()),
3073 caption_parent: CaptionParent::Item,
3074 ..Table::default()
3075 }));
3076 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3077 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
3078 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
3079 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
3080 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
3081 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
3082 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
3083 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
3084 }
3085
3086 #[test]
3090 fn a_container_caption_is_the_items_sibling() {
3091 let mut doc = DoclingDocument::new("t");
3092 doc.push(group(vec![
3093 picture("chart", CaptionParent::Container),
3094 Node::Table(Table {
3095 rows: vec![vec!["a".into()]],
3096 caption: Some("figcaption".into()),
3097 caption_parent: CaptionParent::ContainerAfter,
3098 ..Table::default()
3099 }),
3100 ]));
3101 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3102 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
3103 assert_eq!(
3104 refs(&v["groups"][0]["children"]),
3105 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
3106 );
3107 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
3108 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
3109 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
3110 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
3111 }
3112}