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 num_rows = t.rows.len();
418 let num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
419 let mut grid = Vec::with_capacity(num_rows);
420 let mut cells = Vec::new();
421 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
425 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
426 for c in first_class {
427 let idx = cells.len();
428 cells.push(cell_value(
429 c.row_span,
430 c.col_span,
431 c.start_row,
432 c.start_row + c.row_span,
433 c.start_col,
434 c.start_col + c.col_span,
435 cell_text(&c.text),
436 c.column_header,
437 c.row_header,
438 c.row_section,
439 c.bbox,
440 ));
441 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
442 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
443 slot[r * num_cols + k] = Some(idx);
444 }
445 }
446 }
447 for r in 0..num_rows {
448 let mut grid_row = Vec::with_capacity(num_cols);
449 for c in 0..num_cols {
450 grid_row.push(match slot[r * num_cols + c] {
451 Some(i) => cells[i].clone(),
452 None => cell_value(
453 1,
454 1,
455 r,
456 r + 1,
457 c,
458 c + 1,
459 String::new(),
460 false,
461 false,
462 false,
463 None,
464 ),
465 });
466 }
467 grid.push(grid_row);
468 }
469 } else {
470 let s = t.structure.as_ref();
471 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
472 grid.and_then(|g| g.get(r))
473 .and_then(|row| row.get(c))
474 .copied()
475 .unwrap_or(false)
476 };
477 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
478 let (mut r0, mut c0) = (r, c);
479 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
480 c0 -= 1;
481 }
482 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
483 r0 -= 1;
484 }
485 (r0, c0)
486 };
487 let anchors: Vec<(usize, usize)> = (0..num_rows)
490 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
491 .map(|(r, c)| anchor_of(r, c))
492 .collect();
493 let mut extent: Vec<(usize, usize)> = (0..num_rows)
494 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
495 .collect();
496 for (i, &(ar, ac)) in anchors.iter().enumerate() {
497 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
498 let e = &mut extent[ar * num_cols + ac];
499 e.0 = e.0.max(r);
500 e.1 = e.1.max(c);
501 }
502 for (r, row) in t.rows.iter().enumerate() {
503 let mut grid_row = Vec::with_capacity(num_cols);
504 for c in 0..num_cols {
505 let (ar, ac) = anchors[r * num_cols + c];
506 if (ar, ac) == (r, c) {
507 let (er, ec) = extent[r * num_cols + c];
508 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
509 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
510 Some(s) => flag(Some(&s.col_header), r, c),
511 None => r == 0,
512 };
513 slot[r * num_cols + c] = Some(cells.len());
514 cells.push(cell_value(
515 er - r + 1,
516 ec - c + 1,
517 r,
518 er + 1,
519 c,
520 ec + 1,
521 text,
522 column_header,
523 flag(s.map(|s| &s.row_header), r, c),
524 false,
525 None,
526 ));
527 }
528 grid_row.push(match slot[ar * num_cols + ac] {
529 Some(i) => cells[i].clone(),
530 None => Value::Null,
531 });
532 }
533 grid.push(grid_row);
534 }
535 }
536 json!({
537 "table_cells": cells,
538 "num_rows": num_rows,
539 "num_cols": num_cols,
540 "orientation": "rot_0",
541 "grid": grid,
542 })
543}
544
545#[derive(Default)]
546struct Builder {
547 texts: Vec<Value>,
548 groups: Vec<Value>,
549 tables: Vec<Value>,
550 pictures: Vec<Value>,
551 field_regions: Vec<Value>,
552 field_items: Vec<Value>,
553 pages: Vec<(usize, f64, f64)>,
557 cur_page: usize,
559 cur_w: f64,
560 cur_h: f64,
561 pending_loc: Option<[u16; 4]>,
564 pending_exact: Option<ExactProv>,
567 pending_siblings: Vec<Value>,
571 pending_after: Vec<Value>,
575 pending_body: Vec<Value>,
579 comment_groups: Vec<String>,
583 pending_comments: Vec<(String, Vec<usize>)>,
587}
588
589impl Builder {
590 fn take_prov(&mut self, char_len: usize) -> Value {
596 let prov = self.prov_json(char_len, false);
597 self.pending_exact = None;
598 self.pending_loc = None;
599 prov
600 }
601
602 fn prov_json(&self, char_len: usize, span_over_text: bool) -> Value {
607 let r2 = |v: f64| (v * 100.0).round() / 100.0;
608 if let Some(ExactProv {
609 page_no,
610 bbox: [l, t, r, b],
611 bottom_left,
612 charspan,
613 }) = self.pending_exact
614 {
615 let charspan = if span_over_text {
616 [0, char_len]
617 } else {
618 charspan
619 };
620 return json!([{
621 "page_no": page_no,
622 "bbox": {
623 "l": r2(l), "t": r2(t), "r": r2(r), "b": r2(b),
624 "coord_origin": if bottom_left { "BOTTOMLEFT" } else { "TOPLEFT" },
625 },
626 "charspan": charspan,
627 }]);
628 }
629 let Some([x0, y0, x1, y1]) = self.pending_loc else {
630 return json!([]);
631 };
632 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
636 return json!([{
637 "page_no": self.cur_page,
638 "bbox": { "l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT" },
639 "charspan": [0, char_len],
640 }]);
641 }
642 json!([{
643 "page_no": self.cur_page,
644 "bbox": {
645 "l": r2(x0 as f64 * self.cur_w / 512.0),
646 "t": r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
647 "r": r2(x1 as f64 * self.cur_w / 512.0),
648 "b": r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
649 "coord_origin": "BOTTOMLEFT",
650 },
651 "charspan": [0, char_len],
652 }])
653 }
654
655 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
658 if self.pending_loc.is_none() && self.cur_page > 0 {
659 self.pending_loc = loc;
660 }
661 }
662
663 fn link_comments(&mut self) {
671 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
672 .into_iter()
673 .map(|(item, comments)| {
674 let refs = comments
675 .iter()
676 .filter_map(|i| self.comment_groups.get(*i))
677 .map(|r| json!({ "$ref": r }))
678 .collect();
679 (item, refs)
680 })
681 .collect();
682 for (item, comment_refs) in refs {
683 if comment_refs.is_empty() {
684 continue;
685 }
686 let Some(target) = self.item_mut(&item) else {
687 continue;
688 };
689 let Some(obj) = target.as_object_mut() else {
690 continue;
691 };
692 let tail: Vec<(String, Value)> = obj
693 .iter()
694 .skip_while(|(k, _)| k.as_str() != "prov")
695 .skip(1)
696 .map(|(k, v)| (k.clone(), v.clone()))
697 .collect();
698 for (k, _) in &tail {
699 obj.shift_remove(k);
700 }
701 obj.insert("comments".into(), Value::Array(comment_refs));
702 for (k, v) in tail {
703 obj.insert(k, v);
704 }
705 }
706 }
707
708 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
710 let idx = ref_index(self_ref)?;
711 let bucket = if self_ref.starts_with("#/texts/") {
712 &mut self.texts
713 } else if self_ref.starts_with("#/tables/") {
714 &mut self.tables
715 } else if self_ref.starts_with("#/pictures/") {
716 &mut self.pictures
717 } else if self_ref.starts_with("#/groups/") {
718 &mut self.groups
719 } else {
720 return None;
721 };
722 bucket.get_mut(idx)
723 }
724
725 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
729 use crate::tree::TreeKind;
730 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
733 let (mut nt, mut ng, mut ntb, mut np, mut nf) = (0, 0, 0, 0, 0);
734 for item in &tree.items {
735 if item.deleted {
736 refs.push(String::new());
737 continue;
738 }
739 let r = match &item.kind {
740 TreeKind::Text { .. } | TreeKind::Code { .. } => {
741 nt += 1;
742 format!("#/texts/{}", nt - 1)
743 }
744 TreeKind::Group { .. } => {
745 ng += 1;
746 format!("#/groups/{}", ng - 1)
747 }
748 TreeKind::Table { .. } => {
749 ntb += 1;
750 format!("#/tables/{}", ntb - 1)
751 }
752 TreeKind::Picture { .. } => {
753 np += 1;
754 format!("#/pictures/{}", np - 1)
755 }
756 TreeKind::FieldRegion { items } => {
757 nt += items
760 .iter()
761 .map(|i| {
762 [&i.marker, &i.key, &i.value]
763 .iter()
764 .filter(|p| p.is_some())
765 .count()
766 })
767 .sum::<usize>();
768 nf += 1;
769 format!("#/field_regions/{}", nf - 1)
770 }
771 };
772 refs.push(r);
773 }
774 let ref_of = |id: usize| json!({ "$ref": refs[id] });
775 for (id, item) in tree.items.iter().enumerate() {
776 if item.deleted {
777 continue;
778 }
779 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
780 let children: Vec<Value> = item.children.iter().map(|&c| ref_of(c)).collect();
781 let layer = item.layer.map_or("body", |l| l.value());
782 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
785 let self_ref = match &item.kind {
786 TreeKind::Text {
787 label,
788 text,
789 orig,
790 formatting,
791 hyperlink,
792 level,
793 list,
794 } => {
795 let mut tail = serde_json::Map::new();
798 if let Some(f) = formatting {
799 tail.insert("formatting".into(), formatting_json(f));
800 }
801 if let Some(h) = hyperlink {
802 tail.insert("hyperlink".into(), json!(h));
803 }
804 if let Some(l) = level {
805 tail.insert("level".into(), json!(l));
806 }
807 if let Some(l) = list {
808 tail.insert("enumerated".into(), json!(l.enumerated));
809 tail.insert("marker".into(), json!(l.marker));
810 }
811 let r = format!("#/texts/{}", self.texts.len());
812 let prov = self.take_prov(text.chars().count());
813 let mut item_json = json!({
814 "self_ref": r,
815 "parent": { "$ref": parent },
816 "children": children,
817 "content_layer": layer,
818 "label": label,
819 "prov": prov,
820 });
821 if !item.comments.is_empty() {
824 item_json["comments"] =
825 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
826 }
827 if let Some(track) = &item.source {
830 item_json["source"] = json!([track_json(track)]);
831 }
832 merge(
833 &mut item_json,
834 json!({
835 "orig": orig.as_deref().unwrap_or(text),
836 "text": text,
837 }),
838 );
839 merge(&mut item_json, Value::Object(tail));
840 self.texts.push(item_json);
841 r
842 }
843 TreeKind::Code {
844 text,
845 orig,
846 language,
847 formatting,
848 hyperlink,
849 } => {
850 let r = format!("#/texts/{}", self.texts.len());
851 let prov = self.take_prov(text.chars().count());
852 let mut item_json = json!({
853 "self_ref": r,
854 "parent": { "$ref": parent },
855 "children": children,
856 "content_layer": layer,
857 "label": "code",
858 "prov": prov,
859 });
860 if !item.comments.is_empty() {
861 item_json["comments"] =
862 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
863 }
864 merge(
865 &mut item_json,
866 json!({
867 "orig": orig.as_deref().unwrap_or(text),
868 "text": text,
869 }),
870 );
871 if let Some(f) = formatting {
872 item_json["formatting"] = formatting_json(f);
873 }
874 if let Some(h) = hyperlink {
875 item_json["hyperlink"] = json!(h);
876 }
877 merge(
878 &mut item_json,
879 json!({
880 "captions": [],
881 "references": [],
882 "footnotes": [],
883 "code_language": code_language(language.as_deref()),
884 }),
885 );
886 self.texts.push(item_json);
887 r
888 }
889 TreeKind::Group { label, name } => {
890 self.pending_exact = None;
891 let r = format!("#/groups/{}", self.groups.len());
892 self.groups.push(json!({
893 "self_ref": r,
894 "parent": { "$ref": parent },
895 "children": children,
896 "content_layer": layer,
897 "name": name,
898 "label": label,
899 }));
900 r
901 }
902 TreeKind::Table {
903 table,
904 rich_cells,
905 captions,
906 } => {
907 let r = self.add_table_with(table, parent, true);
909 let idx = ref_index(&r).expect("table ref");
910 let t = &mut self.tables[idx];
911 t["children"] = Value::Array(children);
912 t["content_layer"] = json!(layer);
913 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
914 for &(row, col, group) in rich_cells {
918 let cell_ref = ref_of(group);
919 let hit = |c: &Value| {
920 c["start_row_offset_idx"] == json!(row)
921 && c["start_col_offset_idx"] == json!(col)
922 };
923 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
924 for c in cells.iter_mut().filter(|c| hit(c)) {
925 c["ref"] = cell_ref.clone();
926 }
927 }
928 }
929 r
930 }
931 TreeKind::Picture {
932 captions,
933 image,
934 classification,
935 confidence,
936 chart,
937 dpi,
938 } => {
939 let mut meta = classification.as_ref().map(|c| {
944 let mut pred = serde_json::Map::new();
945 if let Some(conf) = confidence {
946 pred.insert("confidence".into(), json!(conf));
947 }
948 pred.insert("class_name".into(), json!(c));
949 json!({ "classification": { "predictions": [pred] } })
950 });
951 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
952 if !t.rows.is_empty() {
953 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
954 }
955 }
956 let prov = self.take_prov(0);
957 let r = self.push_picture(
958 prov,
959 captions.iter().map(|&c| ref_of(c)).collect(),
960 children,
961 image.as_ref(),
962 meta,
963 parent,
964 );
965 if let Some(idx) = ref_index(&r) {
966 self.pictures[idx]["content_layer"] = json!(layer);
967 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
970 img["dpi"] = json!(dpi);
971 }
972 }
973 r
974 }
975 TreeKind::FieldRegion { items } => {
976 self.pending_exact = None;
977 let r = self.add_field_region(items, parent);
978 if let Some(region) = self.field_regions.last_mut() {
979 region["content_layer"] = json!(layer);
980 }
981 r
982 }
983 };
984 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
985 }
986 tree.body.iter().map(|&c| ref_of(c)).collect()
987 }
988
989 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
990 match node {
991 Node::Heading { level: 1, text } => {
992 Some(self.add_text("title", text, parent, json!({})))
993 }
994 Node::Heading { level, text } => Some(self.add_text(
995 "section_header",
996 text,
997 parent,
998 json!({ "level": level.saturating_sub(1) }),
999 )),
1000 Node::Caption { text, href } => {
1001 let extra = match href {
1002 Some(url) => json!({ "hyperlink": url }),
1003 None => json!({}),
1004 };
1005 Some(self.add_text("caption", text, parent, extra))
1006 }
1007 Node::Paragraph { text } => {
1008 let t = text.trim();
1011 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1012 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1013 _ => Some(self.add_text("text", text, parent, json!({}))),
1014 }
1015 }
1016 Node::CheckboxItem { checked, text } => {
1017 let mark = if *checked { "- [x] " } else { "- [ ] " };
1020 Some(self.add_text("text", &format!("{mark}{text}"), parent, json!({})))
1021 }
1022 Node::Code {
1023 language,
1024 text,
1025 orig,
1026 ..
1027 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1028 Node::Formula {
1031 latex,
1032 orig,
1033 location,
1034 } => {
1035 self.adopt_loc(*location);
1036 Some(self.add_formula_item(latex, orig, parent))
1037 }
1038 Node::CommentSection {
1044 name,
1045 text,
1046 refs_note_text,
1047 grouped,
1048 } => {
1049 if !*grouped {
1050 let child =
1054 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1055 self.comment_groups.push(child.clone());
1056 return Some(child);
1057 }
1058 let self_ref = format!("#/groups/{}", self.groups.len());
1059 self.groups.push(Value::Null);
1060 let child =
1061 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1062 self.groups[group_index(&self_ref)] = json!({
1063 "self_ref": self_ref,
1064 "parent": { "$ref": parent },
1065 "children": [{ "$ref": child }],
1066 "content_layer": "notes",
1067 "name": name,
1068 "label": "comment_section",
1069 });
1070 self.comment_groups.push(if *refs_note_text {
1071 child
1072 } else {
1073 self_ref.clone()
1074 });
1075 Some(self_ref)
1076 }
1077 Node::Commented { comments, inner } => {
1080 let item = self.add_node(inner, parent)?;
1081 if !comments.is_empty() {
1082 self.pending_comments.push((item.clone(), comments.clone()));
1083 }
1084 Some(item)
1085 }
1086 Node::Table(t) => Some(self.add_table(t, parent)),
1087 Node::Picture {
1088 caption,
1089 caption_href,
1090 image,
1091 classification,
1092 caption_parent,
1093 } => Some(self.add_picture(
1094 caption.as_deref(),
1095 caption_href.as_deref(),
1096 image.as_ref(),
1097 classification.as_deref().map(classification_meta),
1098 parent,
1099 *caption_parent,
1100 )),
1101 Node::Chart {
1106 kind,
1107 table,
1108 caption,
1109 location,
1110 } => {
1111 self.adopt_loc(*location);
1112 let mut meta = json!({
1113 "classification": { "predictions": [{ "class_name": kind }] },
1114 });
1115 if !table.rows.is_empty() {
1116 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1117 }
1118 let mut captions = Vec::new();
1124 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1125 let prov = self.prov_json(unescape_text(cap).chars().count(), true);
1126 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1127 self.pending_siblings.push(json!({ "$ref": cap_ref }));
1128 captions.push(json!({ "$ref": cap_ref }));
1129 }
1130 let prov = self.take_prov(0);
1131 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1132 }
1133 Node::DoclangOnly(_) => None,
1135 Node::Group {
1136 label,
1137 name,
1138 layer,
1139 children,
1140 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1141 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1142 Node::InlineGroup { md_text, .. } => {
1145 Some(self.add_text("text", md_text, parent, json!({})))
1146 }
1147 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1149 Node::Furniture {
1155 layer: ContentLayer::Notes,
1156 inner,
1157 } => {
1158 let item = self.add_node(inner, parent)?;
1159 self.set_layer(&item, "notes");
1160 Some(item)
1161 }
1162 Node::Furniture { .. } => None,
1163 Node::PageFurniture { .. } => None,
1164 Node::Located { location, inner } => {
1169 if self.cur_page > 0 {
1170 self.pending_loc = Some(*location);
1171 }
1172 let r = self.add_node(inner, parent);
1173 self.pending_loc = None;
1174 r
1175 }
1176 Node::Prov {
1177 page_no,
1178 bbox,
1179 charspan,
1180 inner,
1181 ..
1182 } => {
1183 self.pending_exact = Some(ExactProv {
1184 page_no: *page_no,
1185 bbox: bbox.map(f64::from),
1186 bottom_left: false,
1187 charspan: *charspan,
1188 });
1189 let r = self.add_node(inner, parent);
1190 self.pending_exact = None;
1191 r
1192 }
1193 Node::PageBreak => None,
1195 Node::PageInfo {
1198 page_no,
1199 width,
1200 height,
1201 } => {
1202 self.cur_page = *page_no;
1203 self.cur_w = *width as f64;
1204 self.cur_h = *height as f64;
1205 if *page_no > 0 {
1206 self.pages.push((*page_no, self.cur_w, self.cur_h));
1207 }
1208 None
1209 }
1210 Node::ListItem { .. } => None,
1212 }
1213 }
1214
1215 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1219 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1220 self.field_regions.push(Value::Null);
1221 let region_index = self.field_regions.len() - 1;
1222 let mut item_refs = Vec::new();
1223 for item in items {
1224 item_refs.push(json!({ "$ref": self.add_field_item(item, &self_ref) }));
1225 }
1226 self.field_regions[region_index] = json!({
1227 "self_ref": self_ref,
1228 "parent": { "$ref": parent },
1229 "children": item_refs,
1230 "content_layer": "body",
1231 "label": "field_region",
1232 "prov": [],
1233 });
1234 self_ref
1235 }
1236
1237 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1238 let self_ref = format!("#/field_items/{}", self.field_items.len());
1239 self.field_items.push(Value::Null);
1240 let item_index = self.field_items.len() - 1;
1241 let mut child_refs = Vec::new();
1242 for (label, text) in [
1243 ("marker", &item.marker),
1244 ("field_key", &item.key),
1245 ("field_value", &item.value),
1246 ] {
1247 if let Some(text) = text {
1248 let extra = match (label, &item.value_kind) {
1251 ("field_value", Some(kind)) => json!({ "kind": kind }),
1252 _ => json!({}),
1253 };
1254 child_refs.push(json!({ "$ref": self.add_text(label, text, &self_ref, extra) }));
1255 }
1256 }
1257 self.field_items[item_index] = json!({
1258 "self_ref": self_ref,
1259 "parent": { "$ref": parent },
1260 "children": child_refs,
1261 "content_layer": "body",
1262 "label": "field_item",
1263 "prov": [],
1264 });
1265 self_ref
1266 }
1267
1268 fn set_layer(&mut self, self_ref: &str, layer: &str) {
1272 let bucket = match self_ref.split('/').nth(1) {
1273 Some("texts") => &mut self.texts,
1274 Some("tables") => &mut self.tables,
1275 Some("pictures") => &mut self.pictures,
1276 Some("groups") => &mut self.groups,
1277 _ => return,
1278 };
1279 if let Some(item) = self_ref
1280 .rsplit('/')
1281 .next()
1282 .and_then(|i| i.parse::<usize>().ok())
1283 .and_then(|i| bucket.get_mut(i))
1284 {
1285 item["content_layer"] = json!(layer);
1286 }
1287 }
1288
1289 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
1290 let prov = self.take_prov(unescape_text(text).chars().count());
1291 self.add_text_with(label, text, parent, extra, prov)
1292 }
1293
1294 fn add_text_with(
1297 &mut self,
1298 label: &str,
1299 text: &str,
1300 parent: &str,
1301 extra: Value,
1302 prov: Value,
1303 ) -> String {
1304 let self_ref = format!("#/texts/{}", self.texts.len());
1305 let raw = unescape_text(text);
1306 let mut item = json!({
1307 "self_ref": self_ref,
1308 "parent": { "$ref": parent },
1309 "children": [],
1310 "content_layer": "body",
1311 "label": label,
1312 "prov": prov,
1313 "orig": raw,
1314 "text": raw,
1315 });
1316 merge(&mut item, extra);
1317 self.texts.push(item);
1318 self_ref
1319 }
1320
1321 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
1324 let self_ref = format!("#/texts/{}", self.texts.len());
1325 let prov = self.take_prov(latex.chars().count());
1326 self.texts.push(json!({
1327 "self_ref": self_ref,
1328 "parent": { "$ref": parent },
1329 "children": [],
1330 "content_layer": "body",
1331 "label": "formula",
1332 "prov": prov,
1333 "orig": latex,
1334 "text": latex,
1335 }));
1336 self_ref
1337 }
1338
1339 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
1343 let self_ref = format!("#/texts/{}", self.texts.len());
1344 let prov = self.take_prov(latex.chars().count());
1345 self.texts.push(json!({
1346 "self_ref": self_ref,
1347 "parent": { "$ref": parent },
1348 "children": [],
1349 "content_layer": "body",
1350 "label": "formula",
1351 "prov": prov,
1352 "orig": orig,
1353 "text": latex,
1354 }));
1355 self_ref
1356 }
1357
1358 fn add_code(
1359 &mut self,
1360 text: &str,
1361 language: Option<&str>,
1362 orig: Option<&str>,
1363 parent: &str,
1364 ) -> String {
1365 let self_ref = format!("#/texts/{}", self.texts.len());
1366 let raw = unescape_text(text);
1367 let prov = self.take_prov(raw.chars().count());
1368 self.texts.push(json!({
1369 "self_ref": self_ref,
1370 "parent": { "$ref": parent },
1371 "children": [],
1372 "content_layer": "body",
1373 "label": "code",
1374 "prov": prov,
1375 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
1378 "text": raw,
1379 "captions": [],
1380 "references": [],
1381 "footnotes": [],
1382 "code_language": code_language(language),
1383 }));
1384 self_ref
1385 }
1386
1387 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
1390 let self_ref = format!("#/groups/{}", self.groups.len());
1391 self.groups.push(Value::Null);
1393 let base = level_of(&items[0]);
1394 let mut children = Vec::new();
1395 let mut i = 0;
1396 while i < items.len() {
1397 if !matches!(items[i], Node::ListItem { .. }) {
1400 i += 1;
1401 continue;
1402 }
1403 let lvl = level_of(&items[i]);
1404 if lvl > base {
1405 i += 1;
1407 continue;
1408 }
1409 let item_ref = self.add_list_item(&items[i], &self_ref);
1410 let mut j = i + 1;
1412 while j < items.len() && level_of(&items[j]) > base {
1413 j += 1;
1414 }
1415 if j > i + 1 {
1416 let mut nested = Vec::new();
1417 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
1418 if let Some(idx) = ref_index(&item_ref) {
1420 self.texts[idx]["children"]
1421 .as_array_mut()
1422 .unwrap()
1423 .extend(nested);
1424 }
1425 }
1426 children.push(json!({ "$ref": item_ref }));
1427 i = j;
1428 }
1429 self.groups[group_index(&self_ref)] = json!({
1430 "self_ref": self_ref,
1431 "parent": { "$ref": parent },
1432 "children": children,
1433 "content_layer": "body",
1434 "name": "list",
1435 "label": "list",
1436 });
1437 self_ref
1438 }
1439
1440 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
1441 let Node::ListItem {
1442 ordered,
1443 number,
1444 text,
1445 location,
1446 ..
1447 } = node
1448 else {
1449 unreachable!()
1450 };
1451 self.adopt_loc(*location);
1452 let self_ref = format!("#/texts/{}", self.texts.len());
1453 let raw = unescape_text(text);
1454 let prov = self.take_prov(raw.chars().count());
1455 let marker = if *ordered {
1456 format!("{number}.")
1457 } else {
1458 "-".to_string()
1459 };
1460 self.texts.push(json!({
1461 "self_ref": self_ref,
1462 "parent": { "$ref": parent },
1463 "children": [],
1464 "content_layer": "body",
1465 "label": "list_item",
1466 "prov": prov,
1467 "orig": raw,
1468 "text": raw,
1469 "enumerated": ordered,
1470 "marker": marker,
1471 }));
1472 self_ref
1473 }
1474
1475 fn add_table(&mut self, t: &Table, parent: &str) -> String {
1476 self.add_table_with(t, parent, false)
1477 }
1478
1479 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
1482 let self_ref = format!("#/tables/{}", self.tables.len());
1483 self.adopt_loc(t.location);
1484 let prov = self.take_prov(0);
1485 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
1489 Some(cap) => self.add_caption(cap, json!({}), &self_ref, parent, t.caption_parent),
1490 None => (Vec::new(), Vec::new()),
1491 };
1492 let data = table_data_with(t, raw);
1493 self.tables.push(json!({
1494 "self_ref": self_ref,
1495 "parent": { "$ref": parent },
1496 "children": children,
1497 "content_layer": "body",
1498 "label": "table",
1499 "prov": prov,
1500 "captions": captions,
1501 "references": [],
1502 "footnotes": [],
1503 "data": data,
1504 "annotations": [],
1505 }));
1506 self_ref
1507 }
1508
1509 fn add_caption(
1515 &mut self,
1516 text: &str,
1517 extra: Value,
1518 self_ref: &str,
1519 parent: &str,
1520 choice: CaptionParent,
1521 ) -> (Vec<Value>, Vec<Value>) {
1522 let cap_parent = match choice {
1527 CaptionParent::Item => self_ref,
1528 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
1529 CaptionParent::Body => "#/body",
1530 };
1531 let cap_ref = json!({ "$ref": self.add_text("caption", text, cap_parent, extra) });
1532 match choice {
1533 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
1534 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
1537 CaptionParent::Body if parent == "#/body" => {
1538 self.pending_siblings.push(cap_ref.clone())
1539 }
1540 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
1541 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
1544 }
1545 (vec![cap_ref], Vec::new())
1546 }
1547
1548 fn add_picture(
1551 &mut self,
1552 caption: Option<&str>,
1553 caption_href: Option<&str>,
1554 image: Option<&crate::PictureImage>,
1555 meta: Option<Value>,
1556 parent: &str,
1557 caption_parent: CaptionParent,
1558 ) -> String {
1559 let self_ref = format!("#/pictures/{}", self.pictures.len());
1560 let prov = self.take_prov(0);
1563 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
1564 Some(cap) => {
1565 let extra = match caption_href {
1569 Some(href) => json!({ "hyperlink": href }),
1570 None => json!({}),
1571 };
1572 self.add_caption(cap, extra, &self_ref, parent, caption_parent)
1573 }
1574 None => (Vec::new(), Vec::new()),
1575 };
1576 self.push_picture(prov, captions, children, image, meta, parent)
1577 }
1578
1579 fn push_picture(
1582 &mut self,
1583 prov: Value,
1584 captions: Vec<Value>,
1585 children: Vec<Value>,
1586 image: Option<&crate::PictureImage>,
1587 meta: Option<Value>,
1588 parent: &str,
1589 ) -> String {
1590 let self_ref = format!("#/pictures/{}", self.pictures.len());
1591 let annotations = meta
1595 .as_ref()
1596 .and_then(|m| m.get("annotations").cloned())
1597 .unwrap_or_else(|| json!([]));
1598 let meta = meta.map(|mut m| {
1599 if let Some(obj) = m.as_object_mut() {
1600 obj.remove("annotations");
1601 }
1602 m
1603 });
1604 let mut item = match meta {
1608 Some(meta) => json!({
1609 "self_ref": self_ref,
1610 "parent": { "$ref": parent },
1611 "children": children,
1612 "content_layer": "body",
1613 "meta": meta,
1614 "label": "picture",
1615 "prov": prov,
1616 "captions": captions,
1617 "references": [],
1618 "footnotes": [],
1619 "annotations": annotations,
1620 }),
1621 None => json!({
1622 "self_ref": self_ref,
1623 "parent": { "$ref": parent },
1624 "children": children,
1625 "content_layer": "body",
1626 "label": "picture",
1627 "prov": prov,
1628 "captions": captions,
1629 "references": [],
1630 "footnotes": [],
1631 "annotations": annotations,
1632 }),
1633 };
1634 if let Some(img) = image {
1639 let image = json!({
1640 "mimetype": img.mimetype,
1641 "dpi": 72,
1642 "size": { "width": img.width as f64, "height": img.height as f64 },
1643 "uri": img.data_uri(),
1644 });
1645 if let Some(obj) = item.as_object_mut() {
1646 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
1647 obj.insert("image".into(), image);
1648 obj.insert("annotations".into(), annotations);
1649 }
1650 }
1651 self.pictures.push(item);
1652 self_ref
1653 }
1654
1655 fn add_group(
1656 &mut self,
1657 label: &str,
1658 name: Option<&str>,
1659 layer: Option<ContentLayer>,
1660 nodes: &[Node],
1661 parent: &str,
1662 ) -> String {
1663 let self_ref = format!("#/groups/{}", self.groups.len());
1664 self.groups.push(Value::Null);
1665 let mark = (
1669 self.texts.len(),
1670 self.tables.len(),
1671 self.pictures.len(),
1672 self.groups.len(),
1673 );
1674 let children = self.walk_into(nodes, &self_ref);
1675 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
1676 let content_layer = layer.map_or("body", |l| l.value());
1677 self.groups[group_index(&self_ref)] = json!({
1678 "self_ref": self_ref,
1679 "parent": { "$ref": parent },
1680 "children": children,
1681 "content_layer": content_layer,
1682 "name": name,
1683 "label": label,
1684 });
1685 if layer.is_some() {
1686 let (t, tb, p, g) = mark;
1687 for item in self.texts[t..]
1688 .iter_mut()
1689 .chain(self.tables[tb..].iter_mut())
1690 .chain(self.pictures[p..].iter_mut())
1691 .chain(self.groups[g..].iter_mut())
1692 {
1693 if let Some(obj) = item.as_object_mut() {
1694 obj.insert("content_layer".into(), json!(content_layer));
1695 }
1696 }
1697 }
1698 self_ref
1699 }
1700
1701 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Value> {
1704 let seqs: Option<Vec<usize>> = nodes
1709 .iter()
1710 .map(|n| match n {
1711 Node::Prov { seq: Some(s), .. } => Some(*s),
1712 _ => None,
1713 })
1714 .collect();
1715 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
1716 let mut order: Vec<usize> = (0..nodes.len()).collect();
1717 order.sort_by_key(|&i| seqs[i]);
1718 let mut slots: Vec<Vec<Value>> = vec![Vec::new(); nodes.len()];
1719 for i in order {
1720 if let Some(r) = self.add_node(&nodes[i], parent) {
1721 slots[i].append(&mut self.pending_siblings);
1722 slots[i].push(json!({ "$ref": r }));
1723 slots[i].append(&mut self.pending_after);
1724 }
1725 if parent == "#/body" {
1726 slots[i].append(&mut self.pending_body);
1727 }
1728 }
1729 return slots.into_iter().flatten().collect();
1730 }
1731 let mut children = Vec::new();
1732 let mut i = 0;
1733 while i < nodes.len() {
1734 if matches!(nodes[i], Node::ListItem { .. }) {
1735 let start = i;
1736 i += 1;
1737 loop {
1738 match nodes.get(i) {
1739 Some(Node::ListItem { .. }) => i += 1,
1740 Some(Node::Paragraph { text })
1743 if text.is_empty()
1744 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
1745 {
1746 i += 1
1747 }
1748 _ => break,
1749 }
1750 }
1751 self.add_sibling_lists(&nodes[start..i], parent, &mut children);
1752 } else {
1753 if let Some(r) = self.add_node(&nodes[i], parent) {
1754 children.append(&mut self.pending_siblings);
1755 children.push(json!({ "$ref": r }));
1756 children.append(&mut self.pending_after);
1757 }
1758 i += 1;
1759 }
1760 if parent == "#/body" {
1763 children.append(&mut self.pending_body);
1764 }
1765 }
1766 children
1767 }
1768
1769 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
1774 let base = level_of(&run[0]);
1775 let mut seg = 0;
1776 for k in 0..run.len() {
1777 let Node::ListItem {
1778 first_in_list,
1779 level,
1780 ..
1781 } = &run[k]
1782 else {
1783 continue;
1784 };
1785 if *level != base {
1786 continue; }
1788 if k > seg && *first_in_list {
1789 out.push(json!({ "$ref": self.add_list(&run[seg..k], parent) }));
1790 seg = k;
1791 }
1792 }
1793 out.push(json!({ "$ref": self.add_list(&run[seg..], parent) }));
1794 }
1795}
1796
1797fn level_of(node: &Node) -> u8 {
1798 match node {
1799 Node::ListItem { level, .. } => *level,
1800 _ => 0,
1801 }
1802}
1803
1804fn group_index(self_ref: &str) -> usize {
1805 self_ref.rsplit('/').next().unwrap().parse().unwrap()
1806}
1807
1808fn ref_index(self_ref: &str) -> Option<usize> {
1809 self_ref.rsplit('/').next()?.parse().ok()
1810}
1811
1812fn merge(target: &mut Value, extra: Value) {
1814 if let (Some(t), Some(e)) = (target.as_object_mut(), extra.as_object()) {
1815 for (k, v) in e {
1816 t.insert(k.clone(), v.clone());
1817 }
1818 }
1819}
1820
1821fn unescape_text(s: &str) -> String {
1823 s.replace("<", "<")
1824 .replace(">", ">")
1825 .replace("&", "&")
1826 .replace("\\_", "_")
1827}
1828
1829fn fnv1a(s: &str) -> u64 {
1832 let mut h: u64 = 0xcbf29ce484222325;
1833 for b in s.bytes() {
1834 h ^= b as u64;
1835 h = h.wrapping_mul(0x100000001b3);
1836 }
1837 h
1838}
1839
1840#[cfg(test)]
1841mod tests {
1842 use crate::{
1843 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
1844 };
1845 use serde_json::Value;
1846
1847 fn doc_with_image() -> DoclingDocument {
1848 let mut doc = DoclingDocument::new("t");
1849 doc.push(Node::Picture {
1850 caption: Some("Fig 1".into()),
1851 caption_href: None,
1852 image: Some(PictureImage {
1853 mimetype: "image/png".into(),
1854 width: 4,
1855 height: 2,
1856 data: b"foobar".to_vec(),
1857 }),
1858 classification: None,
1859 caption_parent: Default::default(),
1860 });
1861 doc
1862 }
1863
1864 #[test]
1869 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
1870 let mut doc = DoclingDocument::new("t");
1871 doc.push(Node::Heading {
1872 level: 1,
1873 text: "Slide One".into(),
1874 });
1875 doc.push(Node::Furniture {
1876 layer: ContentLayer::Notes,
1877 inner: Box::new(Node::Located {
1878 location: [0, 0, 0, 0],
1879 inner: Box::new(Node::Paragraph {
1880 text: "Speaker note for slide 1.".into(),
1881 }),
1882 }),
1883 });
1884 doc.push(Node::Furniture {
1885 layer: ContentLayer::Furniture,
1886 inner: Box::new(Node::Paragraph {
1887 text: "page header".into(),
1888 }),
1889 });
1890
1891 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1892 let texts = v["texts"].as_array().unwrap();
1893 assert_eq!(
1894 texts
1895 .iter()
1896 .map(|t| (
1897 t["label"].as_str().unwrap(),
1898 t["content_layer"].as_str().unwrap(),
1899 t["text"].as_str().unwrap()
1900 ))
1901 .collect::<Vec<_>>(),
1902 vec![
1903 ("title", "body", "Slide One"),
1904 ("text", "notes", "Speaker note for slide 1."),
1905 ],
1906 "the note is carried on its own layer; the furniture is not carried"
1907 );
1908 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
1910 }
1911
1912 #[test]
1918 fn continuation_flags_become_spanning_cells() {
1919 let mut doc = DoclingDocument::new("t");
1920 let rows = vec![
1922 vec!["merged".to_string(), "merged".into(), "merged".into()],
1923 vec!["merged".to_string(), "merged".into(), "merged".into()],
1924 vec!["a".to_string(), "b".into(), "c".into()],
1925 ];
1926 doc.push(Node::Table(crate::Table {
1927 rows,
1928 location: None,
1929 structure: Some(crate::TableStructure {
1930 header_row: vec![true, false, false],
1931 col_continuation: vec![
1932 vec![false, true, true],
1933 vec![false, true, true],
1934 vec![false, false, false],
1935 ],
1936 row_continuation: vec![
1937 vec![false, false, false],
1938 vec![true, true, true],
1939 vec![false, false, false],
1940 ],
1941 row_header: Vec::new(),
1942 col_header: Vec::new(),
1943 }),
1944 cell_blocks: None,
1945 cells: None,
1946 caption: None,
1947 caption_parent: Default::default(),
1948 }));
1949 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
1950 let data = &v["tables"][0]["data"];
1951 assert_eq!(data["num_rows"], 3);
1952 assert_eq!(data["num_cols"], 3);
1953 let cells = data["table_cells"].as_array().unwrap();
1954 assert_eq!(
1955 cells.len(),
1956 4,
1957 "one cell for the range, three for the plain row"
1958 );
1959 assert_eq!(
1960 cells[0],
1961 serde_json::json!({
1962 "row_span": 2, "col_span": 3,
1963 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
1964 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
1965 "text": "merged", "column_header": true, "row_header": false,
1966 "row_section": false, "fillable": false,
1967 })
1968 );
1969 assert_eq!(cells[1]["text"], "a");
1970 assert_eq!(cells[1]["row_span"], 1);
1971 assert_eq!(cells[1]["column_header"], false);
1972 let grid = data["grid"].as_array().unwrap();
1974 assert_eq!(grid.len(), 3);
1975 for (r, row) in grid.iter().take(2).enumerate() {
1976 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
1977 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
1978 }
1979 }
1980 assert_eq!(grid[2][2]["text"], "c");
1981 }
1982
1983 #[test]
1989 fn exact_provenance_pages_and_chart_captions_follow_docling() {
1990 let mut doc = DoclingDocument::new("t");
1991 doc.push(Node::PageInfo {
1992 page_no: 1,
1993 width: 3.0,
1994 height: 4.0,
1995 });
1996 let table = crate::Table {
1997 rows: vec![vec!["a".to_string(), "b".into()]],
1998 ..Default::default()
1999 };
2000 doc.push(Node::Group {
2001 label: "sheet".into(),
2002 name: Some("Data".into()),
2003 layer: None,
2004 children: vec![
2005 Node::Prov {
2010 page_no: 1,
2011 bbox: [0.0, 0.0, 3.0, 4.0],
2012 charspan: [0, 0],
2013 seq: Some(1),
2014 inner: Box::new(Node::Table(table.clone())),
2015 },
2016 Node::Prov {
2017 page_no: 1,
2018 bbox: [0.0, 1.0, 1.0, 1.0],
2019 charspan: [0, 0],
2020 seq: Some(0),
2021 inner: Box::new(Node::Chart {
2022 kind: "bar_chart".into(),
2023 table,
2024 caption: Some("Sales".into()),
2025 location: Some([0, 128, 170, 128]),
2026 }),
2027 },
2028 ],
2029 });
2030 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2031 assert_eq!(
2032 v["pages"],
2033 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2034 );
2035 assert_eq!(
2036 v["tables"][0]["prov"],
2037 serde_json::json!([{
2038 "page_no": 1,
2039 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2040 "charspan": [0, 0],
2041 }])
2042 );
2043 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2044 let sheet = &v["groups"][0];
2047 assert_eq!(
2048 sheet["children"],
2049 serde_json::json!([
2050 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2051 ])
2052 );
2053 let cap = &v["texts"][0];
2054 assert_eq!(cap["label"], "caption");
2055 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2056 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2057 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2058 let pic = &v["pictures"][0];
2059 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2060 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2061 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2062 assert_eq!(
2063 pic["meta"]["classification"]["predictions"][0]["class_name"],
2064 "bar_chart"
2065 );
2066 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2067 }
2068
2069 #[test]
2073 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2074 let mut doc = DoclingDocument::new("t");
2075 doc.push(Node::PageInfo {
2076 page_no: 1,
2077 width: 12192000.0,
2078 height: 6858000.0,
2079 });
2080 doc.push(Node::Furniture {
2081 layer: ContentLayer::Notes,
2082 inner: Box::new(Node::Located {
2083 location: [0, 0, 0, 0],
2084 inner: Box::new(Node::Paragraph {
2085 text: "a note".into(),
2086 }),
2087 }),
2088 });
2089 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2090 let prov = &v["texts"][0]["prov"][0];
2091 assert_eq!(prov["page_no"], 1);
2092 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2093 assert_eq!(
2094 prov["bbox"],
2095 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2096 );
2097 assert_eq!(
2099 v["pages"]["1"]["size"],
2100 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2101 );
2102 }
2103
2104 #[test]
2110 fn page_markers_produce_pages_and_prov() {
2111 let mut doc = DoclingDocument::new("t");
2112 doc.push(Node::PageInfo {
2113 page_no: 1,
2114 width: 512.0,
2115 height: 1024.0,
2116 });
2117 doc.push(Node::Located {
2118 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
2120 text: "hello".into(),
2121 }),
2122 });
2123 doc.push(Node::Table(Table {
2124 rows: vec![vec!["a".into()]],
2125 location: Some([0, 0, 512, 512]),
2126 ..Table::default()
2127 }));
2128 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2129 assert_eq!(v["pages"]["1"]["page_no"], 1);
2130 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
2131 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
2132 let prov = &v["texts"][0]["prov"][0];
2135 assert_eq!(prov["page_no"], 1);
2136 assert_eq!(prov["bbox"]["l"], 128.0);
2137 assert_eq!(prov["bbox"]["t"], 896.0);
2138 assert_eq!(prov["bbox"]["r"], 256.0);
2139 assert_eq!(prov["bbox"]["b"], 768.0);
2140 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
2141 assert_eq!(prov["charspan"][1], 5);
2142 let tprov = &v["tables"][0]["prov"][0];
2144 assert_eq!(tprov["bbox"]["t"], 1024.0);
2145 assert_eq!(tprov["bbox"]["b"], 0.0);
2146 assert_eq!(tprov["charspan"][1], 0);
2147
2148 let mut plain = DoclingDocument::new("t");
2150 plain.push(Node::Located {
2151 location: [1, 2, 3, 4],
2152 inner: Box::new(Node::Paragraph { text: "x".into() }),
2153 });
2154 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
2155 assert_eq!(v["pages"], serde_json::json!({}));
2156 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
2157 }
2158
2159 #[test]
2160 fn picture_image_in_markdown_modes_and_json() {
2161 let doc = doc_with_image();
2162 assert!(doc.export_to_markdown().contains("<!-- image -->"));
2164 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
2166 assert!(
2167 md.contains(""),
2168 "got:\n{md}"
2169 );
2170 assert!(files.is_empty());
2171 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
2173 assert!(
2174 md.contains(""),
2175 "got:\n{md}"
2176 );
2177 assert_eq!(
2178 files,
2179 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
2180 );
2181 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2184 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
2185 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
2186 let keys: Vec<&str> = v["pictures"][0]
2187 .as_object()
2188 .unwrap()
2189 .keys()
2190 .map(String::as_str)
2191 .collect();
2192 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
2193 assert_eq!(
2194 v["pictures"][0]["image"]["uri"],
2195 "data:image/png;base64,Zm9vYmFy"
2196 );
2197 }
2198
2199 #[test]
2200 fn exports_docling_schema() {
2201 let mut doc = DoclingDocument::new("t");
2202 doc.push(Node::Heading {
2203 level: 1,
2204 text: "Title".into(),
2205 });
2206 doc.push(Node::Heading {
2207 level: 2,
2208 text: "Sec".into(),
2209 });
2210 doc.push(Node::Paragraph {
2211 text: "Body & more".into(),
2212 }); doc.push(Node::ListItem {
2214 ordered: false,
2215 number: 0,
2216 first_in_list: true,
2217 text: "one".into(),
2218 level: 0,
2219 marker: None,
2220 location: None,
2221 dclx: None,
2222 href: None,
2223 layer: None,
2224 });
2225 doc.push(Node::ListItem {
2226 ordered: false,
2227 number: 0,
2228 first_in_list: false,
2229 text: "two".into(),
2230 level: 0,
2231 marker: None,
2232 location: None,
2233 dclx: None,
2234 href: None,
2235 layer: None,
2236 });
2237 doc.push(Node::Table(Table {
2238 rows: vec![vec!["A".into(), "B".into()]],
2239 location: None,
2240 structure: None,
2241 cell_blocks: None,
2242 cells: None,
2243 caption: None,
2244 caption_parent: Default::default(),
2245 }));
2246
2247 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2248 assert_eq!(v["schema_name"], "DoclingDocument");
2249 assert_eq!(v["version"], "1.10.0");
2250 assert_eq!(v["texts"][0]["label"], "title");
2251 assert_eq!(v["texts"][1]["label"], "section_header");
2252 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
2256 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
2257 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
2258 assert_eq!(v["texts"][3]["marker"], "-");
2259 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
2261 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
2262 }
2263 #[test]
2267 fn a_layered_group_stamps_its_whole_subtree() {
2268 let doc = DoclingDocument {
2269 name: "s".into(),
2270 nodes: vec![
2271 Node::Group {
2272 label: "sheet".into(),
2273 name: Some("Sheet1".into()),
2274 layer: None,
2275 children: vec![Node::Paragraph {
2276 text: "visible".into(),
2277 }],
2278 },
2279 Node::Group {
2280 label: "sheet".into(),
2281 name: Some("Sheet2".into()),
2282 layer: Some(ContentLayer::Invisible),
2283 children: vec![Node::Paragraph {
2284 text: "hidden".into(),
2285 }],
2286 },
2287 ],
2288 ..DoclingDocument::new("s")
2289 };
2290 let v = crate::json::to_json(&doc);
2291 assert_eq!(v["groups"][0]["label"], "sheet");
2292 assert_eq!(v["groups"][0]["name"], "Sheet1");
2293 assert_eq!(v["groups"][0]["content_layer"], "body");
2294 assert_eq!(v["texts"][0]["content_layer"], "body");
2295 assert_eq!(v["groups"][1]["name"], "Sheet2");
2296 assert_eq!(v["groups"][1]["content_layer"], "invisible");
2297 assert_eq!(v["texts"][1]["content_layer"], "invisible");
2298 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
2300 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
2301 }
2302
2303 #[test]
2313 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
2314 use crate::tree::{ItemTree, TreeKind};
2315 let mut t = ItemTree::default();
2316 let text = |txt: &str| TreeKind::Text {
2317 label: "text".into(),
2318 text: txt.into(),
2319 orig: None,
2320 formatting: None,
2321 hyperlink: None,
2322 level: None,
2323 list: None,
2324 };
2325 let a = t.add(None, None, text("a"));
2326 let blank = t.add(None, None, text(""));
2327 let b = t.add(None, None, text("b"));
2328 t.delete(blank);
2329 let group = t.add(
2330 None,
2331 Some(ContentLayer::Notes),
2332 TreeKind::Group {
2333 label: "comment_section".into(),
2334 name: "comment-0".into(),
2335 },
2336 );
2337 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
2338 t.items[a].comments.push(group);
2339 t.add(
2340 None,
2341 None,
2342 TreeKind::Picture {
2343 captions: Vec::new(),
2344 image: None,
2345 classification: Some("bar_chart".into()),
2346 confidence: None,
2347 chart: Some(Table {
2348 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
2349 ..Table::default()
2350 }),
2351 dpi: None,
2352 },
2353 );
2354 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
2355 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
2356 let mut doc = DoclingDocument::new("t");
2357 doc.tree = Some(t);
2358 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2359 let texts = v["texts"].as_array().unwrap();
2360 assert_eq!(texts.len(), 3);
2361 assert_eq!(texts[1]["text"], "b");
2362 assert_eq!(texts[1]["self_ref"], "#/texts/1");
2363 assert_eq!(
2364 v["body"]["children"],
2365 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
2366 );
2367 let keys: Vec<&str> = texts[0]
2368 .as_object()
2369 .unwrap()
2370 .keys()
2371 .map(String::as_str)
2372 .collect();
2373 assert_eq!(
2374 keys,
2375 vec![
2376 "self_ref",
2377 "parent",
2378 "children",
2379 "content_layer",
2380 "label",
2381 "prov",
2382 "comments",
2383 "orig",
2384 "text"
2385 ]
2386 );
2387 assert_eq!(
2388 texts[0]["comments"],
2389 serde_json::json!([{"$ref": "#/groups/0"}])
2390 );
2391 assert!(texts[1].get("comments").is_none());
2392 let meta = &v["pictures"][0]["meta"];
2393 assert_eq!(
2394 meta["classification"]["predictions"][0]["class_name"],
2395 "bar_chart"
2396 );
2397 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
2398 }
2399
2400 #[test]
2406 fn tree_items_carry_exact_provenance_and_dpi() {
2407 use crate::tree::{ItemTree, TreeKind, TreeProv};
2408 let text = |label: &str, t: &str| TreeKind::Text {
2409 label: label.into(),
2410 text: t.into(),
2411 orig: None,
2412 formatting: None,
2413 hyperlink: None,
2414 level: None,
2415 list: None,
2416 };
2417 let mut t = ItemTree::default();
2418 let slide = t.add(
2419 None,
2420 None,
2421 TreeKind::Group {
2422 label: "chapter".into(),
2423 name: "slide-0".into(),
2424 },
2425 );
2426 t.add_with_prov(
2427 Some(slide),
2428 None,
2429 text("paragraph", "héllo"),
2430 TreeProv {
2431 page_no: 1,
2432 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
2433 bottom_left: true,
2434 charspan: [0, 5],
2435 },
2436 );
2437 t.add_with_prov(
2438 Some(slide),
2439 None,
2440 TreeKind::Picture {
2441 captions: Vec::new(),
2442 image: Some(crate::PictureImage {
2443 mimetype: "image/png".into(),
2444 width: 2,
2445 height: 2,
2446 data: vec![0],
2447 }),
2448 classification: None,
2449 confidence: None,
2450 chart: None,
2451 dpi: Some(300),
2452 },
2453 TreeProv {
2454 page_no: 1,
2455 bbox: [0.0; 4],
2456 bottom_left: false,
2457 charspan: [0, 0],
2458 },
2459 );
2460 t.add(
2461 Some(slide),
2462 Some(ContentLayer::Notes),
2463 text("text", "no geometry"),
2464 );
2465 let mut doc = DoclingDocument::new("t");
2466 doc.push(Node::PageInfo {
2467 page_no: 1,
2468 width: 9144000.0,
2469 height: 6858000.0,
2470 });
2471 doc.tree = Some(t);
2472 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2473 assert_eq!(
2474 v["texts"][0]["prov"],
2475 serde_json::json!([{
2476 "page_no": 1,
2477 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
2478 "charspan": [0, 5],
2479 }])
2480 );
2481 assert_eq!(v["texts"][0]["label"], "paragraph");
2482 assert_eq!(
2483 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
2484 "TOPLEFT"
2485 );
2486 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
2487 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
2488 assert_eq!(v["texts"][1]["content_layer"], "notes");
2489 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
2490 assert_eq!(v["pages"]["1"]["page_no"], 1);
2491 }
2492
2493 #[test]
2498 fn provenance_boxes_are_clamped_to_their_page() {
2499 let mut doc = DoclingDocument::new("t");
2500 doc.push(Node::PageInfo {
2501 page_no: 1,
2502 width: 10.0,
2503 height: 8.0,
2504 });
2505 doc.push(Node::Prov {
2506 page_no: 1,
2507 bbox: [-1.0, 2.0, 12.0, 9.5],
2508 charspan: [0, 1],
2509 seq: None,
2510 inner: Box::new(Node::Paragraph { text: "x".into() }),
2511 });
2512 let mut table = Table {
2513 rows: vec![vec!["a".into()]],
2514 ..Table::default()
2515 };
2516 table.cells = Some(vec![crate::TableCell {
2517 text: "a".into(),
2518 bbox: Some([1.0, 1.0, 11.0, 9.0]),
2519 start_row: 0,
2520 start_col: 0,
2521 row_span: 1,
2522 col_span: 1,
2523 column_header: false,
2524 row_header: false,
2525 row_section: false,
2526 }]);
2527 doc.push(Node::Prov {
2528 page_no: 1,
2529 bbox: [0.0, 0.0, 10.0, 8.0],
2530 charspan: [0, 0],
2531 seq: None,
2532 inner: Box::new(Node::Table(table)),
2533 });
2534 doc.push(Node::Prov {
2535 page_no: 7,
2536 bbox: [-5.0, 0.0, 50.0, 50.0],
2537 charspan: [0, 1],
2538 seq: None,
2539 inner: Box::new(Node::Paragraph { text: "y".into() }),
2540 });
2541 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2542 assert_eq!(
2543 v["texts"][0]["prov"][0]["bbox"],
2544 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
2545 );
2546 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
2547 assert_eq!(
2548 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
2549 (Some(1.0), Some(10.0), Some(8.0))
2550 );
2551 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
2552 assert_eq!(
2553 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
2554 "page 7 is not described"
2555 );
2556 }
2557
2558 #[test]
2559 fn a_backend_item_tree_is_written_verbatim() {
2560 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
2561 let mut t = ItemTree::default();
2562 let text = |label: &str, txt: &str| TreeKind::Text {
2563 label: label.into(),
2564 text: txt.into(),
2565 orig: None,
2566 formatting: None,
2567 hyperlink: None,
2568 level: None,
2569 list: None,
2570 };
2571 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
2572 let h = t.add(None, None, text("title", "Heading"));
2573 let group = t.add(
2574 Some(h),
2575 None,
2576 TreeKind::Group {
2577 label: "inline".into(),
2578 name: "group".into(),
2579 },
2580 );
2581 t.add(
2582 Some(group),
2583 None,
2584 TreeKind::Text {
2585 label: "text".into(),
2586 text: "bold".into(),
2587 orig: None,
2588 formatting: Some(Formatting {
2589 bold: true,
2590 ..Formatting::default()
2591 }),
2592 hyperlink: Some("https://example.com/".into()),
2593 level: None,
2594 list: None,
2595 },
2596 );
2597 t.add(
2598 Some(group),
2599 None,
2600 TreeKind::Code {
2601 text: "x = 1".into(),
2602 orig: None,
2603 language: Some("python".into()),
2604 formatting: None,
2605 hyperlink: None,
2606 },
2607 );
2608 let sub = t.add(
2609 Some(h),
2610 None,
2611 TreeKind::Text {
2612 label: "section_header".into(),
2613 text: "Sub".into(),
2614 orig: Some("Sub\u{2019}".into()),
2615 formatting: None,
2616 hyperlink: None,
2617 level: Some(1),
2618 list: None,
2619 },
2620 );
2621 t.add(
2622 Some(sub),
2623 None,
2624 TreeKind::Text {
2625 label: "list_item".into(),
2626 text: "item".into(),
2627 orig: None,
2628 formatting: None,
2629 hyperlink: None,
2630 level: None,
2631 list: Some(ListMeta {
2632 enumerated: true,
2633 marker: "3.".into(),
2634 }),
2635 },
2636 );
2637 let _region = t.add(
2638 Some(sub),
2639 None,
2640 TreeKind::FieldRegion {
2641 items: vec![crate::FieldItem {
2642 marker: None,
2643 key: Some("Name".into()),
2644 value: Some("Duck".into()),
2645 value_kind: Some("read_only".into()),
2646 }],
2647 },
2648 );
2649 let table = t.add(
2650 Some(sub),
2651 None,
2652 TreeKind::Table {
2653 table: Table {
2654 rows: vec![vec!["a \n<".into(), "b".into()]],
2655 cells: Some(vec![
2656 crate::TableCell {
2657 text: "a \n<".into(),
2658 bbox: None,
2659 start_row: 0,
2660 start_col: 0,
2661 row_span: 3,
2662 col_span: 1,
2663 column_header: false,
2664 row_header: true,
2665 row_section: false,
2666 },
2667 crate::TableCell {
2668 text: "b".into(),
2669 bbox: None,
2670 start_row: 0,
2671 start_col: 1,
2672 row_span: 1,
2673 col_span: 1,
2674 column_header: false,
2675 row_header: false,
2676 row_section: false,
2677 },
2678 ]),
2679 ..Table::default()
2680 },
2681 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
2683 },
2684 );
2685 let cell_group = t.add(
2686 Some(table),
2687 None,
2688 TreeKind::Group {
2689 label: "unspecified".into(),
2690 name: "rich_cell_group_1_0_0".into(),
2691 },
2692 );
2693 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
2694 *rich_cells = vec![(0, 1, cell_group)];
2695 }
2696 let after = t.add(Some(sub), None, text("text", "after the region"));
2697 let _ = (title, after);
2698
2699 let doc = DoclingDocument {
2700 tree: Some(t),
2701 ..DoclingDocument::new("t")
2702 };
2703 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2704 let texts: Vec<&str> = v["texts"]
2706 .as_array()
2707 .unwrap()
2708 .iter()
2709 .map(|t| t["text"].as_str().unwrap())
2710 .collect();
2711 assert_eq!(
2712 texts,
2713 [
2714 "Page",
2715 "Heading",
2716 "bold",
2717 "x = 1",
2718 "Sub",
2719 "item",
2720 "Name",
2721 "Duck",
2722 "after the region"
2723 ]
2724 );
2725 assert_eq!(
2726 v["body"]["children"],
2727 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
2728 );
2729 assert_eq!(v["texts"][0]["content_layer"], "furniture");
2730 assert_eq!(
2731 v["texts"][1]["children"],
2732 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
2733 );
2734 let bold = &v["texts"][2];
2735 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
2736 let keys: Vec<&str> = bold
2737 .as_object()
2738 .unwrap()
2739 .keys()
2740 .map(String::as_str)
2741 .collect();
2742 assert_eq!(
2743 keys,
2744 [
2745 "self_ref",
2746 "parent",
2747 "children",
2748 "content_layer",
2749 "label",
2750 "prov",
2751 "orig",
2752 "text",
2753 "formatting",
2754 "hyperlink"
2755 ]
2756 );
2757 assert_eq!(
2758 bold["formatting"],
2759 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
2760 );
2761 let code = &v["texts"][3];
2762 assert_eq!(code["label"], "code");
2763 assert_eq!(code["code_language"], "Python");
2764 let sub = &v["texts"][4];
2765 assert_eq!(sub["orig"], "Sub\u{2019}");
2766 assert_eq!(sub["level"], 1);
2767 let item = &v["texts"][5];
2768 let keys: Vec<&str> = item
2769 .as_object()
2770 .unwrap()
2771 .keys()
2772 .map(String::as_str)
2773 .collect();
2774 assert_eq!(
2775 keys,
2776 [
2777 "self_ref",
2778 "parent",
2779 "children",
2780 "content_layer",
2781 "label",
2782 "prov",
2783 "orig",
2784 "text",
2785 "enumerated",
2786 "marker"
2787 ]
2788 );
2789 assert_eq!(item["marker"], "3.");
2790 assert_eq!(v["texts"][7]["kind"], "read_only");
2791 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
2792 let table = &v["tables"][0];
2793 assert_eq!(
2794 table["children"],
2795 serde_json::json!([{"$ref": "#/groups/1"}])
2796 );
2797 let cells = table["data"]["table_cells"].as_array().unwrap();
2798 assert_eq!(
2799 cells[0]["text"], "a \n<",
2800 "raw cell text is written verbatim"
2801 );
2802 assert_eq!(
2803 cells[0]["end_row_offset_idx"], 3,
2804 "declared spans are not clamped"
2805 );
2806 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
2807 assert!(cells[0].get("ref").is_none());
2808 assert!(
2809 table["data"]["grid"][0][1].get("ref").is_none(),
2810 "the grid shows plain cells"
2811 );
2812 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
2813 }
2814
2815 #[test]
2819 fn a_comment_section_can_be_referenced_by_its_note_text() {
2820 let doc = DoclingDocument {
2821 name: "c".into(),
2822 nodes: vec![
2823 Node::Commented {
2824 comments: vec![0],
2825 inner: Box::new(Node::Paragraph {
2826 text: "annotated".into(),
2827 }),
2828 },
2829 Node::CommentSection {
2830 name: "comment-Sheet1-A1".into(),
2831 text: "[author: A]: note".into(),
2832 refs_note_text: true,
2833 grouped: true,
2834 },
2835 ],
2836 ..DoclingDocument::new("c")
2837 };
2838 let v = crate::json::to_json(&doc);
2839 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
2840 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
2841 }
2842
2843 #[test]
2847 fn comment_sections_link_back_to_their_items() {
2848 let doc = DoclingDocument {
2849 name: "c".into(),
2850 nodes: vec![
2851 Node::Commented {
2852 comments: vec![0],
2853 inner: Box::new(Node::Paragraph {
2854 text: "annotated".into(),
2855 }),
2856 },
2857 Node::Paragraph {
2858 text: "plain".into(),
2859 },
2860 Node::CommentSection {
2861 name: "comment-7".into(),
2862 text: "[time: t]: note".into(),
2863 refs_note_text: false,
2864 grouped: true,
2865 },
2866 ],
2867 ..DoclingDocument::new("c")
2868 };
2869 let v = crate::json::to_json(&doc);
2870 assert_eq!(v["groups"][0]["label"], "comment_section");
2872 assert_eq!(v["groups"][0]["name"], "comment-7");
2873 assert_eq!(v["groups"][0]["content_layer"], "notes");
2874 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
2875 assert_eq!(v["texts"][2]["content_layer"], "notes");
2876 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
2878 assert!(v["texts"][1].get("comments").is_none());
2879 let keys: Vec<&str> = v["texts"][0]
2881 .as_object()
2882 .unwrap()
2883 .keys()
2884 .map(String::as_str)
2885 .collect();
2886 assert_eq!(
2887 &keys[keys.len() - 4..],
2888 &["prov", "comments", "orig", "text"]
2889 );
2890 }
2891
2892 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
2893 Node::Picture {
2894 caption: Some(caption.into()),
2895 caption_href: None,
2896 image: None,
2897 classification: None,
2898 caption_parent,
2899 }
2900 }
2901
2902 fn group(children: Vec<Node>) -> Node {
2903 Node::Group {
2904 label: "section".into(),
2905 name: None,
2906 layer: None,
2907 children,
2908 }
2909 }
2910
2911 fn refs(v: &Value) -> Vec<&str> {
2912 v.as_array()
2913 .unwrap()
2914 .iter()
2915 .map(|r| r["$ref"].as_str().unwrap())
2916 .collect()
2917 }
2918
2919 #[test]
2924 fn a_body_caption_follows_the_enclosing_top_level_item() {
2925 let mut doc = DoclingDocument::new("t");
2926 doc.push(picture("top", CaptionParent::Body));
2927 doc.push(group(vec![
2928 Node::Paragraph { text: "p".into() },
2929 picture("nested", CaptionParent::Body),
2930 ]));
2931 doc.push(Node::Paragraph {
2932 text: "after".into(),
2933 });
2934 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2935 assert_eq!(
2936 refs(&v["body"]["children"]),
2937 [
2938 "#/texts/0",
2939 "#/pictures/0",
2940 "#/groups/0",
2941 "#/texts/2",
2942 "#/texts/3"
2943 ]
2944 );
2945 assert_eq!(
2946 refs(&v["groups"][0]["children"]),
2947 ["#/texts/1", "#/pictures/1"]
2948 );
2949 for (cap, pic) in [(0, 0), (2, 1)] {
2950 assert_eq!(v["texts"][cap]["label"], "caption");
2951 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
2952 assert_eq!(
2953 refs(&v["pictures"][pic]["captions"]),
2954 [format!("#/texts/{cap}")]
2955 );
2956 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
2957 }
2958 }
2959
2960 #[test]
2963 fn an_item_caption_is_the_items_first_child() {
2964 let mut doc = DoclingDocument::new("t");
2965 doc.push(picture("fig", CaptionParent::Item));
2966 doc.push(Node::Table(Table {
2967 rows: vec![vec!["a".into()]],
2968 caption: Some("tab".into()),
2969 caption_parent: CaptionParent::Item,
2970 ..Table::default()
2971 }));
2972 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2973 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
2974 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
2975 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
2976 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
2977 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
2978 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
2979 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
2980 }
2981
2982 #[test]
2986 fn a_container_caption_is_the_items_sibling() {
2987 let mut doc = DoclingDocument::new("t");
2988 doc.push(group(vec![
2989 picture("chart", CaptionParent::Container),
2990 Node::Table(Table {
2991 rows: vec![vec!["a".into()]],
2992 caption: Some("figcaption".into()),
2993 caption_parent: CaptionParent::ContainerAfter,
2994 ..Table::default()
2995 }),
2996 ]));
2997 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2998 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
2999 assert_eq!(
3000 refs(&v["groups"][0]["children"]),
3001 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
3002 );
3003 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
3004 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
3005 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
3006 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
3007 }
3008}