1use serde_json::{json, Map, 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
124type PageSizes = std::collections::HashMap<usize, (f64, f64)>;
126
127fn page_sizes(pages: &[(usize, f64, f64)]) -> PageSizes {
128 let mut sizes = PageSizes::with_capacity(pages.len());
129 for &(p, w, h) in pages {
130 sizes.entry(p).or_insert((w, h));
131 }
132 sizes
133}
134
135fn clamp_boxes_to_pages(b: &mut Builder) {
142 if b.pages.is_empty() {
143 return;
144 }
145 let sizes = page_sizes(&b.pages);
147 for item in &mut b.texts {
148 match item {
149 Item::Text(t) => t.prov.clamp(&sizes),
150 Item::Json(v) => clamp_item(v, &sizes),
151 }
152 }
153 for item in b
154 .pictures
155 .iter_mut()
156 .chain(b.tables.iter_mut())
157 .chain(b.key_value_items.iter_mut())
158 {
159 clamp_item(item, &sizes);
160 }
161}
162
163fn clamp_item(item: &mut Value, sizes: &PageSizes) {
164 let size = |page_no: &Value| -> Option<(f64, f64)> {
165 let n = page_no.as_u64()? as usize;
166 sizes.get(&n).copied()
167 };
168 let clamp_bbox = |bbox: &mut Value, (w, h): (f64, f64)| {
169 for (key, hi) in [("l", w), ("r", w), ("t", h), ("b", h)] {
170 if let Some(v) = bbox.get(key).and_then(Value::as_f64) {
171 bbox[key] = json!(round2(v.clamp(0.0, hi.max(0.0))));
172 }
173 }
174 };
175 let mut table_page: Option<Option<(f64, f64)>> = None;
176 if let Some(provs) = item.get_mut("prov").and_then(Value::as_array_mut) {
177 let mut page_nos: Vec<u64> = Vec::new();
178 for prov in provs.iter_mut() {
179 if let Some(n) = prov.get("page_no").and_then(Value::as_u64) {
180 page_nos.push(n);
181 }
182 let Some(sz) = prov.get("page_no").and_then(size) else {
183 continue;
184 };
185 if let Some(bbox) = prov.get_mut("bbox") {
186 clamp_bbox(bbox, sz);
187 }
188 }
189 page_nos.sort_unstable();
190 page_nos.dedup();
191 if let [only] = page_nos[..] {
192 table_page = Some(size(&json!(only)));
193 }
194 }
195 if let (Some(Some(sz)), Some(data)) = (table_page, item.get_mut("data")) {
197 for key in ["table_cells", "grid"] {
198 let Some(rows) = data.get_mut(key).and_then(Value::as_array_mut) else {
199 continue;
200 };
201 for entry in rows.iter_mut() {
202 let cells: Vec<&mut Value> = match entry {
203 Value::Array(row) => row.iter_mut().collect(),
204 other => vec![other],
205 };
206 for cell in cells {
207 if let Some(bbox) = cell.get_mut("bbox").filter(|b| b.is_object()) {
208 clamp_bbox(bbox, sz);
209 }
210 }
211 }
212 }
213 }
214}
215
216fn formatting_json(f: &crate::tree::Formatting) -> Value {
218 json!({
219 "bold": f.bold,
220 "italic": f.italic,
221 "underline": f.underline,
222 "strikethrough": f.strikethrough,
223 "script": match f.script {
224 crate::Script::Baseline => "baseline",
225 crate::Script::Sub => "sub",
226 crate::Script::Super => "super",
227 },
228 })
229}
230
231fn track_json(t: &crate::tree::TreeTrack) -> Value {
234 let mut m = serde_json::Map::new();
235 m.insert("kind".into(), json!("track"));
236 m.insert("start_time".into(), json!(t.start_time));
237 m.insert("end_time".into(), json!(t.end_time));
238 if let Some(id) = &t.identifier {
239 m.insert("identifier".into(), json!(id));
240 }
241 if let Some(v) = &t.voice {
242 m.insert("voice".into(), json!(v));
243 }
244 Value::Object(m)
245}
246
247fn image_ref_json(img: &crate::PictureImage) -> Value {
251 let (mimetype, uri) = crate::pixel_digest::docling_data_uri(img);
252 json!({
253 "mimetype": mimetype,
254 "dpi": img.dpi,
255 "size": { "width": img.width as f64, "height": img.height as f64 },
256 "uri": uri,
257 })
258}
259
260pub fn to_json(doc: &DoclingDocument) -> Value {
262 build_json(doc, false)
263}
264
265pub(crate) fn to_json_with_notes(doc: &DoclingDocument) -> Value {
277 build_json(doc, true)
278}
279
280fn build_parts(doc: &DoclingDocument, note_keys: bool) -> Parts {
281 let mut b = Builder {
282 note_keys,
283 ..Builder::default()
284 };
285 let body = match &doc.tree {
289 Some(tree) => {
290 for n in &doc.nodes {
293 if let Node::PageInfo {
294 page_no,
295 width,
296 height,
297 } = n
298 {
299 if *page_no > 0 {
300 b.pages.push((*page_no, *width as f64, *height as f64));
301 }
302 }
303 }
304 let mut tree = tree.clone();
308 tree.wrap_misplaced_list_items();
309 b.write_tree(&tree).into_iter().map(Child::json).collect()
310 }
311 None => b.walk_into(&doc.nodes, "#/body"),
312 };
313 b.link_comments();
314
315 clamp_boxes_to_pages(&mut b);
324
325 let pages = b
326 .pages
327 .iter()
328 .map(|(n, w, h)| {
329 let r2 = |v: f64| (v * 100.0).round() / 100.0;
330 let mut page = serde_json::Map::new();
333 page.insert("size".into(), json!({ "width": r2(*w), "height": r2(*h) }));
334 if let Some(img) = doc.page_images.get(n) {
335 page.insert("image".into(), image_ref_json(img));
336 }
337 page.insert("page_no".into(), json!(n));
338 (n.to_string(), Value::Object(page))
339 })
340 .collect::<serde_json::Map<String, Value>>();
341 let fields = (!b.field_regions.is_empty()).then_some((b.field_regions, b.field_items));
344 Parts {
345 name: doc.name.clone(),
346 metadata: (doc.metadata_output.in_json() || note_keys)
349 .then(|| doc.metadata.to_json())
350 .filter(|m| m.as_object().is_some_and(|o| !o.is_empty())),
351 origin: json!({
352 "mimetype": "text/plain",
353 "binary_hash": fnv1a(&doc.name),
354 "filename": doc.name,
355 }),
356 furniture: json!({
357 "self_ref": "#/furniture",
358 "children": [],
359 "content_layer": "furniture",
360 "name": "_root_",
361 "label": "unspecified",
362 }),
363 body,
364 groups: b.groups,
365 texts: b.texts,
366 pictures: b.pictures,
367 tables: b.tables,
368 key_value_items: b.key_value_items,
369 fields,
370 pages,
371 }
372}
373
374fn build_json(doc: &DoclingDocument, note_keys: bool) -> Value {
375 build_parts(doc, note_keys).into_value()
376}
377
378struct Parts {
383 name: String,
384 metadata: Option<Value>,
387 origin: Value,
388 furniture: Value,
389 body: Vec<Child>,
391 groups: Vec<Value>,
392 texts: Vec<Item>,
393 pictures: Vec<Value>,
394 tables: Vec<Value>,
395 key_value_items: Vec<Value>,
396 fields: Option<(Vec<Value>, Vec<Value>)>,
399 pages: serde_json::Map<String, Value>,
400}
401
402impl Parts {
403 fn into_value(self) -> Value {
404 let mut out = serde_json::Map::with_capacity(15);
406 out.insert("schema_name".into(), "DoclingDocument".into());
407 out.insert("version".into(), SCHEMA_VERSION.into());
408 out.insert("name".into(), Value::String(self.name));
409 out.insert("origin".into(), self.origin);
410 if let Some(m) = self.metadata {
411 out.insert("metadata".into(), m);
412 }
413 out.insert("furniture".into(), self.furniture);
414 out.insert(
415 "body".into(),
416 object([
417 ("self_ref", "#/body".into()),
418 (
419 "children",
420 Value::Array(self.body.into_iter().map(Child::into_value).collect()),
421 ),
422 ("content_layer", "body".into()),
423 ("name", "_root_".into()),
424 ("label", "unspecified".into()),
425 ]),
426 );
427 out.insert("groups".into(), Value::Array(self.groups));
428 out.insert(
429 "texts".into(),
430 Value::Array(
431 self.texts
432 .into_iter()
433 .enumerate()
434 .map(|(i, item)| item.into_value(i))
435 .collect(),
436 ),
437 );
438 out.insert("pictures".into(), Value::Array(self.pictures));
439 out.insert("tables".into(), Value::Array(self.tables));
440 out.insert("key_value_items".into(), Value::Array(self.key_value_items));
441 out.insert("form_items".into(), Value::Array(Vec::new()));
442 if let Some((regions, items)) = self.fields {
443 out.insert("field_regions".into(), Value::Array(regions));
444 out.insert("field_items".into(), Value::Array(items));
445 }
446 out.insert("pages".into(), Value::Object(self.pages));
447 Value::Object(out)
448 }
449}
450
451impl serde::Serialize for Parts {
452 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
453 use serde::ser::SerializeMap;
454 let mut m = s.serialize_map(Some(
455 13 + 2 * usize::from(self.fields.is_some()) + usize::from(self.metadata.is_some()),
456 ))?;
457 m.serialize_entry("schema_name", "DoclingDocument")?;
458 m.serialize_entry("version", SCHEMA_VERSION)?;
459 m.serialize_entry("name", &self.name)?;
460 m.serialize_entry("origin", &self.origin)?;
461 if let Some(meta) = &self.metadata {
462 m.serialize_entry("metadata", meta)?;
463 }
464 m.serialize_entry("furniture", &self.furniture)?;
465 m.serialize_entry("body", &Body(&self.body))?;
466 m.serialize_entry("groups", &self.groups)?;
467 m.serialize_entry("texts", &Texts(&self.texts))?;
468 m.serialize_entry("pictures", &self.pictures)?;
469 m.serialize_entry("tables", &self.tables)?;
470 m.serialize_entry("key_value_items", &self.key_value_items)?;
471 m.serialize_entry("form_items", &[(); 0])?;
472 if let Some((regions, items)) = &self.fields {
473 m.serialize_entry("field_regions", regions)?;
474 m.serialize_entry("field_items", items)?;
475 }
476 m.serialize_entry("pages", &self.pages)?;
477 m.end()
478 }
479}
480
481struct Body<'a>(&'a [Child]);
483
484impl serde::Serialize for Body<'_> {
485 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
486 use serde::ser::SerializeMap;
487 let mut m = s.serialize_map(Some(5))?;
488 m.serialize_entry("self_ref", "#/body")?;
489 m.serialize_entry("children", self.0)?;
490 m.serialize_entry("content_layer", "body")?;
491 m.serialize_entry("name", "_root_")?;
492 m.serialize_entry("label", "unspecified")?;
493 m.end()
494 }
495}
496
497pub fn write_json<W: std::io::Write>(
504 doc: &DoclingDocument,
505 w: W,
506 pretty: bool,
507) -> serde_json::Result<()> {
508 use serde::Serialize;
509 let parts = build_parts(doc, false);
510 if pretty {
511 parts.serialize(&mut serde_json::Serializer::pretty(w))
512 } else {
513 parts.serialize(&mut serde_json::Serializer::new(w))
514 }
515}
516
517fn classification_meta(classes: &[crate::PictureClass]) -> Value {
524 json!({
525 "classification": {
526 "predictions": classes.iter().map(|c| json!({
527 "confidence": c.confidence as f64,
528 "created_by": "DocumentPictureClassifier",
529 "class_name": c.class_name,
530 })).collect::<Vec<_>>(),
531 },
532 "annotations": [{
533 "kind": "classification",
534 "provenance": "DocumentPictureClassifier",
535 "predicted_classes": classes.iter().map(|c| json!({
536 "class_name": c.class_name,
537 "confidence": c.confidence as f64,
538 })).collect::<Vec<_>>(),
539 }],
540 })
541}
542
543fn description_meta(desc: &crate::PictureDescription) -> Value {
551 json!({
552 "description": {
553 "created_by": desc.provenance,
554 "text": desc.text,
555 },
556 "annotations": [{
557 "kind": "description",
558 "text": desc.text,
559 "provenance": desc.provenance,
560 }],
561 })
562}
563
564fn picture_meta(
570 classes: Option<&[crate::PictureClass]>,
571 desc: Option<&crate::PictureDescription>,
572) -> Option<Value> {
573 let mut meta = Map::new();
574 let mut annotations = Vec::new();
575 if let Some(desc) = desc {
576 let mut m = description_meta(desc);
577 annotations.push(m["annotations"][0].take());
578 meta.insert("description".into(), m["description"].take());
579 }
580 if let Some(classes) = classes {
581 let mut m = classification_meta(classes);
582 annotations.insert(0, m["annotations"][0].take());
583 meta.insert("classification".into(), m["classification"].take());
584 }
585 if meta.is_empty() {
586 return None;
587 }
588 meta.insert("annotations".into(), Value::Array(annotations));
589 Some(Value::Object(meta))
590}
591
592#[allow(clippy::too_many_arguments)]
603fn cell_value(
604 row_span: usize,
605 col_span: usize,
606 start_row: usize,
607 end_row: usize,
608 start_col: usize,
609 end_col: usize,
610 text: String,
611 column_header: bool,
612 row_header: bool,
613 row_section: bool,
614 bbox: Option<[f32; 4]>,
615) -> Value {
616 let mut m = serde_json::Map::with_capacity(12);
617 m.insert("row_span".into(), row_span.into());
618 m.insert("col_span".into(), col_span.into());
619 m.insert("start_row_offset_idx".into(), start_row.into());
620 m.insert("end_row_offset_idx".into(), end_row.into());
621 m.insert("start_col_offset_idx".into(), start_col.into());
622 m.insert("end_col_offset_idx".into(), end_col.into());
623 m.insert("text".into(), Value::String(text));
624 m.insert("column_header".into(), column_header.into());
625 m.insert("row_header".into(), row_header.into());
626 m.insert("row_section".into(), row_section.into());
627 m.insert("fillable".into(), false.into());
628 if let Some(b) = bbox {
629 m.insert(
630 "bbox".into(),
631 json!({
632 "l": b[0], "t": b[1], "r": b[2], "b": b[3],
633 "coord_origin": "TOPLEFT",
634 }),
635 );
636 }
637 Value::Object(m)
638}
639
640fn table_data(t: &Table) -> Value {
643 table_data_with(t, false)
644}
645
646fn table_data_with(t: &Table, raw: bool) -> Value {
649 let cell_text = |s: &str| {
650 if raw {
651 s.to_string()
652 } else {
653 unescape_text(&crate::markdown::strip_hard_breaks(s))
654 }
655 };
656 let mut num_rows = t.rows.len();
657 let mut num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
658 let ragged = t.rows.iter().any(|r| r.len() != num_cols);
666 if let Some(first_class) = t.cells.as_ref().filter(|c| ragged && !c.is_empty()) {
667 let (mut rows, mut cols) = (num_rows, num_cols);
668 for c in first_class {
669 rows = rows.max(c.start_row.saturating_add(c.row_span));
670 cols = cols.max(c.start_col.saturating_add(c.col_span));
671 }
672 if rows.saturating_mul(cols) <= crate::limits::max_table_cells() {
676 num_rows = rows;
677 num_cols = cols;
678 }
679 }
680 let mut grid = Vec::with_capacity(num_rows);
681 let mut cells = Vec::new();
682 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
686 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
687 for c in first_class {
688 let idx = cells.len();
689 cells.push(cell_value(
690 c.row_span,
691 c.col_span,
692 c.start_row,
693 c.start_row + c.row_span,
694 c.start_col,
695 c.start_col + c.col_span,
696 cell_text(&c.text),
697 c.column_header,
698 c.row_header,
699 c.row_section,
700 c.bbox,
701 ));
702 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
703 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
704 slot[r * num_cols + k] = Some(idx);
705 }
706 }
707 }
708 for r in 0..num_rows {
709 let mut grid_row = Vec::with_capacity(num_cols);
710 for c in 0..num_cols {
711 grid_row.push(match slot[r * num_cols + c] {
712 Some(i) => cells[i].clone(),
713 None => cell_value(
714 1,
715 1,
716 r,
717 r + 1,
718 c,
719 c + 1,
720 String::new(),
721 false,
722 false,
723 false,
724 None,
725 ),
726 });
727 }
728 grid.push(grid_row);
729 }
730 } else {
731 let s = t.structure.as_ref();
732 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
733 grid.and_then(|g| g.get(r))
734 .and_then(|row| row.get(c))
735 .copied()
736 .unwrap_or(false)
737 };
738 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
739 let (mut r0, mut c0) = (r, c);
740 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
741 c0 -= 1;
742 }
743 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
744 r0 -= 1;
745 }
746 (r0, c0)
747 };
748 let anchors: Vec<(usize, usize)> = (0..num_rows)
751 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
752 .map(|(r, c)| anchor_of(r, c))
753 .collect();
754 let mut extent: Vec<(usize, usize)> = (0..num_rows)
755 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
756 .collect();
757 for (i, &(ar, ac)) in anchors.iter().enumerate() {
758 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
759 let e = &mut extent[ar * num_cols + ac];
760 e.0 = e.0.max(r);
761 e.1 = e.1.max(c);
762 }
763 for (r, row) in t.rows.iter().enumerate() {
764 let mut grid_row = Vec::with_capacity(num_cols);
765 for c in 0..num_cols {
766 let (ar, ac) = anchors[r * num_cols + c];
767 if (ar, ac) == (r, c) {
768 let (er, ec) = extent[r * num_cols + c];
769 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
770 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
771 Some(s) => flag(Some(&s.col_header), r, c),
772 None => r == 0,
773 };
774 slot[r * num_cols + c] = Some(cells.len());
775 cells.push(cell_value(
776 er - r + 1,
777 ec - c + 1,
778 r,
779 er + 1,
780 c,
781 ec + 1,
782 text,
783 column_header,
784 flag(s.map(|s| &s.row_header), r, c),
785 false,
786 None,
787 ));
788 }
789 grid_row.push(match slot[ar * num_cols + ac] {
790 Some(i) => cells[i].clone(),
791 None => Value::Null,
792 });
793 }
794 grid.push(grid_row);
795 }
796 }
797 json!({
798 "table_cells": cells,
799 "num_rows": num_rows,
800 "num_cols": num_cols,
801 "orientation": "rot_0",
802 "grid": grid,
803 })
804}
805
806#[derive(Clone, Copy)]
810enum ProvSlot {
811 None,
812 One {
813 page_no: usize,
814 bbox: [f64; 4],
816 bottom_left: bool,
817 charspan: [usize; 2],
818 },
819}
820
821impl ProvSlot {
822 fn origin(bottom_left: bool) -> &'static str {
823 if bottom_left {
824 "BOTTOMLEFT"
825 } else {
826 "TOPLEFT"
827 }
828 }
829
830 fn to_value(self) -> Value {
833 let ProvSlot::One {
834 page_no,
835 bbox: [l, t, r, b],
836 bottom_left,
837 charspan,
838 } = self
839 else {
840 return Value::Array(Vec::new());
841 };
842 let bbox = object([
843 ("l", l.into()),
844 ("t", t.into()),
845 ("r", r.into()),
846 ("b", b.into()),
847 ("coord_origin", Self::origin(bottom_left).into()),
848 ]);
849 let span = Value::Array(vec![charspan[0].into(), charspan[1].into()]);
850 Value::Array(vec![object([
851 ("page_no", page_no.into()),
852 ("bbox", bbox),
853 ("charspan", span),
854 ])])
855 }
856
857 fn clamp(&mut self, sizes: &PageSizes) {
859 if let ProvSlot::One { page_no, bbox, .. } = self {
860 if let Some(&(w, h)) = sizes.get(page_no) {
861 for (v, hi) in bbox.iter_mut().zip([w, h, w, h]) {
862 if v.is_finite() {
865 *v = round2(v.clamp(0.0, hi.max(0.0)));
866 }
867 }
868 }
869 }
870 }
871}
872
873impl serde::Serialize for ProvSlot {
874 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
875 use serde::ser::{SerializeMap, SerializeSeq};
876 let ProvSlot::One {
877 page_no,
878 bbox: [l, t, r, b],
879 bottom_left,
880 charspan,
881 } = *self
882 else {
883 return s.serialize_seq(Some(0))?.end();
884 };
885 struct Bbox([f64; 4], &'static str);
886 impl serde::Serialize for Bbox {
887 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
888 let mut m = s.serialize_map(Some(5))?;
889 for (k, v) in ["l", "t", "r", "b"].iter().zip(self.0) {
890 m.serialize_entry(k, &v)?;
891 }
892 m.serialize_entry("coord_origin", self.1)?;
893 m.end()
894 }
895 }
896 struct Entry(usize, Bbox, [usize; 2]);
897 impl serde::Serialize for Entry {
898 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
899 let mut m = s.serialize_map(Some(3))?;
900 m.serialize_entry("page_no", &self.0)?;
901 m.serialize_entry("bbox", &self.1)?;
902 m.serialize_entry("charspan", &self.2)?;
903 m.end()
904 }
905 }
906 let mut seq = s.serialize_seq(Some(1))?;
907 seq.serialize_element(&Entry(
908 page_no,
909 Bbox([l, t, r, b], Self::origin(bottom_left)),
910 charspan,
911 ))?;
912 seq.end()
913 }
914}
915
916struct TextItem {
923 parent: Box<str>,
924 label: Box<str>,
925 prov: ProvSlot,
926 text: Box<str>,
927}
928
929impl TextItem {
930 fn into_value(self, index: usize) -> Value {
931 let text = String::from(self.text);
932 object([
933 ("self_ref", Value::String(format!("#/texts/{index}"))),
934 ("parent", ref_value(self.parent)),
935 ("children", Value::Array(Vec::new())),
936 ("content_layer", "body".into()),
937 ("label", Value::String(self.label.into())),
938 ("prov", self.prov.to_value()),
939 ("orig", Value::String(text.clone())),
940 ("text", Value::String(text)),
941 ])
942 }
943}
944
945enum Item {
949 Text(Box<TextItem>),
950 Json(Box<Value>),
951}
952
953impl Item {
954 fn json(v: Value) -> Self {
955 Item::Json(Box::new(v))
956 }
957
958 fn value_mut(&mut self, index: usize) -> &mut Value {
961 if let Item::Text(t) = self {
962 let t = std::mem::replace(
963 t,
964 Box::new(TextItem {
965 parent: "".into(),
966 label: "".into(),
967 prov: ProvSlot::None,
968 text: "".into(),
969 }),
970 );
971 *self = Item::json(t.into_value(index));
972 }
973 match self {
974 Item::Json(v) => v,
975 Item::Text(_) => unreachable!("converted above"),
976 }
977 }
978
979 fn into_value(self, index: usize) -> Value {
980 match self {
981 Item::Text(t) => t.into_value(index),
982 Item::Json(v) => *v,
983 }
984 }
985}
986
987struct Texts<'a>(&'a [Item]);
989
990impl serde::Serialize for Texts<'_> {
991 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
992 use serde::ser::{SerializeMap, SerializeSeq};
993 struct Text<'a>(usize, &'a TextItem);
994 impl serde::Serialize for Text<'_> {
995 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
996 let Text(i, t) = *self;
997 let mut m = s.serialize_map(Some(8))?;
998 m.serialize_entry("self_ref", &format_args!("#/texts/{i}"))?;
999 m.serialize_entry("parent", &Child::Ref(t.parent.clone()))?;
1000 m.serialize_entry("children", &[(); 0])?;
1001 m.serialize_entry("content_layer", "body")?;
1002 m.serialize_entry("label", &t.label)?;
1003 m.serialize_entry("prov", &t.prov)?;
1004 m.serialize_entry("orig", &t.text)?;
1005 m.serialize_entry("text", &t.text)?;
1006 m.end()
1007 }
1008 }
1009 let mut seq = s.serialize_seq(Some(self.0.len()))?;
1010 for (i, item) in self.0.iter().enumerate() {
1011 match item {
1012 Item::Text(t) => seq.serialize_element(&Text(i, t))?,
1013 Item::Json(v) => seq.serialize_element(v)?,
1014 }
1015 }
1016 seq.end()
1017 }
1018}
1019
1020enum Child {
1024 Ref(Box<str>),
1025 Json(Box<Value>),
1026}
1027
1028impl Child {
1029 fn json(v: Value) -> Self {
1030 Child::Json(Box::new(v))
1031 }
1032
1033 fn into_value(self) -> Value {
1034 match self {
1035 Child::Ref(r) => ref_value(r),
1036 Child::Json(v) => *v,
1037 }
1038 }
1039}
1040
1041impl serde::Serialize for Child {
1042 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1043 use serde::ser::SerializeMap;
1044 match self {
1045 Child::Ref(r) => {
1046 let mut m = s.serialize_map(Some(1))?;
1047 m.serialize_entry("$ref", r)?;
1048 m.end()
1049 }
1050 Child::Json(v) => v.serialize(s),
1051 }
1052 }
1053}
1054
1055fn round2(v: f64) -> f64 {
1056 (v * 100.0).round() / 100.0
1057}
1058
1059#[derive(Default)]
1060struct Builder {
1061 note_keys: bool,
1065 texts: Vec<Item>,
1066 groups: Vec<Value>,
1067 tables: Vec<Value>,
1068 pictures: Vec<Value>,
1069 field_regions: Vec<Value>,
1070 field_items: Vec<Value>,
1071 key_value_items: Vec<Value>,
1072 pages: Vec<(usize, f64, f64)>,
1076 cur_page: usize,
1078 cur_w: f64,
1079 cur_h: f64,
1080 pending_loc: Option<[u16; 4]>,
1083 pending_exact: Option<ExactProv>,
1086 pending_track: Option<(crate::tree::TreeTrack, String)>,
1089 pending_anchors: Vec<String>,
1092 pending_siblings: Vec<Value>,
1096 pending_after: Vec<Value>,
1100 pending_body: Vec<Value>,
1104 comment_groups: Vec<String>,
1108 last_comment_section: Option<(String, String)>,
1113 pending_comments: Vec<(String, Vec<usize>)>,
1117}
1118
1119impl Builder {
1120 fn take_prov(&mut self, char_len: usize) -> Value {
1126 self.take_prov_slot(char_len).to_value()
1127 }
1128
1129 fn take_prov_slot(&mut self, char_len: usize) -> ProvSlot {
1131 let prov = self.prov_slot(char_len, false);
1132 self.pending_exact = None;
1133 self.pending_loc = None;
1134 prov
1135 }
1136
1137 fn prov_slot(&self, char_len: usize, span_over_text: bool) -> ProvSlot {
1142 let r2 = round2;
1143 if let Some(ExactProv {
1144 page_no,
1145 bbox: [l, t, r, b],
1146 bottom_left,
1147 charspan,
1148 }) = self.pending_exact
1149 {
1150 let charspan = if span_over_text {
1151 [0, char_len]
1152 } else {
1153 charspan
1154 };
1155 return ProvSlot::One {
1156 page_no,
1157 bbox: [r2(l), r2(t), r2(r), r2(b)],
1158 bottom_left,
1159 charspan,
1160 };
1161 }
1162 let Some([x0, y0, x1, y1]) = self.pending_loc else {
1163 return ProvSlot::None;
1164 };
1165 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
1169 return ProvSlot::One {
1170 page_no: self.cur_page,
1171 bbox: [0.0; 4],
1172 bottom_left: true,
1173 charspan: [0, char_len],
1174 };
1175 }
1176 ProvSlot::One {
1177 page_no: self.cur_page,
1178 bbox: [
1179 r2(x0 as f64 * self.cur_w / 512.0),
1180 r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
1181 r2(x1 as f64 * self.cur_w / 512.0),
1182 r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
1183 ],
1184 bottom_left: true,
1185 charspan: [0, char_len],
1186 }
1187 }
1188
1189 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
1192 if self.pending_loc.is_none() && self.cur_page > 0 {
1193 self.pending_loc = loc;
1194 }
1195 }
1196
1197 fn link_comments(&mut self) {
1205 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
1206 .into_iter()
1207 .map(|(item, comments)| {
1208 let refs = comments
1209 .iter()
1210 .filter_map(|i| self.comment_groups.get(*i))
1211 .map(ref_value)
1212 .collect();
1213 (item, refs)
1214 })
1215 .collect();
1216 for (item, comment_refs) in refs {
1217 if comment_refs.is_empty() {
1218 continue;
1219 }
1220 let Some(target) = self.item_mut(&item) else {
1221 continue;
1222 };
1223 let Some(obj) = target.as_object_mut() else {
1224 continue;
1225 };
1226 let tail: Vec<(String, Value)> = obj
1227 .iter()
1228 .skip_while(|(k, _)| k.as_str() != "prov")
1229 .skip(1)
1230 .map(|(k, v)| (k.clone(), v.clone()))
1231 .collect();
1232 for (k, _) in &tail {
1233 obj.shift_remove(k);
1234 }
1235 obj.insert("comments".into(), Value::Array(comment_refs));
1236 for (k, v) in tail {
1237 obj.insert(k, v);
1238 }
1239 }
1240 }
1241
1242 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
1244 let idx = ref_index(self_ref)?;
1245 if self_ref.starts_with("#/texts/") {
1246 return self.texts.get_mut(idx).map(|item| item.value_mut(idx));
1247 }
1248 let bucket = if self_ref.starts_with("#/tables/") {
1249 &mut self.tables
1250 } else if self_ref.starts_with("#/pictures/") {
1251 &mut self.pictures
1252 } else if self_ref.starts_with("#/groups/") {
1253 &mut self.groups
1254 } else {
1255 return None;
1256 };
1257 bucket.get_mut(idx)
1258 }
1259
1260 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
1264 use crate::tree::TreeKind;
1265 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
1268 let (mut nt, mut ng, mut ntb, mut np, mut nf, mut nk) = (0, 0, 0, 0, 0, 0);
1269 for item in &tree.items {
1270 if item.deleted {
1271 refs.push(String::new());
1272 continue;
1273 }
1274 let r = match &item.kind {
1275 TreeKind::Text { .. } | TreeKind::Code { .. } => {
1276 nt += 1;
1277 format!("#/texts/{}", nt - 1)
1278 }
1279 TreeKind::Group { .. } => {
1280 ng += 1;
1281 format!("#/groups/{}", ng - 1)
1282 }
1283 TreeKind::Table { .. } => {
1284 ntb += 1;
1285 format!("#/tables/{}", ntb - 1)
1286 }
1287 TreeKind::Picture { .. } => {
1288 np += 1;
1289 format!("#/pictures/{}", np - 1)
1290 }
1291 TreeKind::FieldRegion { items } => {
1292 nt += items
1295 .iter()
1296 .map(|i| {
1297 [&i.marker, &i.key, &i.value]
1298 .iter()
1299 .filter(|p| p.is_some())
1300 .count()
1301 })
1302 .sum::<usize>();
1303 nf += 1;
1304 format!("#/field_regions/{}", nf - 1)
1305 }
1306 TreeKind::KeyValueGraph { .. } => {
1307 nk += 1;
1308 format!("#/key_value_items/{}", nk - 1)
1309 }
1310 };
1311 refs.push(r);
1312 }
1313 let ref_of = |id: usize| ref_value(refs[id].as_str());
1314 for (id, item) in tree.items.iter().enumerate() {
1315 if item.deleted {
1316 continue;
1317 }
1318 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
1319 let children: Vec<Value> = item
1323 .children
1324 .iter()
1325 .filter(|&&c| !tree.items[c].deleted)
1326 .map(|&c| ref_of(c))
1327 .collect();
1328 let layer = item.layer.map_or("body", |l| l.value());
1329 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
1332 let self_ref = match &item.kind {
1333 TreeKind::Text {
1334 label,
1335 text,
1336 orig,
1337 formatting,
1338 hyperlink,
1339 level,
1340 list,
1341 } => {
1342 let mut tail = serde_json::Map::new();
1345 if let Some(f) = formatting {
1346 tail.insert("formatting".into(), formatting_json(f));
1347 }
1348 if let Some(h) = hyperlink {
1349 tail.insert("hyperlink".into(), json!(h));
1350 }
1351 if let Some(l) = level {
1352 tail.insert("level".into(), json!(l));
1353 }
1354 if let Some(l) = list {
1355 tail.insert("enumerated".into(), json!(l.enumerated));
1356 tail.insert("marker".into(), json!(l.marker));
1357 }
1358 let r = format!("#/texts/{}", self.texts.len());
1359 let prov = self.take_prov(text.chars().count());
1360 let mut item_json = json!({
1361 "self_ref": r,
1362 "parent": { "$ref": parent },
1363 "children": children,
1364 "content_layer": layer,
1365 "label": label,
1366 "prov": prov,
1367 });
1368 if !item.comments.is_empty() {
1371 item_json["comments"] =
1372 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
1373 }
1374 if let Some(track) = &item.source {
1377 item_json["source"] = json!([track_json(track)]);
1378 }
1379 merge(
1380 &mut item_json,
1381 json!({
1382 "orig": orig.as_deref().unwrap_or(text),
1383 "text": text,
1384 }),
1385 );
1386 merge(&mut item_json, Value::Object(tail));
1387 if self.note_keys {
1388 if !item.notes.is_empty() {
1389 item_json["_notes"] = Value::Array(
1390 item.notes
1391 .iter()
1392 .map(|n| json!([n.offset, n.text]))
1393 .collect(),
1394 );
1395 }
1396 if item.note_body {
1397 item_json["_note_body"] = json!(true);
1398 }
1399 if !item.anchors.is_empty() {
1400 item_json["_anchors"] = json!(item.anchors);
1401 }
1402 if let Some(number) = &item.number {
1403 item_json["_number"] = json!(number);
1404 }
1405 if let Some(rev) = &item.revision {
1406 let mut r = serde_json::Map::new();
1407 r.insert("kind".into(), json!(rev.kind.as_str()));
1408 if let Some(a) = &rev.author {
1409 r.insert("author".into(), json!(a));
1410 }
1411 if let Some(d) = &rev.date {
1412 r.insert("date".into(), json!(d));
1413 }
1414 item_json["_revision"] = Value::Object(r);
1415 }
1416 }
1417 self.texts.push(Item::json(item_json));
1418 r
1419 }
1420 TreeKind::Code {
1421 text,
1422 orig,
1423 language,
1424 formatting,
1425 hyperlink,
1426 captions,
1427 } => {
1428 let r = format!("#/texts/{}", self.texts.len());
1429 let prov = self.take_prov(text.chars().count());
1430 let mut item_json = json!({
1431 "self_ref": r,
1432 "parent": { "$ref": parent },
1433 "children": children,
1434 "content_layer": layer,
1435 "label": "code",
1436 "prov": prov,
1437 });
1438 if !item.comments.is_empty() {
1439 item_json["comments"] =
1440 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
1441 }
1442 merge(
1443 &mut item_json,
1444 json!({
1445 "orig": orig.as_deref().unwrap_or(text),
1446 "text": text,
1447 }),
1448 );
1449 if let Some(f) = formatting {
1450 item_json["formatting"] = formatting_json(f);
1451 }
1452 if let Some(h) = hyperlink {
1453 item_json["hyperlink"] = json!(h);
1454 }
1455 merge(
1456 &mut item_json,
1457 json!({
1458 "captions": captions.iter().map(|&c| ref_of(c)).collect::<Vec<_>>(),
1459 "references": [],
1460 "footnotes": [],
1461 "code_language": code_language(language.as_deref()),
1462 }),
1463 );
1464 self.texts.push(Item::json(item_json));
1465 r
1466 }
1467 TreeKind::Group { label, name } => {
1468 self.pending_exact = None;
1469 let r = format!("#/groups/{}", self.groups.len());
1470 self.groups.push(json!({
1471 "self_ref": r,
1472 "parent": { "$ref": parent },
1473 "children": children,
1474 "content_layer": layer,
1475 "name": name,
1476 "label": label,
1477 }));
1478 r
1479 }
1480 TreeKind::Table {
1481 table,
1482 rich_cells,
1483 captions,
1484 } => {
1485 let r = self.add_table_with(table, parent, true);
1487 let idx = ref_index(&r).expect("table ref");
1488 let t = &mut self.tables[idx];
1489 t["children"] = Value::Array(children);
1490 t["content_layer"] = json!(layer);
1491 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
1492 for &(row, col, group) in rich_cells {
1496 let cell_ref = ref_of(group);
1497 let hit = |c: &Value| {
1498 c["start_row_offset_idx"] == json!(row)
1499 && c["start_col_offset_idx"] == json!(col)
1500 };
1501 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
1502 for c in cells.iter_mut().filter(|c| hit(c)) {
1503 c["ref"] = cell_ref.clone();
1504 }
1505 }
1506 }
1507 r
1508 }
1509 TreeKind::Picture {
1510 captions,
1511 image,
1512 classification,
1513 extra_classes,
1514 description,
1515 confidence,
1516 chart,
1517 dpi,
1518 } => {
1519 let mut meta = classification.as_ref().map(|c| {
1524 let mut pred = serde_json::Map::new();
1525 if let Some(conf) = confidence {
1526 pred.insert("confidence".into(), json!(conf));
1527 }
1528 pred.insert("class_name".into(), json!(c));
1529 let mut preds = vec![Value::Object(pred)];
1530 preds.extend(extra_classes.iter().map(|c| json!({ "class_name": c })));
1531 json!({ "classification": { "predictions": preds } })
1532 });
1533 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
1534 if !t.rows.is_empty() {
1535 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
1536 }
1537 }
1538 if let Some(desc) = description {
1542 let mut d = description_meta(desc);
1543 let mut merged = Map::new();
1544 merged.insert("description".into(), d["description"].take());
1545 if let Some(Value::Object(rest)) = meta {
1546 merged.extend(rest);
1547 }
1548 merged.insert("annotations".into(), d["annotations"].take());
1549 meta = Some(Value::Object(merged));
1550 }
1551 let prov = self.take_prov(0);
1552 let r = self.push_picture(
1553 prov,
1554 captions.iter().map(|&c| ref_of(c)).collect(),
1555 children,
1556 image.as_ref(),
1557 meta,
1558 parent,
1559 );
1560 if let Some(idx) = ref_index(&r) {
1561 self.pictures[idx]["content_layer"] = json!(layer);
1562 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
1565 img["dpi"] = json!(dpi);
1566 }
1567 }
1568 r
1569 }
1570 TreeKind::FieldRegion { items } => {
1571 self.pending_exact = None;
1572 let r = self.add_field_region(items, parent);
1573 if let Some(region) = self.field_regions.last_mut() {
1574 region["content_layer"] = json!(layer);
1575 }
1576 r
1577 }
1578 TreeKind::KeyValueGraph { cells, links } => {
1579 self.pending_exact = None;
1580 let r = self.add_key_value_graph(cells, links, parent);
1581 if let Some(item) = self.key_value_items.last_mut() {
1582 item["content_layer"] = json!(layer);
1583 }
1584 r
1585 }
1586 };
1587 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1588 }
1589 tree.body
1590 .iter()
1591 .filter(|&&c| !tree.items[c].deleted)
1592 .map(|&c| ref_of(c))
1593 .collect()
1594 }
1595
1596 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1597 match node {
1598 Node::Heading { level: 1, text } => {
1599 Some(self.add_text("title", text, parent, json!({})))
1600 }
1601 Node::Heading { level, text } => Some(self.add_text(
1602 "section_header",
1603 text,
1604 parent,
1605 json!({ "level": level.saturating_sub(1) }),
1606 )),
1607 Node::Caption { text, href } => {
1608 let extra = match href {
1609 Some(url) => json!({ "hyperlink": url }),
1610 None => json!({}),
1611 };
1612 Some(self.add_text("caption", text, parent, extra))
1613 }
1614 Node::LabeledText { label, text, href } => {
1617 let extra = match href {
1618 Some(url) => json!({ "hyperlink": url }),
1619 None => json!({}),
1620 };
1621 Some(self.add_text(label, text, parent, extra))
1622 }
1623 Node::Paragraph { text } | Node::ListText { text, .. } => {
1624 let t = text.trim();
1627 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1628 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1629 _ => Some(self.add_text("text", text, parent, json!({}))),
1630 }
1631 }
1632 Node::CheckboxItem { checked, text } => {
1633 let label = if *checked {
1639 "checkbox_selected"
1640 } else {
1641 "checkbox_unselected"
1642 };
1643 Some(self.add_text(label, text, parent, json!({})))
1644 }
1645 Node::Code {
1646 language,
1647 text,
1648 orig,
1649 ..
1650 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1651 Node::Formula {
1654 latex,
1655 orig,
1656 location,
1657 } => {
1658 self.adopt_loc(*location);
1659 Some(self.add_formula_item(latex, orig, parent))
1660 }
1661 Node::CommentSection {
1667 name,
1668 text,
1669 refs_note_text,
1670 grouped,
1671 } => {
1672 if !*grouped {
1673 let child =
1677 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1678 self.comment_groups.push(child.clone());
1679 return Some(child);
1680 }
1681 if let Some((_, self_ref)) = self
1682 .last_comment_section
1683 .as_ref()
1684 .filter(|(n, _)| n == name)
1685 .cloned()
1686 {
1687 let child =
1690 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1691 if let Some(children) =
1692 self.groups[group_index(&self_ref)]["children"].as_array_mut()
1693 {
1694 children.push(ref_value(child.as_str()));
1695 }
1696 self.comment_groups.push(if *refs_note_text {
1697 child
1698 } else {
1699 self_ref.clone()
1700 });
1701 return Some(self_ref);
1702 }
1703 let self_ref = format!("#/groups/{}", self.groups.len());
1704 self.groups.push(Value::Null);
1705 let child =
1706 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1707 self.groups[group_index(&self_ref)] = json!({
1708 "self_ref": self_ref,
1709 "parent": { "$ref": parent },
1710 "children": [{ "$ref": child }],
1711 "content_layer": "notes",
1712 "name": name,
1713 "label": "comment_section",
1714 });
1715 self.last_comment_section = Some((name.clone(), self_ref.clone()));
1716 self.comment_groups.push(if *refs_note_text {
1717 child
1718 } else {
1719 self_ref.clone()
1720 });
1721 Some(self_ref)
1722 }
1723 Node::Commented { comments, inner } => {
1726 let item = self.add_node(inner, parent)?;
1727 if !comments.is_empty() {
1728 self.pending_comments.push((item.clone(), comments.clone()));
1729 }
1730 Some(item)
1731 }
1732 Node::Table(t) => Some(self.add_table(t, parent)),
1733 Node::Picture {
1734 caption,
1735 caption_href,
1736 image,
1737 classification,
1738 description,
1739 caption_parent,
1740 caption_location,
1741 } => Some(self.add_picture(
1742 caption.as_deref(),
1743 caption_href.as_deref(),
1744 image.as_ref(),
1745 picture_meta(classification.as_deref(), description.as_ref()),
1746 parent,
1747 (*caption_parent, *caption_location),
1748 )),
1749 Node::Chart {
1754 kind,
1755 table,
1756 caption,
1757 location,
1758 } => {
1759 self.adopt_loc(*location);
1760 let mut meta = json!({
1761 "classification": { "predictions": [{ "class_name": kind }] },
1762 });
1763 if !table.rows.is_empty() {
1764 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1765 }
1766 let mut captions = Vec::new();
1772 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1773 let prov = self.prov_slot(unescape_text(cap).chars().count(), true);
1774 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1775 self.pending_siblings.push(ref_value(cap_ref.as_str()));
1776 captions.push(ref_value(cap_ref));
1777 }
1778 let prov = self.take_prov(0);
1779 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1780 }
1781 Node::DoclangOnly(_) => None,
1783 Node::Group {
1784 label,
1785 name,
1786 layer,
1787 children,
1788 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1789 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1790 Node::KeyValueGraph { cells, links } => {
1791 Some(self.add_key_value_graph(cells, links, parent))
1792 }
1793 Node::InlineGroup { md_text, .. } => {
1796 Some(self.add_text("text", md_text, parent, json!({})))
1797 }
1798 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1800 Node::Furniture {
1806 layer: ContentLayer::Notes,
1807 inner,
1808 } => {
1809 let item = self.add_node(inner, parent)?;
1810 self.set_layer(&item, "notes");
1811 Some(item)
1812 }
1813 Node::Furniture { .. } => None,
1814 Node::PageFurniture {
1818 footer,
1819 location,
1820 text,
1821 } => {
1822 if self.cur_page > 0 {
1823 self.pending_loc = Some(*location);
1824 }
1825 let label = if *footer {
1826 "page_footer"
1827 } else {
1828 "page_header"
1829 };
1830 let item = self.add_text(label, text, parent, json!({}));
1831 self.pending_loc = None;
1832 self.set_layer(&item, "furniture");
1833 Some(item)
1834 }
1835 Node::FurnitureText { label, text } => {
1838 let item = self.add_text(label, text, parent, json!({}));
1839 self.set_layer(&item, "furniture");
1840 Some(item)
1841 }
1842 Node::PictureChildren(children) => {
1846 let pic = self.pictures.len().checked_sub(1)?;
1847 let pic_ref = format!("#/pictures/{pic}");
1848 let refs = self.walk_into(children, &pic_ref);
1849 if let Some(list) = self.pictures[pic]["children"].as_array_mut() {
1850 list.extend(refs.into_iter().map(Child::into_value));
1851 }
1852 None
1853 }
1854 Node::Located { location, inner } => {
1859 if self.cur_page > 0 {
1860 self.pending_loc = Some(*location);
1861 }
1862 let r = self.add_node(inner, parent);
1863 self.pending_loc = None;
1864 r
1865 }
1866 Node::Anchor { id } => {
1867 self.pending_anchors.push(id.clone());
1868 None
1869 }
1870 Node::Track { track, cue, inner } => {
1871 self.pending_track = Some((track.clone(), cue.clone()));
1872 let r = self.add_node(inner, parent);
1873 self.pending_track = None;
1874 r
1875 }
1876 Node::Prov {
1877 page_no,
1878 bbox,
1879 charspan,
1880 inner,
1881 ..
1882 } => {
1883 self.pending_exact = Some(ExactProv {
1884 page_no: *page_no,
1885 bbox: bbox.map(f64::from),
1886 bottom_left: false,
1887 charspan: *charspan,
1888 });
1889 let r = self.add_node(inner, parent);
1890 self.pending_exact = None;
1891 r
1892 }
1893 Node::PageBreak => None,
1895 Node::PageInfo {
1898 page_no,
1899 width,
1900 height,
1901 } => {
1902 self.cur_page = *page_no;
1903 self.cur_w = *width as f64;
1904 self.cur_h = *height as f64;
1905 if *page_no > 0 {
1906 self.pages.push((*page_no, self.cur_w, self.cur_h));
1907 }
1908 None
1909 }
1910 Node::ListItem { .. } => None,
1912 }
1913 }
1914
1915 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1919 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1920 self.field_regions.push(Value::Null);
1921 let region_index = self.field_regions.len() - 1;
1922 let mut item_refs = Vec::new();
1923 for item in items {
1924 item_refs.push(ref_value(self.add_field_item(item, &self_ref)));
1925 }
1926 self.field_regions[region_index] = json!({
1927 "self_ref": self_ref,
1928 "parent": { "$ref": parent },
1929 "children": item_refs,
1930 "content_layer": "body",
1931 "label": "field_region",
1932 "prov": [],
1933 });
1934 self_ref
1935 }
1936
1937 fn add_key_value_graph(
1941 &mut self,
1942 cells: &[crate::GraphCell],
1943 links: &[crate::GraphLink],
1944 parent: &str,
1945 ) -> String {
1946 let self_ref = format!("#/key_value_items/{}", self.key_value_items.len());
1947 let cells: Vec<Value> = cells
1948 .iter()
1949 .map(|c| {
1950 json!({
1951 "label": c.label,
1952 "cell_id": c.cell_id,
1953 "text": c.text,
1954 "orig": c.orig,
1955 })
1956 })
1957 .collect();
1958 let links: Vec<Value> = links
1959 .iter()
1960 .map(|l| {
1961 json!({
1962 "label": l.label,
1963 "source_cell_id": l.source_cell_id,
1964 "target_cell_id": l.target_cell_id,
1965 })
1966 })
1967 .collect();
1968 self.key_value_items.push(json!({
1969 "self_ref": self_ref,
1970 "parent": { "$ref": parent },
1971 "children": [],
1972 "content_layer": "body",
1973 "label": "key_value_region",
1974 "prov": [],
1975 "captions": [],
1976 "references": [],
1977 "footnotes": [],
1978 "graph": { "cells": cells, "links": links },
1979 }));
1980 self_ref
1981 }
1982
1983 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1984 let self_ref = format!("#/field_items/{}", self.field_items.len());
1985 self.field_items.push(Value::Null);
1986 let item_index = self.field_items.len() - 1;
1987 let mut child_refs = Vec::new();
1988 for (label, text) in [
1989 ("marker", &item.marker),
1990 ("field_key", &item.key),
1991 ("field_value", &item.value),
1992 ] {
1993 if let Some(text) = text {
1994 let extra = match (label, &item.value_kind) {
1997 ("field_value", Some(kind)) => json!({ "kind": kind }),
1998 _ => json!({}),
1999 };
2000 child_refs.push(ref_value(self.add_text(label, text, &self_ref, extra)));
2001 }
2002 }
2003 self.field_items[item_index] = json!({
2004 "self_ref": self_ref,
2005 "parent": { "$ref": parent },
2006 "children": child_refs,
2007 "content_layer": "body",
2008 "label": "field_item",
2009 "prov": [],
2010 });
2011 self_ref
2012 }
2013
2014 fn set_layer(&mut self, self_ref: &str, layer: &str) {
2018 if let Some(item) = self.item_mut(self_ref) {
2019 item["content_layer"] = json!(layer);
2020 }
2021 }
2022
2023 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
2024 let prov = self.take_prov_slot(unescape_text(text).chars().count());
2025 self.add_text_with(label, text, parent, extra, prov)
2026 }
2027
2028 fn add_text_with(
2031 &mut self,
2032 label: &str,
2033 text: &str,
2034 parent: &str,
2035 extra: Value,
2036 prov: ProvSlot,
2037 ) -> String {
2038 let index = self.texts.len();
2039 let self_ref = format!("#/texts/{index}");
2040 let mut item = match self.pending_track.take() {
2045 None => Item::Text(Box::new(TextItem {
2046 parent: parent.into(),
2047 label: label.into(),
2048 prov,
2049 text: unescape_text(text).into(),
2050 })),
2051 Some((track, cue)) => {
2052 let mut m = serde_json::Map::with_capacity(9);
2053 m.insert("self_ref".into(), Value::String(self_ref.clone()));
2054 m.insert("parent".into(), ref_value(parent));
2055 m.insert("children".into(), Value::Array(Vec::new()));
2056 m.insert("content_layer".into(), "body".into());
2057 m.insert("label".into(), label.into());
2058 m.insert("prov".into(), prov.to_value());
2059 m.insert("source".into(), Value::Array(vec![track_json(&track)]));
2061 m.insert("orig".into(), Value::String(cue.clone()));
2062 m.insert("text".into(), Value::String(cue));
2063 Item::json(Value::Object(m))
2064 }
2065 };
2066 if extra.as_object().is_some_and(|e| !e.is_empty()) {
2067 merge(item.value_mut(index), extra);
2068 }
2069 let anchors = std::mem::take(&mut self.pending_anchors);
2072 if self.note_keys && !anchors.is_empty() {
2073 merge(item.value_mut(index), json!({ "_anchors": anchors }));
2074 }
2075 self.texts.push(item);
2076 self_ref
2077 }
2078
2079 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
2082 let self_ref = format!("#/texts/{}", self.texts.len());
2083 let prov = self.take_prov(latex.chars().count());
2084 self.texts.push(Item::json(json!({
2085 "self_ref": self_ref,
2086 "parent": { "$ref": parent },
2087 "children": [],
2088 "content_layer": "body",
2089 "label": "formula",
2090 "prov": prov,
2091 "orig": latex,
2092 "text": latex,
2093 })));
2094 self_ref
2095 }
2096
2097 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
2101 let self_ref = format!("#/texts/{}", self.texts.len());
2102 let prov = self.take_prov(latex.chars().count());
2103 self.texts.push(Item::json(json!({
2104 "self_ref": self_ref,
2105 "parent": { "$ref": parent },
2106 "children": [],
2107 "content_layer": "body",
2108 "label": "formula",
2109 "prov": prov,
2110 "orig": orig,
2111 "text": latex,
2112 })));
2113 self_ref
2114 }
2115
2116 fn add_code(
2117 &mut self,
2118 text: &str,
2119 language: Option<&str>,
2120 orig: Option<&str>,
2121 parent: &str,
2122 ) -> String {
2123 let self_ref = format!("#/texts/{}", self.texts.len());
2124 let raw = unescape_text(text);
2125 let prov = self.take_prov(raw.chars().count());
2126 self.texts.push(Item::json(json!({
2127 "self_ref": self_ref,
2128 "parent": { "$ref": parent },
2129 "children": [],
2130 "content_layer": "body",
2131 "label": "code",
2132 "prov": prov,
2133 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
2136 "text": raw,
2137 "captions": [],
2138 "references": [],
2139 "footnotes": [],
2140 "code_language": code_language(language),
2141 })));
2142 self_ref
2143 }
2144
2145 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
2148 let self_ref = format!("#/groups/{}", self.groups.len());
2149 self.groups.push(Value::Null);
2151 let base = level_of(&items[0]);
2152 let mut children = Vec::new();
2153 let mut i = 0;
2154 while i < items.len() {
2155 if !matches!(items[i], Node::ListItem { .. }) {
2158 i += 1;
2159 continue;
2160 }
2161 let lvl = level_of(&items[i]);
2162 if lvl > base {
2163 i += 1;
2165 continue;
2166 }
2167 let item_ref = self.add_list_item(&items[i], &self_ref);
2168 let mut j = i + 1;
2170 while j < items.len() && level_of(&items[j]) > base {
2171 j += 1;
2172 }
2173 if j > i + 1 {
2174 let mut nested = Vec::new();
2175 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
2176 if let Some(idx) = ref_index(&item_ref) {
2178 self.texts[idx].value_mut(idx)["children"]
2179 .as_array_mut()
2180 .unwrap()
2181 .extend(nested);
2182 }
2183 }
2184 children.push(ref_value(item_ref));
2185 i = j;
2186 }
2187 self.groups[group_index(&self_ref)] = json!({
2188 "self_ref": self_ref,
2189 "parent": { "$ref": parent },
2190 "children": children,
2191 "content_layer": "body",
2192 "name": "list",
2193 "label": "list",
2194 });
2195 self_ref
2196 }
2197
2198 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
2199 let Node::ListItem {
2200 ordered,
2201 number,
2202 text,
2203 location,
2204 ..
2205 } = node
2206 else {
2207 unreachable!()
2208 };
2209 self.adopt_loc(*location);
2210 let self_ref = format!("#/texts/{}", self.texts.len());
2211 let raw = unescape_text(text);
2212 let prov = self.take_prov(raw.chars().count());
2213 let marker = if *ordered {
2214 format!("{number}.")
2215 } else {
2216 "-".to_string()
2217 };
2218 self.texts.push(Item::json(json!({
2219 "self_ref": self_ref,
2220 "parent": { "$ref": parent },
2221 "children": [],
2222 "content_layer": "body",
2223 "label": "list_item",
2224 "prov": prov,
2225 "orig": raw,
2226 "text": raw,
2227 "enumerated": ordered,
2228 "marker": marker,
2229 })));
2230 self_ref
2231 }
2232
2233 fn add_table(&mut self, t: &Table, parent: &str) -> String {
2234 self.add_table_with(t, parent, false)
2235 }
2236
2237 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
2240 let self_ref = format!("#/tables/{}", self.tables.len());
2241 self.adopt_loc(t.location);
2242 let prov = self.take_prov(0);
2243 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
2247 Some(cap) => self.add_caption(
2248 cap,
2249 json!({}),
2250 &self_ref,
2251 parent,
2252 (t.caption_parent, t.caption_location),
2253 ),
2254 None => (Vec::new(), Vec::new()),
2255 };
2256 let data = table_data_with(t, raw);
2257 self.tables.push(json!({
2258 "self_ref": self_ref,
2259 "parent": { "$ref": parent },
2260 "children": children,
2261 "content_layer": "body",
2262 "label": "table",
2263 "prov": prov,
2264 "captions": captions,
2265 "references": [],
2266 "footnotes": [],
2267 "data": data,
2268 "annotations": [],
2269 }));
2270 self_ref
2271 }
2272
2273 fn add_caption(
2282 &mut self,
2283 text: &str,
2284 extra: Value,
2285 self_ref: &str,
2286 parent: &str,
2287 (choice, location): (CaptionParent, Option<[u16; 4]>),
2288 ) -> (Vec<Value>, Vec<Value>) {
2289 let cap_parent = match choice {
2294 CaptionParent::Item => self_ref,
2295 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
2296 CaptionParent::Body => "#/body",
2297 };
2298 if self.cur_page > 0 && location.is_some() {
2299 self.pending_loc = location;
2300 }
2301 let cap_ref = ref_value(self.add_text("caption", text, cap_parent, extra));
2302 match choice {
2303 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
2304 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
2307 CaptionParent::Body if parent == "#/body" => {
2308 self.pending_siblings.push(cap_ref.clone())
2309 }
2310 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
2311 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
2314 }
2315 (vec![cap_ref], Vec::new())
2316 }
2317
2318 fn add_picture(
2321 &mut self,
2322 caption: Option<&str>,
2323 caption_href: Option<&str>,
2324 image: Option<&crate::PictureImage>,
2325 meta: Option<Value>,
2326 parent: &str,
2327 caption_at: (CaptionParent, Option<[u16; 4]>),
2328 ) -> String {
2329 let self_ref = format!("#/pictures/{}", self.pictures.len());
2330 let prov = self.take_prov(0);
2333 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
2334 Some(cap) => {
2335 let extra = match caption_href {
2339 Some(href) => json!({ "hyperlink": href }),
2340 None => json!({}),
2341 };
2342 self.add_caption(cap, extra, &self_ref, parent, caption_at)
2343 }
2344 None => (Vec::new(), Vec::new()),
2345 };
2346 self.push_picture(prov, captions, children, image, meta, parent)
2347 }
2348
2349 fn push_picture(
2352 &mut self,
2353 prov: Value,
2354 captions: Vec<Value>,
2355 children: Vec<Value>,
2356 image: Option<&crate::PictureImage>,
2357 meta: Option<Value>,
2358 parent: &str,
2359 ) -> String {
2360 let self_ref = format!("#/pictures/{}", self.pictures.len());
2361 let annotations = meta
2365 .as_ref()
2366 .and_then(|m| m.get("annotations").cloned())
2367 .unwrap_or_else(|| json!([]));
2368 let meta = meta.map(|mut m| {
2369 if let Some(obj) = m.as_object_mut() {
2370 obj.remove("annotations");
2371 }
2372 m
2373 });
2374 let mut item = match meta {
2378 Some(meta) => json!({
2379 "self_ref": self_ref,
2380 "parent": { "$ref": parent },
2381 "children": children,
2382 "content_layer": "body",
2383 "meta": meta,
2384 "label": "picture",
2385 "prov": prov,
2386 "captions": captions,
2387 "references": [],
2388 "footnotes": [],
2389 "annotations": annotations,
2390 }),
2391 None => json!({
2392 "self_ref": self_ref,
2393 "parent": { "$ref": parent },
2394 "children": children,
2395 "content_layer": "body",
2396 "label": "picture",
2397 "prov": prov,
2398 "captions": captions,
2399 "references": [],
2400 "footnotes": [],
2401 "annotations": annotations,
2402 }),
2403 };
2404 if let Some(img) = image {
2409 let mut image = image_ref_json(img);
2410 if self.note_keys {
2413 if let Some([w, h]) = img.display_size {
2414 image["_display"] = json!([w, h]);
2415 }
2416 }
2417 if let Some(obj) = item.as_object_mut() {
2418 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
2419 obj.insert("image".into(), image);
2420 obj.insert("annotations".into(), annotations);
2421 }
2422 }
2423 self.pictures.push(item);
2424 self_ref
2425 }
2426
2427 fn add_group(
2428 &mut self,
2429 label: &str,
2430 name: Option<&str>,
2431 layer: Option<ContentLayer>,
2432 nodes: &[Node],
2433 parent: &str,
2434 ) -> String {
2435 let self_ref = format!("#/groups/{}", self.groups.len());
2436 self.groups.push(Value::Null);
2437 let mark = (
2441 self.texts.len(),
2442 self.tables.len(),
2443 self.pictures.len(),
2444 self.groups.len(),
2445 );
2446 let children: Vec<Value> = self
2447 .walk_into(nodes, &self_ref)
2448 .into_iter()
2449 .map(Child::into_value)
2450 .collect();
2451 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
2452 let content_layer = layer.map_or("body", |l| l.value());
2453 self.groups[group_index(&self_ref)] = json!({
2454 "self_ref": self_ref,
2455 "parent": { "$ref": parent },
2456 "children": children,
2457 "content_layer": content_layer,
2458 "name": name,
2459 "label": label,
2460 });
2461 if layer.is_some() {
2462 let (t, tb, p, g) = mark;
2463 for item in self.texts[t..]
2464 .iter_mut()
2465 .enumerate()
2466 .map(|(k, item)| item.value_mut(t + k))
2467 .chain(self.tables[tb..].iter_mut())
2468 .chain(self.pictures[p..].iter_mut())
2469 .chain(self.groups[g..].iter_mut())
2470 {
2471 if let Some(obj) = item.as_object_mut() {
2472 obj.insert("content_layer".into(), json!(content_layer));
2473 }
2474 }
2475 }
2476 self_ref
2477 }
2478
2479 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Child> {
2482 let seqs: Option<Vec<usize>> = nodes
2487 .iter()
2488 .map(|n| match n {
2489 Node::Prov { seq: Some(s), .. } => Some(*s),
2490 _ => None,
2491 })
2492 .collect();
2493 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
2494 let mut order: Vec<usize> = (0..nodes.len()).collect();
2495 order.sort_by_key(|&i| seqs[i]);
2496 let mut slots: Vec<Vec<Child>> = (0..nodes.len()).map(|_| Vec::new()).collect();
2497 for i in order {
2498 if let Some(r) = self.add_node(&nodes[i], parent) {
2499 slots[i].extend(self.pending_siblings.drain(..).map(Child::json));
2500 slots[i].push(Child::Ref(r.into()));
2501 slots[i].extend(self.pending_after.drain(..).map(Child::json));
2502 }
2503 if parent == "#/body" {
2504 slots[i].extend(self.pending_body.drain(..).map(Child::json));
2505 }
2506 }
2507 return slots.into_iter().flatten().collect();
2508 }
2509 let mut children = Vec::new();
2510 let mut i = 0;
2511 while i < nodes.len() {
2512 if matches!(nodes[i], Node::ListItem { .. }) {
2513 let start = i;
2514 i += 1;
2515 loop {
2516 match nodes.get(i) {
2517 Some(Node::ListItem { .. }) => i += 1,
2518 Some(Node::Paragraph { text })
2521 if text.is_empty()
2522 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
2523 {
2524 i += 1
2525 }
2526 _ => break,
2527 }
2528 }
2529 let mut lists = Vec::new();
2530 self.add_sibling_lists(&nodes[start..i], parent, &mut lists);
2531 children.extend(lists.into_iter().map(Child::json));
2532 } else {
2533 if let Some(r) = self.add_node(&nodes[i], parent) {
2534 children.extend(self.pending_siblings.drain(..).map(Child::json));
2535 children.push(Child::Ref(r.into()));
2536 children.extend(self.pending_after.drain(..).map(Child::json));
2537 }
2538 i += 1;
2539 }
2540 if parent == "#/body" {
2543 children.extend(self.pending_body.drain(..).map(Child::json));
2544 }
2545 }
2546 children
2547 }
2548
2549 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
2554 let base = level_of(&run[0]);
2555 let mut seg = 0;
2556 for k in 0..run.len() {
2557 let Node::ListItem {
2558 first_in_list,
2559 level,
2560 ..
2561 } = &run[k]
2562 else {
2563 continue;
2564 };
2565 if *level != base {
2566 continue; }
2568 if k > seg && *first_in_list {
2569 out.push(ref_value(self.add_list(&run[seg..k], parent)));
2570 seg = k;
2571 }
2572 }
2573 out.push(ref_value(self.add_list(&run[seg..], parent)));
2574 }
2575}
2576
2577fn level_of(node: &Node) -> u8 {
2578 match node {
2579 Node::ListItem { level, .. } => *level,
2580 _ => 0,
2581 }
2582}
2583
2584fn group_index(self_ref: &str) -> usize {
2585 self_ref.rsplit('/').next().unwrap().parse().unwrap()
2586}
2587
2588fn ref_index(self_ref: &str) -> Option<usize> {
2589 self_ref.rsplit('/').next()?.parse().ok()
2590}
2591
2592fn merge(target: &mut Value, extra: Value) {
2594 if let (Some(t), Value::Object(e)) = (target.as_object_mut(), extra) {
2595 t.extend(e);
2596 }
2597}
2598
2599fn object<const N: usize>(entries: [(&str, Value); N]) -> Value {
2605 let mut m = serde_json::Map::with_capacity(N);
2606 for (k, v) in entries {
2607 m.insert(k.to_owned(), v);
2608 }
2609 Value::Object(m)
2610}
2611
2612fn ref_value(r: impl Into<String>) -> Value {
2614 object([("$ref", Value::String(r.into()))])
2615}
2616
2617fn unescape_text(s: &str) -> String {
2619 s.replace("<", "<")
2620 .replace(">", ">")
2621 .replace("&", "&")
2622 .replace("\\_", "_")
2623}
2624
2625fn fnv1a(s: &str) -> u64 {
2628 let mut h: u64 = 0xcbf29ce484222325;
2629 for b in s.bytes() {
2630 h ^= b as u64;
2631 h = h.wrapping_mul(0x100000001b3);
2632 }
2633 h
2634}
2635
2636#[cfg(test)]
2637mod tests {
2638 use crate::{
2639 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
2640 };
2641 use serde_json::Value;
2642
2643 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
2644 crate::TableCell {
2645 text: text.into(),
2646 bbox: None,
2647 start_row: row,
2648 start_col: col,
2649 row_span: 1,
2650 col_span,
2651 column_header: row == 0,
2652 row_header: false,
2653 row_section: false,
2654 }
2655 }
2656
2657 #[test]
2663 fn ragged_rows_take_their_grid_from_first_class_cells() {
2664 let cells = vec![
2665 cell_at("h0", 0, 0, 1),
2666 cell_at("h2", 0, 2, 1),
2667 cell_at("wide", 1, 0, 3),
2668 ];
2669 let export = |rows: Vec<Vec<String>>| -> Value {
2670 let mut doc = DoclingDocument::new("t");
2671 doc.push(Node::Table(Table {
2672 rows,
2673 cells: Some(cells.clone()),
2674 ..Table::default()
2675 }));
2676 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2677 v["tables"][0]["data"].clone()
2678 };
2679 let data = export(vec![
2681 vec!["h0".into(), "h2".into()],
2682 vec!["wide".into(), "wide".into(), "wide".into()],
2683 ]);
2684 assert_eq!(data["num_rows"], 2);
2685 assert_eq!(data["num_cols"], 3);
2686 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
2687 let row0: Vec<&str> = data["grid"][0]
2688 .as_array()
2689 .unwrap()
2690 .iter()
2691 .map(|c| c["text"].as_str().unwrap())
2692 .collect();
2693 assert_eq!(row0, ["h0", "", "h2"]);
2694 assert_eq!(data["table_cells"][2]["col_span"], 3);
2695 let data = export(vec![
2698 vec!["h0".into(), "h2".into()],
2699 vec!["wide".into(), "wide".into()],
2700 ]);
2701 assert_eq!(data["num_cols"], 2);
2702 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
2703 }
2704
2705 fn doc_with_image() -> DoclingDocument {
2706 let mut doc = DoclingDocument::new("t");
2707 doc.push(Node::Picture {
2708 caption: Some("Fig 1".into()),
2709 caption_href: None,
2710 image: Some(PictureImage {
2711 dpi: crate::PictureImage::DEFAULT_DPI,
2712 mimetype: "image/png".into(),
2713 width: 4,
2714 height: 2,
2715 data: b"foobar".to_vec(),
2716 display_size: None,
2717 }),
2718 classification: None,
2719 description: None,
2720 caption_parent: Default::default(),
2721 caption_location: None,
2722 });
2723 doc
2724 }
2725
2726 #[test]
2731 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
2732 let mut doc = DoclingDocument::new("t");
2733 doc.push(Node::Heading {
2734 level: 1,
2735 text: "Slide One".into(),
2736 });
2737 doc.push(Node::Furniture {
2738 layer: ContentLayer::Notes,
2739 inner: Box::new(Node::Located {
2740 location: [0, 0, 0, 0],
2741 inner: Box::new(Node::Paragraph {
2742 text: "Speaker note for slide 1.".into(),
2743 }),
2744 }),
2745 });
2746 doc.push(Node::Furniture {
2747 layer: ContentLayer::Furniture,
2748 inner: Box::new(Node::Paragraph {
2749 text: "page header".into(),
2750 }),
2751 });
2752
2753 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2754 let texts = v["texts"].as_array().unwrap();
2755 assert_eq!(
2756 texts
2757 .iter()
2758 .map(|t| (
2759 t["label"].as_str().unwrap(),
2760 t["content_layer"].as_str().unwrap(),
2761 t["text"].as_str().unwrap()
2762 ))
2763 .collect::<Vec<_>>(),
2764 vec![
2765 ("title", "body", "Slide One"),
2766 ("text", "notes", "Speaker note for slide 1."),
2767 ],
2768 "the note is carried on its own layer; the furniture is not carried"
2769 );
2770 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
2772 }
2773
2774 #[test]
2780 fn continuation_flags_become_spanning_cells() {
2781 let mut doc = DoclingDocument::new("t");
2782 let rows = vec![
2784 vec!["merged".to_string(), "merged".into(), "merged".into()],
2785 vec!["merged".to_string(), "merged".into(), "merged".into()],
2786 vec!["a".to_string(), "b".into(), "c".into()],
2787 ];
2788 doc.push(Node::Table(crate::Table {
2789 rows,
2790 location: None,
2791 structure: Some(crate::TableStructure {
2792 header_row: vec![true, false, false],
2793 col_continuation: vec![
2794 vec![false, true, true],
2795 vec![false, true, true],
2796 vec![false, false, false],
2797 ],
2798 row_continuation: vec![
2799 vec![false, false, false],
2800 vec![true, true, true],
2801 vec![false, false, false],
2802 ],
2803 row_header: Vec::new(),
2804 col_header: Vec::new(),
2805 }),
2806 cell_blocks: None,
2807 cells: None,
2808 caption: None,
2809 caption_parent: Default::default(),
2810 caption_location: None,
2811 }));
2812 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2813 let data = &v["tables"][0]["data"];
2814 assert_eq!(data["num_rows"], 3);
2815 assert_eq!(data["num_cols"], 3);
2816 let cells = data["table_cells"].as_array().unwrap();
2817 assert_eq!(
2818 cells.len(),
2819 4,
2820 "one cell for the range, three for the plain row"
2821 );
2822 assert_eq!(
2823 cells[0],
2824 serde_json::json!({
2825 "row_span": 2, "col_span": 3,
2826 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2827 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2828 "text": "merged", "column_header": true, "row_header": false,
2829 "row_section": false, "fillable": false,
2830 })
2831 );
2832 assert_eq!(cells[1]["text"], "a");
2833 assert_eq!(cells[1]["row_span"], 1);
2834 assert_eq!(cells[1]["column_header"], false);
2835 let grid = data["grid"].as_array().unwrap();
2837 assert_eq!(grid.len(), 3);
2838 for (r, row) in grid.iter().take(2).enumerate() {
2839 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2840 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2841 }
2842 }
2843 assert_eq!(grid[2][2]["text"], "c");
2844 }
2845
2846 #[test]
2852 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2853 let mut doc = DoclingDocument::new("t");
2854 doc.push(Node::PageInfo {
2855 page_no: 1,
2856 width: 3.0,
2857 height: 4.0,
2858 });
2859 let table = crate::Table {
2860 rows: vec![vec!["a".to_string(), "b".into()]],
2861 ..Default::default()
2862 };
2863 doc.push(Node::Group {
2864 label: "sheet".into(),
2865 name: Some("Data".into()),
2866 layer: None,
2867 children: vec![
2868 Node::Prov {
2873 page_no: 1,
2874 bbox: [0.0, 0.0, 3.0, 4.0],
2875 charspan: [0, 0],
2876 seq: Some(1),
2877 inner: Box::new(Node::Table(table.clone())),
2878 },
2879 Node::Prov {
2880 page_no: 1,
2881 bbox: [0.0, 1.0, 1.0, 1.0],
2882 charspan: [0, 0],
2883 seq: Some(0),
2884 inner: Box::new(Node::Chart {
2885 kind: "bar_chart".into(),
2886 table,
2887 caption: Some("Sales".into()),
2888 location: Some([0, 128, 170, 128]),
2889 }),
2890 },
2891 ],
2892 });
2893 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2894 assert_eq!(
2895 v["pages"],
2896 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2897 );
2898 assert_eq!(
2899 v["tables"][0]["prov"],
2900 serde_json::json!([{
2901 "page_no": 1,
2902 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2903 "charspan": [0, 0],
2904 }])
2905 );
2906 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2907 let sheet = &v["groups"][0];
2910 assert_eq!(
2911 sheet["children"],
2912 serde_json::json!([
2913 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2914 ])
2915 );
2916 let cap = &v["texts"][0];
2917 assert_eq!(cap["label"], "caption");
2918 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2919 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2920 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2921 let pic = &v["pictures"][0];
2922 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2923 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2924 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2925 assert_eq!(
2926 pic["meta"]["classification"]["predictions"][0]["class_name"],
2927 "bar_chart"
2928 );
2929 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2930 }
2931
2932 #[test]
2936 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2937 let mut doc = DoclingDocument::new("t");
2938 doc.push(Node::PageInfo {
2939 page_no: 1,
2940 width: 12192000.0,
2941 height: 6858000.0,
2942 });
2943 doc.push(Node::Furniture {
2944 layer: ContentLayer::Notes,
2945 inner: Box::new(Node::Located {
2946 location: [0, 0, 0, 0],
2947 inner: Box::new(Node::Paragraph {
2948 text: "a note".into(),
2949 }),
2950 }),
2951 });
2952 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2953 let prov = &v["texts"][0]["prov"][0];
2954 assert_eq!(prov["page_no"], 1);
2955 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2956 assert_eq!(
2957 prov["bbox"],
2958 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2959 );
2960 assert_eq!(
2962 v["pages"]["1"]["size"],
2963 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2964 );
2965 }
2966
2967 #[test]
2977 fn captions_and_checkboxes_carry_their_own_prov() {
2978 let mut doc = DoclingDocument::new("t");
2979 doc.push(Node::PageInfo {
2980 page_no: 1,
2981 width: 512.0,
2982 height: 512.0,
2983 });
2984 doc.push(Node::Located {
2985 location: [64, 64, 448, 256],
2986 inner: Box::new(Node::Picture {
2987 caption: Some("Figure 1".into()),
2988 caption_href: None,
2989 image: None,
2990 classification: None,
2991 description: None,
2992 caption_parent: CaptionParent::Item,
2993 caption_location: Some([64, 264, 448, 280]),
2994 }),
2995 });
2996 doc.push(Node::PageInfo {
2997 page_no: 2,
2998 width: 512.0,
2999 height: 512.0,
3000 });
3001 doc.push(Node::Located {
3002 location: [64, 120, 448, 300],
3003 inner: Box::new(Node::Table(Table {
3004 rows: vec![vec!["a".into()]],
3005 caption: Some("Table 1".into()),
3006 caption_parent: CaptionParent::Item,
3007 caption_location: Some([64, 100, 300, 112]),
3008 ..Table::default()
3009 })),
3010 });
3011 doc.push(Node::Located {
3012 location: [64, 320, 200, 332],
3013 inner: Box::new(Node::CheckboxItem {
3014 checked: false,
3015 text: "First option".into(),
3016 }),
3017 });
3018 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3019 let texts = v["texts"].as_array().unwrap();
3020 let by_text = |t: &str| texts.iter().find(|x| x["text"] == t).expect(t).clone();
3021 let bbox = |p: &Value| {
3022 let b = &p["prov"][0]["bbox"];
3023 [&b["l"], &b["t"], &b["r"], &b["b"]].map(|x| x.as_f64().unwrap())
3024 };
3025 let fig = by_text("Figure 1");
3028 assert_eq!(fig["prov"][0]["page_no"], 1);
3029 assert_eq!(bbox(&fig), [64.0, 248.0, 448.0, 232.0]);
3030 assert_eq!(fig["prov"][0]["charspan"], serde_json::json!([0, 8]));
3031 assert_eq!(fig["parent"]["$ref"], "#/pictures/0");
3032 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
3033 let tab = by_text("Table 1");
3035 assert_eq!(tab["prov"][0]["page_no"], 2);
3036 assert_eq!(bbox(&tab), [64.0, 412.0, 300.0, 400.0]);
3037 assert_eq!(v["tables"][0]["prov"][0]["bbox"]["t"], 392.0);
3038 let cb = by_text("First option");
3040 assert_eq!(cb["label"], "checkbox_unselected");
3041 assert_eq!(cb["prov"][0]["page_no"], 2);
3042 assert_eq!(bbox(&cb), [64.0, 192.0, 200.0, 180.0]);
3043 assert!(texts
3044 .iter()
3045 .all(|t| !t["prov"].as_array().unwrap().is_empty()));
3046
3047 let mut plain = DoclingDocument::new("t");
3050 plain.push(Node::PageInfo {
3051 page_no: 1,
3052 width: 512.0,
3053 height: 512.0,
3054 });
3055 plain.push(Node::Located {
3056 location: [64, 64, 448, 256],
3057 inner: Box::new(Node::Picture {
3058 caption: Some("Figure 1".into()),
3059 caption_href: None,
3060 image: None,
3061 classification: None,
3062 description: None,
3063 caption_parent: CaptionParent::Item,
3064 caption_location: None,
3065 }),
3066 });
3067 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
3068 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
3069 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
3070 }
3071
3072 #[test]
3073 fn page_markers_produce_pages_and_prov() {
3074 let mut doc = DoclingDocument::new("t");
3075 doc.push(Node::PageInfo {
3076 page_no: 1,
3077 width: 512.0,
3078 height: 1024.0,
3079 });
3080 doc.push(Node::Located {
3081 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
3083 text: "hello".into(),
3084 }),
3085 });
3086 doc.push(Node::Table(Table {
3087 rows: vec![vec!["a".into()]],
3088 location: Some([0, 0, 512, 512]),
3089 ..Table::default()
3090 }));
3091 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3092 assert_eq!(v["pages"]["1"]["page_no"], 1);
3093 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
3094 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
3095 let prov = &v["texts"][0]["prov"][0];
3098 assert_eq!(prov["page_no"], 1);
3099 assert_eq!(prov["bbox"]["l"], 128.0);
3100 assert_eq!(prov["bbox"]["t"], 896.0);
3101 assert_eq!(prov["bbox"]["r"], 256.0);
3102 assert_eq!(prov["bbox"]["b"], 768.0);
3103 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
3104 assert_eq!(prov["charspan"][1], 5);
3105 let tprov = &v["tables"][0]["prov"][0];
3107 assert_eq!(tprov["bbox"]["t"], 1024.0);
3108 assert_eq!(tprov["bbox"]["b"], 0.0);
3109 assert_eq!(tprov["charspan"][1], 0);
3110
3111 let mut plain = DoclingDocument::new("t");
3113 plain.push(Node::Located {
3114 location: [1, 2, 3, 4],
3115 inner: Box::new(Node::Paragraph { text: "x".into() }),
3116 });
3117 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
3118 assert_eq!(v["pages"], serde_json::json!({}));
3119 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
3120 }
3121
3122 #[test]
3125 fn page_furniture_becomes_furniture_layer_text() {
3126 let mut doc = DoclingDocument::new("t");
3127 doc.push(Node::PageInfo {
3128 page_no: 1,
3129 width: 512.0,
3130 height: 512.0,
3131 });
3132 doc.push(Node::PageFurniture {
3133 footer: false,
3134 location: [10, 0, 100, 20],
3135 text: "Chapter 1".into(),
3136 });
3137 doc.push(Node::Paragraph {
3138 text: "body".into(),
3139 });
3140 doc.push(Node::PageFurniture {
3141 footer: true,
3142 location: [400, 490, 500, 512],
3143 text: "1.10.2".into(),
3144 });
3145 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3146 let texts = v["texts"].as_array().unwrap();
3147 assert_eq!(texts.len(), 3);
3148 assert_eq!(texts[0]["label"], "page_header");
3149 assert_eq!(texts[0]["content_layer"], "furniture");
3150 assert_eq!(texts[0]["parent"]["$ref"], "#/body");
3151 assert_eq!(texts[0]["prov"][0]["bbox"]["l"], 10.0);
3152 assert_eq!(texts[1]["content_layer"], "body");
3153 assert_eq!(texts[1]["prov"], serde_json::json!([]));
3154 assert_eq!(texts[2]["label"], "page_footer");
3155 assert_eq!(texts[2]["text"], "1.10.2");
3156 assert_eq!(texts[2]["content_layer"], "furniture");
3157 }
3158
3159 #[test]
3160 fn picture_image_in_markdown_modes_and_json() {
3161 let doc = doc_with_image();
3162 assert!(doc.export_to_markdown().contains("<!-- image -->"));
3164 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
3166 assert!(
3167 md.contains(""),
3168 "got:\n{md}"
3169 );
3170 assert!(files.is_empty());
3171 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
3173 assert!(
3174 md.contains(""),
3175 "got:\n{md}"
3176 );
3177 assert_eq!(
3178 files,
3179 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
3180 );
3181 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3184 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
3185 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
3186 let keys: Vec<&str> = v["pictures"][0]
3187 .as_object()
3188 .unwrap()
3189 .keys()
3190 .map(String::as_str)
3191 .collect();
3192 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
3193 assert_eq!(
3194 v["pictures"][0]["image"]["uri"],
3195 "data:image/png;base64,Zm9vYmFy"
3196 );
3197 }
3198
3199 #[test]
3200 fn exports_docling_schema() {
3201 let mut doc = DoclingDocument::new("t");
3202 doc.push(Node::Heading {
3203 level: 1,
3204 text: "Title".into(),
3205 });
3206 doc.push(Node::Heading {
3207 level: 2,
3208 text: "Sec".into(),
3209 });
3210 doc.push(Node::Paragraph {
3211 text: "Body & more".into(),
3212 }); doc.push(Node::ListItem {
3214 ordered: false,
3215 number: 0,
3216 first_in_list: true,
3217 text: "one".into(),
3218 level: 0,
3219 marker: None,
3220 location: None,
3221 dclx: None,
3222 href: None,
3223 layer: None,
3224 });
3225 doc.push(Node::ListItem {
3226 ordered: false,
3227 number: 0,
3228 first_in_list: false,
3229 text: "two".into(),
3230 level: 0,
3231 marker: None,
3232 location: None,
3233 dclx: None,
3234 href: None,
3235 layer: None,
3236 });
3237 doc.push(Node::Table(Table {
3238 rows: vec![vec!["A".into(), "B".into()]],
3239 location: None,
3240 structure: None,
3241 cell_blocks: None,
3242 cells: None,
3243 caption: None,
3244 caption_parent: Default::default(),
3245 caption_location: None,
3246 }));
3247
3248 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3249 assert_eq!(v["schema_name"], "DoclingDocument");
3250 assert_eq!(v["version"], "1.10.0");
3251 assert_eq!(v["texts"][0]["label"], "title");
3252 assert_eq!(v["texts"][1]["label"], "section_header");
3253 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
3257 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
3258 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
3259 assert_eq!(v["texts"][3]["marker"], "-");
3260 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
3262 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
3263 }
3264 #[test]
3268 fn a_layered_group_stamps_its_whole_subtree() {
3269 let doc = DoclingDocument {
3270 name: "s".into(),
3271 nodes: vec![
3272 Node::Group {
3273 label: "sheet".into(),
3274 name: Some("Sheet1".into()),
3275 layer: None,
3276 children: vec![Node::Paragraph {
3277 text: "visible".into(),
3278 }],
3279 },
3280 Node::Group {
3281 label: "sheet".into(),
3282 name: Some("Sheet2".into()),
3283 layer: Some(ContentLayer::Invisible),
3284 children: vec![Node::Paragraph {
3285 text: "hidden".into(),
3286 }],
3287 },
3288 ],
3289 ..DoclingDocument::new("s")
3290 };
3291 let v = crate::json::to_json(&doc);
3292 assert_eq!(v["groups"][0]["label"], "sheet");
3293 assert_eq!(v["groups"][0]["name"], "Sheet1");
3294 assert_eq!(v["groups"][0]["content_layer"], "body");
3295 assert_eq!(v["texts"][0]["content_layer"], "body");
3296 assert_eq!(v["groups"][1]["name"], "Sheet2");
3297 assert_eq!(v["groups"][1]["content_layer"], "invisible");
3298 assert_eq!(v["texts"][1]["content_layer"], "invisible");
3299 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
3301 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
3302 }
3303
3304 #[test]
3314 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
3315 use crate::tree::{ItemTree, TreeKind};
3316 let mut t = ItemTree::default();
3317 let text = |txt: &str| TreeKind::Text {
3318 label: "text".into(),
3319 text: txt.into(),
3320 orig: None,
3321 formatting: None,
3322 hyperlink: None,
3323 level: None,
3324 list: None,
3325 };
3326 let a = t.add(None, None, text("a"));
3327 let blank = t.add(None, None, text(""));
3328 let b = t.add(None, None, text("b"));
3329 t.delete(blank);
3330 let group = t.add(
3331 None,
3332 Some(ContentLayer::Notes),
3333 TreeKind::Group {
3334 label: "comment_section".into(),
3335 name: "comment-0".into(),
3336 },
3337 );
3338 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
3339 t.items[a].comments.push(group);
3340 t.add(
3341 None,
3342 None,
3343 TreeKind::Picture {
3344 captions: Vec::new(),
3345 image: None,
3346 extra_classes: Vec::new(),
3347 classification: Some("bar_chart".into()),
3348 description: None,
3349 confidence: None,
3350 chart: Some(Table {
3351 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
3352 ..Table::default()
3353 }),
3354 dpi: None,
3355 },
3356 );
3357 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
3358 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
3359 let mut doc = DoclingDocument::new("t");
3360 doc.tree = Some(t);
3361 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3362 let texts = v["texts"].as_array().unwrap();
3363 assert_eq!(texts.len(), 3);
3364 assert_eq!(texts[1]["text"], "b");
3365 assert_eq!(texts[1]["self_ref"], "#/texts/1");
3366 assert_eq!(
3367 v["body"]["children"],
3368 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
3369 );
3370 let keys: Vec<&str> = texts[0]
3371 .as_object()
3372 .unwrap()
3373 .keys()
3374 .map(String::as_str)
3375 .collect();
3376 assert_eq!(
3377 keys,
3378 vec![
3379 "self_ref",
3380 "parent",
3381 "children",
3382 "content_layer",
3383 "label",
3384 "prov",
3385 "comments",
3386 "orig",
3387 "text"
3388 ]
3389 );
3390 assert_eq!(
3391 texts[0]["comments"],
3392 serde_json::json!([{"$ref": "#/groups/0"}])
3393 );
3394 assert!(texts[1].get("comments").is_none());
3395 let meta = &v["pictures"][0]["meta"];
3396 assert_eq!(
3397 meta["classification"]["predictions"][0]["class_name"],
3398 "bar_chart"
3399 );
3400 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
3401 }
3402
3403 #[test]
3409 fn tree_items_carry_exact_provenance_and_dpi() {
3410 use crate::tree::{ItemTree, TreeKind, TreeProv};
3411 let text = |label: &str, t: &str| TreeKind::Text {
3412 label: label.into(),
3413 text: t.into(),
3414 orig: None,
3415 formatting: None,
3416 hyperlink: None,
3417 level: None,
3418 list: None,
3419 };
3420 let mut t = ItemTree::default();
3421 let slide = t.add(
3422 None,
3423 None,
3424 TreeKind::Group {
3425 label: "chapter".into(),
3426 name: "slide-0".into(),
3427 },
3428 );
3429 t.add_with_prov(
3430 Some(slide),
3431 None,
3432 text("paragraph", "héllo"),
3433 TreeProv {
3434 page_no: 1,
3435 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
3436 bottom_left: true,
3437 charspan: [0, 5],
3438 },
3439 );
3440 t.add_with_prov(
3441 Some(slide),
3442 None,
3443 TreeKind::Picture {
3444 captions: Vec::new(),
3445 image: Some(crate::PictureImage {
3446 dpi: crate::PictureImage::DEFAULT_DPI,
3447 mimetype: "image/png".into(),
3448 width: 2,
3449 height: 2,
3450 data: vec![0],
3451 display_size: None,
3452 }),
3453 extra_classes: Vec::new(),
3454 classification: None,
3455 description: None,
3456 confidence: None,
3457 chart: None,
3458 dpi: Some(300),
3459 },
3460 TreeProv {
3461 page_no: 1,
3462 bbox: [0.0; 4],
3463 bottom_left: false,
3464 charspan: [0, 0],
3465 },
3466 );
3467 t.add(
3468 Some(slide),
3469 Some(ContentLayer::Notes),
3470 text("text", "no geometry"),
3471 );
3472 let mut doc = DoclingDocument::new("t");
3473 doc.push(Node::PageInfo {
3474 page_no: 1,
3475 width: 9144000.0,
3476 height: 6858000.0,
3477 });
3478 doc.tree = Some(t);
3479 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3480 assert_eq!(
3481 v["texts"][0]["prov"],
3482 serde_json::json!([{
3483 "page_no": 1,
3484 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
3485 "charspan": [0, 5],
3486 }])
3487 );
3488 assert_eq!(v["texts"][0]["label"], "paragraph");
3489 assert_eq!(
3490 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
3491 "TOPLEFT"
3492 );
3493 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
3494 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
3495 assert_eq!(v["texts"][1]["content_layer"], "notes");
3496 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
3497 assert_eq!(v["pages"]["1"]["page_no"], 1);
3498 }
3499
3500 #[test]
3505 fn provenance_boxes_are_clamped_to_their_page() {
3506 let mut doc = DoclingDocument::new("t");
3507 doc.push(Node::PageInfo {
3508 page_no: 1,
3509 width: 10.0,
3510 height: 8.0,
3511 });
3512 doc.push(Node::Prov {
3513 page_no: 1,
3514 bbox: [-1.0, 2.0, 12.0, 9.5],
3515 charspan: [0, 1],
3516 seq: None,
3517 inner: Box::new(Node::Paragraph { text: "x".into() }),
3518 });
3519 let mut table = Table {
3520 rows: vec![vec!["a".into()]],
3521 ..Table::default()
3522 };
3523 table.cells = Some(vec![crate::TableCell {
3524 text: "a".into(),
3525 bbox: Some([1.0, 1.0, 11.0, 9.0]),
3526 start_row: 0,
3527 start_col: 0,
3528 row_span: 1,
3529 col_span: 1,
3530 column_header: false,
3531 row_header: false,
3532 row_section: false,
3533 }]);
3534 doc.push(Node::Prov {
3535 page_no: 1,
3536 bbox: [0.0, 0.0, 10.0, 8.0],
3537 charspan: [0, 0],
3538 seq: None,
3539 inner: Box::new(Node::Table(table)),
3540 });
3541 doc.push(Node::Prov {
3542 page_no: 7,
3543 bbox: [-5.0, 0.0, 50.0, 50.0],
3544 charspan: [0, 1],
3545 seq: None,
3546 inner: Box::new(Node::Paragraph { text: "y".into() }),
3547 });
3548 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3549 assert_eq!(
3550 v["texts"][0]["prov"][0]["bbox"],
3551 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
3552 );
3553 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
3554 assert_eq!(
3555 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
3556 (Some(1.0), Some(10.0), Some(8.0))
3557 );
3558 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
3559 assert_eq!(
3560 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
3561 "page 7 is not described"
3562 );
3563 }
3564
3565 #[test]
3572 fn misplaced_list_items_are_wrapped_like_docling_core() {
3573 use crate::tree::{ItemTree, ListMeta, TreeKind};
3574 let mut t = ItemTree::default();
3575 let li = |txt: &str| TreeKind::Text {
3576 label: "list_item".into(),
3577 text: txt.into(),
3578 orig: None,
3579 formatting: None,
3580 hyperlink: None,
3581 level: None,
3582 list: Some(ListMeta {
3583 enumerated: false,
3584 marker: "-".into(),
3585 }),
3586 };
3587 let group = |label: &str, name: &str| TreeKind::Group {
3588 label: label.into(),
3589 name: name.into(),
3590 };
3591 t.add(None, None, li("A"));
3592 t.add(None, None, li("B"));
3593 t.add(
3594 None,
3595 None,
3596 TreeKind::Text {
3597 label: "text".into(),
3598 text: "plain".into(),
3599 orig: None,
3600 formatting: None,
3601 hyperlink: None,
3602 level: None,
3603 list: None,
3604 },
3605 );
3606 let cell = t.add(None, None, group("unspecified", "cell"));
3607 t.add(Some(cell), None, li("C"));
3608 t.add(Some(cell), None, li("D"));
3609 let list = t.add(None, None, group("list", "list"));
3610 t.add(Some(list), None, li("E"));
3611 let doc = DoclingDocument {
3612 tree: Some(t),
3613 ..DoclingDocument::new("t")
3614 };
3615 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3616 let refs = |x: &Value| -> Vec<String> {
3617 x.as_array()
3618 .unwrap()
3619 .iter()
3620 .map(|r| r["$ref"].as_str().unwrap().to_string())
3621 .collect()
3622 };
3623 assert_eq!(
3624 refs(&v["body"]["children"]),
3625 ["#/groups/4", "#/texts/0", "#/groups/0", "#/groups/1"]
3626 );
3627 assert_eq!(
3628 refs(&v["groups"][0]["children"]),
3629 ["#/groups/3", "#/groups/2"]
3630 );
3631 for (g, name, kids) in [
3632 (2, "group", vec!["#/texts/2"]),
3633 (3, "group", vec!["#/texts/3"]),
3634 (4, "group", vec!["#/texts/4", "#/texts/5"]),
3635 (1, "list", vec!["#/texts/1"]),
3636 ] {
3637 assert_eq!(v["groups"][g]["label"], "list");
3638 assert_eq!(v["groups"][g]["name"], name);
3639 assert_eq!(refs(&v["groups"][g]["children"]), kids);
3640 }
3641 let texts: Vec<&str> = v["texts"]
3642 .as_array()
3643 .unwrap()
3644 .iter()
3645 .map(|t| t["text"].as_str().unwrap())
3646 .collect();
3647 assert_eq!(texts, ["plain", "E", "D", "C", "A", "B"]);
3648 assert_eq!(v["texts"][2]["parent"]["$ref"], "#/groups/2");
3649 }
3650
3651 #[test]
3654 fn a_deleted_child_is_not_written_as_an_empty_ref() {
3655 use crate::tree::{ItemTree, TreeKind};
3656 let mut t = ItemTree::default();
3657 let text = |txt: &str| TreeKind::Text {
3658 label: "text".into(),
3659 text: txt.into(),
3660 orig: None,
3661 formatting: None,
3662 hyperlink: None,
3663 level: None,
3664 list: None,
3665 };
3666 let g = t.add(
3667 None,
3668 None,
3669 TreeKind::Group {
3670 label: "unspecified".into(),
3671 name: "g".into(),
3672 },
3673 );
3674 let gone = t.add(Some(g), None, text(""));
3675 t.add(Some(g), None, text("kept"));
3676 t.items[gone].deleted = true; let doc = DoclingDocument {
3678 tree: Some(t),
3679 ..DoclingDocument::new("t")
3680 };
3681 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3682 assert_eq!(
3683 v["groups"][0]["children"],
3684 serde_json::json!([{"$ref": "#/texts/0"}])
3685 );
3686 }
3687
3688 #[test]
3689 fn a_backend_item_tree_is_written_verbatim() {
3690 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
3691 let mut t = ItemTree::default();
3692 let text = |label: &str, txt: &str| TreeKind::Text {
3693 label: label.into(),
3694 text: txt.into(),
3695 orig: None,
3696 formatting: None,
3697 hyperlink: None,
3698 level: None,
3699 list: None,
3700 };
3701 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
3702 let h = t.add(None, None, text("title", "Heading"));
3703 let group = t.add(
3704 Some(h),
3705 None,
3706 TreeKind::Group {
3707 label: "inline".into(),
3708 name: "group".into(),
3709 },
3710 );
3711 t.add(
3712 Some(group),
3713 None,
3714 TreeKind::Text {
3715 label: "text".into(),
3716 text: "bold".into(),
3717 orig: None,
3718 formatting: Some(Formatting {
3719 bold: true,
3720 ..Formatting::default()
3721 }),
3722 hyperlink: Some("https://example.com/".into()),
3723 level: None,
3724 list: None,
3725 },
3726 );
3727 t.add(
3728 Some(group),
3729 None,
3730 TreeKind::Code {
3731 text: "x = 1".into(),
3732 orig: None,
3733 language: Some("python".into()),
3734 formatting: None,
3735 hyperlink: None,
3736 captions: Vec::new(),
3737 },
3738 );
3739 let sub = t.add(
3740 Some(h),
3741 None,
3742 TreeKind::Text {
3743 label: "section_header".into(),
3744 text: "Sub".into(),
3745 orig: Some("Sub\u{2019}".into()),
3746 formatting: None,
3747 hyperlink: None,
3748 level: Some(1),
3749 list: None,
3750 },
3751 );
3752 let item = t.add(
3753 Some(sub),
3754 None,
3755 TreeKind::Text {
3756 label: "list_item".into(),
3757 text: "item".into(),
3758 orig: None,
3759 formatting: None,
3760 hyperlink: None,
3761 level: None,
3762 list: Some(ListMeta {
3763 enumerated: true,
3764 marker: "3.".into(),
3765 }),
3766 },
3767 );
3768 let _region = t.add(
3769 Some(sub),
3770 None,
3771 TreeKind::FieldRegion {
3772 items: vec![crate::FieldItem {
3773 marker: None,
3774 key: Some("Name".into()),
3775 value: Some("Duck".into()),
3776 value_kind: Some("read_only".into()),
3777 }],
3778 },
3779 );
3780 let table = t.add(
3781 Some(sub),
3782 None,
3783 TreeKind::Table {
3784 table: Table {
3785 rows: vec![vec!["a \n<".into(), "b".into()]],
3786 cells: Some(vec![
3787 crate::TableCell {
3788 text: "a \n<".into(),
3789 bbox: None,
3790 start_row: 0,
3791 start_col: 0,
3792 row_span: 3,
3793 col_span: 1,
3794 column_header: false,
3795 row_header: true,
3796 row_section: false,
3797 },
3798 crate::TableCell {
3799 text: "b".into(),
3800 bbox: None,
3801 start_row: 0,
3802 start_col: 1,
3803 row_span: 1,
3804 col_span: 1,
3805 column_header: false,
3806 row_header: false,
3807 row_section: false,
3808 },
3809 ]),
3810 ..Table::default()
3811 },
3812 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
3814 },
3815 );
3816 let cell_group = t.add(
3817 Some(table),
3818 None,
3819 TreeKind::Group {
3820 label: "unspecified".into(),
3821 name: "rich_cell_group_1_0_0".into(),
3822 },
3823 );
3824 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
3825 *rich_cells = vec![(0, 1, cell_group)];
3826 }
3827 let after = t.add(Some(sub), None, text("text", "after the region"));
3828 let _ = (title, after);
3829 let list = t.add(
3833 Some(sub),
3834 None,
3835 TreeKind::Group {
3836 label: "list".into(),
3837 name: "list".into(),
3838 },
3839 );
3840 t.reparent(item, Some(list));
3841
3842 let doc = DoclingDocument {
3843 tree: Some(t),
3844 ..DoclingDocument::new("t")
3845 };
3846 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3847 let texts: Vec<&str> = v["texts"]
3849 .as_array()
3850 .unwrap()
3851 .iter()
3852 .map(|t| t["text"].as_str().unwrap())
3853 .collect();
3854 assert_eq!(
3855 texts,
3856 [
3857 "Page",
3858 "Heading",
3859 "bold",
3860 "x = 1",
3861 "Sub",
3862 "item",
3863 "Name",
3864 "Duck",
3865 "after the region"
3866 ]
3867 );
3868 assert_eq!(
3869 v["body"]["children"],
3870 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
3871 );
3872 assert_eq!(v["texts"][0]["content_layer"], "furniture");
3873 assert_eq!(
3874 v["texts"][1]["children"],
3875 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
3876 );
3877 let bold = &v["texts"][2];
3878 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
3879 let keys: Vec<&str> = bold
3880 .as_object()
3881 .unwrap()
3882 .keys()
3883 .map(String::as_str)
3884 .collect();
3885 assert_eq!(
3886 keys,
3887 [
3888 "self_ref",
3889 "parent",
3890 "children",
3891 "content_layer",
3892 "label",
3893 "prov",
3894 "orig",
3895 "text",
3896 "formatting",
3897 "hyperlink"
3898 ]
3899 );
3900 assert_eq!(
3901 bold["formatting"],
3902 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
3903 );
3904 let code = &v["texts"][3];
3905 assert_eq!(code["label"], "code");
3906 assert_eq!(code["code_language"], "Python");
3907 let sub = &v["texts"][4];
3908 assert_eq!(sub["orig"], "Sub\u{2019}");
3909 assert_eq!(sub["level"], 1);
3910 let item = &v["texts"][5];
3911 let keys: Vec<&str> = item
3912 .as_object()
3913 .unwrap()
3914 .keys()
3915 .map(String::as_str)
3916 .collect();
3917 assert_eq!(
3918 keys,
3919 [
3920 "self_ref",
3921 "parent",
3922 "children",
3923 "content_layer",
3924 "label",
3925 "prov",
3926 "orig",
3927 "text",
3928 "enumerated",
3929 "marker"
3930 ]
3931 );
3932 assert_eq!(item["marker"], "3.");
3933 assert_eq!(v["texts"][7]["kind"], "read_only");
3934 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
3935 let table = &v["tables"][0];
3936 assert_eq!(
3937 table["children"],
3938 serde_json::json!([{"$ref": "#/groups/1"}])
3939 );
3940 let cells = table["data"]["table_cells"].as_array().unwrap();
3941 assert_eq!(
3942 cells[0]["text"], "a \n<",
3943 "raw cell text is written verbatim"
3944 );
3945 assert_eq!(
3946 cells[0]["end_row_offset_idx"], 3,
3947 "declared spans are not clamped"
3948 );
3949 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
3950 assert!(cells[0].get("ref").is_none());
3951 assert!(
3952 table["data"]["grid"][0][1].get("ref").is_none(),
3953 "the grid shows plain cells"
3954 );
3955 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
3956 }
3957
3958 #[test]
3962 fn a_comment_section_can_be_referenced_by_its_note_text() {
3963 let doc = DoclingDocument {
3964 name: "c".into(),
3965 nodes: vec![
3966 Node::Commented {
3967 comments: vec![0],
3968 inner: Box::new(Node::Paragraph {
3969 text: "annotated".into(),
3970 }),
3971 },
3972 Node::CommentSection {
3973 name: "comment-Sheet1-A1".into(),
3974 text: "[author: A]: note".into(),
3975 refs_note_text: true,
3976 grouped: true,
3977 },
3978 ],
3979 ..DoclingDocument::new("c")
3980 };
3981 let v = crate::json::to_json(&doc);
3982 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
3983 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
3984 }
3985
3986 #[test]
3990 fn comment_sections_link_back_to_their_items() {
3991 let doc = DoclingDocument {
3992 name: "c".into(),
3993 nodes: vec![
3994 Node::Commented {
3995 comments: vec![0],
3996 inner: Box::new(Node::Paragraph {
3997 text: "annotated".into(),
3998 }),
3999 },
4000 Node::Paragraph {
4001 text: "plain".into(),
4002 },
4003 Node::CommentSection {
4004 name: "comment-7".into(),
4005 text: "[time: t]: note".into(),
4006 refs_note_text: false,
4007 grouped: true,
4008 },
4009 ],
4010 ..DoclingDocument::new("c")
4011 };
4012 let v = crate::json::to_json(&doc);
4013 assert_eq!(v["groups"][0]["label"], "comment_section");
4015 assert_eq!(v["groups"][0]["name"], "comment-7");
4016 assert_eq!(v["groups"][0]["content_layer"], "notes");
4017 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
4018 assert_eq!(v["texts"][2]["content_layer"], "notes");
4019 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
4021 assert!(v["texts"][1].get("comments").is_none());
4022 let keys: Vec<&str> = v["texts"][0]
4024 .as_object()
4025 .unwrap()
4026 .keys()
4027 .map(String::as_str)
4028 .collect();
4029 assert_eq!(
4030 &keys[keys.len() - 4..],
4031 &["prov", "comments", "orig", "text"]
4032 );
4033 }
4034
4035 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
4036 Node::Picture {
4037 caption: Some(caption.into()),
4038 caption_href: None,
4039 image: None,
4040 classification: None,
4041 description: None,
4042 caption_parent,
4043 caption_location: None,
4044 }
4045 }
4046
4047 fn group(children: Vec<Node>) -> Node {
4048 Node::Group {
4049 label: "section".into(),
4050 name: None,
4051 layer: None,
4052 children,
4053 }
4054 }
4055
4056 fn refs(v: &Value) -> Vec<&str> {
4057 v.as_array()
4058 .unwrap()
4059 .iter()
4060 .map(|r| r["$ref"].as_str().unwrap())
4061 .collect()
4062 }
4063
4064 #[test]
4069 fn a_body_caption_follows_the_enclosing_top_level_item() {
4070 let mut doc = DoclingDocument::new("t");
4071 doc.push(picture("top", CaptionParent::Body));
4072 doc.push(group(vec![
4073 Node::Paragraph { text: "p".into() },
4074 picture("nested", CaptionParent::Body),
4075 ]));
4076 doc.push(Node::Paragraph {
4077 text: "after".into(),
4078 });
4079 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4080 assert_eq!(
4081 refs(&v["body"]["children"]),
4082 [
4083 "#/texts/0",
4084 "#/pictures/0",
4085 "#/groups/0",
4086 "#/texts/2",
4087 "#/texts/3"
4088 ]
4089 );
4090 assert_eq!(
4091 refs(&v["groups"][0]["children"]),
4092 ["#/texts/1", "#/pictures/1"]
4093 );
4094 for (cap, pic) in [(0, 0), (2, 1)] {
4095 assert_eq!(v["texts"][cap]["label"], "caption");
4096 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
4097 assert_eq!(
4098 refs(&v["pictures"][pic]["captions"]),
4099 [format!("#/texts/{cap}")]
4100 );
4101 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
4102 }
4103 }
4104
4105 #[test]
4108 fn an_item_caption_is_the_items_first_child() {
4109 let mut doc = DoclingDocument::new("t");
4110 doc.push(picture("fig", CaptionParent::Item));
4111 doc.push(Node::Table(Table {
4112 rows: vec![vec!["a".into()]],
4113 caption: Some("tab".into()),
4114 caption_parent: CaptionParent::Item,
4115 ..Table::default()
4116 }));
4117 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4118 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
4119 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
4120 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
4121 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
4122 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
4123 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
4124 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
4125 }
4126
4127 #[test]
4133 fn picture_children_hang_off_the_picture_after_its_caption() {
4134 let mut doc = DoclingDocument::new("t");
4135 doc.push(picture("fig", CaptionParent::Item));
4136 doc.push(Node::PictureChildren(vec![
4137 Node::Heading {
4138 level: 2,
4139 text: "in-figure title".into(),
4140 },
4141 Node::Paragraph {
4142 text: "axis label".into(),
4143 },
4144 Node::ListItem {
4145 ordered: false,
4146 number: 0,
4147 first_in_list: true,
4148 text: "callout".into(),
4149 level: 0,
4150 marker: None,
4151 location: None,
4152 dclx: None,
4153 href: None,
4154 layer: None,
4155 },
4156 ]));
4157 doc.push(Node::Paragraph {
4158 text: "after".into(),
4159 });
4160 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4161 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/texts/4"]);
4162 assert_eq!(
4163 refs(&v["pictures"][0]["children"]),
4164 ["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"]
4165 );
4166 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
4167 assert_eq!(v["texts"][1]["label"], "section_header");
4168 assert_eq!(v["texts"][2]["label"], "text");
4169 for t in 1..=2 {
4170 assert_eq!(v["texts"][t]["parent"]["$ref"], "#/pictures/0");
4171 assert_eq!(v["texts"][t]["content_layer"], "body");
4172 }
4173 assert_eq!(v["groups"][0]["parent"]["$ref"], "#/pictures/0");
4174 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
4175 let md = doc.export_to_markdown();
4176 assert!(
4177 !md.contains("axis label") && !md.contains("callout"),
4178 "{md}"
4179 );
4180 assert!(md.contains("after"), "{md}");
4181 }
4182
4183 #[test]
4187 fn a_container_caption_is_the_items_sibling() {
4188 let mut doc = DoclingDocument::new("t");
4189 doc.push(group(vec![
4190 picture("chart", CaptionParent::Container),
4191 Node::Table(Table {
4192 rows: vec![vec!["a".into()]],
4193 caption: Some("figcaption".into()),
4194 caption_parent: CaptionParent::ContainerAfter,
4195 ..Table::default()
4196 }),
4197 ]));
4198 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4199 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
4200 assert_eq!(
4201 refs(&v["groups"][0]["children"]),
4202 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
4203 );
4204 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
4205 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
4206 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
4207 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
4208 }
4209
4210 #[test]
4215 fn a_labeled_text_keeps_its_label_and_hyperlink_out_of_the_text() {
4216 let mut doc = DoclingDocument::new("t");
4217 doc.push(Node::LabeledText {
4218 label: "footnote".into(),
4219 text: "1 https://example.com/a\\_b".into(),
4220 href: Some("https://example.com/a_b".into()),
4221 });
4222 doc.push(Node::LabeledText {
4223 label: "footnote".into(),
4224 text: "© 2022 the authors".into(),
4225 href: None,
4226 });
4227 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4228 let t = &v["texts"][0];
4229 assert_eq!(t["label"], "footnote");
4230 assert_eq!(t["text"], "1 https://example.com/a_b");
4231 assert_eq!(t["orig"], "1 https://example.com/a_b");
4232 assert_eq!(t["hyperlink"], "https://example.com/a_b");
4233 assert_eq!(v["texts"][1]["label"], "footnote");
4234 assert!(v["texts"][1].get("hyperlink").is_none());
4235 assert_eq!(
4236 doc.export_to_markdown(),
4237 "[1 https://example.com/a\\_b](https://example.com/a_b)\n\n© 2022 the authors\n"
4238 );
4239 assert_eq!(
4240 doc.export_to_text(),
4241 "1 https://example.com/a_b\n\n© 2022 the authors"
4242 );
4243 }
4244}