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
218pub fn to_json(doc: &DoclingDocument) -> Value {
220 let mut b = Builder::default();
221 let body = match &doc.tree {
225 Some(tree) => {
226 for n in &doc.nodes {
229 if let Node::PageInfo {
230 page_no,
231 width,
232 height,
233 } = n
234 {
235 if *page_no > 0 {
236 b.pages.push((*page_no, *width as f64, *height as f64));
237 }
238 }
239 }
240 b.write_tree(tree)
241 }
242 None => b.walk_into(&doc.nodes, "#/body"),
243 };
244 b.link_comments();
245
246 let mut out = json!({
247 "schema_name": "DoclingDocument",
248 "version": SCHEMA_VERSION,
249 "name": doc.name,
250 "origin": {
251 "mimetype": "text/plain",
252 "binary_hash": fnv1a(&doc.name),
253 "filename": doc.name,
254 },
255 "furniture": {
256 "self_ref": "#/furniture",
257 "children": [],
258 "content_layer": "furniture",
259 "name": "_root_",
260 "label": "unspecified",
261 },
262 "body": {
263 "self_ref": "#/body",
264 "children": body,
265 "content_layer": "body",
266 "name": "_root_",
267 "label": "unspecified",
268 },
269 "groups": b.groups,
270 "texts": b.texts,
271 "pictures": b.pictures,
272 "tables": b.tables,
273 "key_value_items": [],
274 "form_items": [],
275 "pages": b.pages.iter().map(|(n, w, h)| {
276 let r2 = |v: f64| (v * 100.0).round() / 100.0;
277 (n.to_string(), json!({
278 "size": { "width": r2(*w), "height": r2(*h) },
279 "page_no": n,
280 }))
281 }).collect::<serde_json::Map<String, Value>>(),
282 });
283
284 clamp_boxes_to_pages(&mut out, &b.pages);
293
294 if !b.field_regions.is_empty() {
299 if let Some(obj) = out.as_object_mut() {
300 let pages = obj.remove("pages");
301 obj.insert("field_regions".into(), Value::Array(b.field_regions));
302 obj.insert("field_items".into(), Value::Array(b.field_items));
303 if let Some(pages) = pages {
304 obj.insert("pages".into(), pages);
305 }
306 }
307 }
308 out
309}
310
311fn classification_meta(classes: &[crate::PictureClass]) -> Value {
318 json!({
319 "classification": {
320 "predictions": classes.iter().map(|c| json!({
321 "confidence": c.confidence as f64,
322 "created_by": "DocumentPictureClassifier",
323 "class_name": c.class_name,
324 })).collect::<Vec<_>>(),
325 },
326 "annotations": [{
327 "kind": "classification",
328 "provenance": "DocumentPictureClassifier",
329 "predicted_classes": classes.iter().map(|c| json!({
330 "class_name": c.class_name,
331 "confidence": c.confidence as f64,
332 })).collect::<Vec<_>>(),
333 }],
334 })
335}
336
337#[allow(clippy::too_many_arguments)]
348fn cell_value(
349 row_span: usize,
350 col_span: usize,
351 start_row: usize,
352 end_row: usize,
353 start_col: usize,
354 end_col: usize,
355 text: String,
356 column_header: bool,
357 row_header: bool,
358 row_section: bool,
359 bbox: Option<[f32; 4]>,
360) -> Value {
361 let mut m = serde_json::Map::with_capacity(12);
362 m.insert("row_span".into(), row_span.into());
363 m.insert("col_span".into(), col_span.into());
364 m.insert("start_row_offset_idx".into(), start_row.into());
365 m.insert("end_row_offset_idx".into(), end_row.into());
366 m.insert("start_col_offset_idx".into(), start_col.into());
367 m.insert("end_col_offset_idx".into(), end_col.into());
368 m.insert("text".into(), Value::String(text));
369 m.insert("column_header".into(), column_header.into());
370 m.insert("row_header".into(), row_header.into());
371 m.insert("row_section".into(), row_section.into());
372 m.insert("fillable".into(), false.into());
373 if let Some(b) = bbox {
374 m.insert(
375 "bbox".into(),
376 json!({
377 "l": b[0], "t": b[1], "r": b[2], "b": b[3],
378 "coord_origin": "TOPLEFT",
379 }),
380 );
381 }
382 Value::Object(m)
383}
384
385fn table_data(t: &Table) -> Value {
388 table_data_with(t, false)
389}
390
391fn table_data_with(t: &Table, raw: bool) -> Value {
394 let cell_text = |s: &str| {
395 if raw {
396 s.to_string()
397 } else {
398 unescape_text(&crate::markdown::strip_hard_breaks(s))
399 }
400 };
401 let num_rows = t.rows.len();
402 let num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
403 let mut grid = Vec::with_capacity(num_rows);
404 let mut cells = Vec::new();
405 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
409 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
410 for c in first_class {
411 let idx = cells.len();
412 cells.push(cell_value(
413 c.row_span,
414 c.col_span,
415 c.start_row,
416 c.start_row + c.row_span,
417 c.start_col,
418 c.start_col + c.col_span,
419 cell_text(&c.text),
420 c.column_header,
421 c.row_header,
422 c.row_section,
423 c.bbox,
424 ));
425 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
426 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
427 slot[r * num_cols + k] = Some(idx);
428 }
429 }
430 }
431 for r in 0..num_rows {
432 let mut grid_row = Vec::with_capacity(num_cols);
433 for c in 0..num_cols {
434 grid_row.push(match slot[r * num_cols + c] {
435 Some(i) => cells[i].clone(),
436 None => cell_value(
437 1,
438 1,
439 r,
440 r + 1,
441 c,
442 c + 1,
443 String::new(),
444 false,
445 false,
446 false,
447 None,
448 ),
449 });
450 }
451 grid.push(grid_row);
452 }
453 } else {
454 let s = t.structure.as_ref();
455 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
456 grid.and_then(|g| g.get(r))
457 .and_then(|row| row.get(c))
458 .copied()
459 .unwrap_or(false)
460 };
461 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
462 let (mut r0, mut c0) = (r, c);
463 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
464 c0 -= 1;
465 }
466 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
467 r0 -= 1;
468 }
469 (r0, c0)
470 };
471 let anchors: Vec<(usize, usize)> = (0..num_rows)
474 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
475 .map(|(r, c)| anchor_of(r, c))
476 .collect();
477 let mut extent: Vec<(usize, usize)> = (0..num_rows)
478 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
479 .collect();
480 for (i, &(ar, ac)) in anchors.iter().enumerate() {
481 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
482 let e = &mut extent[ar * num_cols + ac];
483 e.0 = e.0.max(r);
484 e.1 = e.1.max(c);
485 }
486 for (r, row) in t.rows.iter().enumerate() {
487 let mut grid_row = Vec::with_capacity(num_cols);
488 for c in 0..num_cols {
489 let (ar, ac) = anchors[r * num_cols + c];
490 if (ar, ac) == (r, c) {
491 let (er, ec) = extent[r * num_cols + c];
492 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
493 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
494 Some(s) => flag(Some(&s.col_header), r, c),
495 None => r == 0,
496 };
497 slot[r * num_cols + c] = Some(cells.len());
498 cells.push(cell_value(
499 er - r + 1,
500 ec - c + 1,
501 r,
502 er + 1,
503 c,
504 ec + 1,
505 text,
506 column_header,
507 flag(s.map(|s| &s.row_header), r, c),
508 false,
509 None,
510 ));
511 }
512 grid_row.push(match slot[ar * num_cols + ac] {
513 Some(i) => cells[i].clone(),
514 None => Value::Null,
515 });
516 }
517 grid.push(grid_row);
518 }
519 }
520 json!({
521 "table_cells": cells,
522 "num_rows": num_rows,
523 "num_cols": num_cols,
524 "orientation": "rot_0",
525 "grid": grid,
526 })
527}
528
529#[derive(Default)]
530struct Builder {
531 texts: Vec<Value>,
532 groups: Vec<Value>,
533 tables: Vec<Value>,
534 pictures: Vec<Value>,
535 field_regions: Vec<Value>,
536 field_items: Vec<Value>,
537 pages: Vec<(usize, f64, f64)>,
541 cur_page: usize,
543 cur_w: f64,
544 cur_h: f64,
545 pending_loc: Option<[u16; 4]>,
548 pending_exact: Option<ExactProv>,
551 pending_siblings: Vec<Value>,
555 pending_after: Vec<Value>,
559 pending_body: Vec<Value>,
563 comment_groups: Vec<String>,
567 pending_comments: Vec<(String, Vec<usize>)>,
571}
572
573impl Builder {
574 fn take_prov(&mut self, char_len: usize) -> Value {
580 let prov = self.prov_json(char_len, false);
581 self.pending_exact = None;
582 self.pending_loc = None;
583 prov
584 }
585
586 fn prov_json(&self, char_len: usize, span_over_text: bool) -> Value {
591 let r2 = |v: f64| (v * 100.0).round() / 100.0;
592 if let Some(ExactProv {
593 page_no,
594 bbox: [l, t, r, b],
595 bottom_left,
596 charspan,
597 }) = self.pending_exact
598 {
599 let charspan = if span_over_text {
600 [0, char_len]
601 } else {
602 charspan
603 };
604 return json!([{
605 "page_no": page_no,
606 "bbox": {
607 "l": r2(l), "t": r2(t), "r": r2(r), "b": r2(b),
608 "coord_origin": if bottom_left { "BOTTOMLEFT" } else { "TOPLEFT" },
609 },
610 "charspan": charspan,
611 }]);
612 }
613 let Some([x0, y0, x1, y1]) = self.pending_loc else {
614 return json!([]);
615 };
616 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
620 return json!([{
621 "page_no": self.cur_page,
622 "bbox": { "l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT" },
623 "charspan": [0, char_len],
624 }]);
625 }
626 json!([{
627 "page_no": self.cur_page,
628 "bbox": {
629 "l": r2(x0 as f64 * self.cur_w / 512.0),
630 "t": r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
631 "r": r2(x1 as f64 * self.cur_w / 512.0),
632 "b": r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
633 "coord_origin": "BOTTOMLEFT",
634 },
635 "charspan": [0, char_len],
636 }])
637 }
638
639 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
642 if self.pending_loc.is_none() && self.cur_page > 0 {
643 self.pending_loc = loc;
644 }
645 }
646
647 fn link_comments(&mut self) {
655 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
656 .into_iter()
657 .map(|(item, comments)| {
658 let refs = comments
659 .iter()
660 .filter_map(|i| self.comment_groups.get(*i))
661 .map(|r| json!({ "$ref": r }))
662 .collect();
663 (item, refs)
664 })
665 .collect();
666 for (item, comment_refs) in refs {
667 if comment_refs.is_empty() {
668 continue;
669 }
670 let Some(target) = self.item_mut(&item) else {
671 continue;
672 };
673 let Some(obj) = target.as_object_mut() else {
674 continue;
675 };
676 let tail: Vec<(String, Value)> = obj
677 .iter()
678 .skip_while(|(k, _)| k.as_str() != "prov")
679 .skip(1)
680 .map(|(k, v)| (k.clone(), v.clone()))
681 .collect();
682 for (k, _) in &tail {
683 obj.shift_remove(k);
684 }
685 obj.insert("comments".into(), Value::Array(comment_refs));
686 for (k, v) in tail {
687 obj.insert(k, v);
688 }
689 }
690 }
691
692 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
694 let idx = ref_index(self_ref)?;
695 let bucket = if self_ref.starts_with("#/texts/") {
696 &mut self.texts
697 } else if self_ref.starts_with("#/tables/") {
698 &mut self.tables
699 } else if self_ref.starts_with("#/pictures/") {
700 &mut self.pictures
701 } else if self_ref.starts_with("#/groups/") {
702 &mut self.groups
703 } else {
704 return None;
705 };
706 bucket.get_mut(idx)
707 }
708
709 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
713 use crate::tree::TreeKind;
714 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
717 let (mut nt, mut ng, mut ntb, mut np, mut nf) = (0, 0, 0, 0, 0);
718 for item in &tree.items {
719 if item.deleted {
720 refs.push(String::new());
721 continue;
722 }
723 let r = match &item.kind {
724 TreeKind::Text { .. } | TreeKind::Code { .. } => {
725 nt += 1;
726 format!("#/texts/{}", nt - 1)
727 }
728 TreeKind::Group { .. } => {
729 ng += 1;
730 format!("#/groups/{}", ng - 1)
731 }
732 TreeKind::Table { .. } => {
733 ntb += 1;
734 format!("#/tables/{}", ntb - 1)
735 }
736 TreeKind::Picture { .. } => {
737 np += 1;
738 format!("#/pictures/{}", np - 1)
739 }
740 TreeKind::FieldRegion { items } => {
741 nt += items
744 .iter()
745 .map(|i| {
746 [&i.marker, &i.key, &i.value]
747 .iter()
748 .filter(|p| p.is_some())
749 .count()
750 })
751 .sum::<usize>();
752 nf += 1;
753 format!("#/field_regions/{}", nf - 1)
754 }
755 };
756 refs.push(r);
757 }
758 let ref_of = |id: usize| json!({ "$ref": refs[id] });
759 for (id, item) in tree.items.iter().enumerate() {
760 if item.deleted {
761 continue;
762 }
763 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
764 let children: Vec<Value> = item.children.iter().map(|&c| ref_of(c)).collect();
765 let layer = item.layer.map_or("body", |l| l.value());
766 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
769 let self_ref = match &item.kind {
770 TreeKind::Text {
771 label,
772 text,
773 orig,
774 formatting,
775 hyperlink,
776 level,
777 list,
778 } => {
779 let mut tail = serde_json::Map::new();
782 if let Some(f) = formatting {
783 tail.insert("formatting".into(), formatting_json(f));
784 }
785 if let Some(h) = hyperlink {
786 tail.insert("hyperlink".into(), json!(h));
787 }
788 if let Some(l) = level {
789 tail.insert("level".into(), json!(l));
790 }
791 if let Some(l) = list {
792 tail.insert("enumerated".into(), json!(l.enumerated));
793 tail.insert("marker".into(), json!(l.marker));
794 }
795 let r = format!("#/texts/{}", self.texts.len());
796 let prov = self.take_prov(text.chars().count());
797 let mut item_json = json!({
798 "self_ref": r,
799 "parent": { "$ref": parent },
800 "children": children,
801 "content_layer": layer,
802 "label": label,
803 "prov": prov,
804 });
805 if !item.comments.is_empty() {
808 item_json["comments"] =
809 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
810 }
811 merge(
812 &mut item_json,
813 json!({
814 "orig": orig.as_deref().unwrap_or(text),
815 "text": text,
816 }),
817 );
818 merge(&mut item_json, Value::Object(tail));
819 self.texts.push(item_json);
820 r
821 }
822 TreeKind::Code {
823 text,
824 orig,
825 language,
826 formatting,
827 hyperlink,
828 } => {
829 let r = format!("#/texts/{}", self.texts.len());
830 let prov = self.take_prov(text.chars().count());
831 let mut item_json = json!({
832 "self_ref": r,
833 "parent": { "$ref": parent },
834 "children": children,
835 "content_layer": layer,
836 "label": "code",
837 "prov": prov,
838 });
839 if !item.comments.is_empty() {
840 item_json["comments"] =
841 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
842 }
843 merge(
844 &mut item_json,
845 json!({
846 "orig": orig.as_deref().unwrap_or(text),
847 "text": text,
848 }),
849 );
850 if let Some(f) = formatting {
851 item_json["formatting"] = formatting_json(f);
852 }
853 if let Some(h) = hyperlink {
854 item_json["hyperlink"] = json!(h);
855 }
856 merge(
857 &mut item_json,
858 json!({
859 "captions": [],
860 "references": [],
861 "footnotes": [],
862 "code_language": code_language(language.as_deref()),
863 }),
864 );
865 self.texts.push(item_json);
866 r
867 }
868 TreeKind::Group { label, name } => {
869 self.pending_exact = None;
870 let r = format!("#/groups/{}", self.groups.len());
871 self.groups.push(json!({
872 "self_ref": r,
873 "parent": { "$ref": parent },
874 "children": children,
875 "content_layer": layer,
876 "name": name,
877 "label": label,
878 }));
879 r
880 }
881 TreeKind::Table {
882 table,
883 rich_cells,
884 captions,
885 } => {
886 let r = self.add_table_with(table, parent, true);
888 let idx = ref_index(&r).expect("table ref");
889 let t = &mut self.tables[idx];
890 t["children"] = Value::Array(children);
891 t["content_layer"] = json!(layer);
892 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
893 for &(row, col, group) in rich_cells {
897 let cell_ref = ref_of(group);
898 let hit = |c: &Value| {
899 c["start_row_offset_idx"] == json!(row)
900 && c["start_col_offset_idx"] == json!(col)
901 };
902 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
903 for c in cells.iter_mut().filter(|c| hit(c)) {
904 c["ref"] = cell_ref.clone();
905 }
906 }
907 }
908 r
909 }
910 TreeKind::Picture {
911 captions,
912 image,
913 classification,
914 chart,
915 dpi,
916 } => {
917 let mut meta = classification.as_ref().map(
920 |c| json!({ "classification": { "predictions": [{ "class_name": c }] } }),
921 );
922 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
923 if !t.rows.is_empty() {
924 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
925 }
926 }
927 let prov = self.take_prov(0);
928 let r = self.push_picture(
929 prov,
930 captions.iter().map(|&c| ref_of(c)).collect(),
931 children,
932 image.as_ref(),
933 meta,
934 parent,
935 );
936 if let Some(idx) = ref_index(&r) {
937 self.pictures[idx]["content_layer"] = json!(layer);
938 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
941 img["dpi"] = json!(dpi);
942 }
943 }
944 r
945 }
946 TreeKind::FieldRegion { items } => {
947 self.pending_exact = None;
948 let r = self.add_field_region(items, parent);
949 if let Some(region) = self.field_regions.last_mut() {
950 region["content_layer"] = json!(layer);
951 }
952 r
953 }
954 };
955 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
956 }
957 tree.body.iter().map(|&c| ref_of(c)).collect()
958 }
959
960 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
961 match node {
962 Node::Heading { level: 1, text } => {
963 Some(self.add_text("title", text, parent, json!({})))
964 }
965 Node::Heading { level, text } => Some(self.add_text(
966 "section_header",
967 text,
968 parent,
969 json!({ "level": level.saturating_sub(1) }),
970 )),
971 Node::Caption { text, href } => {
972 let extra = match href {
973 Some(url) => json!({ "hyperlink": url }),
974 None => json!({}),
975 };
976 Some(self.add_text("caption", text, parent, extra))
977 }
978 Node::Paragraph { text } => {
979 let t = text.trim();
982 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
983 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
984 _ => Some(self.add_text("text", text, parent, json!({}))),
985 }
986 }
987 Node::CheckboxItem { checked, text } => {
988 let mark = if *checked { "- [x] " } else { "- [ ] " };
991 Some(self.add_text("text", &format!("{mark}{text}"), parent, json!({})))
992 }
993 Node::Code {
994 language,
995 text,
996 orig,
997 ..
998 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
999 Node::Formula {
1002 latex,
1003 orig,
1004 location,
1005 } => {
1006 self.adopt_loc(*location);
1007 Some(self.add_formula_item(latex, orig, parent))
1008 }
1009 Node::CommentSection {
1015 name,
1016 text,
1017 refs_note_text,
1018 grouped,
1019 } => {
1020 if !*grouped {
1021 let child =
1025 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1026 self.comment_groups.push(child.clone());
1027 return Some(child);
1028 }
1029 let self_ref = format!("#/groups/{}", self.groups.len());
1030 self.groups.push(Value::Null);
1031 let child =
1032 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1033 self.groups[group_index(&self_ref)] = json!({
1034 "self_ref": self_ref,
1035 "parent": { "$ref": parent },
1036 "children": [{ "$ref": child }],
1037 "content_layer": "notes",
1038 "name": name,
1039 "label": "comment_section",
1040 });
1041 self.comment_groups.push(if *refs_note_text {
1042 child
1043 } else {
1044 self_ref.clone()
1045 });
1046 Some(self_ref)
1047 }
1048 Node::Commented { comments, inner } => {
1051 let item = self.add_node(inner, parent)?;
1052 if !comments.is_empty() {
1053 self.pending_comments.push((item.clone(), comments.clone()));
1054 }
1055 Some(item)
1056 }
1057 Node::Table(t) => Some(self.add_table(t, parent)),
1058 Node::Picture {
1059 caption,
1060 caption_href,
1061 image,
1062 classification,
1063 caption_parent,
1064 } => Some(self.add_picture(
1065 caption.as_deref(),
1066 caption_href.as_deref(),
1067 image.as_ref(),
1068 classification.as_deref().map(classification_meta),
1069 parent,
1070 *caption_parent,
1071 )),
1072 Node::Chart {
1077 kind,
1078 table,
1079 caption,
1080 location,
1081 } => {
1082 self.adopt_loc(*location);
1083 let mut meta = json!({
1084 "classification": { "predictions": [{ "class_name": kind }] },
1085 });
1086 if !table.rows.is_empty() {
1087 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1088 }
1089 let mut captions = Vec::new();
1095 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1096 let prov = self.prov_json(unescape_text(cap).chars().count(), true);
1097 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1098 self.pending_siblings.push(json!({ "$ref": cap_ref }));
1099 captions.push(json!({ "$ref": cap_ref }));
1100 }
1101 let prov = self.take_prov(0);
1102 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1103 }
1104 Node::DoclangOnly(_) => None,
1106 Node::Group {
1107 label,
1108 name,
1109 layer,
1110 children,
1111 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1112 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1113 Node::InlineGroup { md_text, .. } => {
1116 Some(self.add_text("text", md_text, parent, json!({})))
1117 }
1118 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1120 Node::Furniture {
1126 layer: ContentLayer::Notes,
1127 inner,
1128 } => {
1129 let item = self.add_node(inner, parent)?;
1130 self.set_layer(&item, "notes");
1131 Some(item)
1132 }
1133 Node::Furniture { .. } => None,
1134 Node::PageFurniture { .. } => None,
1135 Node::Located { location, inner } => {
1140 if self.cur_page > 0 {
1141 self.pending_loc = Some(*location);
1142 }
1143 let r = self.add_node(inner, parent);
1144 self.pending_loc = None;
1145 r
1146 }
1147 Node::Prov {
1148 page_no,
1149 bbox,
1150 charspan,
1151 inner,
1152 ..
1153 } => {
1154 self.pending_exact = Some(ExactProv {
1155 page_no: *page_no,
1156 bbox: bbox.map(f64::from),
1157 bottom_left: false,
1158 charspan: *charspan,
1159 });
1160 let r = self.add_node(inner, parent);
1161 self.pending_exact = None;
1162 r
1163 }
1164 Node::PageBreak => None,
1166 Node::PageInfo {
1169 page_no,
1170 width,
1171 height,
1172 } => {
1173 self.cur_page = *page_no;
1174 self.cur_w = *width as f64;
1175 self.cur_h = *height as f64;
1176 if *page_no > 0 {
1177 self.pages.push((*page_no, self.cur_w, self.cur_h));
1178 }
1179 None
1180 }
1181 Node::ListItem { .. } => None,
1183 }
1184 }
1185
1186 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1190 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1191 self.field_regions.push(Value::Null);
1192 let region_index = self.field_regions.len() - 1;
1193 let mut item_refs = Vec::new();
1194 for item in items {
1195 item_refs.push(json!({ "$ref": self.add_field_item(item, &self_ref) }));
1196 }
1197 self.field_regions[region_index] = json!({
1198 "self_ref": self_ref,
1199 "parent": { "$ref": parent },
1200 "children": item_refs,
1201 "content_layer": "body",
1202 "label": "field_region",
1203 "prov": [],
1204 });
1205 self_ref
1206 }
1207
1208 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1209 let self_ref = format!("#/field_items/{}", self.field_items.len());
1210 self.field_items.push(Value::Null);
1211 let item_index = self.field_items.len() - 1;
1212 let mut child_refs = Vec::new();
1213 for (label, text) in [
1214 ("marker", &item.marker),
1215 ("field_key", &item.key),
1216 ("field_value", &item.value),
1217 ] {
1218 if let Some(text) = text {
1219 let extra = match (label, &item.value_kind) {
1222 ("field_value", Some(kind)) => json!({ "kind": kind }),
1223 _ => json!({}),
1224 };
1225 child_refs.push(json!({ "$ref": self.add_text(label, text, &self_ref, extra) }));
1226 }
1227 }
1228 self.field_items[item_index] = json!({
1229 "self_ref": self_ref,
1230 "parent": { "$ref": parent },
1231 "children": child_refs,
1232 "content_layer": "body",
1233 "label": "field_item",
1234 "prov": [],
1235 });
1236 self_ref
1237 }
1238
1239 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1243 let bucket = match self_ref.split('/').nth(1) {
1244 Some("texts") => &mut self.texts,
1245 Some("tables") => &mut self.tables,
1246 Some("pictures") => &mut self.pictures,
1247 Some("groups") => &mut self.groups,
1248 _ => return,
1249 };
1250 if let Some(item) = self_ref
1251 .rsplit('/')
1252 .next()
1253 .and_then(|i| i.parse::<usize>().ok())
1254 .and_then(|i| bucket.get_mut(i))
1255 {
1256 item["content_layer"] = json!(layer);
1257 }
1258 }
1259
1260 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1261 let prov = self.take_prov(unescape_text(text).chars().count());
1262 self.add_text_with(label, text, parent, extra, prov)
1263 }
1264
1265 fn add_text_with(
1268 &mut self,
1269 label: &str,
1270 text: &str,
1271 parent: &str,
1272 extra: Value,
1273 prov: Value,
1274 ) -> String {
1275 let self_ref = format!("#/texts/{}", self.texts.len());
1276 let raw = unescape_text(text);
1277 let mut item = json!({
1278 "self_ref": self_ref,
1279 "parent": { "$ref": parent },
1280 "children": [],
1281 "content_layer": "body",
1282 "label": label,
1283 "prov": prov,
1284 "orig": raw,
1285 "text": raw,
1286 });
1287 merge(&mut item, extra);
1288 self.texts.push(item);
1289 self_ref
1290 }
1291
1292 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
1295 let self_ref = format!("#/texts/{}", self.texts.len());
1296 let prov = self.take_prov(latex.chars().count());
1297 self.texts.push(json!({
1298 "self_ref": self_ref,
1299 "parent": { "$ref": parent },
1300 "children": [],
1301 "content_layer": "body",
1302 "label": "formula",
1303 "prov": prov,
1304 "orig": latex,
1305 "text": latex,
1306 }));
1307 self_ref
1308 }
1309
1310 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
1314 let self_ref = format!("#/texts/{}", self.texts.len());
1315 let prov = self.take_prov(latex.chars().count());
1316 self.texts.push(json!({
1317 "self_ref": self_ref,
1318 "parent": { "$ref": parent },
1319 "children": [],
1320 "content_layer": "body",
1321 "label": "formula",
1322 "prov": prov,
1323 "orig": orig,
1324 "text": latex,
1325 }));
1326 self_ref
1327 }
1328
1329 fn add_code(
1330 &mut self,
1331 text: &str,
1332 language: Option<&str>,
1333 orig: Option<&str>,
1334 parent: &str,
1335 ) -> String {
1336 let self_ref = format!("#/texts/{}", self.texts.len());
1337 let raw = unescape_text(text);
1338 let prov = self.take_prov(raw.chars().count());
1339 self.texts.push(json!({
1340 "self_ref": self_ref,
1341 "parent": { "$ref": parent },
1342 "children": [],
1343 "content_layer": "body",
1344 "label": "code",
1345 "prov": prov,
1346 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
1349 "text": raw,
1350 "captions": [],
1351 "references": [],
1352 "footnotes": [],
1353 "code_language": code_language(language),
1354 }));
1355 self_ref
1356 }
1357
1358 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
1361 let self_ref = format!("#/groups/{}", self.groups.len());
1362 self.groups.push(Value::Null);
1364 let base = level_of(&items[0]);
1365 let mut children = Vec::new();
1366 let mut i = 0;
1367 while i < items.len() {
1368 if !matches!(items[i], Node::ListItem { .. }) {
1371 i += 1;
1372 continue;
1373 }
1374 let lvl = level_of(&items[i]);
1375 if lvl > base {
1376 i += 1;
1378 continue;
1379 }
1380 let item_ref = self.add_list_item(&items[i], &self_ref);
1381 let mut j = i + 1;
1383 while j < items.len() && level_of(&items[j]) > base {
1384 j += 1;
1385 }
1386 if j > i + 1 {
1387 let mut nested = Vec::new();
1388 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
1389 if let Some(idx) = ref_index(&item_ref) {
1391 self.texts[idx]["children"]
1392 .as_array_mut()
1393 .unwrap()
1394 .extend(nested);
1395 }
1396 }
1397 children.push(json!({ "$ref": item_ref }));
1398 i = j;
1399 }
1400 self.groups[group_index(&self_ref)] = json!({
1401 "self_ref": self_ref,
1402 "parent": { "$ref": parent },
1403 "children": children,
1404 "content_layer": "body",
1405 "name": "list",
1406 "label": "list",
1407 });
1408 self_ref
1409 }
1410
1411 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
1412 let Node::ListItem {
1413 ordered,
1414 number,
1415 text,
1416 location,
1417 ..
1418 } = node
1419 else {
1420 unreachable!()
1421 };
1422 self.adopt_loc(*location);
1423 let self_ref = format!("#/texts/{}", self.texts.len());
1424 let raw = unescape_text(text);
1425 let prov = self.take_prov(raw.chars().count());
1426 let marker = if *ordered {
1427 format!("{number}.")
1428 } else {
1429 "-".to_string()
1430 };
1431 self.texts.push(json!({
1432 "self_ref": self_ref,
1433 "parent": { "$ref": parent },
1434 "children": [],
1435 "content_layer": "body",
1436 "label": "list_item",
1437 "prov": prov,
1438 "orig": raw,
1439 "text": raw,
1440 "enumerated": ordered,
1441 "marker": marker,
1442 }));
1443 self_ref
1444 }
1445
1446 fn add_table(&mut self, t: &Table, parent: &str) -> String {
1447 self.add_table_with(t, parent, false)
1448 }
1449
1450 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
1453 let self_ref = format!("#/tables/{}", self.tables.len());
1454 self.adopt_loc(t.location);
1455 let prov = self.take_prov(0);
1456 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
1460 Some(cap) => self.add_caption(cap, json!({}), &self_ref, parent, t.caption_parent),
1461 None => (Vec::new(), Vec::new()),
1462 };
1463 let data = table_data_with(t, raw);
1464 self.tables.push(json!({
1465 "self_ref": self_ref,
1466 "parent": { "$ref": parent },
1467 "children": children,
1468 "content_layer": "body",
1469 "label": "table",
1470 "prov": prov,
1471 "captions": captions,
1472 "references": [],
1473 "footnotes": [],
1474 "data": data,
1475 "annotations": [],
1476 }));
1477 self_ref
1478 }
1479
1480 fn add_caption(
1486 &mut self,
1487 text: &str,
1488 extra: Value,
1489 self_ref: &str,
1490 parent: &str,
1491 choice: CaptionParent,
1492 ) -> (Vec<Value>, Vec<Value>) {
1493 let cap_parent = match choice {
1498 CaptionParent::Item => self_ref,
1499 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
1500 CaptionParent::Body => "#/body",
1501 };
1502 let cap_ref = json!({ "$ref": self.add_text("caption", text, cap_parent, extra) });
1503 match choice {
1504 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
1505 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
1508 CaptionParent::Body if parent == "#/body" => {
1509 self.pending_siblings.push(cap_ref.clone())
1510 }
1511 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
1512 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
1515 }
1516 (vec![cap_ref], Vec::new())
1517 }
1518
1519 fn add_picture(
1522 &mut self,
1523 caption: Option<&str>,
1524 caption_href: Option<&str>,
1525 image: Option<&crate::PictureImage>,
1526 meta: Option<Value>,
1527 parent: &str,
1528 caption_parent: CaptionParent,
1529 ) -> String {
1530 let self_ref = format!("#/pictures/{}", self.pictures.len());
1531 let prov = self.take_prov(0);
1534 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
1535 Some(cap) => {
1536 let extra = match caption_href {
1540 Some(href) => json!({ "hyperlink": href }),
1541 None => json!({}),
1542 };
1543 self.add_caption(cap, extra, &self_ref, parent, caption_parent)
1544 }
1545 None => (Vec::new(), Vec::new()),
1546 };
1547 self.push_picture(prov, captions, children, image, meta, parent)
1548 }
1549
1550 fn push_picture(
1553 &mut self,
1554 prov: Value,
1555 captions: Vec<Value>,
1556 children: Vec<Value>,
1557 image: Option<&crate::PictureImage>,
1558 meta: Option<Value>,
1559 parent: &str,
1560 ) -> String {
1561 let self_ref = format!("#/pictures/{}", self.pictures.len());
1562 let annotations = meta
1566 .as_ref()
1567 .and_then(|m| m.get("annotations").cloned())
1568 .unwrap_or_else(|| json!([]));
1569 let meta = meta.map(|mut m| {
1570 if let Some(obj) = m.as_object_mut() {
1571 obj.remove("annotations");
1572 }
1573 m
1574 });
1575 let mut item = match meta {
1579 Some(meta) => json!({
1580 "self_ref": self_ref,
1581 "parent": { "$ref": parent },
1582 "children": children,
1583 "content_layer": "body",
1584 "meta": meta,
1585 "label": "picture",
1586 "prov": prov,
1587 "captions": captions,
1588 "references": [],
1589 "footnotes": [],
1590 "annotations": annotations,
1591 }),
1592 None => json!({
1593 "self_ref": self_ref,
1594 "parent": { "$ref": parent },
1595 "children": children,
1596 "content_layer": "body",
1597 "label": "picture",
1598 "prov": prov,
1599 "captions": captions,
1600 "references": [],
1601 "footnotes": [],
1602 "annotations": annotations,
1603 }),
1604 };
1605 if let Some(img) = image {
1610 let image = json!({
1611 "mimetype": img.mimetype,
1612 "dpi": 72,
1613 "size": { "width": img.width as f64, "height": img.height as f64 },
1614 "uri": img.data_uri(),
1615 });
1616 if let Some(obj) = item.as_object_mut() {
1617 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
1618 obj.insert("image".into(), image);
1619 obj.insert("annotations".into(), annotations);
1620 }
1621 }
1622 self.pictures.push(item);
1623 self_ref
1624 }
1625
1626 fn add_group(
1627 &mut self,
1628 label: &str,
1629 name: Option<&str>,
1630 layer: Option<ContentLayer>,
1631 nodes: &[Node],
1632 parent: &str,
1633 ) -> String {
1634 let self_ref = format!("#/groups/{}", self.groups.len());
1635 self.groups.push(Value::Null);
1636 let mark = (
1640 self.texts.len(),
1641 self.tables.len(),
1642 self.pictures.len(),
1643 self.groups.len(),
1644 );
1645 let children = self.walk_into(nodes, &self_ref);
1646 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
1647 let content_layer = layer.map_or("body", |l| l.value());
1648 self.groups[group_index(&self_ref)] = json!({
1649 "self_ref": self_ref,
1650 "parent": { "$ref": parent },
1651 "children": children,
1652 "content_layer": content_layer,
1653 "name": name,
1654 "label": label,
1655 });
1656 if layer.is_some() {
1657 let (t, tb, p, g) = mark;
1658 for item in self.texts[t..]
1659 .iter_mut()
1660 .chain(self.tables[tb..].iter_mut())
1661 .chain(self.pictures[p..].iter_mut())
1662 .chain(self.groups[g..].iter_mut())
1663 {
1664 if let Some(obj) = item.as_object_mut() {
1665 obj.insert("content_layer".into(), json!(content_layer));
1666 }
1667 }
1668 }
1669 self_ref
1670 }
1671
1672 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Value> {
1675 let seqs: Option<Vec<usize>> = nodes
1680 .iter()
1681 .map(|n| match n {
1682 Node::Prov { seq: Some(s), .. } => Some(*s),
1683 _ => None,
1684 })
1685 .collect();
1686 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
1687 let mut order: Vec<usize> = (0..nodes.len()).collect();
1688 order.sort_by_key(|&i| seqs[i]);
1689 let mut slots: Vec<Vec<Value>> = vec![Vec::new(); nodes.len()];
1690 for i in order {
1691 if let Some(r) = self.add_node(&nodes[i], parent) {
1692 slots[i].append(&mut self.pending_siblings);
1693 slots[i].push(json!({ "$ref": r }));
1694 slots[i].append(&mut self.pending_after);
1695 }
1696 if parent == "#/body" {
1697 slots[i].append(&mut self.pending_body);
1698 }
1699 }
1700 return slots.into_iter().flatten().collect();
1701 }
1702 let mut children = Vec::new();
1703 let mut i = 0;
1704 while i < nodes.len() {
1705 if matches!(nodes[i], Node::ListItem { .. }) {
1706 let start = i;
1707 i += 1;
1708 loop {
1709 match nodes.get(i) {
1710 Some(Node::ListItem { .. }) => i += 1,
1711 Some(Node::Paragraph { text })
1714 if text.is_empty()
1715 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
1716 {
1717 i += 1
1718 }
1719 _ => break,
1720 }
1721 }
1722 self.add_sibling_lists(&nodes[start..i], parent, &mut children);
1723 } else {
1724 if let Some(r) = self.add_node(&nodes[i], parent) {
1725 children.append(&mut self.pending_siblings);
1726 children.push(json!({ "$ref": r }));
1727 children.append(&mut self.pending_after);
1728 }
1729 i += 1;
1730 }
1731 if parent == "#/body" {
1734 children.append(&mut self.pending_body);
1735 }
1736 }
1737 children
1738 }
1739
1740 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
1745 let base = level_of(&run[0]);
1746 let mut seg = 0;
1747 for k in 0..run.len() {
1748 let Node::ListItem {
1749 first_in_list,
1750 level,
1751 ..
1752 } = &run[k]
1753 else {
1754 continue;
1755 };
1756 if *level != base {
1757 continue; }
1759 if k > seg && *first_in_list {
1760 out.push(json!({ "$ref": self.add_list(&run[seg..k], parent) }));
1761 seg = k;
1762 }
1763 }
1764 out.push(json!({ "$ref": self.add_list(&run[seg..], parent) }));
1765 }
1766}
1767
1768fn level_of(node: &Node) -> u8 {
1769 match node {
1770 Node::ListItem { level, .. } => *level,
1771 _ => 0,
1772 }
1773}
1774
1775fn group_index(self_ref: &str) -> usize {
1776 self_ref.rsplit('/').next().unwrap().parse().unwrap()
1777}
1778
1779fn ref_index(self_ref: &str) -> Option<usize> {
1780 self_ref.rsplit('/').next()?.parse().ok()
1781}
1782
1783fn merge(target: &mut Value, extra: Value) {
1785 if let (Some(t), Some(e)) = (target.as_object_mut(), extra.as_object()) {
1786 for (k, v) in e {
1787 t.insert(k.clone(), v.clone());
1788 }
1789 }
1790}
1791
1792fn unescape_text(s: &str) -> String {
1794 s.replace("<", "<")
1795 .replace(">", ">")
1796 .replace("&", "&")
1797 .replace("\\_", "_")
1798}
1799
1800fn fnv1a(s: &str) -> u64 {
1803 let mut h: u64 = 0xcbf29ce484222325;
1804 for b in s.bytes() {
1805 h ^= b as u64;
1806 h = h.wrapping_mul(0x100000001b3);
1807 }
1808 h
1809}
1810
1811#[cfg(test)]
1812mod tests {
1813 use crate::{
1814 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
1815 };
1816 use serde_json::Value;
1817
1818 fn doc_with_image() -> DoclingDocument {
1819 let mut doc = DoclingDocument::new("t");
1820 doc.push(Node::Picture {
1821 caption: Some("Fig 1".into()),
1822 caption_href: None,
1823 image: Some(PictureImage {
1824 mimetype: "image/png".into(),
1825 width: 4,
1826 height: 2,
1827 data: b"foobar".to_vec(),
1828 }),
1829 classification: None,
1830 caption_parent: Default::default(),
1831 });
1832 doc
1833 }
1834
1835 #[test]
1840 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
1841 let mut doc = DoclingDocument::new("t");
1842 doc.push(Node::Heading {
1843 level: 1,
1844 text: "Slide One".into(),
1845 });
1846 doc.push(Node::Furniture {
1847 layer: ContentLayer::Notes,
1848 inner: Box::new(Node::Located {
1849 location: [0, 0, 0, 0],
1850 inner: Box::new(Node::Paragraph {
1851 text: "Speaker note for slide 1.".into(),
1852 }),
1853 }),
1854 });
1855 doc.push(Node::Furniture {
1856 layer: ContentLayer::Furniture,
1857 inner: Box::new(Node::Paragraph {
1858 text: "page header".into(),
1859 }),
1860 });
1861
1862 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1863 let texts = v["texts"].as_array().unwrap();
1864 assert_eq!(
1865 texts
1866 .iter()
1867 .map(|t| (
1868 t["label"].as_str().unwrap(),
1869 t["content_layer"].as_str().unwrap(),
1870 t["text"].as_str().unwrap()
1871 ))
1872 .collect::<Vec<_>>(),
1873 vec![
1874 ("title", "body", "Slide One"),
1875 ("text", "notes", "Speaker note for slide 1."),
1876 ],
1877 "the note is carried on its own layer; the furniture is not carried"
1878 );
1879 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
1881 }
1882
1883 #[test]
1889 fn continuation_flags_become_spanning_cells() {
1890 let mut doc = DoclingDocument::new("t");
1891 let rows = vec![
1893 vec!["merged".to_string(), "merged".into(), "merged".into()],
1894 vec!["merged".to_string(), "merged".into(), "merged".into()],
1895 vec!["a".to_string(), "b".into(), "c".into()],
1896 ];
1897 doc.push(Node::Table(crate::Table {
1898 rows,
1899 location: None,
1900 structure: Some(crate::TableStructure {
1901 header_row: vec![true, false, false],
1902 col_continuation: vec![
1903 vec![false, true, true],
1904 vec![false, true, true],
1905 vec![false, false, false],
1906 ],
1907 row_continuation: vec![
1908 vec![false, false, false],
1909 vec![true, true, true],
1910 vec![false, false, false],
1911 ],
1912 row_header: Vec::new(),
1913 col_header: Vec::new(),
1914 }),
1915 cell_blocks: None,
1916 cells: None,
1917 caption: None,
1918 caption_parent: Default::default(),
1919 }));
1920 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1921 let data = &v["tables"][0]["data"];
1922 assert_eq!(data["num_rows"], 3);
1923 assert_eq!(data["num_cols"], 3);
1924 let cells = data["table_cells"].as_array().unwrap();
1925 assert_eq!(
1926 cells.len(),
1927 4,
1928 "one cell for the range, three for the plain row"
1929 );
1930 assert_eq!(
1931 cells[0],
1932 serde_json::json!({
1933 "row_span": 2, "col_span": 3,
1934 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
1935 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
1936 "text": "merged", "column_header": true, "row_header": false,
1937 "row_section": false, "fillable": false,
1938 })
1939 );
1940 assert_eq!(cells[1]["text"], "a");
1941 assert_eq!(cells[1]["row_span"], 1);
1942 assert_eq!(cells[1]["column_header"], false);
1943 let grid = data["grid"].as_array().unwrap();
1945 assert_eq!(grid.len(), 3);
1946 for (r, row) in grid.iter().take(2).enumerate() {
1947 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
1948 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
1949 }
1950 }
1951 assert_eq!(grid[2][2]["text"], "c");
1952 }
1953
1954 #[test]
1960 fn exact_provenance_pages_and_chart_captions_follow_docling() {
1961 let mut doc = DoclingDocument::new("t");
1962 doc.push(Node::PageInfo {
1963 page_no: 1,
1964 width: 3.0,
1965 height: 4.0,
1966 });
1967 let table = crate::Table {
1968 rows: vec![vec!["a".to_string(), "b".into()]],
1969 ..Default::default()
1970 };
1971 doc.push(Node::Group {
1972 label: "sheet".into(),
1973 name: Some("Data".into()),
1974 layer: None,
1975 children: vec![
1976 Node::Prov {
1981 page_no: 1,
1982 bbox: [0.0, 0.0, 3.0, 4.0],
1983 charspan: [0, 0],
1984 seq: Some(1),
1985 inner: Box::new(Node::Table(table.clone())),
1986 },
1987 Node::Prov {
1988 page_no: 1,
1989 bbox: [0.0, 1.0, 1.0, 1.0],
1990 charspan: [0, 0],
1991 seq: Some(0),
1992 inner: Box::new(Node::Chart {
1993 kind: "bar_chart".into(),
1994 table,
1995 caption: Some("Sales".into()),
1996 location: Some([0, 128, 170, 128]),
1997 }),
1998 },
1999 ],
2000 });
2001 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2002 assert_eq!(
2003 v["pages"],
2004 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2005 );
2006 assert_eq!(
2007 v["tables"][0]["prov"],
2008 serde_json::json!([{
2009 "page_no": 1,
2010 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2011 "charspan": [0, 0],
2012 }])
2013 );
2014 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2015 let sheet = &v["groups"][0];
2018 assert_eq!(
2019 sheet["children"],
2020 serde_json::json!([
2021 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2022 ])
2023 );
2024 let cap = &v["texts"][0];
2025 assert_eq!(cap["label"], "caption");
2026 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2027 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2028 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2029 let pic = &v["pictures"][0];
2030 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2031 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2032 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2033 assert_eq!(
2034 pic["meta"]["classification"]["predictions"][0]["class_name"],
2035 "bar_chart"
2036 );
2037 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2038 }
2039
2040 #[test]
2044 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2045 let mut doc = DoclingDocument::new("t");
2046 doc.push(Node::PageInfo {
2047 page_no: 1,
2048 width: 12192000.0,
2049 height: 6858000.0,
2050 });
2051 doc.push(Node::Furniture {
2052 layer: ContentLayer::Notes,
2053 inner: Box::new(Node::Located {
2054 location: [0, 0, 0, 0],
2055 inner: Box::new(Node::Paragraph {
2056 text: "a note".into(),
2057 }),
2058 }),
2059 });
2060 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2061 let prov = &v["texts"][0]["prov"][0];
2062 assert_eq!(prov["page_no"], 1);
2063 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2064 assert_eq!(
2065 prov["bbox"],
2066 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2067 );
2068 assert_eq!(
2070 v["pages"]["1"]["size"],
2071 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2072 );
2073 }
2074
2075 #[test]
2081 fn page_markers_produce_pages_and_prov() {
2082 let mut doc = DoclingDocument::new("t");
2083 doc.push(Node::PageInfo {
2084 page_no: 1,
2085 width: 512.0,
2086 height: 1024.0,
2087 });
2088 doc.push(Node::Located {
2089 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
2091 text: "hello".into(),
2092 }),
2093 });
2094 doc.push(Node::Table(Table {
2095 rows: vec![vec!["a".into()]],
2096 location: Some([0, 0, 512, 512]),
2097 ..Table::default()
2098 }));
2099 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2100 assert_eq!(v["pages"]["1"]["page_no"], 1);
2101 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
2102 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
2103 let prov = &v["texts"][0]["prov"][0];
2106 assert_eq!(prov["page_no"], 1);
2107 assert_eq!(prov["bbox"]["l"], 128.0);
2108 assert_eq!(prov["bbox"]["t"], 896.0);
2109 assert_eq!(prov["bbox"]["r"], 256.0);
2110 assert_eq!(prov["bbox"]["b"], 768.0);
2111 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
2112 assert_eq!(prov["charspan"][1], 5);
2113 let tprov = &v["tables"][0]["prov"][0];
2115 assert_eq!(tprov["bbox"]["t"], 1024.0);
2116 assert_eq!(tprov["bbox"]["b"], 0.0);
2117 assert_eq!(tprov["charspan"][1], 0);
2118
2119 let mut plain = DoclingDocument::new("t");
2121 plain.push(Node::Located {
2122 location: [1, 2, 3, 4],
2123 inner: Box::new(Node::Paragraph { text: "x".into() }),
2124 });
2125 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2126 assert_eq!(v["pages"], serde_json::json!({}));
2127 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2128 }
2129
2130 #[test]
2131 fn picture_image_in_markdown_modes_and_json() {
2132 let doc = doc_with_image();
2133 assert!(doc.export_to_markdown().contains("<!-- image -->"));
2135 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
2137 assert!(
2138 md.contains(""),
2139 "got:\n{md}"
2140 );
2141 assert!(files.is_empty());
2142 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
2144 assert!(
2145 md.contains(""),
2146 "got:\n{md}"
2147 );
2148 assert_eq!(
2149 files,
2150 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
2151 );
2152 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2155 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
2156 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
2157 let keys: Vec<&str> = v["pictures"][0]
2158 .as_object()
2159 .unwrap()
2160 .keys()
2161 .map(String::as_str)
2162 .collect();
2163 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
2164 assert_eq!(
2165 v["pictures"][0]["image"]["uri"],
2166 "data:image/png;base64,Zm9vYmFy"
2167 );
2168 }
2169
2170 #[test]
2171 fn exports_docling_schema() {
2172 let mut doc = DoclingDocument::new("t");
2173 doc.push(Node::Heading {
2174 level: 1,
2175 text: "Title".into(),
2176 });
2177 doc.push(Node::Heading {
2178 level: 2,
2179 text: "Sec".into(),
2180 });
2181 doc.push(Node::Paragraph {
2182 text: "Body & more".into(),
2183 }); doc.push(Node::ListItem {
2185 ordered: false,
2186 number: 0,
2187 first_in_list: true,
2188 text: "one".into(),
2189 level: 0,
2190 marker: None,
2191 location: None,
2192 dclx: None,
2193 href: None,
2194 layer: None,
2195 });
2196 doc.push(Node::ListItem {
2197 ordered: false,
2198 number: 0,
2199 first_in_list: false,
2200 text: "two".into(),
2201 level: 0,
2202 marker: None,
2203 location: None,
2204 dclx: None,
2205 href: None,
2206 layer: None,
2207 });
2208 doc.push(Node::Table(Table {
2209 rows: vec![vec!["A".into(), "B".into()]],
2210 location: None,
2211 structure: None,
2212 cell_blocks: None,
2213 cells: None,
2214 caption: None,
2215 caption_parent: Default::default(),
2216 }));
2217
2218 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2219 assert_eq!(v["schema_name"], "DoclingDocument");
2220 assert_eq!(v["version"], "1.10.0");
2221 assert_eq!(v["texts"][0]["label"], "title");
2222 assert_eq!(v["texts"][1]["label"], "section_header");
2223 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
2227 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
2228 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
2229 assert_eq!(v["texts"][3]["marker"], "-");
2230 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
2232 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
2233 }
2234 #[test]
2238 fn a_layered_group_stamps_its_whole_subtree() {
2239 let doc = DoclingDocument {
2240 name: "s".into(),
2241 nodes: vec![
2242 Node::Group {
2243 label: "sheet".into(),
2244 name: Some("Sheet1".into()),
2245 layer: None,
2246 children: vec![Node::Paragraph {
2247 text: "visible".into(),
2248 }],
2249 },
2250 Node::Group {
2251 label: "sheet".into(),
2252 name: Some("Sheet2".into()),
2253 layer: Some(ContentLayer::Invisible),
2254 children: vec![Node::Paragraph {
2255 text: "hidden".into(),
2256 }],
2257 },
2258 ],
2259 ..DoclingDocument::new("s")
2260 };
2261 let v = crate::json::to_json(&doc);
2262 assert_eq!(v["groups"][0]["label"], "sheet");
2263 assert_eq!(v["groups"][0]["name"], "Sheet1");
2264 assert_eq!(v["groups"][0]["content_layer"], "body");
2265 assert_eq!(v["texts"][0]["content_layer"], "body");
2266 assert_eq!(v["groups"][1]["name"], "Sheet2");
2267 assert_eq!(v["groups"][1]["content_layer"], "invisible");
2268 assert_eq!(v["texts"][1]["content_layer"], "invisible");
2269 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
2271 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
2272 }
2273
2274 #[test]
2284 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
2285 use crate::tree::{ItemTree, TreeKind};
2286 let mut t = ItemTree::default();
2287 let text = |txt: &str| TreeKind::Text {
2288 label: "text".into(),
2289 text: txt.into(),
2290 orig: None,
2291 formatting: None,
2292 hyperlink: None,
2293 level: None,
2294 list: None,
2295 };
2296 let a = t.add(None, None, text("a"));
2297 let blank = t.add(None, None, text(""));
2298 let b = t.add(None, None, text("b"));
2299 t.delete(blank);
2300 let group = t.add(
2301 None,
2302 Some(ContentLayer::Notes),
2303 TreeKind::Group {
2304 label: "comment_section".into(),
2305 name: "comment-0".into(),
2306 },
2307 );
2308 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
2309 t.items[a].comments.push(group);
2310 t.add(
2311 None,
2312 None,
2313 TreeKind::Picture {
2314 captions: Vec::new(),
2315 image: None,
2316 classification: Some("bar_chart".into()),
2317 chart: Some(Table {
2318 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
2319 ..Table::default()
2320 }),
2321 dpi: None,
2322 },
2323 );
2324 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
2325 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
2326 let mut doc = DoclingDocument::new("t");
2327 doc.tree = Some(t);
2328 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2329 let texts = v["texts"].as_array().unwrap();
2330 assert_eq!(texts.len(), 3);
2331 assert_eq!(texts[1]["text"], "b");
2332 assert_eq!(texts[1]["self_ref"], "#/texts/1");
2333 assert_eq!(
2334 v["body"]["children"],
2335 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
2336 );
2337 let keys: Vec<&str> = texts[0]
2338 .as_object()
2339 .unwrap()
2340 .keys()
2341 .map(String::as_str)
2342 .collect();
2343 assert_eq!(
2344 keys,
2345 vec![
2346 "self_ref",
2347 "parent",
2348 "children",
2349 "content_layer",
2350 "label",
2351 "prov",
2352 "comments",
2353 "orig",
2354 "text"
2355 ]
2356 );
2357 assert_eq!(
2358 texts[0]["comments"],
2359 serde_json::json!([{"$ref": "#/groups/0"}])
2360 );
2361 assert!(texts[1].get("comments").is_none());
2362 let meta = &v["pictures"][0]["meta"];
2363 assert_eq!(
2364 meta["classification"]["predictions"][0]["class_name"],
2365 "bar_chart"
2366 );
2367 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
2368 }
2369
2370 #[test]
2376 fn tree_items_carry_exact_provenance_and_dpi() {
2377 use crate::tree::{ItemTree, TreeKind, TreeProv};
2378 let text = |label: &str, t: &str| TreeKind::Text {
2379 label: label.into(),
2380 text: t.into(),
2381 orig: None,
2382 formatting: None,
2383 hyperlink: None,
2384 level: None,
2385 list: None,
2386 };
2387 let mut t = ItemTree::default();
2388 let slide = t.add(
2389 None,
2390 None,
2391 TreeKind::Group {
2392 label: "chapter".into(),
2393 name: "slide-0".into(),
2394 },
2395 );
2396 t.add_with_prov(
2397 Some(slide),
2398 None,
2399 text("paragraph", "héllo"),
2400 TreeProv {
2401 page_no: 1,
2402 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
2403 bottom_left: true,
2404 charspan: [0, 5],
2405 },
2406 );
2407 t.add_with_prov(
2408 Some(slide),
2409 None,
2410 TreeKind::Picture {
2411 captions: Vec::new(),
2412 image: Some(crate::PictureImage {
2413 mimetype: "image/png".into(),
2414 width: 2,
2415 height: 2,
2416 data: vec![0],
2417 }),
2418 classification: None,
2419 chart: None,
2420 dpi: Some(300),
2421 },
2422 TreeProv {
2423 page_no: 1,
2424 bbox: [0.0; 4],
2425 bottom_left: false,
2426 charspan: [0, 0],
2427 },
2428 );
2429 t.add(
2430 Some(slide),
2431 Some(ContentLayer::Notes),
2432 text("text", "no geometry"),
2433 );
2434 let mut doc = DoclingDocument::new("t");
2435 doc.push(Node::PageInfo {
2436 page_no: 1,
2437 width: 9144000.0,
2438 height: 6858000.0,
2439 });
2440 doc.tree = Some(t);
2441 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2442 assert_eq!(
2443 v["texts"][0]["prov"],
2444 serde_json::json!([{
2445 "page_no": 1,
2446 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
2447 "charspan": [0, 5],
2448 }])
2449 );
2450 assert_eq!(v["texts"][0]["label"], "paragraph");
2451 assert_eq!(
2452 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
2453 "TOPLEFT"
2454 );
2455 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
2456 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
2457 assert_eq!(v["texts"][1]["content_layer"], "notes");
2458 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
2459 assert_eq!(v["pages"]["1"]["page_no"], 1);
2460 }
2461
2462 #[test]
2467 fn provenance_boxes_are_clamped_to_their_page() {
2468 let mut doc = DoclingDocument::new("t");
2469 doc.push(Node::PageInfo {
2470 page_no: 1,
2471 width: 10.0,
2472 height: 8.0,
2473 });
2474 doc.push(Node::Prov {
2475 page_no: 1,
2476 bbox: [-1.0, 2.0, 12.0, 9.5],
2477 charspan: [0, 1],
2478 seq: None,
2479 inner: Box::new(Node::Paragraph { text: "x".into() }),
2480 });
2481 let mut table = Table {
2482 rows: vec![vec!["a".into()]],
2483 ..Table::default()
2484 };
2485 table.cells = Some(vec![crate::TableCell {
2486 text: "a".into(),
2487 bbox: Some([1.0, 1.0, 11.0, 9.0]),
2488 start_row: 0,
2489 start_col: 0,
2490 row_span: 1,
2491 col_span: 1,
2492 column_header: false,
2493 row_header: false,
2494 row_section: false,
2495 }]);
2496 doc.push(Node::Prov {
2497 page_no: 1,
2498 bbox: [0.0, 0.0, 10.0, 8.0],
2499 charspan: [0, 0],
2500 seq: None,
2501 inner: Box::new(Node::Table(table)),
2502 });
2503 doc.push(Node::Prov {
2504 page_no: 7,
2505 bbox: [-5.0, 0.0, 50.0, 50.0],
2506 charspan: [0, 1],
2507 seq: None,
2508 inner: Box::new(Node::Paragraph { text: "y".into() }),
2509 });
2510 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2511 assert_eq!(
2512 v["texts"][0]["prov"][0]["bbox"],
2513 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
2514 );
2515 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
2516 assert_eq!(
2517 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
2518 (Some(1.0), Some(10.0), Some(8.0))
2519 );
2520 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
2521 assert_eq!(
2522 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
2523 "page 7 is not described"
2524 );
2525 }
2526
2527 #[test]
2528 fn a_backend_item_tree_is_written_verbatim() {
2529 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
2530 let mut t = ItemTree::default();
2531 let text = |label: &str, txt: &str| TreeKind::Text {
2532 label: label.into(),
2533 text: txt.into(),
2534 orig: None,
2535 formatting: None,
2536 hyperlink: None,
2537 level: None,
2538 list: None,
2539 };
2540 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
2541 let h = t.add(None, None, text("title", "Heading"));
2542 let group = t.add(
2543 Some(h),
2544 None,
2545 TreeKind::Group {
2546 label: "inline".into(),
2547 name: "group".into(),
2548 },
2549 );
2550 t.add(
2551 Some(group),
2552 None,
2553 TreeKind::Text {
2554 label: "text".into(),
2555 text: "bold".into(),
2556 orig: None,
2557 formatting: Some(Formatting {
2558 bold: true,
2559 ..Formatting::default()
2560 }),
2561 hyperlink: Some("https://example.com/".into()),
2562 level: None,
2563 list: None,
2564 },
2565 );
2566 t.add(
2567 Some(group),
2568 None,
2569 TreeKind::Code {
2570 text: "x = 1".into(),
2571 orig: None,
2572 language: Some("python".into()),
2573 formatting: None,
2574 hyperlink: None,
2575 },
2576 );
2577 let sub = t.add(
2578 Some(h),
2579 None,
2580 TreeKind::Text {
2581 label: "section_header".into(),
2582 text: "Sub".into(),
2583 orig: Some("Sub\u{2019}".into()),
2584 formatting: None,
2585 hyperlink: None,
2586 level: Some(1),
2587 list: None,
2588 },
2589 );
2590 t.add(
2591 Some(sub),
2592 None,
2593 TreeKind::Text {
2594 label: "list_item".into(),
2595 text: "item".into(),
2596 orig: None,
2597 formatting: None,
2598 hyperlink: None,
2599 level: None,
2600 list: Some(ListMeta {
2601 enumerated: true,
2602 marker: "3.".into(),
2603 }),
2604 },
2605 );
2606 let _region = t.add(
2607 Some(sub),
2608 None,
2609 TreeKind::FieldRegion {
2610 items: vec![crate::FieldItem {
2611 marker: None,
2612 key: Some("Name".into()),
2613 value: Some("Duck".into()),
2614 value_kind: Some("read_only".into()),
2615 }],
2616 },
2617 );
2618 let table = t.add(
2619 Some(sub),
2620 None,
2621 TreeKind::Table {
2622 table: Table {
2623 rows: vec![vec!["a \n<".into(), "b".into()]],
2624 cells: Some(vec![
2625 crate::TableCell {
2626 text: "a \n<".into(),
2627 bbox: None,
2628 start_row: 0,
2629 start_col: 0,
2630 row_span: 3,
2631 col_span: 1,
2632 column_header: false,
2633 row_header: true,
2634 row_section: false,
2635 },
2636 crate::TableCell {
2637 text: "b".into(),
2638 bbox: None,
2639 start_row: 0,
2640 start_col: 1,
2641 row_span: 1,
2642 col_span: 1,
2643 column_header: false,
2644 row_header: false,
2645 row_section: false,
2646 },
2647 ]),
2648 ..Table::default()
2649 },
2650 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
2652 },
2653 );
2654 let cell_group = t.add(
2655 Some(table),
2656 None,
2657 TreeKind::Group {
2658 label: "unspecified".into(),
2659 name: "rich_cell_group_1_0_0".into(),
2660 },
2661 );
2662 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
2663 *rich_cells = vec![(0, 1, cell_group)];
2664 }
2665 let after = t.add(Some(sub), None, text("text", "after the region"));
2666 let _ = (title, after);
2667
2668 let doc = DoclingDocument {
2669 tree: Some(t),
2670 ..DoclingDocument::new("t")
2671 };
2672 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2673 let texts: Vec<&str> = v["texts"]
2675 .as_array()
2676 .unwrap()
2677 .iter()
2678 .map(|t| t["text"].as_str().unwrap())
2679 .collect();
2680 assert_eq!(
2681 texts,
2682 [
2683 "Page",
2684 "Heading",
2685 "bold",
2686 "x = 1",
2687 "Sub",
2688 "item",
2689 "Name",
2690 "Duck",
2691 "after the region"
2692 ]
2693 );
2694 assert_eq!(
2695 v["body"]["children"],
2696 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
2697 );
2698 assert_eq!(v["texts"][0]["content_layer"], "furniture");
2699 assert_eq!(
2700 v["texts"][1]["children"],
2701 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
2702 );
2703 let bold = &v["texts"][2];
2704 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
2705 let keys: Vec<&str> = bold
2706 .as_object()
2707 .unwrap()
2708 .keys()
2709 .map(String::as_str)
2710 .collect();
2711 assert_eq!(
2712 keys,
2713 [
2714 "self_ref",
2715 "parent",
2716 "children",
2717 "content_layer",
2718 "label",
2719 "prov",
2720 "orig",
2721 "text",
2722 "formatting",
2723 "hyperlink"
2724 ]
2725 );
2726 assert_eq!(
2727 bold["formatting"],
2728 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
2729 );
2730 let code = &v["texts"][3];
2731 assert_eq!(code["label"], "code");
2732 assert_eq!(code["code_language"], "Python");
2733 let sub = &v["texts"][4];
2734 assert_eq!(sub["orig"], "Sub\u{2019}");
2735 assert_eq!(sub["level"], 1);
2736 let item = &v["texts"][5];
2737 let keys: Vec<&str> = item
2738 .as_object()
2739 .unwrap()
2740 .keys()
2741 .map(String::as_str)
2742 .collect();
2743 assert_eq!(
2744 keys,
2745 [
2746 "self_ref",
2747 "parent",
2748 "children",
2749 "content_layer",
2750 "label",
2751 "prov",
2752 "orig",
2753 "text",
2754 "enumerated",
2755 "marker"
2756 ]
2757 );
2758 assert_eq!(item["marker"], "3.");
2759 assert_eq!(v["texts"][7]["kind"], "read_only");
2760 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
2761 let table = &v["tables"][0];
2762 assert_eq!(
2763 table["children"],
2764 serde_json::json!([{"$ref": "#/groups/1"}])
2765 );
2766 let cells = table["data"]["table_cells"].as_array().unwrap();
2767 assert_eq!(
2768 cells[0]["text"], "a \n<",
2769 "raw cell text is written verbatim"
2770 );
2771 assert_eq!(
2772 cells[0]["end_row_offset_idx"], 3,
2773 "declared spans are not clamped"
2774 );
2775 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
2776 assert!(cells[0].get("ref").is_none());
2777 assert!(
2778 table["data"]["grid"][0][1].get("ref").is_none(),
2779 "the grid shows plain cells"
2780 );
2781 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
2782 }
2783
2784 #[test]
2788 fn a_comment_section_can_be_referenced_by_its_note_text() {
2789 let doc = DoclingDocument {
2790 name: "c".into(),
2791 nodes: vec![
2792 Node::Commented {
2793 comments: vec![0],
2794 inner: Box::new(Node::Paragraph {
2795 text: "annotated".into(),
2796 }),
2797 },
2798 Node::CommentSection {
2799 name: "comment-Sheet1-A1".into(),
2800 text: "[author: A]: note".into(),
2801 refs_note_text: true,
2802 grouped: true,
2803 },
2804 ],
2805 ..DoclingDocument::new("c")
2806 };
2807 let v = crate::json::to_json(&doc);
2808 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
2809 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
2810 }
2811
2812 #[test]
2816 fn comment_sections_link_back_to_their_items() {
2817 let doc = DoclingDocument {
2818 name: "c".into(),
2819 nodes: vec![
2820 Node::Commented {
2821 comments: vec![0],
2822 inner: Box::new(Node::Paragraph {
2823 text: "annotated".into(),
2824 }),
2825 },
2826 Node::Paragraph {
2827 text: "plain".into(),
2828 },
2829 Node::CommentSection {
2830 name: "comment-7".into(),
2831 text: "[time: t]: note".into(),
2832 refs_note_text: false,
2833 grouped: true,
2834 },
2835 ],
2836 ..DoclingDocument::new("c")
2837 };
2838 let v = crate::json::to_json(&doc);
2839 assert_eq!(v["groups"][0]["label"], "comment_section");
2841 assert_eq!(v["groups"][0]["name"], "comment-7");
2842 assert_eq!(v["groups"][0]["content_layer"], "notes");
2843 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
2844 assert_eq!(v["texts"][2]["content_layer"], "notes");
2845 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
2847 assert!(v["texts"][1].get("comments").is_none());
2848 let keys: Vec<&str> = v["texts"][0]
2850 .as_object()
2851 .unwrap()
2852 .keys()
2853 .map(String::as_str)
2854 .collect();
2855 assert_eq!(
2856 &keys[keys.len() - 4..],
2857 &["prov", "comments", "orig", "text"]
2858 );
2859 }
2860
2861 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
2862 Node::Picture {
2863 caption: Some(caption.into()),
2864 caption_href: None,
2865 image: None,
2866 classification: None,
2867 caption_parent,
2868 }
2869 }
2870
2871 fn group(children: Vec<Node>) -> Node {
2872 Node::Group {
2873 label: "section".into(),
2874 name: None,
2875 layer: None,
2876 children,
2877 }
2878 }
2879
2880 fn refs(v: &Value) -> Vec<&str> {
2881 v.as_array()
2882 .unwrap()
2883 .iter()
2884 .map(|r| r["$ref"].as_str().unwrap())
2885 .collect()
2886 }
2887
2888 #[test]
2893 fn a_body_caption_follows_the_enclosing_top_level_item() {
2894 let mut doc = DoclingDocument::new("t");
2895 doc.push(picture("top", CaptionParent::Body));
2896 doc.push(group(vec![
2897 Node::Paragraph { text: "p".into() },
2898 picture("nested", CaptionParent::Body),
2899 ]));
2900 doc.push(Node::Paragraph {
2901 text: "after".into(),
2902 });
2903 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2904 assert_eq!(
2905 refs(&v["body"]["children"]),
2906 [
2907 "#/texts/0",
2908 "#/pictures/0",
2909 "#/groups/0",
2910 "#/texts/2",
2911 "#/texts/3"
2912 ]
2913 );
2914 assert_eq!(
2915 refs(&v["groups"][0]["children"]),
2916 ["#/texts/1", "#/pictures/1"]
2917 );
2918 for (cap, pic) in [(0, 0), (2, 1)] {
2919 assert_eq!(v["texts"][cap]["label"], "caption");
2920 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
2921 assert_eq!(
2922 refs(&v["pictures"][pic]["captions"]),
2923 [format!("#/texts/{cap}")]
2924 );
2925 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
2926 }
2927 }
2928
2929 #[test]
2932 fn an_item_caption_is_the_items_first_child() {
2933 let mut doc = DoclingDocument::new("t");
2934 doc.push(picture("fig", CaptionParent::Item));
2935 doc.push(Node::Table(Table {
2936 rows: vec![vec!["a".into()]],
2937 caption: Some("tab".into()),
2938 caption_parent: CaptionParent::Item,
2939 ..Table::default()
2940 }));
2941 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2942 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
2943 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
2944 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
2945 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
2946 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
2947 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
2948 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
2949 }
2950
2951 #[test]
2955 fn a_container_caption_is_the_items_sibling() {
2956 let mut doc = DoclingDocument::new("t");
2957 doc.push(group(vec![
2958 picture("chart", CaptionParent::Container),
2959 Node::Table(Table {
2960 rows: vec![vec!["a".into()]],
2961 caption: Some("figcaption".into()),
2962 caption_parent: CaptionParent::ContainerAfter,
2963 ..Table::default()
2964 }),
2965 ]));
2966 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2967 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
2968 assert_eq!(
2969 refs(&v["groups"][0]["children"]),
2970 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
2971 );
2972 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
2973 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
2974 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
2975 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
2976 }
2977}