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 for c in first_class {
668 num_rows = num_rows.max(c.start_row + c.row_span);
669 num_cols = num_cols.max(c.start_col + c.col_span);
670 }
671 }
672 let mut grid = Vec::with_capacity(num_rows);
673 let mut cells = Vec::new();
674 let mut slot: Vec<Option<usize>> = vec![None; num_rows * num_cols];
678 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
679 for c in first_class {
680 let idx = cells.len();
681 cells.push(cell_value(
682 c.row_span,
683 c.col_span,
684 c.start_row,
685 c.start_row + c.row_span,
686 c.start_col,
687 c.start_col + c.col_span,
688 cell_text(&c.text),
689 c.column_header,
690 c.row_header,
691 c.row_section,
692 c.bbox,
693 ));
694 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
695 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
696 slot[r * num_cols + k] = Some(idx);
697 }
698 }
699 }
700 for r in 0..num_rows {
701 let mut grid_row = Vec::with_capacity(num_cols);
702 for c in 0..num_cols {
703 grid_row.push(match slot[r * num_cols + c] {
704 Some(i) => cells[i].clone(),
705 None => cell_value(
706 1,
707 1,
708 r,
709 r + 1,
710 c,
711 c + 1,
712 String::new(),
713 false,
714 false,
715 false,
716 None,
717 ),
718 });
719 }
720 grid.push(grid_row);
721 }
722 } else {
723 let s = t.structure.as_ref();
724 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
725 grid.and_then(|g| g.get(r))
726 .and_then(|row| row.get(c))
727 .copied()
728 .unwrap_or(false)
729 };
730 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
731 let (mut r0, mut c0) = (r, c);
732 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
733 c0 -= 1;
734 }
735 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
736 r0 -= 1;
737 }
738 (r0, c0)
739 };
740 let anchors: Vec<(usize, usize)> = (0..num_rows)
743 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
744 .map(|(r, c)| anchor_of(r, c))
745 .collect();
746 let mut extent: Vec<(usize, usize)> = (0..num_rows)
747 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
748 .collect();
749 for (i, &(ar, ac)) in anchors.iter().enumerate() {
750 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
751 let e = &mut extent[ar * num_cols + ac];
752 e.0 = e.0.max(r);
753 e.1 = e.1.max(c);
754 }
755 for (r, row) in t.rows.iter().enumerate() {
756 let mut grid_row = Vec::with_capacity(num_cols);
757 for c in 0..num_cols {
758 let (ar, ac) = anchors[r * num_cols + c];
759 if (ar, ac) == (r, c) {
760 let (er, ec) = extent[r * num_cols + c];
761 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
762 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
763 Some(s) => flag(Some(&s.col_header), r, c),
764 None => r == 0,
765 };
766 slot[r * num_cols + c] = Some(cells.len());
767 cells.push(cell_value(
768 er - r + 1,
769 ec - c + 1,
770 r,
771 er + 1,
772 c,
773 ec + 1,
774 text,
775 column_header,
776 flag(s.map(|s| &s.row_header), r, c),
777 false,
778 None,
779 ));
780 }
781 grid_row.push(match slot[ar * num_cols + ac] {
782 Some(i) => cells[i].clone(),
783 None => Value::Null,
784 });
785 }
786 grid.push(grid_row);
787 }
788 }
789 json!({
790 "table_cells": cells,
791 "num_rows": num_rows,
792 "num_cols": num_cols,
793 "orientation": "rot_0",
794 "grid": grid,
795 })
796}
797
798#[derive(Clone, Copy)]
802enum ProvSlot {
803 None,
804 One {
805 page_no: usize,
806 bbox: [f64; 4],
808 bottom_left: bool,
809 charspan: [usize; 2],
810 },
811}
812
813impl ProvSlot {
814 fn origin(bottom_left: bool) -> &'static str {
815 if bottom_left {
816 "BOTTOMLEFT"
817 } else {
818 "TOPLEFT"
819 }
820 }
821
822 fn to_value(self) -> Value {
825 let ProvSlot::One {
826 page_no,
827 bbox: [l, t, r, b],
828 bottom_left,
829 charspan,
830 } = self
831 else {
832 return Value::Array(Vec::new());
833 };
834 let bbox = object([
835 ("l", l.into()),
836 ("t", t.into()),
837 ("r", r.into()),
838 ("b", b.into()),
839 ("coord_origin", Self::origin(bottom_left).into()),
840 ]);
841 let span = Value::Array(vec![charspan[0].into(), charspan[1].into()]);
842 Value::Array(vec![object([
843 ("page_no", page_no.into()),
844 ("bbox", bbox),
845 ("charspan", span),
846 ])])
847 }
848
849 fn clamp(&mut self, sizes: &PageSizes) {
851 if let ProvSlot::One { page_no, bbox, .. } = self {
852 if let Some(&(w, h)) = sizes.get(page_no) {
853 for (v, hi) in bbox.iter_mut().zip([w, h, w, h]) {
854 if v.is_finite() {
857 *v = round2(v.clamp(0.0, hi.max(0.0)));
858 }
859 }
860 }
861 }
862 }
863}
864
865impl serde::Serialize for ProvSlot {
866 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
867 use serde::ser::{SerializeMap, SerializeSeq};
868 let ProvSlot::One {
869 page_no,
870 bbox: [l, t, r, b],
871 bottom_left,
872 charspan,
873 } = *self
874 else {
875 return s.serialize_seq(Some(0))?.end();
876 };
877 struct Bbox([f64; 4], &'static str);
878 impl serde::Serialize for Bbox {
879 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
880 let mut m = s.serialize_map(Some(5))?;
881 for (k, v) in ["l", "t", "r", "b"].iter().zip(self.0) {
882 m.serialize_entry(k, &v)?;
883 }
884 m.serialize_entry("coord_origin", self.1)?;
885 m.end()
886 }
887 }
888 struct Entry(usize, Bbox, [usize; 2]);
889 impl serde::Serialize for Entry {
890 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
891 let mut m = s.serialize_map(Some(3))?;
892 m.serialize_entry("page_no", &self.0)?;
893 m.serialize_entry("bbox", &self.1)?;
894 m.serialize_entry("charspan", &self.2)?;
895 m.end()
896 }
897 }
898 let mut seq = s.serialize_seq(Some(1))?;
899 seq.serialize_element(&Entry(
900 page_no,
901 Bbox([l, t, r, b], Self::origin(bottom_left)),
902 charspan,
903 ))?;
904 seq.end()
905 }
906}
907
908struct TextItem {
915 parent: Box<str>,
916 label: Box<str>,
917 prov: ProvSlot,
918 text: Box<str>,
919}
920
921impl TextItem {
922 fn into_value(self, index: usize) -> Value {
923 let text = String::from(self.text);
924 object([
925 ("self_ref", Value::String(format!("#/texts/{index}"))),
926 ("parent", ref_value(self.parent)),
927 ("children", Value::Array(Vec::new())),
928 ("content_layer", "body".into()),
929 ("label", Value::String(self.label.into())),
930 ("prov", self.prov.to_value()),
931 ("orig", Value::String(text.clone())),
932 ("text", Value::String(text)),
933 ])
934 }
935}
936
937enum Item {
941 Text(Box<TextItem>),
942 Json(Box<Value>),
943}
944
945impl Item {
946 fn json(v: Value) -> Self {
947 Item::Json(Box::new(v))
948 }
949
950 fn value_mut(&mut self, index: usize) -> &mut Value {
953 if let Item::Text(t) = self {
954 let t = std::mem::replace(
955 t,
956 Box::new(TextItem {
957 parent: "".into(),
958 label: "".into(),
959 prov: ProvSlot::None,
960 text: "".into(),
961 }),
962 );
963 *self = Item::json(t.into_value(index));
964 }
965 match self {
966 Item::Json(v) => v,
967 Item::Text(_) => unreachable!("converted above"),
968 }
969 }
970
971 fn into_value(self, index: usize) -> Value {
972 match self {
973 Item::Text(t) => t.into_value(index),
974 Item::Json(v) => *v,
975 }
976 }
977}
978
979struct Texts<'a>(&'a [Item]);
981
982impl serde::Serialize for Texts<'_> {
983 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
984 use serde::ser::{SerializeMap, SerializeSeq};
985 struct Text<'a>(usize, &'a TextItem);
986 impl serde::Serialize for Text<'_> {
987 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
988 let Text(i, t) = *self;
989 let mut m = s.serialize_map(Some(8))?;
990 m.serialize_entry("self_ref", &format_args!("#/texts/{i}"))?;
991 m.serialize_entry("parent", &Child::Ref(t.parent.clone()))?;
992 m.serialize_entry("children", &[(); 0])?;
993 m.serialize_entry("content_layer", "body")?;
994 m.serialize_entry("label", &t.label)?;
995 m.serialize_entry("prov", &t.prov)?;
996 m.serialize_entry("orig", &t.text)?;
997 m.serialize_entry("text", &t.text)?;
998 m.end()
999 }
1000 }
1001 let mut seq = s.serialize_seq(Some(self.0.len()))?;
1002 for (i, item) in self.0.iter().enumerate() {
1003 match item {
1004 Item::Text(t) => seq.serialize_element(&Text(i, t))?,
1005 Item::Json(v) => seq.serialize_element(v)?,
1006 }
1007 }
1008 seq.end()
1009 }
1010}
1011
1012enum Child {
1016 Ref(Box<str>),
1017 Json(Box<Value>),
1018}
1019
1020impl Child {
1021 fn json(v: Value) -> Self {
1022 Child::Json(Box::new(v))
1023 }
1024
1025 fn into_value(self) -> Value {
1026 match self {
1027 Child::Ref(r) => ref_value(r),
1028 Child::Json(v) => *v,
1029 }
1030 }
1031}
1032
1033impl serde::Serialize for Child {
1034 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1035 use serde::ser::SerializeMap;
1036 match self {
1037 Child::Ref(r) => {
1038 let mut m = s.serialize_map(Some(1))?;
1039 m.serialize_entry("$ref", r)?;
1040 m.end()
1041 }
1042 Child::Json(v) => v.serialize(s),
1043 }
1044 }
1045}
1046
1047fn round2(v: f64) -> f64 {
1048 (v * 100.0).round() / 100.0
1049}
1050
1051#[derive(Default)]
1052struct Builder {
1053 note_keys: bool,
1057 texts: Vec<Item>,
1058 groups: Vec<Value>,
1059 tables: Vec<Value>,
1060 pictures: Vec<Value>,
1061 field_regions: Vec<Value>,
1062 field_items: Vec<Value>,
1063 key_value_items: Vec<Value>,
1064 pages: Vec<(usize, f64, f64)>,
1068 cur_page: usize,
1070 cur_w: f64,
1071 cur_h: f64,
1072 pending_loc: Option<[u16; 4]>,
1075 pending_exact: Option<ExactProv>,
1078 pending_track: Option<(crate::tree::TreeTrack, String)>,
1081 pending_anchors: Vec<String>,
1084 pending_siblings: Vec<Value>,
1088 pending_after: Vec<Value>,
1092 pending_body: Vec<Value>,
1096 comment_groups: Vec<String>,
1100 last_comment_section: Option<(String, String)>,
1105 pending_comments: Vec<(String, Vec<usize>)>,
1109}
1110
1111impl Builder {
1112 fn take_prov(&mut self, char_len: usize) -> Value {
1118 self.take_prov_slot(char_len).to_value()
1119 }
1120
1121 fn take_prov_slot(&mut self, char_len: usize) -> ProvSlot {
1123 let prov = self.prov_slot(char_len, false);
1124 self.pending_exact = None;
1125 self.pending_loc = None;
1126 prov
1127 }
1128
1129 fn prov_slot(&self, char_len: usize, span_over_text: bool) -> ProvSlot {
1134 let r2 = round2;
1135 if let Some(ExactProv {
1136 page_no,
1137 bbox: [l, t, r, b],
1138 bottom_left,
1139 charspan,
1140 }) = self.pending_exact
1141 {
1142 let charspan = if span_over_text {
1143 [0, char_len]
1144 } else {
1145 charspan
1146 };
1147 return ProvSlot::One {
1148 page_no,
1149 bbox: [r2(l), r2(t), r2(r), r2(b)],
1150 bottom_left,
1151 charspan,
1152 };
1153 }
1154 let Some([x0, y0, x1, y1]) = self.pending_loc else {
1155 return ProvSlot::None;
1156 };
1157 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
1161 return ProvSlot::One {
1162 page_no: self.cur_page,
1163 bbox: [0.0; 4],
1164 bottom_left: true,
1165 charspan: [0, char_len],
1166 };
1167 }
1168 ProvSlot::One {
1169 page_no: self.cur_page,
1170 bbox: [
1171 r2(x0 as f64 * self.cur_w / 512.0),
1172 r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
1173 r2(x1 as f64 * self.cur_w / 512.0),
1174 r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
1175 ],
1176 bottom_left: true,
1177 charspan: [0, char_len],
1178 }
1179 }
1180
1181 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
1184 if self.pending_loc.is_none() && self.cur_page > 0 {
1185 self.pending_loc = loc;
1186 }
1187 }
1188
1189 fn link_comments(&mut self) {
1197 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
1198 .into_iter()
1199 .map(|(item, comments)| {
1200 let refs = comments
1201 .iter()
1202 .filter_map(|i| self.comment_groups.get(*i))
1203 .map(ref_value)
1204 .collect();
1205 (item, refs)
1206 })
1207 .collect();
1208 for (item, comment_refs) in refs {
1209 if comment_refs.is_empty() {
1210 continue;
1211 }
1212 let Some(target) = self.item_mut(&item) else {
1213 continue;
1214 };
1215 let Some(obj) = target.as_object_mut() else {
1216 continue;
1217 };
1218 let tail: Vec<(String, Value)> = obj
1219 .iter()
1220 .skip_while(|(k, _)| k.as_str() != "prov")
1221 .skip(1)
1222 .map(|(k, v)| (k.clone(), v.clone()))
1223 .collect();
1224 for (k, _) in &tail {
1225 obj.shift_remove(k);
1226 }
1227 obj.insert("comments".into(), Value::Array(comment_refs));
1228 for (k, v) in tail {
1229 obj.insert(k, v);
1230 }
1231 }
1232 }
1233
1234 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
1236 let idx = ref_index(self_ref)?;
1237 if self_ref.starts_with("#/texts/") {
1238 return self.texts.get_mut(idx).map(|item| item.value_mut(idx));
1239 }
1240 let bucket = if self_ref.starts_with("#/tables/") {
1241 &mut self.tables
1242 } else if self_ref.starts_with("#/pictures/") {
1243 &mut self.pictures
1244 } else if self_ref.starts_with("#/groups/") {
1245 &mut self.groups
1246 } else {
1247 return None;
1248 };
1249 bucket.get_mut(idx)
1250 }
1251
1252 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
1256 use crate::tree::TreeKind;
1257 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
1260 let (mut nt, mut ng, mut ntb, mut np, mut nf, mut nk) = (0, 0, 0, 0, 0, 0);
1261 for item in &tree.items {
1262 if item.deleted {
1263 refs.push(String::new());
1264 continue;
1265 }
1266 let r = match &item.kind {
1267 TreeKind::Text { .. } | TreeKind::Code { .. } => {
1268 nt += 1;
1269 format!("#/texts/{}", nt - 1)
1270 }
1271 TreeKind::Group { .. } => {
1272 ng += 1;
1273 format!("#/groups/{}", ng - 1)
1274 }
1275 TreeKind::Table { .. } => {
1276 ntb += 1;
1277 format!("#/tables/{}", ntb - 1)
1278 }
1279 TreeKind::Picture { .. } => {
1280 np += 1;
1281 format!("#/pictures/{}", np - 1)
1282 }
1283 TreeKind::FieldRegion { items } => {
1284 nt += items
1287 .iter()
1288 .map(|i| {
1289 [&i.marker, &i.key, &i.value]
1290 .iter()
1291 .filter(|p| p.is_some())
1292 .count()
1293 })
1294 .sum::<usize>();
1295 nf += 1;
1296 format!("#/field_regions/{}", nf - 1)
1297 }
1298 TreeKind::KeyValueGraph { .. } => {
1299 nk += 1;
1300 format!("#/key_value_items/{}", nk - 1)
1301 }
1302 };
1303 refs.push(r);
1304 }
1305 let ref_of = |id: usize| ref_value(refs[id].as_str());
1306 for (id, item) in tree.items.iter().enumerate() {
1307 if item.deleted {
1308 continue;
1309 }
1310 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
1311 let children: Vec<Value> = item
1315 .children
1316 .iter()
1317 .filter(|&&c| !tree.items[c].deleted)
1318 .map(|&c| ref_of(c))
1319 .collect();
1320 let layer = item.layer.map_or("body", |l| l.value());
1321 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
1324 let self_ref = match &item.kind {
1325 TreeKind::Text {
1326 label,
1327 text,
1328 orig,
1329 formatting,
1330 hyperlink,
1331 level,
1332 list,
1333 } => {
1334 let mut tail = serde_json::Map::new();
1337 if let Some(f) = formatting {
1338 tail.insert("formatting".into(), formatting_json(f));
1339 }
1340 if let Some(h) = hyperlink {
1341 tail.insert("hyperlink".into(), json!(h));
1342 }
1343 if let Some(l) = level {
1344 tail.insert("level".into(), json!(l));
1345 }
1346 if let Some(l) = list {
1347 tail.insert("enumerated".into(), json!(l.enumerated));
1348 tail.insert("marker".into(), json!(l.marker));
1349 }
1350 let r = format!("#/texts/{}", self.texts.len());
1351 let prov = self.take_prov(text.chars().count());
1352 let mut item_json = json!({
1353 "self_ref": r,
1354 "parent": { "$ref": parent },
1355 "children": children,
1356 "content_layer": layer,
1357 "label": label,
1358 "prov": prov,
1359 });
1360 if !item.comments.is_empty() {
1363 item_json["comments"] =
1364 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
1365 }
1366 if let Some(track) = &item.source {
1369 item_json["source"] = json!([track_json(track)]);
1370 }
1371 merge(
1372 &mut item_json,
1373 json!({
1374 "orig": orig.as_deref().unwrap_or(text),
1375 "text": text,
1376 }),
1377 );
1378 merge(&mut item_json, Value::Object(tail));
1379 if self.note_keys {
1380 if !item.notes.is_empty() {
1381 item_json["_notes"] = Value::Array(
1382 item.notes
1383 .iter()
1384 .map(|n| json!([n.offset, n.text]))
1385 .collect(),
1386 );
1387 }
1388 if item.note_body {
1389 item_json["_note_body"] = json!(true);
1390 }
1391 if !item.anchors.is_empty() {
1392 item_json["_anchors"] = json!(item.anchors);
1393 }
1394 if let Some(number) = &item.number {
1395 item_json["_number"] = json!(number);
1396 }
1397 if let Some(rev) = &item.revision {
1398 let mut r = serde_json::Map::new();
1399 r.insert("kind".into(), json!(rev.kind.as_str()));
1400 if let Some(a) = &rev.author {
1401 r.insert("author".into(), json!(a));
1402 }
1403 if let Some(d) = &rev.date {
1404 r.insert("date".into(), json!(d));
1405 }
1406 item_json["_revision"] = Value::Object(r);
1407 }
1408 }
1409 self.texts.push(Item::json(item_json));
1410 r
1411 }
1412 TreeKind::Code {
1413 text,
1414 orig,
1415 language,
1416 formatting,
1417 hyperlink,
1418 captions,
1419 } => {
1420 let r = format!("#/texts/{}", self.texts.len());
1421 let prov = self.take_prov(text.chars().count());
1422 let mut item_json = json!({
1423 "self_ref": r,
1424 "parent": { "$ref": parent },
1425 "children": children,
1426 "content_layer": layer,
1427 "label": "code",
1428 "prov": prov,
1429 });
1430 if !item.comments.is_empty() {
1431 item_json["comments"] =
1432 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
1433 }
1434 merge(
1435 &mut item_json,
1436 json!({
1437 "orig": orig.as_deref().unwrap_or(text),
1438 "text": text,
1439 }),
1440 );
1441 if let Some(f) = formatting {
1442 item_json["formatting"] = formatting_json(f);
1443 }
1444 if let Some(h) = hyperlink {
1445 item_json["hyperlink"] = json!(h);
1446 }
1447 merge(
1448 &mut item_json,
1449 json!({
1450 "captions": captions.iter().map(|&c| ref_of(c)).collect::<Vec<_>>(),
1451 "references": [],
1452 "footnotes": [],
1453 "code_language": code_language(language.as_deref()),
1454 }),
1455 );
1456 self.texts.push(Item::json(item_json));
1457 r
1458 }
1459 TreeKind::Group { label, name } => {
1460 self.pending_exact = None;
1461 let r = format!("#/groups/{}", self.groups.len());
1462 self.groups.push(json!({
1463 "self_ref": r,
1464 "parent": { "$ref": parent },
1465 "children": children,
1466 "content_layer": layer,
1467 "name": name,
1468 "label": label,
1469 }));
1470 r
1471 }
1472 TreeKind::Table {
1473 table,
1474 rich_cells,
1475 captions,
1476 } => {
1477 let r = self.add_table_with(table, parent, true);
1479 let idx = ref_index(&r).expect("table ref");
1480 let t = &mut self.tables[idx];
1481 t["children"] = Value::Array(children);
1482 t["content_layer"] = json!(layer);
1483 t["captions"] = Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
1484 for &(row, col, group) in rich_cells {
1488 let cell_ref = ref_of(group);
1489 let hit = |c: &Value| {
1490 c["start_row_offset_idx"] == json!(row)
1491 && c["start_col_offset_idx"] == json!(col)
1492 };
1493 if let Some(cells) = t["data"]["table_cells"].as_array_mut() {
1494 for c in cells.iter_mut().filter(|c| hit(c)) {
1495 c["ref"] = cell_ref.clone();
1496 }
1497 }
1498 }
1499 r
1500 }
1501 TreeKind::Picture {
1502 captions,
1503 image,
1504 classification,
1505 extra_classes,
1506 description,
1507 confidence,
1508 chart,
1509 dpi,
1510 } => {
1511 let mut meta = classification.as_ref().map(|c| {
1516 let mut pred = serde_json::Map::new();
1517 if let Some(conf) = confidence {
1518 pred.insert("confidence".into(), json!(conf));
1519 }
1520 pred.insert("class_name".into(), json!(c));
1521 let mut preds = vec![Value::Object(pred)];
1522 preds.extend(extra_classes.iter().map(|c| json!({ "class_name": c })));
1523 json!({ "classification": { "predictions": preds } })
1524 });
1525 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
1526 if !t.rows.is_empty() {
1527 m["tabular_chart"] = json!({ "chart_data": table_data(t) });
1528 }
1529 }
1530 if let Some(desc) = description {
1534 let mut d = description_meta(desc);
1535 let mut merged = Map::new();
1536 merged.insert("description".into(), d["description"].take());
1537 if let Some(Value::Object(rest)) = meta {
1538 merged.extend(rest);
1539 }
1540 merged.insert("annotations".into(), d["annotations"].take());
1541 meta = Some(Value::Object(merged));
1542 }
1543 let prov = self.take_prov(0);
1544 let r = self.push_picture(
1545 prov,
1546 captions.iter().map(|&c| ref_of(c)).collect(),
1547 children,
1548 image.as_ref(),
1549 meta,
1550 parent,
1551 );
1552 if let Some(idx) = ref_index(&r) {
1553 self.pictures[idx]["content_layer"] = json!(layer);
1554 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
1557 img["dpi"] = json!(dpi);
1558 }
1559 }
1560 r
1561 }
1562 TreeKind::FieldRegion { items } => {
1563 self.pending_exact = None;
1564 let r = self.add_field_region(items, parent);
1565 if let Some(region) = self.field_regions.last_mut() {
1566 region["content_layer"] = json!(layer);
1567 }
1568 r
1569 }
1570 TreeKind::KeyValueGraph { cells, links } => {
1571 self.pending_exact = None;
1572 let r = self.add_key_value_graph(cells, links, parent);
1573 if let Some(item) = self.key_value_items.last_mut() {
1574 item["content_layer"] = json!(layer);
1575 }
1576 r
1577 }
1578 };
1579 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1580 }
1581 tree.body
1582 .iter()
1583 .filter(|&&c| !tree.items[c].deleted)
1584 .map(|&c| ref_of(c))
1585 .collect()
1586 }
1587
1588 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1589 match node {
1590 Node::Heading { level: 1, text } => {
1591 Some(self.add_text("title", text, parent, json!({})))
1592 }
1593 Node::Heading { level, text } => Some(self.add_text(
1594 "section_header",
1595 text,
1596 parent,
1597 json!({ "level": level.saturating_sub(1) }),
1598 )),
1599 Node::Caption { text, href } => {
1600 let extra = match href {
1601 Some(url) => json!({ "hyperlink": url }),
1602 None => json!({}),
1603 };
1604 Some(self.add_text("caption", text, parent, extra))
1605 }
1606 Node::LabeledText { label, text, href } => {
1609 let extra = match href {
1610 Some(url) => json!({ "hyperlink": url }),
1611 None => json!({}),
1612 };
1613 Some(self.add_text(label, text, parent, extra))
1614 }
1615 Node::Paragraph { text } | Node::ListText { text, .. } => {
1616 let t = text.trim();
1619 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1620 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1621 _ => Some(self.add_text("text", text, parent, json!({}))),
1622 }
1623 }
1624 Node::CheckboxItem { checked, text } => {
1625 let label = if *checked {
1631 "checkbox_selected"
1632 } else {
1633 "checkbox_unselected"
1634 };
1635 Some(self.add_text(label, text, parent, json!({})))
1636 }
1637 Node::Code {
1638 language,
1639 text,
1640 orig,
1641 ..
1642 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
1643 Node::Formula {
1646 latex,
1647 orig,
1648 location,
1649 } => {
1650 self.adopt_loc(*location);
1651 Some(self.add_formula_item(latex, orig, parent))
1652 }
1653 Node::CommentSection {
1659 name,
1660 text,
1661 refs_note_text,
1662 grouped,
1663 } => {
1664 if !*grouped {
1665 let child =
1669 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
1670 self.comment_groups.push(child.clone());
1671 return Some(child);
1672 }
1673 if let Some((_, self_ref)) = self
1674 .last_comment_section
1675 .as_ref()
1676 .filter(|(n, _)| n == name)
1677 .cloned()
1678 {
1679 let child =
1682 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1683 if let Some(children) =
1684 self.groups[group_index(&self_ref)]["children"].as_array_mut()
1685 {
1686 children.push(ref_value(child.as_str()));
1687 }
1688 self.comment_groups.push(if *refs_note_text {
1689 child
1690 } else {
1691 self_ref.clone()
1692 });
1693 return Some(self_ref);
1694 }
1695 let self_ref = format!("#/groups/{}", self.groups.len());
1696 self.groups.push(Value::Null);
1697 let child =
1698 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
1699 self.groups[group_index(&self_ref)] = json!({
1700 "self_ref": self_ref,
1701 "parent": { "$ref": parent },
1702 "children": [{ "$ref": child }],
1703 "content_layer": "notes",
1704 "name": name,
1705 "label": "comment_section",
1706 });
1707 self.last_comment_section = Some((name.clone(), self_ref.clone()));
1708 self.comment_groups.push(if *refs_note_text {
1709 child
1710 } else {
1711 self_ref.clone()
1712 });
1713 Some(self_ref)
1714 }
1715 Node::Commented { comments, inner } => {
1718 let item = self.add_node(inner, parent)?;
1719 if !comments.is_empty() {
1720 self.pending_comments.push((item.clone(), comments.clone()));
1721 }
1722 Some(item)
1723 }
1724 Node::Table(t) => Some(self.add_table(t, parent)),
1725 Node::Picture {
1726 caption,
1727 caption_href,
1728 image,
1729 classification,
1730 description,
1731 caption_parent,
1732 caption_location,
1733 } => Some(self.add_picture(
1734 caption.as_deref(),
1735 caption_href.as_deref(),
1736 image.as_ref(),
1737 picture_meta(classification.as_deref(), description.as_ref()),
1738 parent,
1739 (*caption_parent, *caption_location),
1740 )),
1741 Node::Chart {
1746 kind,
1747 table,
1748 caption,
1749 location,
1750 } => {
1751 self.adopt_loc(*location);
1752 let mut meta = json!({
1753 "classification": { "predictions": [{ "class_name": kind }] },
1754 });
1755 if !table.rows.is_empty() {
1756 meta["tabular_chart"] = json!({ "chart_data": table_data(table) });
1757 }
1758 let mut captions = Vec::new();
1764 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
1765 let prov = self.prov_slot(unescape_text(cap).chars().count(), true);
1766 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
1767 self.pending_siblings.push(ref_value(cap_ref.as_str()));
1768 captions.push(ref_value(cap_ref));
1769 }
1770 let prov = self.take_prov(0);
1771 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
1772 }
1773 Node::DoclangOnly(_) => None,
1775 Node::Group {
1776 label,
1777 name,
1778 layer,
1779 children,
1780 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
1781 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
1782 Node::KeyValueGraph { cells, links } => {
1783 Some(self.add_key_value_graph(cells, links, parent))
1784 }
1785 Node::InlineGroup { md_text, .. } => {
1788 Some(self.add_text("text", md_text, parent, json!({})))
1789 }
1790 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
1792 Node::Furniture {
1798 layer: ContentLayer::Notes,
1799 inner,
1800 } => {
1801 let item = self.add_node(inner, parent)?;
1802 self.set_layer(&item, "notes");
1803 Some(item)
1804 }
1805 Node::Furniture { .. } => None,
1806 Node::PageFurniture {
1810 footer,
1811 location,
1812 text,
1813 } => {
1814 if self.cur_page > 0 {
1815 self.pending_loc = Some(*location);
1816 }
1817 let label = if *footer {
1818 "page_footer"
1819 } else {
1820 "page_header"
1821 };
1822 let item = self.add_text(label, text, parent, json!({}));
1823 self.pending_loc = None;
1824 self.set_layer(&item, "furniture");
1825 Some(item)
1826 }
1827 Node::FurnitureText { label, text } => {
1830 let item = self.add_text(label, text, parent, json!({}));
1831 self.set_layer(&item, "furniture");
1832 Some(item)
1833 }
1834 Node::PictureChildren(children) => {
1838 let pic = self.pictures.len().checked_sub(1)?;
1839 let pic_ref = format!("#/pictures/{pic}");
1840 let refs = self.walk_into(children, &pic_ref);
1841 if let Some(list) = self.pictures[pic]["children"].as_array_mut() {
1842 list.extend(refs.into_iter().map(Child::into_value));
1843 }
1844 None
1845 }
1846 Node::Located { location, inner } => {
1851 if self.cur_page > 0 {
1852 self.pending_loc = Some(*location);
1853 }
1854 let r = self.add_node(inner, parent);
1855 self.pending_loc = None;
1856 r
1857 }
1858 Node::Anchor { id } => {
1859 self.pending_anchors.push(id.clone());
1860 None
1861 }
1862 Node::Track { track, cue, inner } => {
1863 self.pending_track = Some((track.clone(), cue.clone()));
1864 let r = self.add_node(inner, parent);
1865 self.pending_track = None;
1866 r
1867 }
1868 Node::Prov {
1869 page_no,
1870 bbox,
1871 charspan,
1872 inner,
1873 ..
1874 } => {
1875 self.pending_exact = Some(ExactProv {
1876 page_no: *page_no,
1877 bbox: bbox.map(f64::from),
1878 bottom_left: false,
1879 charspan: *charspan,
1880 });
1881 let r = self.add_node(inner, parent);
1882 self.pending_exact = None;
1883 r
1884 }
1885 Node::PageBreak => None,
1887 Node::PageInfo {
1890 page_no,
1891 width,
1892 height,
1893 } => {
1894 self.cur_page = *page_no;
1895 self.cur_w = *width as f64;
1896 self.cur_h = *height as f64;
1897 if *page_no > 0 {
1898 self.pages.push((*page_no, self.cur_w, self.cur_h));
1899 }
1900 None
1901 }
1902 Node::ListItem { .. } => None,
1904 }
1905 }
1906
1907 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
1911 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
1912 self.field_regions.push(Value::Null);
1913 let region_index = self.field_regions.len() - 1;
1914 let mut item_refs = Vec::new();
1915 for item in items {
1916 item_refs.push(ref_value(self.add_field_item(item, &self_ref)));
1917 }
1918 self.field_regions[region_index] = json!({
1919 "self_ref": self_ref,
1920 "parent": { "$ref": parent },
1921 "children": item_refs,
1922 "content_layer": "body",
1923 "label": "field_region",
1924 "prov": [],
1925 });
1926 self_ref
1927 }
1928
1929 fn add_key_value_graph(
1933 &mut self,
1934 cells: &[crate::GraphCell],
1935 links: &[crate::GraphLink],
1936 parent: &str,
1937 ) -> String {
1938 let self_ref = format!("#/key_value_items/{}", self.key_value_items.len());
1939 let cells: Vec<Value> = cells
1940 .iter()
1941 .map(|c| {
1942 json!({
1943 "label": c.label,
1944 "cell_id": c.cell_id,
1945 "text": c.text,
1946 "orig": c.orig,
1947 })
1948 })
1949 .collect();
1950 let links: Vec<Value> = links
1951 .iter()
1952 .map(|l| {
1953 json!({
1954 "label": l.label,
1955 "source_cell_id": l.source_cell_id,
1956 "target_cell_id": l.target_cell_id,
1957 })
1958 })
1959 .collect();
1960 self.key_value_items.push(json!({
1961 "self_ref": self_ref,
1962 "parent": { "$ref": parent },
1963 "children": [],
1964 "content_layer": "body",
1965 "label": "key_value_region",
1966 "prov": [],
1967 "captions": [],
1968 "references": [],
1969 "footnotes": [],
1970 "graph": { "cells": cells, "links": links },
1971 }));
1972 self_ref
1973 }
1974
1975 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
1976 let self_ref = format!("#/field_items/{}", self.field_items.len());
1977 self.field_items.push(Value::Null);
1978 let item_index = self.field_items.len() - 1;
1979 let mut child_refs = Vec::new();
1980 for (label, text) in [
1981 ("marker", &item.marker),
1982 ("field_key", &item.key),
1983 ("field_value", &item.value),
1984 ] {
1985 if let Some(text) = text {
1986 let extra = match (label, &item.value_kind) {
1989 ("field_value", Some(kind)) => json!({ "kind": kind }),
1990 _ => json!({}),
1991 };
1992 child_refs.push(ref_value(self.add_text(label, text, &self_ref, extra)));
1993 }
1994 }
1995 self.field_items[item_index] = json!({
1996 "self_ref": self_ref,
1997 "parent": { "$ref": parent },
1998 "children": child_refs,
1999 "content_layer": "body",
2000 "label": "field_item",
2001 "prov": [],
2002 });
2003 self_ref
2004 }
2005
2006 fn set_layer(&mut self, self_ref: &str, layer: &str) {
2010 if let Some(item) = self.item_mut(self_ref) {
2011 item["content_layer"] = json!(layer);
2012 }
2013 }
2014
2015 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
2016 let prov = self.take_prov_slot(unescape_text(text).chars().count());
2017 self.add_text_with(label, text, parent, extra, prov)
2018 }
2019
2020 fn add_text_with(
2023 &mut self,
2024 label: &str,
2025 text: &str,
2026 parent: &str,
2027 extra: Value,
2028 prov: ProvSlot,
2029 ) -> String {
2030 let index = self.texts.len();
2031 let self_ref = format!("#/texts/{index}");
2032 let mut item = match self.pending_track.take() {
2037 None => Item::Text(Box::new(TextItem {
2038 parent: parent.into(),
2039 label: label.into(),
2040 prov,
2041 text: unescape_text(text).into(),
2042 })),
2043 Some((track, cue)) => {
2044 let mut m = serde_json::Map::with_capacity(9);
2045 m.insert("self_ref".into(), Value::String(self_ref.clone()));
2046 m.insert("parent".into(), ref_value(parent));
2047 m.insert("children".into(), Value::Array(Vec::new()));
2048 m.insert("content_layer".into(), "body".into());
2049 m.insert("label".into(), label.into());
2050 m.insert("prov".into(), prov.to_value());
2051 m.insert("source".into(), Value::Array(vec![track_json(&track)]));
2053 m.insert("orig".into(), Value::String(cue.clone()));
2054 m.insert("text".into(), Value::String(cue));
2055 Item::json(Value::Object(m))
2056 }
2057 };
2058 if extra.as_object().is_some_and(|e| !e.is_empty()) {
2059 merge(item.value_mut(index), extra);
2060 }
2061 let anchors = std::mem::take(&mut self.pending_anchors);
2064 if self.note_keys && !anchors.is_empty() {
2065 merge(item.value_mut(index), json!({ "_anchors": anchors }));
2066 }
2067 self.texts.push(item);
2068 self_ref
2069 }
2070
2071 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
2074 let self_ref = format!("#/texts/{}", self.texts.len());
2075 let prov = self.take_prov(latex.chars().count());
2076 self.texts.push(Item::json(json!({
2077 "self_ref": self_ref,
2078 "parent": { "$ref": parent },
2079 "children": [],
2080 "content_layer": "body",
2081 "label": "formula",
2082 "prov": prov,
2083 "orig": latex,
2084 "text": latex,
2085 })));
2086 self_ref
2087 }
2088
2089 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
2093 let self_ref = format!("#/texts/{}", self.texts.len());
2094 let prov = self.take_prov(latex.chars().count());
2095 self.texts.push(Item::json(json!({
2096 "self_ref": self_ref,
2097 "parent": { "$ref": parent },
2098 "children": [],
2099 "content_layer": "body",
2100 "label": "formula",
2101 "prov": prov,
2102 "orig": orig,
2103 "text": latex,
2104 })));
2105 self_ref
2106 }
2107
2108 fn add_code(
2109 &mut self,
2110 text: &str,
2111 language: Option<&str>,
2112 orig: Option<&str>,
2113 parent: &str,
2114 ) -> String {
2115 let self_ref = format!("#/texts/{}", self.texts.len());
2116 let raw = unescape_text(text);
2117 let prov = self.take_prov(raw.chars().count());
2118 self.texts.push(Item::json(json!({
2119 "self_ref": self_ref,
2120 "parent": { "$ref": parent },
2121 "children": [],
2122 "content_layer": "body",
2123 "label": "code",
2124 "prov": prov,
2125 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
2128 "text": raw,
2129 "captions": [],
2130 "references": [],
2131 "footnotes": [],
2132 "code_language": code_language(language),
2133 })));
2134 self_ref
2135 }
2136
2137 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
2140 let self_ref = format!("#/groups/{}", self.groups.len());
2141 self.groups.push(Value::Null);
2143 let base = level_of(&items[0]);
2144 let mut children = Vec::new();
2145 let mut i = 0;
2146 while i < items.len() {
2147 if !matches!(items[i], Node::ListItem { .. }) {
2150 i += 1;
2151 continue;
2152 }
2153 let lvl = level_of(&items[i]);
2154 if lvl > base {
2155 i += 1;
2157 continue;
2158 }
2159 let item_ref = self.add_list_item(&items[i], &self_ref);
2160 let mut j = i + 1;
2162 while j < items.len() && level_of(&items[j]) > base {
2163 j += 1;
2164 }
2165 if j > i + 1 {
2166 let mut nested = Vec::new();
2167 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
2168 if let Some(idx) = ref_index(&item_ref) {
2170 self.texts[idx].value_mut(idx)["children"]
2171 .as_array_mut()
2172 .unwrap()
2173 .extend(nested);
2174 }
2175 }
2176 children.push(ref_value(item_ref));
2177 i = j;
2178 }
2179 self.groups[group_index(&self_ref)] = json!({
2180 "self_ref": self_ref,
2181 "parent": { "$ref": parent },
2182 "children": children,
2183 "content_layer": "body",
2184 "name": "list",
2185 "label": "list",
2186 });
2187 self_ref
2188 }
2189
2190 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
2191 let Node::ListItem {
2192 ordered,
2193 number,
2194 text,
2195 location,
2196 ..
2197 } = node
2198 else {
2199 unreachable!()
2200 };
2201 self.adopt_loc(*location);
2202 let self_ref = format!("#/texts/{}", self.texts.len());
2203 let raw = unescape_text(text);
2204 let prov = self.take_prov(raw.chars().count());
2205 let marker = if *ordered {
2206 format!("{number}.")
2207 } else {
2208 "-".to_string()
2209 };
2210 self.texts.push(Item::json(json!({
2211 "self_ref": self_ref,
2212 "parent": { "$ref": parent },
2213 "children": [],
2214 "content_layer": "body",
2215 "label": "list_item",
2216 "prov": prov,
2217 "orig": raw,
2218 "text": raw,
2219 "enumerated": ordered,
2220 "marker": marker,
2221 })));
2222 self_ref
2223 }
2224
2225 fn add_table(&mut self, t: &Table, parent: &str) -> String {
2226 self.add_table_with(t, parent, false)
2227 }
2228
2229 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
2232 let self_ref = format!("#/tables/{}", self.tables.len());
2233 self.adopt_loc(t.location);
2234 let prov = self.take_prov(0);
2235 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
2239 Some(cap) => self.add_caption(
2240 cap,
2241 json!({}),
2242 &self_ref,
2243 parent,
2244 (t.caption_parent, t.caption_location),
2245 ),
2246 None => (Vec::new(), Vec::new()),
2247 };
2248 let data = table_data_with(t, raw);
2249 self.tables.push(json!({
2250 "self_ref": self_ref,
2251 "parent": { "$ref": parent },
2252 "children": children,
2253 "content_layer": "body",
2254 "label": "table",
2255 "prov": prov,
2256 "captions": captions,
2257 "references": [],
2258 "footnotes": [],
2259 "data": data,
2260 "annotations": [],
2261 }));
2262 self_ref
2263 }
2264
2265 fn add_caption(
2274 &mut self,
2275 text: &str,
2276 extra: Value,
2277 self_ref: &str,
2278 parent: &str,
2279 (choice, location): (CaptionParent, Option<[u16; 4]>),
2280 ) -> (Vec<Value>, Vec<Value>) {
2281 let cap_parent = match choice {
2286 CaptionParent::Item => self_ref,
2287 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
2288 CaptionParent::Body => "#/body",
2289 };
2290 if self.cur_page > 0 && location.is_some() {
2291 self.pending_loc = location;
2292 }
2293 let cap_ref = ref_value(self.add_text("caption", text, cap_parent, extra));
2294 match choice {
2295 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
2296 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
2299 CaptionParent::Body if parent == "#/body" => {
2300 self.pending_siblings.push(cap_ref.clone())
2301 }
2302 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
2303 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
2306 }
2307 (vec![cap_ref], Vec::new())
2308 }
2309
2310 fn add_picture(
2313 &mut self,
2314 caption: Option<&str>,
2315 caption_href: Option<&str>,
2316 image: Option<&crate::PictureImage>,
2317 meta: Option<Value>,
2318 parent: &str,
2319 caption_at: (CaptionParent, Option<[u16; 4]>),
2320 ) -> String {
2321 let self_ref = format!("#/pictures/{}", self.pictures.len());
2322 let prov = self.take_prov(0);
2325 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
2326 Some(cap) => {
2327 let extra = match caption_href {
2331 Some(href) => json!({ "hyperlink": href }),
2332 None => json!({}),
2333 };
2334 self.add_caption(cap, extra, &self_ref, parent, caption_at)
2335 }
2336 None => (Vec::new(), Vec::new()),
2337 };
2338 self.push_picture(prov, captions, children, image, meta, parent)
2339 }
2340
2341 fn push_picture(
2344 &mut self,
2345 prov: Value,
2346 captions: Vec<Value>,
2347 children: Vec<Value>,
2348 image: Option<&crate::PictureImage>,
2349 meta: Option<Value>,
2350 parent: &str,
2351 ) -> String {
2352 let self_ref = format!("#/pictures/{}", self.pictures.len());
2353 let annotations = meta
2357 .as_ref()
2358 .and_then(|m| m.get("annotations").cloned())
2359 .unwrap_or_else(|| json!([]));
2360 let meta = meta.map(|mut m| {
2361 if let Some(obj) = m.as_object_mut() {
2362 obj.remove("annotations");
2363 }
2364 m
2365 });
2366 let mut item = match meta {
2370 Some(meta) => json!({
2371 "self_ref": self_ref,
2372 "parent": { "$ref": parent },
2373 "children": children,
2374 "content_layer": "body",
2375 "meta": meta,
2376 "label": "picture",
2377 "prov": prov,
2378 "captions": captions,
2379 "references": [],
2380 "footnotes": [],
2381 "annotations": annotations,
2382 }),
2383 None => json!({
2384 "self_ref": self_ref,
2385 "parent": { "$ref": parent },
2386 "children": children,
2387 "content_layer": "body",
2388 "label": "picture",
2389 "prov": prov,
2390 "captions": captions,
2391 "references": [],
2392 "footnotes": [],
2393 "annotations": annotations,
2394 }),
2395 };
2396 if let Some(img) = image {
2401 let mut image = image_ref_json(img);
2402 if self.note_keys {
2405 if let Some([w, h]) = img.display_size {
2406 image["_display"] = json!([w, h]);
2407 }
2408 }
2409 if let Some(obj) = item.as_object_mut() {
2410 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
2411 obj.insert("image".into(), image);
2412 obj.insert("annotations".into(), annotations);
2413 }
2414 }
2415 self.pictures.push(item);
2416 self_ref
2417 }
2418
2419 fn add_group(
2420 &mut self,
2421 label: &str,
2422 name: Option<&str>,
2423 layer: Option<ContentLayer>,
2424 nodes: &[Node],
2425 parent: &str,
2426 ) -> String {
2427 let self_ref = format!("#/groups/{}", self.groups.len());
2428 self.groups.push(Value::Null);
2429 let mark = (
2433 self.texts.len(),
2434 self.tables.len(),
2435 self.pictures.len(),
2436 self.groups.len(),
2437 );
2438 let children: Vec<Value> = self
2439 .walk_into(nodes, &self_ref)
2440 .into_iter()
2441 .map(Child::into_value)
2442 .collect();
2443 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
2444 let content_layer = layer.map_or("body", |l| l.value());
2445 self.groups[group_index(&self_ref)] = json!({
2446 "self_ref": self_ref,
2447 "parent": { "$ref": parent },
2448 "children": children,
2449 "content_layer": content_layer,
2450 "name": name,
2451 "label": label,
2452 });
2453 if layer.is_some() {
2454 let (t, tb, p, g) = mark;
2455 for item in self.texts[t..]
2456 .iter_mut()
2457 .enumerate()
2458 .map(|(k, item)| item.value_mut(t + k))
2459 .chain(self.tables[tb..].iter_mut())
2460 .chain(self.pictures[p..].iter_mut())
2461 .chain(self.groups[g..].iter_mut())
2462 {
2463 if let Some(obj) = item.as_object_mut() {
2464 obj.insert("content_layer".into(), json!(content_layer));
2465 }
2466 }
2467 }
2468 self_ref
2469 }
2470
2471 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Child> {
2474 let seqs: Option<Vec<usize>> = nodes
2479 .iter()
2480 .map(|n| match n {
2481 Node::Prov { seq: Some(s), .. } => Some(*s),
2482 _ => None,
2483 })
2484 .collect();
2485 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
2486 let mut order: Vec<usize> = (0..nodes.len()).collect();
2487 order.sort_by_key(|&i| seqs[i]);
2488 let mut slots: Vec<Vec<Child>> = (0..nodes.len()).map(|_| Vec::new()).collect();
2489 for i in order {
2490 if let Some(r) = self.add_node(&nodes[i], parent) {
2491 slots[i].extend(self.pending_siblings.drain(..).map(Child::json));
2492 slots[i].push(Child::Ref(r.into()));
2493 slots[i].extend(self.pending_after.drain(..).map(Child::json));
2494 }
2495 if parent == "#/body" {
2496 slots[i].extend(self.pending_body.drain(..).map(Child::json));
2497 }
2498 }
2499 return slots.into_iter().flatten().collect();
2500 }
2501 let mut children = Vec::new();
2502 let mut i = 0;
2503 while i < nodes.len() {
2504 if matches!(nodes[i], Node::ListItem { .. }) {
2505 let start = i;
2506 i += 1;
2507 loop {
2508 match nodes.get(i) {
2509 Some(Node::ListItem { .. }) => i += 1,
2510 Some(Node::Paragraph { text })
2513 if text.is_empty()
2514 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
2515 {
2516 i += 1
2517 }
2518 _ => break,
2519 }
2520 }
2521 let mut lists = Vec::new();
2522 self.add_sibling_lists(&nodes[start..i], parent, &mut lists);
2523 children.extend(lists.into_iter().map(Child::json));
2524 } else {
2525 if let Some(r) = self.add_node(&nodes[i], parent) {
2526 children.extend(self.pending_siblings.drain(..).map(Child::json));
2527 children.push(Child::Ref(r.into()));
2528 children.extend(self.pending_after.drain(..).map(Child::json));
2529 }
2530 i += 1;
2531 }
2532 if parent == "#/body" {
2535 children.extend(self.pending_body.drain(..).map(Child::json));
2536 }
2537 }
2538 children
2539 }
2540
2541 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
2546 let base = level_of(&run[0]);
2547 let mut seg = 0;
2548 for k in 0..run.len() {
2549 let Node::ListItem {
2550 first_in_list,
2551 level,
2552 ..
2553 } = &run[k]
2554 else {
2555 continue;
2556 };
2557 if *level != base {
2558 continue; }
2560 if k > seg && *first_in_list {
2561 out.push(ref_value(self.add_list(&run[seg..k], parent)));
2562 seg = k;
2563 }
2564 }
2565 out.push(ref_value(self.add_list(&run[seg..], parent)));
2566 }
2567}
2568
2569fn level_of(node: &Node) -> u8 {
2570 match node {
2571 Node::ListItem { level, .. } => *level,
2572 _ => 0,
2573 }
2574}
2575
2576fn group_index(self_ref: &str) -> usize {
2577 self_ref.rsplit('/').next().unwrap().parse().unwrap()
2578}
2579
2580fn ref_index(self_ref: &str) -> Option<usize> {
2581 self_ref.rsplit('/').next()?.parse().ok()
2582}
2583
2584fn merge(target: &mut Value, extra: Value) {
2586 if let (Some(t), Value::Object(e)) = (target.as_object_mut(), extra) {
2587 t.extend(e);
2588 }
2589}
2590
2591fn object<const N: usize>(entries: [(&str, Value); N]) -> Value {
2597 let mut m = serde_json::Map::with_capacity(N);
2598 for (k, v) in entries {
2599 m.insert(k.to_owned(), v);
2600 }
2601 Value::Object(m)
2602}
2603
2604fn ref_value(r: impl Into<String>) -> Value {
2606 object([("$ref", Value::String(r.into()))])
2607}
2608
2609fn unescape_text(s: &str) -> String {
2611 s.replace("<", "<")
2612 .replace(">", ">")
2613 .replace("&", "&")
2614 .replace("\\_", "_")
2615}
2616
2617fn fnv1a(s: &str) -> u64 {
2620 let mut h: u64 = 0xcbf29ce484222325;
2621 for b in s.bytes() {
2622 h ^= b as u64;
2623 h = h.wrapping_mul(0x100000001b3);
2624 }
2625 h
2626}
2627
2628#[cfg(test)]
2629mod tests {
2630 use crate::{
2631 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
2632 };
2633 use serde_json::Value;
2634
2635 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
2636 crate::TableCell {
2637 text: text.into(),
2638 bbox: None,
2639 start_row: row,
2640 start_col: col,
2641 row_span: 1,
2642 col_span,
2643 column_header: row == 0,
2644 row_header: false,
2645 row_section: false,
2646 }
2647 }
2648
2649 #[test]
2655 fn ragged_rows_take_their_grid_from_first_class_cells() {
2656 let cells = vec![
2657 cell_at("h0", 0, 0, 1),
2658 cell_at("h2", 0, 2, 1),
2659 cell_at("wide", 1, 0, 3),
2660 ];
2661 let export = |rows: Vec<Vec<String>>| -> Value {
2662 let mut doc = DoclingDocument::new("t");
2663 doc.push(Node::Table(Table {
2664 rows,
2665 cells: Some(cells.clone()),
2666 ..Table::default()
2667 }));
2668 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2669 v["tables"][0]["data"].clone()
2670 };
2671 let data = export(vec![
2673 vec!["h0".into(), "h2".into()],
2674 vec!["wide".into(), "wide".into(), "wide".into()],
2675 ]);
2676 assert_eq!(data["num_rows"], 2);
2677 assert_eq!(data["num_cols"], 3);
2678 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
2679 let row0: Vec<&str> = data["grid"][0]
2680 .as_array()
2681 .unwrap()
2682 .iter()
2683 .map(|c| c["text"].as_str().unwrap())
2684 .collect();
2685 assert_eq!(row0, ["h0", "", "h2"]);
2686 assert_eq!(data["table_cells"][2]["col_span"], 3);
2687 let data = export(vec![
2690 vec!["h0".into(), "h2".into()],
2691 vec!["wide".into(), "wide".into()],
2692 ]);
2693 assert_eq!(data["num_cols"], 2);
2694 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
2695 }
2696
2697 fn doc_with_image() -> DoclingDocument {
2698 let mut doc = DoclingDocument::new("t");
2699 doc.push(Node::Picture {
2700 caption: Some("Fig 1".into()),
2701 caption_href: None,
2702 image: Some(PictureImage {
2703 dpi: crate::PictureImage::DEFAULT_DPI,
2704 mimetype: "image/png".into(),
2705 width: 4,
2706 height: 2,
2707 data: b"foobar".to_vec(),
2708 display_size: None,
2709 }),
2710 classification: None,
2711 description: None,
2712 caption_parent: Default::default(),
2713 caption_location: None,
2714 });
2715 doc
2716 }
2717
2718 #[test]
2723 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
2724 let mut doc = DoclingDocument::new("t");
2725 doc.push(Node::Heading {
2726 level: 1,
2727 text: "Slide One".into(),
2728 });
2729 doc.push(Node::Furniture {
2730 layer: ContentLayer::Notes,
2731 inner: Box::new(Node::Located {
2732 location: [0, 0, 0, 0],
2733 inner: Box::new(Node::Paragraph {
2734 text: "Speaker note for slide 1.".into(),
2735 }),
2736 }),
2737 });
2738 doc.push(Node::Furniture {
2739 layer: ContentLayer::Furniture,
2740 inner: Box::new(Node::Paragraph {
2741 text: "page header".into(),
2742 }),
2743 });
2744
2745 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2746 let texts = v["texts"].as_array().unwrap();
2747 assert_eq!(
2748 texts
2749 .iter()
2750 .map(|t| (
2751 t["label"].as_str().unwrap(),
2752 t["content_layer"].as_str().unwrap(),
2753 t["text"].as_str().unwrap()
2754 ))
2755 .collect::<Vec<_>>(),
2756 vec![
2757 ("title", "body", "Slide One"),
2758 ("text", "notes", "Speaker note for slide 1."),
2759 ],
2760 "the note is carried on its own layer; the furniture is not carried"
2761 );
2762 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
2764 }
2765
2766 #[test]
2772 fn continuation_flags_become_spanning_cells() {
2773 let mut doc = DoclingDocument::new("t");
2774 let rows = vec![
2776 vec!["merged".to_string(), "merged".into(), "merged".into()],
2777 vec!["merged".to_string(), "merged".into(), "merged".into()],
2778 vec!["a".to_string(), "b".into(), "c".into()],
2779 ];
2780 doc.push(Node::Table(crate::Table {
2781 rows,
2782 location: None,
2783 structure: Some(crate::TableStructure {
2784 header_row: vec![true, false, false],
2785 col_continuation: vec![
2786 vec![false, true, true],
2787 vec![false, true, true],
2788 vec![false, false, false],
2789 ],
2790 row_continuation: vec![
2791 vec![false, false, false],
2792 vec![true, true, true],
2793 vec![false, false, false],
2794 ],
2795 row_header: Vec::new(),
2796 col_header: Vec::new(),
2797 }),
2798 cell_blocks: None,
2799 cells: None,
2800 caption: None,
2801 caption_parent: Default::default(),
2802 caption_location: None,
2803 }));
2804 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2805 let data = &v["tables"][0]["data"];
2806 assert_eq!(data["num_rows"], 3);
2807 assert_eq!(data["num_cols"], 3);
2808 let cells = data["table_cells"].as_array().unwrap();
2809 assert_eq!(
2810 cells.len(),
2811 4,
2812 "one cell for the range, three for the plain row"
2813 );
2814 assert_eq!(
2815 cells[0],
2816 serde_json::json!({
2817 "row_span": 2, "col_span": 3,
2818 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
2819 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
2820 "text": "merged", "column_header": true, "row_header": false,
2821 "row_section": false, "fillable": false,
2822 })
2823 );
2824 assert_eq!(cells[1]["text"], "a");
2825 assert_eq!(cells[1]["row_span"], 1);
2826 assert_eq!(cells[1]["column_header"], false);
2827 let grid = data["grid"].as_array().unwrap();
2829 assert_eq!(grid.len(), 3);
2830 for (r, row) in grid.iter().take(2).enumerate() {
2831 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
2832 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
2833 }
2834 }
2835 assert_eq!(grid[2][2]["text"], "c");
2836 }
2837
2838 #[test]
2844 fn exact_provenance_pages_and_chart_captions_follow_docling() {
2845 let mut doc = DoclingDocument::new("t");
2846 doc.push(Node::PageInfo {
2847 page_no: 1,
2848 width: 3.0,
2849 height: 4.0,
2850 });
2851 let table = crate::Table {
2852 rows: vec![vec!["a".to_string(), "b".into()]],
2853 ..Default::default()
2854 };
2855 doc.push(Node::Group {
2856 label: "sheet".into(),
2857 name: Some("Data".into()),
2858 layer: None,
2859 children: vec![
2860 Node::Prov {
2865 page_no: 1,
2866 bbox: [0.0, 0.0, 3.0, 4.0],
2867 charspan: [0, 0],
2868 seq: Some(1),
2869 inner: Box::new(Node::Table(table.clone())),
2870 },
2871 Node::Prov {
2872 page_no: 1,
2873 bbox: [0.0, 1.0, 1.0, 1.0],
2874 charspan: [0, 0],
2875 seq: Some(0),
2876 inner: Box::new(Node::Chart {
2877 kind: "bar_chart".into(),
2878 table,
2879 caption: Some("Sales".into()),
2880 location: Some([0, 128, 170, 128]),
2881 }),
2882 },
2883 ],
2884 });
2885 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2886 assert_eq!(
2887 v["pages"],
2888 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
2889 );
2890 assert_eq!(
2891 v["tables"][0]["prov"],
2892 serde_json::json!([{
2893 "page_no": 1,
2894 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
2895 "charspan": [0, 0],
2896 }])
2897 );
2898 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
2899 let sheet = &v["groups"][0];
2902 assert_eq!(
2903 sheet["children"],
2904 serde_json::json!([
2905 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
2906 ])
2907 );
2908 let cap = &v["texts"][0];
2909 assert_eq!(cap["label"], "caption");
2910 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
2911 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
2912 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
2913 let pic = &v["pictures"][0];
2914 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
2915 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
2916 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
2917 assert_eq!(
2918 pic["meta"]["classification"]["predictions"][0]["class_name"],
2919 "bar_chart"
2920 );
2921 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
2922 }
2923
2924 #[test]
2928 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
2929 let mut doc = DoclingDocument::new("t");
2930 doc.push(Node::PageInfo {
2931 page_no: 1,
2932 width: 12192000.0,
2933 height: 6858000.0,
2934 });
2935 doc.push(Node::Furniture {
2936 layer: ContentLayer::Notes,
2937 inner: Box::new(Node::Located {
2938 location: [0, 0, 0, 0],
2939 inner: Box::new(Node::Paragraph {
2940 text: "a note".into(),
2941 }),
2942 }),
2943 });
2944 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
2945 let prov = &v["texts"][0]["prov"][0];
2946 assert_eq!(prov["page_no"], 1);
2947 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
2948 assert_eq!(
2949 prov["bbox"],
2950 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
2951 );
2952 assert_eq!(
2954 v["pages"]["1"]["size"],
2955 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
2956 );
2957 }
2958
2959 #[test]
2969 fn captions_and_checkboxes_carry_their_own_prov() {
2970 let mut doc = DoclingDocument::new("t");
2971 doc.push(Node::PageInfo {
2972 page_no: 1,
2973 width: 512.0,
2974 height: 512.0,
2975 });
2976 doc.push(Node::Located {
2977 location: [64, 64, 448, 256],
2978 inner: Box::new(Node::Picture {
2979 caption: Some("Figure 1".into()),
2980 caption_href: None,
2981 image: None,
2982 classification: None,
2983 description: None,
2984 caption_parent: CaptionParent::Item,
2985 caption_location: Some([64, 264, 448, 280]),
2986 }),
2987 });
2988 doc.push(Node::PageInfo {
2989 page_no: 2,
2990 width: 512.0,
2991 height: 512.0,
2992 });
2993 doc.push(Node::Located {
2994 location: [64, 120, 448, 300],
2995 inner: Box::new(Node::Table(Table {
2996 rows: vec![vec!["a".into()]],
2997 caption: Some("Table 1".into()),
2998 caption_parent: CaptionParent::Item,
2999 caption_location: Some([64, 100, 300, 112]),
3000 ..Table::default()
3001 })),
3002 });
3003 doc.push(Node::Located {
3004 location: [64, 320, 200, 332],
3005 inner: Box::new(Node::CheckboxItem {
3006 checked: false,
3007 text: "First option".into(),
3008 }),
3009 });
3010 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3011 let texts = v["texts"].as_array().unwrap();
3012 let by_text = |t: &str| texts.iter().find(|x| x["text"] == t).expect(t).clone();
3013 let bbox = |p: &Value| {
3014 let b = &p["prov"][0]["bbox"];
3015 [&b["l"], &b["t"], &b["r"], &b["b"]].map(|x| x.as_f64().unwrap())
3016 };
3017 let fig = by_text("Figure 1");
3020 assert_eq!(fig["prov"][0]["page_no"], 1);
3021 assert_eq!(bbox(&fig), [64.0, 248.0, 448.0, 232.0]);
3022 assert_eq!(fig["prov"][0]["charspan"], serde_json::json!([0, 8]));
3023 assert_eq!(fig["parent"]["$ref"], "#/pictures/0");
3024 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
3025 let tab = by_text("Table 1");
3027 assert_eq!(tab["prov"][0]["page_no"], 2);
3028 assert_eq!(bbox(&tab), [64.0, 412.0, 300.0, 400.0]);
3029 assert_eq!(v["tables"][0]["prov"][0]["bbox"]["t"], 392.0);
3030 let cb = by_text("First option");
3032 assert_eq!(cb["label"], "checkbox_unselected");
3033 assert_eq!(cb["prov"][0]["page_no"], 2);
3034 assert_eq!(bbox(&cb), [64.0, 192.0, 200.0, 180.0]);
3035 assert!(texts
3036 .iter()
3037 .all(|t| !t["prov"].as_array().unwrap().is_empty()));
3038
3039 let mut plain = DoclingDocument::new("t");
3042 plain.push(Node::PageInfo {
3043 page_no: 1,
3044 width: 512.0,
3045 height: 512.0,
3046 });
3047 plain.push(Node::Located {
3048 location: [64, 64, 448, 256],
3049 inner: Box::new(Node::Picture {
3050 caption: Some("Figure 1".into()),
3051 caption_href: None,
3052 image: None,
3053 classification: None,
3054 description: None,
3055 caption_parent: CaptionParent::Item,
3056 caption_location: None,
3057 }),
3058 });
3059 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
3060 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
3061 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
3062 }
3063
3064 #[test]
3065 fn page_markers_produce_pages_and_prov() {
3066 let mut doc = DoclingDocument::new("t");
3067 doc.push(Node::PageInfo {
3068 page_no: 1,
3069 width: 512.0,
3070 height: 1024.0,
3071 });
3072 doc.push(Node::Located {
3073 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
3075 text: "hello".into(),
3076 }),
3077 });
3078 doc.push(Node::Table(Table {
3079 rows: vec![vec!["a".into()]],
3080 location: Some([0, 0, 512, 512]),
3081 ..Table::default()
3082 }));
3083 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3084 assert_eq!(v["pages"]["1"]["page_no"], 1);
3085 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
3086 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
3087 let prov = &v["texts"][0]["prov"][0];
3090 assert_eq!(prov["page_no"], 1);
3091 assert_eq!(prov["bbox"]["l"], 128.0);
3092 assert_eq!(prov["bbox"]["t"], 896.0);
3093 assert_eq!(prov["bbox"]["r"], 256.0);
3094 assert_eq!(prov["bbox"]["b"], 768.0);
3095 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
3096 assert_eq!(prov["charspan"][1], 5);
3097 let tprov = &v["tables"][0]["prov"][0];
3099 assert_eq!(tprov["bbox"]["t"], 1024.0);
3100 assert_eq!(tprov["bbox"]["b"], 0.0);
3101 assert_eq!(tprov["charspan"][1], 0);
3102
3103 let mut plain = DoclingDocument::new("t");
3105 plain.push(Node::Located {
3106 location: [1, 2, 3, 4],
3107 inner: Box::new(Node::Paragraph { text: "x".into() }),
3108 });
3109 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
3110 assert_eq!(v["pages"], serde_json::json!({}));
3111 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
3112 }
3113
3114 #[test]
3117 fn page_furniture_becomes_furniture_layer_text() {
3118 let mut doc = DoclingDocument::new("t");
3119 doc.push(Node::PageInfo {
3120 page_no: 1,
3121 width: 512.0,
3122 height: 512.0,
3123 });
3124 doc.push(Node::PageFurniture {
3125 footer: false,
3126 location: [10, 0, 100, 20],
3127 text: "Chapter 1".into(),
3128 });
3129 doc.push(Node::Paragraph {
3130 text: "body".into(),
3131 });
3132 doc.push(Node::PageFurniture {
3133 footer: true,
3134 location: [400, 490, 500, 512],
3135 text: "1.10.2".into(),
3136 });
3137 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3138 let texts = v["texts"].as_array().unwrap();
3139 assert_eq!(texts.len(), 3);
3140 assert_eq!(texts[0]["label"], "page_header");
3141 assert_eq!(texts[0]["content_layer"], "furniture");
3142 assert_eq!(texts[0]["parent"]["$ref"], "#/body");
3143 assert_eq!(texts[0]["prov"][0]["bbox"]["l"], 10.0);
3144 assert_eq!(texts[1]["content_layer"], "body");
3145 assert_eq!(texts[1]["prov"], serde_json::json!([]));
3146 assert_eq!(texts[2]["label"], "page_footer");
3147 assert_eq!(texts[2]["text"], "1.10.2");
3148 assert_eq!(texts[2]["content_layer"], "furniture");
3149 }
3150
3151 #[test]
3152 fn picture_image_in_markdown_modes_and_json() {
3153 let doc = doc_with_image();
3154 assert!(doc.export_to_markdown().contains("<!-- image -->"));
3156 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
3158 assert!(
3159 md.contains(""),
3160 "got:\n{md}"
3161 );
3162 assert!(files.is_empty());
3163 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
3165 assert!(
3166 md.contains(""),
3167 "got:\n{md}"
3168 );
3169 assert_eq!(
3170 files,
3171 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
3172 );
3173 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3176 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
3177 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
3178 let keys: Vec<&str> = v["pictures"][0]
3179 .as_object()
3180 .unwrap()
3181 .keys()
3182 .map(String::as_str)
3183 .collect();
3184 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
3185 assert_eq!(
3186 v["pictures"][0]["image"]["uri"],
3187 "data:image/png;base64,Zm9vYmFy"
3188 );
3189 }
3190
3191 #[test]
3192 fn exports_docling_schema() {
3193 let mut doc = DoclingDocument::new("t");
3194 doc.push(Node::Heading {
3195 level: 1,
3196 text: "Title".into(),
3197 });
3198 doc.push(Node::Heading {
3199 level: 2,
3200 text: "Sec".into(),
3201 });
3202 doc.push(Node::Paragraph {
3203 text: "Body & more".into(),
3204 }); doc.push(Node::ListItem {
3206 ordered: false,
3207 number: 0,
3208 first_in_list: true,
3209 text: "one".into(),
3210 level: 0,
3211 marker: None,
3212 location: None,
3213 dclx: None,
3214 href: None,
3215 layer: None,
3216 });
3217 doc.push(Node::ListItem {
3218 ordered: false,
3219 number: 0,
3220 first_in_list: false,
3221 text: "two".into(),
3222 level: 0,
3223 marker: None,
3224 location: None,
3225 dclx: None,
3226 href: None,
3227 layer: None,
3228 });
3229 doc.push(Node::Table(Table {
3230 rows: vec![vec!["A".into(), "B".into()]],
3231 location: None,
3232 structure: None,
3233 cell_blocks: None,
3234 cells: None,
3235 caption: None,
3236 caption_parent: Default::default(),
3237 caption_location: None,
3238 }));
3239
3240 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3241 assert_eq!(v["schema_name"], "DoclingDocument");
3242 assert_eq!(v["version"], "1.10.0");
3243 assert_eq!(v["texts"][0]["label"], "title");
3244 assert_eq!(v["texts"][1]["label"], "section_header");
3245 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
3249 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
3250 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
3251 assert_eq!(v["texts"][3]["marker"], "-");
3252 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
3254 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
3255 }
3256 #[test]
3260 fn a_layered_group_stamps_its_whole_subtree() {
3261 let doc = DoclingDocument {
3262 name: "s".into(),
3263 nodes: vec![
3264 Node::Group {
3265 label: "sheet".into(),
3266 name: Some("Sheet1".into()),
3267 layer: None,
3268 children: vec![Node::Paragraph {
3269 text: "visible".into(),
3270 }],
3271 },
3272 Node::Group {
3273 label: "sheet".into(),
3274 name: Some("Sheet2".into()),
3275 layer: Some(ContentLayer::Invisible),
3276 children: vec![Node::Paragraph {
3277 text: "hidden".into(),
3278 }],
3279 },
3280 ],
3281 ..DoclingDocument::new("s")
3282 };
3283 let v = crate::json::to_json(&doc);
3284 assert_eq!(v["groups"][0]["label"], "sheet");
3285 assert_eq!(v["groups"][0]["name"], "Sheet1");
3286 assert_eq!(v["groups"][0]["content_layer"], "body");
3287 assert_eq!(v["texts"][0]["content_layer"], "body");
3288 assert_eq!(v["groups"][1]["name"], "Sheet2");
3289 assert_eq!(v["groups"][1]["content_layer"], "invisible");
3290 assert_eq!(v["texts"][1]["content_layer"], "invisible");
3291 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
3293 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
3294 }
3295
3296 #[test]
3306 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
3307 use crate::tree::{ItemTree, TreeKind};
3308 let mut t = ItemTree::default();
3309 let text = |txt: &str| TreeKind::Text {
3310 label: "text".into(),
3311 text: txt.into(),
3312 orig: None,
3313 formatting: None,
3314 hyperlink: None,
3315 level: None,
3316 list: None,
3317 };
3318 let a = t.add(None, None, text("a"));
3319 let blank = t.add(None, None, text(""));
3320 let b = t.add(None, None, text("b"));
3321 t.delete(blank);
3322 let group = t.add(
3323 None,
3324 Some(ContentLayer::Notes),
3325 TreeKind::Group {
3326 label: "comment_section".into(),
3327 name: "comment-0".into(),
3328 },
3329 );
3330 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
3331 t.items[a].comments.push(group);
3332 t.add(
3333 None,
3334 None,
3335 TreeKind::Picture {
3336 captions: Vec::new(),
3337 image: None,
3338 extra_classes: Vec::new(),
3339 classification: Some("bar_chart".into()),
3340 description: None,
3341 confidence: None,
3342 chart: Some(Table {
3343 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
3344 ..Table::default()
3345 }),
3346 dpi: None,
3347 },
3348 );
3349 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
3350 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
3351 let mut doc = DoclingDocument::new("t");
3352 doc.tree = Some(t);
3353 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3354 let texts = v["texts"].as_array().unwrap();
3355 assert_eq!(texts.len(), 3);
3356 assert_eq!(texts[1]["text"], "b");
3357 assert_eq!(texts[1]["self_ref"], "#/texts/1");
3358 assert_eq!(
3359 v["body"]["children"],
3360 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
3361 );
3362 let keys: Vec<&str> = texts[0]
3363 .as_object()
3364 .unwrap()
3365 .keys()
3366 .map(String::as_str)
3367 .collect();
3368 assert_eq!(
3369 keys,
3370 vec![
3371 "self_ref",
3372 "parent",
3373 "children",
3374 "content_layer",
3375 "label",
3376 "prov",
3377 "comments",
3378 "orig",
3379 "text"
3380 ]
3381 );
3382 assert_eq!(
3383 texts[0]["comments"],
3384 serde_json::json!([{"$ref": "#/groups/0"}])
3385 );
3386 assert!(texts[1].get("comments").is_none());
3387 let meta = &v["pictures"][0]["meta"];
3388 assert_eq!(
3389 meta["classification"]["predictions"][0]["class_name"],
3390 "bar_chart"
3391 );
3392 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
3393 }
3394
3395 #[test]
3401 fn tree_items_carry_exact_provenance_and_dpi() {
3402 use crate::tree::{ItemTree, TreeKind, TreeProv};
3403 let text = |label: &str, t: &str| TreeKind::Text {
3404 label: label.into(),
3405 text: t.into(),
3406 orig: None,
3407 formatting: None,
3408 hyperlink: None,
3409 level: None,
3410 list: None,
3411 };
3412 let mut t = ItemTree::default();
3413 let slide = t.add(
3414 None,
3415 None,
3416 TreeKind::Group {
3417 label: "chapter".into(),
3418 name: "slide-0".into(),
3419 },
3420 );
3421 t.add_with_prov(
3422 Some(slide),
3423 None,
3424 text("paragraph", "héllo"),
3425 TreeProv {
3426 page_no: 1,
3427 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
3428 bottom_left: true,
3429 charspan: [0, 5],
3430 },
3431 );
3432 t.add_with_prov(
3433 Some(slide),
3434 None,
3435 TreeKind::Picture {
3436 captions: Vec::new(),
3437 image: Some(crate::PictureImage {
3438 dpi: crate::PictureImage::DEFAULT_DPI,
3439 mimetype: "image/png".into(),
3440 width: 2,
3441 height: 2,
3442 data: vec![0],
3443 display_size: None,
3444 }),
3445 extra_classes: Vec::new(),
3446 classification: None,
3447 description: None,
3448 confidence: None,
3449 chart: None,
3450 dpi: Some(300),
3451 },
3452 TreeProv {
3453 page_no: 1,
3454 bbox: [0.0; 4],
3455 bottom_left: false,
3456 charspan: [0, 0],
3457 },
3458 );
3459 t.add(
3460 Some(slide),
3461 Some(ContentLayer::Notes),
3462 text("text", "no geometry"),
3463 );
3464 let mut doc = DoclingDocument::new("t");
3465 doc.push(Node::PageInfo {
3466 page_no: 1,
3467 width: 9144000.0,
3468 height: 6858000.0,
3469 });
3470 doc.tree = Some(t);
3471 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3472 assert_eq!(
3473 v["texts"][0]["prov"],
3474 serde_json::json!([{
3475 "page_no": 1,
3476 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
3477 "charspan": [0, 5],
3478 }])
3479 );
3480 assert_eq!(v["texts"][0]["label"], "paragraph");
3481 assert_eq!(
3482 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
3483 "TOPLEFT"
3484 );
3485 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
3486 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
3487 assert_eq!(v["texts"][1]["content_layer"], "notes");
3488 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
3489 assert_eq!(v["pages"]["1"]["page_no"], 1);
3490 }
3491
3492 #[test]
3497 fn provenance_boxes_are_clamped_to_their_page() {
3498 let mut doc = DoclingDocument::new("t");
3499 doc.push(Node::PageInfo {
3500 page_no: 1,
3501 width: 10.0,
3502 height: 8.0,
3503 });
3504 doc.push(Node::Prov {
3505 page_no: 1,
3506 bbox: [-1.0, 2.0, 12.0, 9.5],
3507 charspan: [0, 1],
3508 seq: None,
3509 inner: Box::new(Node::Paragraph { text: "x".into() }),
3510 });
3511 let mut table = Table {
3512 rows: vec![vec!["a".into()]],
3513 ..Table::default()
3514 };
3515 table.cells = Some(vec![crate::TableCell {
3516 text: "a".into(),
3517 bbox: Some([1.0, 1.0, 11.0, 9.0]),
3518 start_row: 0,
3519 start_col: 0,
3520 row_span: 1,
3521 col_span: 1,
3522 column_header: false,
3523 row_header: false,
3524 row_section: false,
3525 }]);
3526 doc.push(Node::Prov {
3527 page_no: 1,
3528 bbox: [0.0, 0.0, 10.0, 8.0],
3529 charspan: [0, 0],
3530 seq: None,
3531 inner: Box::new(Node::Table(table)),
3532 });
3533 doc.push(Node::Prov {
3534 page_no: 7,
3535 bbox: [-5.0, 0.0, 50.0, 50.0],
3536 charspan: [0, 1],
3537 seq: None,
3538 inner: Box::new(Node::Paragraph { text: "y".into() }),
3539 });
3540 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3541 assert_eq!(
3542 v["texts"][0]["prov"][0]["bbox"],
3543 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
3544 );
3545 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
3546 assert_eq!(
3547 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
3548 (Some(1.0), Some(10.0), Some(8.0))
3549 );
3550 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
3551 assert_eq!(
3552 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
3553 "page 7 is not described"
3554 );
3555 }
3556
3557 #[test]
3564 fn misplaced_list_items_are_wrapped_like_docling_core() {
3565 use crate::tree::{ItemTree, ListMeta, TreeKind};
3566 let mut t = ItemTree::default();
3567 let li = |txt: &str| TreeKind::Text {
3568 label: "list_item".into(),
3569 text: txt.into(),
3570 orig: None,
3571 formatting: None,
3572 hyperlink: None,
3573 level: None,
3574 list: Some(ListMeta {
3575 enumerated: false,
3576 marker: "-".into(),
3577 }),
3578 };
3579 let group = |label: &str, name: &str| TreeKind::Group {
3580 label: label.into(),
3581 name: name.into(),
3582 };
3583 t.add(None, None, li("A"));
3584 t.add(None, None, li("B"));
3585 t.add(
3586 None,
3587 None,
3588 TreeKind::Text {
3589 label: "text".into(),
3590 text: "plain".into(),
3591 orig: None,
3592 formatting: None,
3593 hyperlink: None,
3594 level: None,
3595 list: None,
3596 },
3597 );
3598 let cell = t.add(None, None, group("unspecified", "cell"));
3599 t.add(Some(cell), None, li("C"));
3600 t.add(Some(cell), None, li("D"));
3601 let list = t.add(None, None, group("list", "list"));
3602 t.add(Some(list), None, li("E"));
3603 let doc = DoclingDocument {
3604 tree: Some(t),
3605 ..DoclingDocument::new("t")
3606 };
3607 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3608 let refs = |x: &Value| -> Vec<String> {
3609 x.as_array()
3610 .unwrap()
3611 .iter()
3612 .map(|r| r["$ref"].as_str().unwrap().to_string())
3613 .collect()
3614 };
3615 assert_eq!(
3616 refs(&v["body"]["children"]),
3617 ["#/groups/4", "#/texts/0", "#/groups/0", "#/groups/1"]
3618 );
3619 assert_eq!(
3620 refs(&v["groups"][0]["children"]),
3621 ["#/groups/3", "#/groups/2"]
3622 );
3623 for (g, name, kids) in [
3624 (2, "group", vec!["#/texts/2"]),
3625 (3, "group", vec!["#/texts/3"]),
3626 (4, "group", vec!["#/texts/4", "#/texts/5"]),
3627 (1, "list", vec!["#/texts/1"]),
3628 ] {
3629 assert_eq!(v["groups"][g]["label"], "list");
3630 assert_eq!(v["groups"][g]["name"], name);
3631 assert_eq!(refs(&v["groups"][g]["children"]), kids);
3632 }
3633 let texts: Vec<&str> = v["texts"]
3634 .as_array()
3635 .unwrap()
3636 .iter()
3637 .map(|t| t["text"].as_str().unwrap())
3638 .collect();
3639 assert_eq!(texts, ["plain", "E", "D", "C", "A", "B"]);
3640 assert_eq!(v["texts"][2]["parent"]["$ref"], "#/groups/2");
3641 }
3642
3643 #[test]
3646 fn a_deleted_child_is_not_written_as_an_empty_ref() {
3647 use crate::tree::{ItemTree, TreeKind};
3648 let mut t = ItemTree::default();
3649 let text = |txt: &str| TreeKind::Text {
3650 label: "text".into(),
3651 text: txt.into(),
3652 orig: None,
3653 formatting: None,
3654 hyperlink: None,
3655 level: None,
3656 list: None,
3657 };
3658 let g = t.add(
3659 None,
3660 None,
3661 TreeKind::Group {
3662 label: "unspecified".into(),
3663 name: "g".into(),
3664 },
3665 );
3666 let gone = t.add(Some(g), None, text(""));
3667 t.add(Some(g), None, text("kept"));
3668 t.items[gone].deleted = true; let doc = DoclingDocument {
3670 tree: Some(t),
3671 ..DoclingDocument::new("t")
3672 };
3673 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3674 assert_eq!(
3675 v["groups"][0]["children"],
3676 serde_json::json!([{"$ref": "#/texts/0"}])
3677 );
3678 }
3679
3680 #[test]
3681 fn a_backend_item_tree_is_written_verbatim() {
3682 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
3683 let mut t = ItemTree::default();
3684 let text = |label: &str, txt: &str| TreeKind::Text {
3685 label: label.into(),
3686 text: txt.into(),
3687 orig: None,
3688 formatting: None,
3689 hyperlink: None,
3690 level: None,
3691 list: None,
3692 };
3693 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
3694 let h = t.add(None, None, text("title", "Heading"));
3695 let group = t.add(
3696 Some(h),
3697 None,
3698 TreeKind::Group {
3699 label: "inline".into(),
3700 name: "group".into(),
3701 },
3702 );
3703 t.add(
3704 Some(group),
3705 None,
3706 TreeKind::Text {
3707 label: "text".into(),
3708 text: "bold".into(),
3709 orig: None,
3710 formatting: Some(Formatting {
3711 bold: true,
3712 ..Formatting::default()
3713 }),
3714 hyperlink: Some("https://example.com/".into()),
3715 level: None,
3716 list: None,
3717 },
3718 );
3719 t.add(
3720 Some(group),
3721 None,
3722 TreeKind::Code {
3723 text: "x = 1".into(),
3724 orig: None,
3725 language: Some("python".into()),
3726 formatting: None,
3727 hyperlink: None,
3728 captions: Vec::new(),
3729 },
3730 );
3731 let sub = t.add(
3732 Some(h),
3733 None,
3734 TreeKind::Text {
3735 label: "section_header".into(),
3736 text: "Sub".into(),
3737 orig: Some("Sub\u{2019}".into()),
3738 formatting: None,
3739 hyperlink: None,
3740 level: Some(1),
3741 list: None,
3742 },
3743 );
3744 let item = t.add(
3745 Some(sub),
3746 None,
3747 TreeKind::Text {
3748 label: "list_item".into(),
3749 text: "item".into(),
3750 orig: None,
3751 formatting: None,
3752 hyperlink: None,
3753 level: None,
3754 list: Some(ListMeta {
3755 enumerated: true,
3756 marker: "3.".into(),
3757 }),
3758 },
3759 );
3760 let _region = t.add(
3761 Some(sub),
3762 None,
3763 TreeKind::FieldRegion {
3764 items: vec![crate::FieldItem {
3765 marker: None,
3766 key: Some("Name".into()),
3767 value: Some("Duck".into()),
3768 value_kind: Some("read_only".into()),
3769 }],
3770 },
3771 );
3772 let table = t.add(
3773 Some(sub),
3774 None,
3775 TreeKind::Table {
3776 table: Table {
3777 rows: vec![vec!["a \n<".into(), "b".into()]],
3778 cells: Some(vec![
3779 crate::TableCell {
3780 text: "a \n<".into(),
3781 bbox: None,
3782 start_row: 0,
3783 start_col: 0,
3784 row_span: 3,
3785 col_span: 1,
3786 column_header: false,
3787 row_header: true,
3788 row_section: false,
3789 },
3790 crate::TableCell {
3791 text: "b".into(),
3792 bbox: None,
3793 start_row: 0,
3794 start_col: 1,
3795 row_span: 1,
3796 col_span: 1,
3797 column_header: false,
3798 row_header: false,
3799 row_section: false,
3800 },
3801 ]),
3802 ..Table::default()
3803 },
3804 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
3806 },
3807 );
3808 let cell_group = t.add(
3809 Some(table),
3810 None,
3811 TreeKind::Group {
3812 label: "unspecified".into(),
3813 name: "rich_cell_group_1_0_0".into(),
3814 },
3815 );
3816 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
3817 *rich_cells = vec![(0, 1, cell_group)];
3818 }
3819 let after = t.add(Some(sub), None, text("text", "after the region"));
3820 let _ = (title, after);
3821 let list = t.add(
3825 Some(sub),
3826 None,
3827 TreeKind::Group {
3828 label: "list".into(),
3829 name: "list".into(),
3830 },
3831 );
3832 t.reparent(item, Some(list));
3833
3834 let doc = DoclingDocument {
3835 tree: Some(t),
3836 ..DoclingDocument::new("t")
3837 };
3838 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3839 let texts: Vec<&str> = v["texts"]
3841 .as_array()
3842 .unwrap()
3843 .iter()
3844 .map(|t| t["text"].as_str().unwrap())
3845 .collect();
3846 assert_eq!(
3847 texts,
3848 [
3849 "Page",
3850 "Heading",
3851 "bold",
3852 "x = 1",
3853 "Sub",
3854 "item",
3855 "Name",
3856 "Duck",
3857 "after the region"
3858 ]
3859 );
3860 assert_eq!(
3861 v["body"]["children"],
3862 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
3863 );
3864 assert_eq!(v["texts"][0]["content_layer"], "furniture");
3865 assert_eq!(
3866 v["texts"][1]["children"],
3867 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
3868 );
3869 let bold = &v["texts"][2];
3870 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
3871 let keys: Vec<&str> = bold
3872 .as_object()
3873 .unwrap()
3874 .keys()
3875 .map(String::as_str)
3876 .collect();
3877 assert_eq!(
3878 keys,
3879 [
3880 "self_ref",
3881 "parent",
3882 "children",
3883 "content_layer",
3884 "label",
3885 "prov",
3886 "orig",
3887 "text",
3888 "formatting",
3889 "hyperlink"
3890 ]
3891 );
3892 assert_eq!(
3893 bold["formatting"],
3894 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
3895 );
3896 let code = &v["texts"][3];
3897 assert_eq!(code["label"], "code");
3898 assert_eq!(code["code_language"], "Python");
3899 let sub = &v["texts"][4];
3900 assert_eq!(sub["orig"], "Sub\u{2019}");
3901 assert_eq!(sub["level"], 1);
3902 let item = &v["texts"][5];
3903 let keys: Vec<&str> = item
3904 .as_object()
3905 .unwrap()
3906 .keys()
3907 .map(String::as_str)
3908 .collect();
3909 assert_eq!(
3910 keys,
3911 [
3912 "self_ref",
3913 "parent",
3914 "children",
3915 "content_layer",
3916 "label",
3917 "prov",
3918 "orig",
3919 "text",
3920 "enumerated",
3921 "marker"
3922 ]
3923 );
3924 assert_eq!(item["marker"], "3.");
3925 assert_eq!(v["texts"][7]["kind"], "read_only");
3926 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
3927 let table = &v["tables"][0];
3928 assert_eq!(
3929 table["children"],
3930 serde_json::json!([{"$ref": "#/groups/1"}])
3931 );
3932 let cells = table["data"]["table_cells"].as_array().unwrap();
3933 assert_eq!(
3934 cells[0]["text"], "a \n<",
3935 "raw cell text is written verbatim"
3936 );
3937 assert_eq!(
3938 cells[0]["end_row_offset_idx"], 3,
3939 "declared spans are not clamped"
3940 );
3941 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
3942 assert!(cells[0].get("ref").is_none());
3943 assert!(
3944 table["data"]["grid"][0][1].get("ref").is_none(),
3945 "the grid shows plain cells"
3946 );
3947 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
3948 }
3949
3950 #[test]
3954 fn a_comment_section_can_be_referenced_by_its_note_text() {
3955 let doc = DoclingDocument {
3956 name: "c".into(),
3957 nodes: vec![
3958 Node::Commented {
3959 comments: vec![0],
3960 inner: Box::new(Node::Paragraph {
3961 text: "annotated".into(),
3962 }),
3963 },
3964 Node::CommentSection {
3965 name: "comment-Sheet1-A1".into(),
3966 text: "[author: A]: note".into(),
3967 refs_note_text: true,
3968 grouped: true,
3969 },
3970 ],
3971 ..DoclingDocument::new("c")
3972 };
3973 let v = crate::json::to_json(&doc);
3974 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
3975 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
3976 }
3977
3978 #[test]
3982 fn comment_sections_link_back_to_their_items() {
3983 let doc = DoclingDocument {
3984 name: "c".into(),
3985 nodes: vec![
3986 Node::Commented {
3987 comments: vec![0],
3988 inner: Box::new(Node::Paragraph {
3989 text: "annotated".into(),
3990 }),
3991 },
3992 Node::Paragraph {
3993 text: "plain".into(),
3994 },
3995 Node::CommentSection {
3996 name: "comment-7".into(),
3997 text: "[time: t]: note".into(),
3998 refs_note_text: false,
3999 grouped: true,
4000 },
4001 ],
4002 ..DoclingDocument::new("c")
4003 };
4004 let v = crate::json::to_json(&doc);
4005 assert_eq!(v["groups"][0]["label"], "comment_section");
4007 assert_eq!(v["groups"][0]["name"], "comment-7");
4008 assert_eq!(v["groups"][0]["content_layer"], "notes");
4009 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
4010 assert_eq!(v["texts"][2]["content_layer"], "notes");
4011 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
4013 assert!(v["texts"][1].get("comments").is_none());
4014 let keys: Vec<&str> = v["texts"][0]
4016 .as_object()
4017 .unwrap()
4018 .keys()
4019 .map(String::as_str)
4020 .collect();
4021 assert_eq!(
4022 &keys[keys.len() - 4..],
4023 &["prov", "comments", "orig", "text"]
4024 );
4025 }
4026
4027 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
4028 Node::Picture {
4029 caption: Some(caption.into()),
4030 caption_href: None,
4031 image: None,
4032 classification: None,
4033 description: None,
4034 caption_parent,
4035 caption_location: None,
4036 }
4037 }
4038
4039 fn group(children: Vec<Node>) -> Node {
4040 Node::Group {
4041 label: "section".into(),
4042 name: None,
4043 layer: None,
4044 children,
4045 }
4046 }
4047
4048 fn refs(v: &Value) -> Vec<&str> {
4049 v.as_array()
4050 .unwrap()
4051 .iter()
4052 .map(|r| r["$ref"].as_str().unwrap())
4053 .collect()
4054 }
4055
4056 #[test]
4061 fn a_body_caption_follows_the_enclosing_top_level_item() {
4062 let mut doc = DoclingDocument::new("t");
4063 doc.push(picture("top", CaptionParent::Body));
4064 doc.push(group(vec![
4065 Node::Paragraph { text: "p".into() },
4066 picture("nested", CaptionParent::Body),
4067 ]));
4068 doc.push(Node::Paragraph {
4069 text: "after".into(),
4070 });
4071 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4072 assert_eq!(
4073 refs(&v["body"]["children"]),
4074 [
4075 "#/texts/0",
4076 "#/pictures/0",
4077 "#/groups/0",
4078 "#/texts/2",
4079 "#/texts/3"
4080 ]
4081 );
4082 assert_eq!(
4083 refs(&v["groups"][0]["children"]),
4084 ["#/texts/1", "#/pictures/1"]
4085 );
4086 for (cap, pic) in [(0, 0), (2, 1)] {
4087 assert_eq!(v["texts"][cap]["label"], "caption");
4088 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
4089 assert_eq!(
4090 refs(&v["pictures"][pic]["captions"]),
4091 [format!("#/texts/{cap}")]
4092 );
4093 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
4094 }
4095 }
4096
4097 #[test]
4100 fn an_item_caption_is_the_items_first_child() {
4101 let mut doc = DoclingDocument::new("t");
4102 doc.push(picture("fig", CaptionParent::Item));
4103 doc.push(Node::Table(Table {
4104 rows: vec![vec!["a".into()]],
4105 caption: Some("tab".into()),
4106 caption_parent: CaptionParent::Item,
4107 ..Table::default()
4108 }));
4109 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4110 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
4111 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
4112 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
4113 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
4114 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
4115 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
4116 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
4117 }
4118
4119 #[test]
4125 fn picture_children_hang_off_the_picture_after_its_caption() {
4126 let mut doc = DoclingDocument::new("t");
4127 doc.push(picture("fig", CaptionParent::Item));
4128 doc.push(Node::PictureChildren(vec![
4129 Node::Heading {
4130 level: 2,
4131 text: "in-figure title".into(),
4132 },
4133 Node::Paragraph {
4134 text: "axis label".into(),
4135 },
4136 Node::ListItem {
4137 ordered: false,
4138 number: 0,
4139 first_in_list: true,
4140 text: "callout".into(),
4141 level: 0,
4142 marker: None,
4143 location: None,
4144 dclx: None,
4145 href: None,
4146 layer: None,
4147 },
4148 ]));
4149 doc.push(Node::Paragraph {
4150 text: "after".into(),
4151 });
4152 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4153 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/texts/4"]);
4154 assert_eq!(
4155 refs(&v["pictures"][0]["children"]),
4156 ["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"]
4157 );
4158 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
4159 assert_eq!(v["texts"][1]["label"], "section_header");
4160 assert_eq!(v["texts"][2]["label"], "text");
4161 for t in 1..=2 {
4162 assert_eq!(v["texts"][t]["parent"]["$ref"], "#/pictures/0");
4163 assert_eq!(v["texts"][t]["content_layer"], "body");
4164 }
4165 assert_eq!(v["groups"][0]["parent"]["$ref"], "#/pictures/0");
4166 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
4167 let md = doc.export_to_markdown();
4168 assert!(
4169 !md.contains("axis label") && !md.contains("callout"),
4170 "{md}"
4171 );
4172 assert!(md.contains("after"), "{md}");
4173 }
4174
4175 #[test]
4179 fn a_container_caption_is_the_items_sibling() {
4180 let mut doc = DoclingDocument::new("t");
4181 doc.push(group(vec![
4182 picture("chart", CaptionParent::Container),
4183 Node::Table(Table {
4184 rows: vec![vec!["a".into()]],
4185 caption: Some("figcaption".into()),
4186 caption_parent: CaptionParent::ContainerAfter,
4187 ..Table::default()
4188 }),
4189 ]));
4190 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4191 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
4192 assert_eq!(
4193 refs(&v["groups"][0]["children"]),
4194 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
4195 );
4196 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
4197 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
4198 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
4199 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
4200 }
4201
4202 #[test]
4207 fn a_labeled_text_keeps_its_label_and_hyperlink_out_of_the_text() {
4208 let mut doc = DoclingDocument::new("t");
4209 doc.push(Node::LabeledText {
4210 label: "footnote".into(),
4211 text: "1 https://example.com/a\\_b".into(),
4212 href: Some("https://example.com/a_b".into()),
4213 });
4214 doc.push(Node::LabeledText {
4215 label: "footnote".into(),
4216 text: "© 2022 the authors".into(),
4217 href: None,
4218 });
4219 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4220 let t = &v["texts"][0];
4221 assert_eq!(t["label"], "footnote");
4222 assert_eq!(t["text"], "1 https://example.com/a_b");
4223 assert_eq!(t["orig"], "1 https://example.com/a_b");
4224 assert_eq!(t["hyperlink"], "https://example.com/a_b");
4225 assert_eq!(v["texts"][1]["label"], "footnote");
4226 assert!(v["texts"][1].get("hyperlink").is_none());
4227 assert_eq!(
4228 doc.export_to_markdown(),
4229 "[1 https://example.com/a\\_b](https://example.com/a_b)\n\n© 2022 the authors\n"
4230 );
4231 assert_eq!(
4232 doc.export_to_text(),
4233 "1 https://example.com/a_b\n\n© 2022 the authors"
4234 );
4235 }
4236}