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) => {
151 clamp_item(v, &sizes);
152 }
153 }
154 }
155 for item in b.pictures.iter_mut().chain(b.key_value_items.iter_mut()) {
156 clamp_item(item, &sizes);
157 }
158 for t in &mut b.tables {
159 if let Some(size) = clamp_item(&mut t.item, &sizes) {
160 t.data.clamp(size);
161 }
162 }
163}
164
165fn clamp_item(item: &mut Value, sizes: &PageSizes) -> Option<(f64, f64)> {
168 let size = |page_no: &Value| -> Option<(f64, f64)> {
169 let n = page_no.as_u64()? as usize;
170 sizes.get(&n).copied()
171 };
172 let clamp_bbox = |bbox: &mut Value, (w, h): (f64, f64)| {
173 for (key, hi) in [("l", w), ("r", w), ("t", h), ("b", h)] {
174 if let Some(v) = bbox.get(key).and_then(Value::as_f64) {
175 bbox[key] = json!(round2(v.clamp(0.0, hi.max(0.0))));
176 }
177 }
178 };
179 let mut table_page: Option<Option<(f64, f64)>> = None;
180 if let Some(provs) = item.get_mut("prov").and_then(Value::as_array_mut) {
181 let mut page_nos: Vec<u64> = Vec::new();
182 for prov in provs.iter_mut() {
183 if let Some(n) = prov.get("page_no").and_then(Value::as_u64) {
184 page_nos.push(n);
185 }
186 let Some(sz) = prov.get("page_no").and_then(size) else {
187 continue;
188 };
189 if let Some(bbox) = prov.get_mut("bbox") {
190 clamp_bbox(bbox, sz);
191 }
192 }
193 page_nos.sort_unstable();
194 page_nos.dedup();
195 if let [only] = page_nos[..] {
196 table_page = Some(size(&json!(only)));
197 }
198 }
199 table_page.flatten()
200}
201
202fn formatting_json(f: &crate::tree::Formatting) -> Value {
204 json!({
205 "bold": f.bold,
206 "italic": f.italic,
207 "underline": f.underline,
208 "strikethrough": f.strikethrough,
209 "script": match f.script {
210 crate::Script::Baseline => "baseline",
211 crate::Script::Sub => "sub",
212 crate::Script::Super => "super",
213 },
214 })
215}
216
217fn track_json(t: &crate::tree::TreeTrack) -> Value {
220 let mut m = serde_json::Map::new();
221 m.insert("kind".into(), json!("track"));
222 m.insert("start_time".into(), json!(t.start_time));
223 m.insert("end_time".into(), json!(t.end_time));
224 if let Some(id) = &t.identifier {
225 m.insert("identifier".into(), json!(id));
226 }
227 if let Some(v) = &t.voice {
228 m.insert("voice".into(), json!(v));
229 }
230 Value::Object(m)
231}
232
233fn image_ref_json(img: &crate::PictureImage) -> Value {
237 let (mimetype, uri) = crate::pixel_digest::docling_data_uri(img);
238 json!({
239 "mimetype": mimetype,
240 "dpi": img.dpi,
241 "size": { "width": img.width as f64, "height": img.height as f64 },
242 "uri": uri,
243 })
244}
245
246pub fn to_json(doc: &DoclingDocument) -> Value {
248 build_parts(doc, false).into_value(None)
249}
250
251pub(crate) const TYPED_TABLE: &str = "_typed_table";
254
255pub(crate) fn to_json_typed_tables(
263 doc: &DoclingDocument,
264 note_keys: bool,
265) -> (Value, Vec<TableData>) {
266 let mut tables = Vec::new();
267 let value = build_parts(doc, note_keys).into_value(Some(&mut tables));
268 (value, tables)
269}
270
271pub(crate) fn to_json_with_notes(doc: &DoclingDocument) -> (Value, Vec<TableData>) {
284 to_json_typed_tables(doc, true)
285}
286
287fn build_parts(doc: &DoclingDocument, note_keys: bool) -> Parts {
288 let mut b = Builder {
289 note_keys,
290 ..Builder::default()
291 };
292 let body = match &doc.tree {
296 Some(tree) => {
297 for n in &doc.nodes {
300 if let Node::PageInfo {
301 page_no,
302 width,
303 height,
304 } = n
305 {
306 if *page_no > 0 {
307 b.pages.push((*page_no, *width as f64, *height as f64));
308 }
309 }
310 }
311 let mut tree = tree.clone();
315 tree.wrap_misplaced_list_items();
316 b.write_tree(&tree).into_iter().map(Child::json).collect()
317 }
318 None => b.walk_into(&doc.nodes, "#/body"),
319 };
320 b.link_comments();
321
322 clamp_boxes_to_pages(&mut b);
331
332 let pages = b
333 .pages
334 .iter()
335 .map(|(n, w, h)| {
336 let r2 = |v: f64| (v * 100.0).round() / 100.0;
337 let mut page = serde_json::Map::new();
340 page.insert("size".into(), json!({ "width": r2(*w), "height": r2(*h) }));
341 if let Some(img) = doc.page_images.get(n) {
342 page.insert("image".into(), image_ref_json(img));
343 }
344 page.insert("page_no".into(), json!(n));
345 (n.to_string(), Value::Object(page))
346 })
347 .collect::<serde_json::Map<String, Value>>();
348 let fields = (!b.field_regions.is_empty()).then_some((b.field_regions, b.field_items));
351 Parts {
352 name: doc.name.clone(),
353 metadata: (doc.metadata_output.in_json() || note_keys)
356 .then(|| doc.metadata.to_json())
357 .filter(|m| m.as_object().is_some_and(|o| !o.is_empty())),
358 origin: json!({
359 "mimetype": "text/plain",
360 "binary_hash": fnv1a(&doc.name),
361 "filename": doc.name,
362 }),
363 furniture: json!({
364 "self_ref": "#/furniture",
365 "children": [],
366 "content_layer": "furniture",
367 "name": "_root_",
368 "label": "unspecified",
369 }),
370 body,
371 groups: b.groups,
372 texts: b.texts,
373 pictures: b.pictures,
374 tables: b.tables,
375 key_value_items: b.key_value_items,
376 fields,
377 pages,
378 }
379}
380
381struct Parts {
386 name: String,
387 metadata: Option<Value>,
390 origin: Value,
391 furniture: Value,
392 body: Vec<Child>,
394 groups: Vec<Value>,
395 texts: Vec<Item>,
396 pictures: Vec<Value>,
397 tables: Vec<TableItem>,
398 key_value_items: Vec<Value>,
399 fields: Option<(Vec<Value>, Vec<Value>)>,
402 pages: serde_json::Map<String, Value>,
403}
404
405impl Parts {
406 fn into_value(self, mut typed: Option<&mut Vec<TableData>>) -> Value {
409 let mut out = serde_json::Map::with_capacity(15);
411 out.insert("schema_name".into(), "DoclingDocument".into());
412 out.insert("version".into(), SCHEMA_VERSION.into());
413 out.insert("name".into(), Value::String(self.name));
414 out.insert("origin".into(), self.origin);
415 if let Some(m) = self.metadata {
416 out.insert("metadata".into(), m);
417 }
418 out.insert("furniture".into(), self.furniture);
419 out.insert(
420 "body".into(),
421 object([
422 ("self_ref", "#/body".into()),
423 (
424 "children",
425 Value::Array(self.body.into_iter().map(Child::into_value).collect()),
426 ),
427 ("content_layer", "body".into()),
428 ("name", "_root_".into()),
429 ("label", "unspecified".into()),
430 ]),
431 );
432 out.insert("groups".into(), Value::Array(self.groups));
433 out.insert(
434 "texts".into(),
435 Value::Array(
436 self.texts
437 .into_iter()
438 .enumerate()
439 .map(|(i, item)| item.into_value(i))
440 .collect(),
441 ),
442 );
443 out.insert("pictures".into(), Value::Array(self.pictures));
444 out.insert(
445 "tables".into(),
446 Value::Array(
447 self.tables
448 .into_iter()
449 .map(|t| match typed.as_deref_mut() {
450 Some(store) => {
451 let TableItem { mut item, data } = t;
452 if let Some(slot) = item.get_mut("data") {
453 *slot = object([(TYPED_TABLE, store.len().into())]);
454 }
455 store.push(data);
456 item
457 }
458 None => t.into_value(),
459 })
460 .collect(),
461 ),
462 );
463 out.insert("key_value_items".into(), Value::Array(self.key_value_items));
464 out.insert("form_items".into(), Value::Array(Vec::new()));
465 if let Some((regions, items)) = self.fields {
466 out.insert("field_regions".into(), Value::Array(regions));
467 out.insert("field_items".into(), Value::Array(items));
468 }
469 out.insert("pages".into(), Value::Object(self.pages));
470 Value::Object(out)
471 }
472}
473
474struct PartsWith<'a>(&'a Parts, &'a [(&'a str, &'a Value)]);
477
478impl serde::Serialize for PartsWith<'_> {
479 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
480 use serde::ser::SerializeMap;
481 let PartsWith(parts, extras) = *self;
482 let mut m = s.serialize_map(Some(
483 13 + 2 * usize::from(parts.fields.is_some())
484 + usize::from(parts.metadata.is_some())
485 + extras.len(),
486 ))?;
487 m.serialize_entry("schema_name", "DoclingDocument")?;
488 m.serialize_entry("version", SCHEMA_VERSION)?;
489 m.serialize_entry("name", &parts.name)?;
490 m.serialize_entry("origin", &parts.origin)?;
491 if let Some(meta) = &parts.metadata {
492 m.serialize_entry("metadata", meta)?;
493 }
494 m.serialize_entry("furniture", &parts.furniture)?;
495 m.serialize_entry("body", &Body(&parts.body))?;
496 m.serialize_entry("groups", &parts.groups)?;
497 m.serialize_entry("texts", &Texts(&parts.texts))?;
498 m.serialize_entry("pictures", &parts.pictures)?;
499 m.serialize_entry("tables", &parts.tables)?;
500 m.serialize_entry("key_value_items", &parts.key_value_items)?;
501 m.serialize_entry("form_items", &[(); 0])?;
502 if let Some((regions, items)) = &parts.fields {
503 m.serialize_entry("field_regions", regions)?;
504 m.serialize_entry("field_items", items)?;
505 }
506 m.serialize_entry("pages", &parts.pages)?;
507 for (k, v) in extras {
508 m.serialize_entry(k, v)?;
509 }
510 m.end()
511 }
512}
513
514struct Body<'a>(&'a [Child]);
516
517impl serde::Serialize for Body<'_> {
518 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
519 use serde::ser::SerializeMap;
520 let mut m = s.serialize_map(Some(5))?;
521 m.serialize_entry("self_ref", "#/body")?;
522 m.serialize_entry("children", self.0)?;
523 m.serialize_entry("content_layer", "body")?;
524 m.serialize_entry("name", "_root_")?;
525 m.serialize_entry("label", "unspecified")?;
526 m.end()
527 }
528}
529
530pub fn write_json<W: std::io::Write>(
537 doc: &DoclingDocument,
538 w: W,
539 pretty: bool,
540) -> serde_json::Result<()> {
541 write_json_with(doc, w, pretty, &[])
542}
543
544pub fn write_json_with<W: std::io::Write>(
548 doc: &DoclingDocument,
549 w: W,
550 pretty: bool,
551 extras: &[(&str, &Value)],
552) -> serde_json::Result<()> {
553 use serde::Serialize;
554 let parts = build_parts(doc, false);
555 let parts = PartsWith(&parts, extras);
556 if pretty {
557 parts.serialize(&mut serde_json::Serializer::pretty(w))
558 } else {
559 parts.serialize(&mut serde_json::Serializer::new(w))
560 }
561}
562
563fn classification_meta(classes: &[crate::PictureClass]) -> Value {
570 json!({
571 "classification": {
572 "predictions": classes.iter().map(|c| json!({
573 "confidence": c.confidence as f64,
574 "created_by": "DocumentPictureClassifier",
575 "class_name": c.class_name,
576 })).collect::<Vec<_>>(),
577 },
578 "annotations": [{
579 "kind": "classification",
580 "provenance": "DocumentPictureClassifier",
581 "predicted_classes": classes.iter().map(|c| json!({
582 "class_name": c.class_name,
583 "confidence": c.confidence as f64,
584 })).collect::<Vec<_>>(),
585 }],
586 })
587}
588
589fn description_meta(desc: &crate::PictureDescription) -> Value {
597 json!({
598 "description": {
599 "created_by": desc.provenance,
600 "text": desc.text,
601 },
602 "annotations": [{
603 "kind": "description",
604 "text": desc.text,
605 "provenance": desc.provenance,
606 }],
607 })
608}
609
610fn picture_meta(
616 classes: Option<&[crate::PictureClass]>,
617 desc: Option<&crate::PictureDescription>,
618) -> Option<Value> {
619 let mut meta = Map::new();
620 let mut annotations = Vec::new();
621 if let Some(desc) = desc {
622 let mut m = description_meta(desc);
623 annotations.push(m["annotations"][0].take());
624 meta.insert("description".into(), m["description"].take());
625 }
626 if let Some(classes) = classes {
627 let mut m = classification_meta(classes);
628 annotations.insert(0, m["annotations"][0].take());
629 meta.insert("classification".into(), m["classification"].take());
630 }
631 if meta.is_empty() {
632 return None;
633 }
634 meta.insert("annotations".into(), Value::Array(annotations));
635 Some(Value::Object(meta))
636}
637
638#[derive(Clone)]
644pub(crate) struct GridCell {
645 row_span: usize,
646 col_span: usize,
647 start_row: usize,
648 end_row: usize,
649 start_col: usize,
650 end_col: usize,
651 text: Box<str>,
652 column_header: bool,
653 row_header: bool,
654 row_section: bool,
655 bbox: Option<[f64; 4]>,
658 cell_ref: Option<Box<Value>>,
661}
662
663impl GridCell {
664 fn filler(r: usize, c: usize) -> Self {
666 GridCell {
667 row_span: 1,
668 col_span: 1,
669 start_row: r,
670 end_row: r + 1,
671 start_col: c,
672 end_col: c + 1,
673 text: "".into(),
674 column_header: false,
675 row_header: false,
676 row_section: false,
677 bbox: None,
678 cell_ref: None,
679 }
680 }
681
682 fn to_value(&self, with_ref: bool) -> Value {
687 let mut m = serde_json::Map::with_capacity(13);
688 m.insert("row_span".into(), self.row_span.into());
689 m.insert("col_span".into(), self.col_span.into());
690 m.insert("start_row_offset_idx".into(), self.start_row.into());
691 m.insert("end_row_offset_idx".into(), self.end_row.into());
692 m.insert("start_col_offset_idx".into(), self.start_col.into());
693 m.insert("end_col_offset_idx".into(), self.end_col.into());
694 m.insert("text".into(), Value::String(self.text.to_string()));
695 m.insert("column_header".into(), self.column_header.into());
696 m.insert("row_header".into(), self.row_header.into());
697 m.insert("row_section".into(), self.row_section.into());
698 m.insert("fillable".into(), false.into());
699 if let Some([l, t, r, b]) = self.bbox {
700 m.insert(
701 "bbox".into(),
702 object([
703 ("l", l.into()),
704 ("t", t.into()),
705 ("r", r.into()),
706 ("b", b.into()),
707 ("coord_origin", "TOPLEFT".into()),
708 ]),
709 );
710 }
711 if let Some(r) = self.cell_ref.as_deref().filter(|_| with_ref) {
712 m.insert("ref".into(), r.clone());
713 }
714 Value::Object(m)
715 }
716}
717
718struct CellOut<'a>(&'a GridCell, bool);
720
721impl serde::Serialize for CellOut<'_> {
722 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
723 use serde::ser::SerializeMap;
724 let CellOut(c, with_ref) = *self;
725 let cell_ref = c.cell_ref.as_deref().filter(|_| with_ref);
726 let mut m = s.serialize_map(Some(
727 11 + usize::from(c.bbox.is_some()) + usize::from(cell_ref.is_some()),
728 ))?;
729 m.serialize_entry("row_span", &c.row_span)?;
730 m.serialize_entry("col_span", &c.col_span)?;
731 m.serialize_entry("start_row_offset_idx", &c.start_row)?;
732 m.serialize_entry("end_row_offset_idx", &c.end_row)?;
733 m.serialize_entry("start_col_offset_idx", &c.start_col)?;
734 m.serialize_entry("end_col_offset_idx", &c.end_col)?;
735 m.serialize_entry("text", &c.text)?;
736 m.serialize_entry("column_header", &c.column_header)?;
737 m.serialize_entry("row_header", &c.row_header)?;
738 m.serialize_entry("row_section", &c.row_section)?;
739 m.serialize_entry("fillable", &false)?;
740 if let Some(b) = c.bbox {
741 struct Bbox([f64; 4]);
742 impl serde::Serialize for Bbox {
743 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
744 let mut m = s.serialize_map(Some(5))?;
745 for (k, v) in ["l", "t", "r", "b"].iter().zip(self.0) {
746 m.serialize_entry(k, &v)?;
747 }
748 m.serialize_entry("coord_origin", "TOPLEFT")?;
749 m.end()
750 }
751 }
752 m.serialize_entry("bbox", &Bbox(b))?;
753 }
754 if let Some(r) = cell_ref {
755 m.serialize_entry("ref", r)?;
756 }
757 m.end()
758 }
759}
760
761#[derive(Clone, Copy)]
765enum GridSlot {
766 Cell(usize),
767 Filler,
768 Null,
769}
770
771pub(crate) struct TableData {
777 cells: Vec<GridCell>,
778 num_rows: usize,
779 num_cols: usize,
780 grid: Vec<GridSlot>,
782}
783
784impl TableData {
785 fn slot_cell(
786 &self,
787 slot: GridSlot,
788 r: usize,
789 c: usize,
790 ) -> Option<std::borrow::Cow<'_, GridCell>> {
791 match slot {
792 GridSlot::Cell(i) => Some(std::borrow::Cow::Borrowed(&self.cells[i])),
793 GridSlot::Filler => Some(std::borrow::Cow::Owned(GridCell::filler(r, c))),
794 GridSlot::Null => None,
795 }
796 }
797
798 fn into_value(self) -> Value {
799 let cells = Value::Array(self.cells.iter().map(|c| c.to_value(true)).collect());
800 let mut data = serde_json::Map::with_capacity(5);
801 data.insert("table_cells".into(), cells);
802 data.insert("num_rows".into(), self.num_rows.into());
803 data.insert("num_cols".into(), self.num_cols.into());
804 data.insert("orientation".into(), "rot_0".into());
805 let mut rows = Vec::with_capacity(self.num_rows);
806 for r in 0..self.num_rows {
807 let mut row = Vec::with_capacity(self.num_cols);
808 for c in 0..self.num_cols {
809 let slot = self.grid[r * self.num_cols + c];
810 row.push(match self.slot_cell(slot, r, c) {
811 Some(cell) => cell.to_value(false),
812 None => Value::Null,
813 });
814 }
815 rows.push(Value::Array(row));
816 }
817 data.insert("grid".into(), Value::Array(rows));
818 Value::Object(data)
819 }
820
821 fn clamp(&mut self, (w, h): (f64, f64)) {
824 for bbox in self.cells.iter_mut().filter_map(|c| c.bbox.as_mut()) {
825 for (v, hi) in bbox.iter_mut().zip([w, h, w, h]) {
826 if v.is_finite() {
829 *v = round2(v.clamp(0.0, hi.max(0.0)));
830 }
831 }
832 }
833 }
834}
835
836impl serde::Serialize for TableData {
837 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
838 use serde::ser::{SerializeMap, SerializeSeq};
839 struct Cells<'a>(&'a [GridCell]);
840 impl serde::Serialize for Cells<'_> {
841 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
842 let mut seq = s.serialize_seq(Some(self.0.len()))?;
843 for c in self.0 {
844 seq.serialize_element(&CellOut(c, true))?;
845 }
846 seq.end()
847 }
848 }
849 struct Row<'a>(&'a TableData, usize);
850 impl serde::Serialize for Row<'_> {
851 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
852 let Row(t, r) = *self;
853 let mut seq = s.serialize_seq(Some(t.num_cols))?;
854 for c in 0..t.num_cols {
855 match t.slot_cell(t.grid[r * t.num_cols + c], r, c) {
856 Some(cell) => seq.serialize_element(&CellOut(&cell, false))?,
857 None => seq.serialize_element(&())?,
858 }
859 }
860 seq.end()
861 }
862 }
863 struct Grid<'a>(&'a TableData);
864 impl serde::Serialize for Grid<'_> {
865 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
866 let mut seq = s.serialize_seq(Some(self.0.num_rows))?;
867 for r in 0..self.0.num_rows {
868 seq.serialize_element(&Row(self.0, r))?;
869 }
870 seq.end()
871 }
872 }
873 let mut m = s.serialize_map(Some(5))?;
874 m.serialize_entry("table_cells", &Cells(&self.cells))?;
875 m.serialize_entry("num_rows", &self.num_rows)?;
876 m.serialize_entry("num_cols", &self.num_cols)?;
877 m.serialize_entry("orientation", "rot_0")?;
878 m.serialize_entry("grid", &Grid(self))?;
879 m.end()
880 }
881}
882
883pub(crate) struct CellView<'a> {
889 pub start_row: usize,
890 pub end_row: usize,
891 pub start_col: usize,
892 pub end_col: usize,
893 pub row_span: usize,
894 pub col_span: usize,
895 pub text: &'a str,
896 pub column_header: bool,
897 pub row_header: bool,
898 pub row_section: bool,
899 pub cell_ref: Option<&'a str>,
901}
902
903pub(crate) struct TableView<'a> {
906 pub num_rows: usize,
907 pub num_cols: usize,
908 pub cells: Vec<CellView<'a>>,
909}
910
911pub(crate) fn table_view<'a>(data: &'a Value, typed: &'a [TableData]) -> TableView<'a> {
914 let ref_of = |r: Option<&'a Value>| r.and_then(|r| r.get("$ref")).and_then(Value::as_str);
915 if let Some(t) = data
916 .get(TYPED_TABLE)
917 .and_then(Value::as_u64)
918 .and_then(|i| typed.get(i as usize))
919 {
920 return TableView {
921 num_rows: t.num_rows,
922 num_cols: t.num_cols,
923 cells: t
924 .cells
925 .iter()
926 .map(|c| CellView {
927 start_row: c.start_row,
928 end_row: c.end_row,
929 start_col: c.start_col,
930 end_col: c.end_col,
931 row_span: c.row_span,
932 col_span: c.col_span,
933 text: &c.text,
934 column_header: c.column_header,
935 row_header: c.row_header,
936 row_section: c.row_section,
937 cell_ref: ref_of(c.cell_ref.as_deref()),
938 })
939 .collect(),
940 };
941 }
942 let num = |v: &Value, k: &str| v.get(k).and_then(Value::as_u64).unwrap_or(0) as usize;
943 let flag = |v: &Value, k: &str| v.get(k).and_then(Value::as_bool).unwrap_or(false);
944 TableView {
945 num_rows: num(data, "num_rows"),
946 num_cols: num(data, "num_cols"),
947 cells: data
948 .get("table_cells")
949 .and_then(Value::as_array)
950 .into_iter()
951 .flatten()
952 .map(|c| CellView {
953 start_row: num(c, "start_row_offset_idx"),
954 end_row: num(c, "end_row_offset_idx"),
955 start_col: num(c, "start_col_offset_idx"),
956 end_col: num(c, "end_col_offset_idx"),
957 row_span: num(c, "row_span"),
958 col_span: num(c, "col_span"),
959 text: c.get("text").and_then(Value::as_str).unwrap_or(""),
960 column_header: flag(c, "column_header"),
961 row_header: flag(c, "row_header"),
962 row_section: flag(c, "row_section"),
963 cell_ref: ref_of(c.get("ref")),
964 })
965 .collect(),
966 }
967}
968
969struct TableItem {
975 item: Value,
976 data: TableData,
977}
978
979impl TableItem {
980 fn into_value(self) -> Value {
981 let TableItem { mut item, data } = self;
982 if let Some(slot) = item.get_mut("data") {
983 *slot = data.into_value();
984 }
985 item
986 }
987}
988
989impl serde::Serialize for TableItem {
990 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
991 use serde::ser::SerializeMap;
992 let Value::Object(obj) = &self.item else {
993 return self.item.serialize(s);
994 };
995 let mut m = s.serialize_map(Some(obj.len()))?;
996 for (k, v) in obj {
997 if k == "data" {
998 m.serialize_entry(k, &self.data)?;
999 } else {
1000 m.serialize_entry(k, v)?;
1001 }
1002 }
1003 m.end()
1004 }
1005}
1006
1007fn table_data(t: &Table) -> TableData {
1010 table_data_with(t, false)
1011}
1012
1013fn table_data_with(t: &Table, raw: bool) -> TableData {
1016 let cell_text = |s: &str| -> Box<str> {
1017 if raw {
1018 s.into()
1019 } else {
1020 unescape_text(&crate::markdown::strip_hard_breaks(s)).into()
1021 }
1022 };
1023 let widen = |b: [f32; 4]| b.map(f64::from);
1024 let mut num_rows = t.rows.len();
1025 let mut num_cols = t.rows.iter().map(Vec::len).max().unwrap_or(0);
1026 let ragged = t.rows.iter().any(|r| r.len() != num_cols);
1034 if let Some(first_class) = t.cells.as_ref().filter(|c| ragged && !c.is_empty()) {
1035 let (mut rows, mut cols) = (num_rows, num_cols);
1036 for c in first_class {
1037 rows = rows.max(c.start_row.saturating_add(c.row_span));
1038 cols = cols.max(c.start_col.saturating_add(c.col_span));
1039 }
1040 if rows.saturating_mul(cols) <= crate::limits::max_table_cells() {
1044 num_rows = rows;
1045 num_cols = cols;
1046 }
1047 }
1048 let mut cells = Vec::new();
1049 let mut grid: Vec<GridSlot>;
1052 if let Some(first_class) = t.cells.as_ref().filter(|c| !c.is_empty()) {
1053 grid = vec![GridSlot::Filler; num_rows * num_cols];
1055 cells.reserve(first_class.len());
1056 for c in first_class {
1057 let idx = cells.len();
1058 cells.push(GridCell {
1059 row_span: c.row_span,
1060 col_span: c.col_span,
1061 start_row: c.start_row,
1062 end_row: c.start_row + c.row_span,
1063 start_col: c.start_col,
1064 end_col: c.start_col + c.col_span,
1065 text: cell_text(&c.text),
1066 column_header: c.column_header,
1067 row_header: c.row_header,
1068 row_section: c.row_section,
1069 bbox: c.bbox.map(widen),
1070 cell_ref: None,
1071 });
1072 for r in c.start_row..(c.start_row + c.row_span).min(num_rows) {
1073 for k in c.start_col..(c.start_col + c.col_span).min(num_cols) {
1074 grid[r * num_cols + k] = GridSlot::Cell(idx);
1075 }
1076 }
1077 }
1078 } else {
1079 grid = vec![GridSlot::Null; num_rows * num_cols];
1080 let s = t.structure.as_ref();
1081 let flag = |grid: Option<&Vec<Vec<bool>>>, r: usize, c: usize| -> bool {
1082 grid.and_then(|g| g.get(r))
1083 .and_then(|row| row.get(c))
1084 .copied()
1085 .unwrap_or(false)
1086 };
1087 let anchor_of = |r: usize, c: usize| -> (usize, usize) {
1088 let (mut r0, mut c0) = (r, c);
1089 while c0 > 0 && flag(s.map(|s| &s.col_continuation), r, c0) {
1090 c0 -= 1;
1091 }
1092 while r0 > 0 && flag(s.map(|s| &s.row_continuation), r0, c0) {
1093 r0 -= 1;
1094 }
1095 (r0, c0)
1096 };
1097 let anchors: Vec<(usize, usize)> = (0..num_rows)
1100 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
1101 .map(|(r, c)| anchor_of(r, c))
1102 .collect();
1103 let mut extent: Vec<(usize, usize)> = (0..num_rows)
1104 .flat_map(|r| (0..num_cols).map(move |c| (r, c)))
1105 .collect();
1106 for (i, &(ar, ac)) in anchors.iter().enumerate() {
1107 let (r, c) = (i / num_cols.max(1), i % num_cols.max(1));
1108 let e = &mut extent[ar * num_cols + ac];
1109 e.0 = e.0.max(r);
1110 e.1 = e.1.max(c);
1111 }
1112 let mut anchor_cell: Vec<Option<usize>> = vec![None; num_rows * num_cols];
1115 for (r, row) in t.rows.iter().enumerate() {
1116 for c in 0..num_cols {
1117 let (ar, ac) = anchors[r * num_cols + c];
1118 if (ar, ac) == (r, c) {
1119 let (er, ec) = extent[r * num_cols + c];
1120 let text = row.get(c).map(|s| cell_text(s)).unwrap_or_default();
1121 let column_header = match s.filter(|s| !s.col_header.is_empty()) {
1122 Some(s) => flag(Some(&s.col_header), r, c),
1123 None => r == 0,
1124 };
1125 anchor_cell[r * num_cols + c] = Some(cells.len());
1126 cells.push(GridCell {
1127 row_span: er - r + 1,
1128 col_span: ec - c + 1,
1129 start_row: r,
1130 end_row: er + 1,
1131 start_col: c,
1132 end_col: ec + 1,
1133 text,
1134 column_header,
1135 row_header: flag(s.map(|s| &s.row_header), r, c),
1136 row_section: false,
1137 bbox: None,
1138 cell_ref: None,
1139 });
1140 }
1141 if let Some(i) = anchor_cell[ar * num_cols + ac] {
1142 grid[r * num_cols + c] = GridSlot::Cell(i);
1143 }
1144 }
1145 }
1146 }
1147 TableData {
1148 cells,
1149 num_rows,
1150 num_cols,
1151 grid,
1152 }
1153}
1154
1155#[derive(Clone, Copy)]
1159enum ProvSlot {
1160 None,
1161 One {
1162 page_no: usize,
1163 bbox: [f64; 4],
1165 bottom_left: bool,
1166 charspan: [usize; 2],
1167 },
1168}
1169
1170impl ProvSlot {
1171 fn origin(bottom_left: bool) -> &'static str {
1172 if bottom_left {
1173 "BOTTOMLEFT"
1174 } else {
1175 "TOPLEFT"
1176 }
1177 }
1178
1179 fn to_value(self) -> Value {
1182 let ProvSlot::One {
1183 page_no,
1184 bbox: [l, t, r, b],
1185 bottom_left,
1186 charspan,
1187 } = self
1188 else {
1189 return Value::Array(Vec::new());
1190 };
1191 let bbox = object([
1192 ("l", l.into()),
1193 ("t", t.into()),
1194 ("r", r.into()),
1195 ("b", b.into()),
1196 ("coord_origin", Self::origin(bottom_left).into()),
1197 ]);
1198 let span = Value::Array(vec![charspan[0].into(), charspan[1].into()]);
1199 Value::Array(vec![object([
1200 ("page_no", page_no.into()),
1201 ("bbox", bbox),
1202 ("charspan", span),
1203 ])])
1204 }
1205
1206 fn clamp(&mut self, sizes: &PageSizes) {
1208 if let ProvSlot::One { page_no, bbox, .. } = self {
1209 if let Some(&(w, h)) = sizes.get(page_no) {
1210 for (v, hi) in bbox.iter_mut().zip([w, h, w, h]) {
1211 if v.is_finite() {
1214 *v = round2(v.clamp(0.0, hi.max(0.0)));
1215 }
1216 }
1217 }
1218 }
1219 }
1220}
1221
1222impl serde::Serialize for ProvSlot {
1223 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1224 use serde::ser::{SerializeMap, SerializeSeq};
1225 let ProvSlot::One {
1226 page_no,
1227 bbox: [l, t, r, b],
1228 bottom_left,
1229 charspan,
1230 } = *self
1231 else {
1232 return s.serialize_seq(Some(0))?.end();
1233 };
1234 struct Bbox([f64; 4], &'static str);
1235 impl serde::Serialize for Bbox {
1236 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1237 let mut m = s.serialize_map(Some(5))?;
1238 for (k, v) in ["l", "t", "r", "b"].iter().zip(self.0) {
1239 m.serialize_entry(k, &v)?;
1240 }
1241 m.serialize_entry("coord_origin", self.1)?;
1242 m.end()
1243 }
1244 }
1245 struct Entry(usize, Bbox, [usize; 2]);
1246 impl serde::Serialize for Entry {
1247 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1248 let mut m = s.serialize_map(Some(3))?;
1249 m.serialize_entry("page_no", &self.0)?;
1250 m.serialize_entry("bbox", &self.1)?;
1251 m.serialize_entry("charspan", &self.2)?;
1252 m.end()
1253 }
1254 }
1255 let mut seq = s.serialize_seq(Some(1))?;
1256 seq.serialize_element(&Entry(
1257 page_no,
1258 Bbox([l, t, r, b], Self::origin(bottom_left)),
1259 charspan,
1260 ))?;
1261 seq.end()
1262 }
1263}
1264
1265struct TextItem {
1272 parent: Box<str>,
1273 label: Box<str>,
1274 prov: ProvSlot,
1275 text: Box<str>,
1276}
1277
1278impl TextItem {
1279 fn into_value(self, index: usize) -> Value {
1280 let text = String::from(self.text);
1281 object([
1282 ("self_ref", Value::String(format!("#/texts/{index}"))),
1283 ("parent", ref_value(self.parent)),
1284 ("children", Value::Array(Vec::new())),
1285 ("content_layer", "body".into()),
1286 ("label", Value::String(self.label.into())),
1287 ("prov", self.prov.to_value()),
1288 ("orig", Value::String(text.clone())),
1289 ("text", Value::String(text)),
1290 ])
1291 }
1292}
1293
1294enum Item {
1298 Text(Box<TextItem>),
1299 Json(Box<Value>),
1300}
1301
1302impl Item {
1303 fn json(v: Value) -> Self {
1304 Item::Json(Box::new(v))
1305 }
1306
1307 fn value_mut(&mut self, index: usize) -> &mut Value {
1310 if let Item::Text(t) = self {
1311 let t = std::mem::replace(
1312 t,
1313 Box::new(TextItem {
1314 parent: "".into(),
1315 label: "".into(),
1316 prov: ProvSlot::None,
1317 text: "".into(),
1318 }),
1319 );
1320 *self = Item::json(t.into_value(index));
1321 }
1322 match self {
1323 Item::Json(v) => v,
1324 Item::Text(_) => unreachable!("converted above"),
1325 }
1326 }
1327
1328 fn into_value(self, index: usize) -> Value {
1329 match self {
1330 Item::Text(t) => t.into_value(index),
1331 Item::Json(v) => *v,
1332 }
1333 }
1334}
1335
1336struct Texts<'a>(&'a [Item]);
1338
1339impl serde::Serialize for Texts<'_> {
1340 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1341 use serde::ser::{SerializeMap, SerializeSeq};
1342 struct Text<'a>(usize, &'a TextItem);
1343 impl serde::Serialize for Text<'_> {
1344 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1345 let Text(i, t) = *self;
1346 let mut m = s.serialize_map(Some(8))?;
1347 m.serialize_entry("self_ref", &format_args!("#/texts/{i}"))?;
1348 m.serialize_entry("parent", &Child::Ref(t.parent.clone()))?;
1349 m.serialize_entry("children", &[(); 0])?;
1350 m.serialize_entry("content_layer", "body")?;
1351 m.serialize_entry("label", &t.label)?;
1352 m.serialize_entry("prov", &t.prov)?;
1353 m.serialize_entry("orig", &t.text)?;
1354 m.serialize_entry("text", &t.text)?;
1355 m.end()
1356 }
1357 }
1358 let mut seq = s.serialize_seq(Some(self.0.len()))?;
1359 for (i, item) in self.0.iter().enumerate() {
1360 match item {
1361 Item::Text(t) => seq.serialize_element(&Text(i, t))?,
1362 Item::Json(v) => seq.serialize_element(v)?,
1363 }
1364 }
1365 seq.end()
1366 }
1367}
1368
1369enum Child {
1373 Ref(Box<str>),
1374 Json(Box<Value>),
1375}
1376
1377impl Child {
1378 fn json(v: Value) -> Self {
1379 Child::Json(Box::new(v))
1380 }
1381
1382 fn into_value(self) -> Value {
1383 match self {
1384 Child::Ref(r) => ref_value(r),
1385 Child::Json(v) => *v,
1386 }
1387 }
1388}
1389
1390impl serde::Serialize for Child {
1391 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1392 use serde::ser::SerializeMap;
1393 match self {
1394 Child::Ref(r) => {
1395 let mut m = s.serialize_map(Some(1))?;
1396 m.serialize_entry("$ref", r)?;
1397 m.end()
1398 }
1399 Child::Json(v) => v.serialize(s),
1400 }
1401 }
1402}
1403
1404fn round2(v: f64) -> f64 {
1405 (v * 100.0).round() / 100.0
1406}
1407
1408#[derive(Default)]
1409struct Builder {
1410 note_keys: bool,
1414 texts: Vec<Item>,
1415 groups: Vec<Value>,
1416 tables: Vec<TableItem>,
1417 pictures: Vec<Value>,
1418 field_regions: Vec<Value>,
1419 field_items: Vec<Value>,
1420 key_value_items: Vec<Value>,
1421 pages: Vec<(usize, f64, f64)>,
1425 cur_page: usize,
1427 cur_w: f64,
1428 cur_h: f64,
1429 pending_loc: Option<[u16; 4]>,
1432 pending_exact: Option<ExactProv>,
1435 pending_track: Option<(crate::tree::TreeTrack, String)>,
1438 pending_anchors: Vec<String>,
1441 pending_siblings: Vec<Value>,
1445 pending_after: Vec<Value>,
1449 pending_body: Vec<Value>,
1453 comment_groups: Vec<String>,
1457 last_comment_section: Option<(String, String)>,
1462 pending_comments: Vec<(String, Vec<usize>)>,
1466}
1467
1468impl Builder {
1469 fn take_prov(&mut self, char_len: usize) -> Value {
1475 self.take_prov_slot(char_len).to_value()
1476 }
1477
1478 fn take_prov_slot(&mut self, char_len: usize) -> ProvSlot {
1480 let prov = self.prov_slot(char_len, false);
1481 self.pending_exact = None;
1482 self.pending_loc = None;
1483 prov
1484 }
1485
1486 fn prov_slot(&self, char_len: usize, span_over_text: bool) -> ProvSlot {
1491 let r2 = round2;
1492 if let Some(ExactProv {
1493 page_no,
1494 bbox: [l, t, r, b],
1495 bottom_left,
1496 charspan,
1497 }) = self.pending_exact
1498 {
1499 let charspan = if span_over_text {
1500 [0, char_len]
1501 } else {
1502 charspan
1503 };
1504 return ProvSlot::One {
1505 page_no,
1506 bbox: [r2(l), r2(t), r2(r), r2(b)],
1507 bottom_left,
1508 charspan,
1509 };
1510 }
1511 let Some([x0, y0, x1, y1]) = self.pending_loc else {
1512 return ProvSlot::None;
1513 };
1514 if [x0, y0, x1, y1] == [0, 0, 0, 0] {
1518 return ProvSlot::One {
1519 page_no: self.cur_page,
1520 bbox: [0.0; 4],
1521 bottom_left: true,
1522 charspan: [0, char_len],
1523 };
1524 }
1525 ProvSlot::One {
1526 page_no: self.cur_page,
1527 bbox: [
1528 r2(x0 as f64 * self.cur_w / 512.0),
1529 r2(self.cur_h - y0 as f64 * self.cur_h / 512.0),
1530 r2(x1 as f64 * self.cur_w / 512.0),
1531 r2(self.cur_h - y1 as f64 * self.cur_h / 512.0),
1532 ],
1533 bottom_left: true,
1534 charspan: [0, char_len],
1535 }
1536 }
1537
1538 fn adopt_loc(&mut self, loc: Option<[u16; 4]>) {
1541 if self.pending_loc.is_none() && self.cur_page > 0 {
1542 self.pending_loc = loc;
1543 }
1544 }
1545
1546 fn link_comments(&mut self) {
1554 let refs: Vec<(String, Vec<Value>)> = std::mem::take(&mut self.pending_comments)
1555 .into_iter()
1556 .map(|(item, comments)| {
1557 let refs = comments
1558 .iter()
1559 .filter_map(|i| self.comment_groups.get(*i))
1560 .map(ref_value)
1561 .collect();
1562 (item, refs)
1563 })
1564 .collect();
1565 for (item, comment_refs) in refs {
1566 if comment_refs.is_empty() {
1567 continue;
1568 }
1569 let Some(target) = self.item_mut(&item) else {
1570 continue;
1571 };
1572 let Some(obj) = target.as_object_mut() else {
1573 continue;
1574 };
1575 let tail: Vec<(String, Value)> = obj
1576 .iter()
1577 .skip_while(|(k, _)| k.as_str() != "prov")
1578 .skip(1)
1579 .map(|(k, v)| (k.clone(), v.clone()))
1580 .collect();
1581 for (k, _) in &tail {
1582 obj.shift_remove(k);
1583 }
1584 obj.insert("comments".into(), Value::Array(comment_refs));
1585 for (k, v) in tail {
1586 obj.insert(k, v);
1587 }
1588 }
1589 }
1590
1591 fn item_mut(&mut self, self_ref: &str) -> Option<&mut Value> {
1593 let idx = ref_index(self_ref)?;
1594 if self_ref.starts_with("#/texts/") {
1595 return self.texts.get_mut(idx).map(|item| item.value_mut(idx));
1596 }
1597 if self_ref.starts_with("#/tables/") {
1598 return self.tables.get_mut(idx).map(|t| &mut t.item);
1599 }
1600 let bucket = if self_ref.starts_with("#/pictures/") {
1601 &mut self.pictures
1602 } else if self_ref.starts_with("#/groups/") {
1603 &mut self.groups
1604 } else {
1605 return None;
1606 };
1607 bucket.get_mut(idx)
1608 }
1609
1610 fn write_tree(&mut self, tree: &crate::tree::ItemTree) -> Vec<Value> {
1614 use crate::tree::TreeKind;
1615 let mut refs: Vec<String> = Vec::with_capacity(tree.items.len());
1618 let (mut nt, mut ng, mut ntb, mut np, mut nf, mut nk) = (0, 0, 0, 0, 0, 0);
1619 for item in &tree.items {
1620 if item.deleted {
1621 refs.push(String::new());
1622 continue;
1623 }
1624 let r = match &item.kind {
1625 TreeKind::Text { .. } | TreeKind::Code { .. } => {
1626 nt += 1;
1627 format!("#/texts/{}", nt - 1)
1628 }
1629 TreeKind::Group { .. } => {
1630 ng += 1;
1631 format!("#/groups/{}", ng - 1)
1632 }
1633 TreeKind::Table { .. } => {
1634 ntb += 1;
1635 format!("#/tables/{}", ntb - 1)
1636 }
1637 TreeKind::Picture { .. } => {
1638 np += 1;
1639 format!("#/pictures/{}", np - 1)
1640 }
1641 TreeKind::FieldRegion { items } => {
1642 nt += items
1645 .iter()
1646 .map(|i| {
1647 [&i.marker, &i.key, &i.value]
1648 .iter()
1649 .filter(|p| p.is_some())
1650 .count()
1651 })
1652 .sum::<usize>();
1653 nf += 1;
1654 format!("#/field_regions/{}", nf - 1)
1655 }
1656 TreeKind::KeyValueGraph { .. } => {
1657 nk += 1;
1658 format!("#/key_value_items/{}", nk - 1)
1659 }
1660 };
1661 refs.push(r);
1662 }
1663 let ref_of = |id: usize| ref_value(refs[id].as_str());
1664 for (id, item) in tree.items.iter().enumerate() {
1665 if item.deleted {
1666 continue;
1667 }
1668 let parent = item.parent.map_or("#/body", |p| refs[p].as_str());
1669 let children: Vec<Value> = item
1673 .children
1674 .iter()
1675 .filter(|&&c| !tree.items[c].deleted)
1676 .map(|&c| ref_of(c))
1677 .collect();
1678 let layer = item.layer.map_or("body", |l| l.value());
1679 self.pending_exact = item.prov.as_ref().map(ExactProv::from);
1682 let self_ref = match &item.kind {
1683 TreeKind::Text {
1684 label,
1685 text,
1686 orig,
1687 formatting,
1688 hyperlink,
1689 level,
1690 list,
1691 } => {
1692 let mut tail = serde_json::Map::new();
1695 if let Some(f) = formatting {
1696 tail.insert("formatting".into(), formatting_json(f));
1697 }
1698 if let Some(h) = hyperlink {
1699 tail.insert("hyperlink".into(), json!(h));
1700 }
1701 if let Some(l) = level {
1702 tail.insert("level".into(), json!(l));
1703 }
1704 if let Some(l) = list {
1705 tail.insert("enumerated".into(), json!(l.enumerated));
1706 tail.insert("marker".into(), json!(l.marker));
1707 }
1708 let r = format!("#/texts/{}", self.texts.len());
1709 let prov = self.take_prov(text.chars().count());
1710 let mut item_json = json!({
1711 "self_ref": r,
1712 "parent": { "$ref": parent },
1713 "children": children,
1714 "content_layer": layer,
1715 "label": label,
1716 "prov": prov,
1717 });
1718 if !item.comments.is_empty() {
1721 item_json["comments"] =
1722 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
1723 }
1724 if let Some(track) = &item.source {
1727 item_json["source"] = json!([track_json(track)]);
1728 }
1729 merge(
1730 &mut item_json,
1731 json!({
1732 "orig": orig.as_deref().unwrap_or(text),
1733 "text": text,
1734 }),
1735 );
1736 merge(&mut item_json, Value::Object(tail));
1737 if self.note_keys {
1738 if !item.notes.is_empty() {
1739 item_json["_notes"] = Value::Array(
1740 item.notes
1741 .iter()
1742 .map(|n| json!([n.offset, n.text]))
1743 .collect(),
1744 );
1745 }
1746 if item.note_body {
1747 item_json["_note_body"] = json!(true);
1748 }
1749 if !item.anchors.is_empty() {
1750 item_json["_anchors"] = json!(item.anchors);
1751 }
1752 if let Some(number) = &item.number {
1753 item_json["_number"] = json!(number);
1754 }
1755 if let Some(rev) = &item.revision {
1756 let mut r = serde_json::Map::new();
1757 r.insert("kind".into(), json!(rev.kind.as_str()));
1758 if let Some(a) = &rev.author {
1759 r.insert("author".into(), json!(a));
1760 }
1761 if let Some(d) = &rev.date {
1762 r.insert("date".into(), json!(d));
1763 }
1764 item_json["_revision"] = Value::Object(r);
1765 }
1766 }
1767 self.texts.push(Item::json(item_json));
1768 r
1769 }
1770 TreeKind::Code {
1771 text,
1772 orig,
1773 language,
1774 formatting,
1775 hyperlink,
1776 captions,
1777 } => {
1778 let r = format!("#/texts/{}", self.texts.len());
1779 let prov = self.take_prov(text.chars().count());
1780 let mut item_json = json!({
1781 "self_ref": r,
1782 "parent": { "$ref": parent },
1783 "children": children,
1784 "content_layer": layer,
1785 "label": "code",
1786 "prov": prov,
1787 });
1788 if !item.comments.is_empty() {
1789 item_json["comments"] =
1790 Value::Array(item.comments.iter().map(|&c| ref_of(c)).collect());
1791 }
1792 merge(
1793 &mut item_json,
1794 json!({
1795 "orig": orig.as_deref().unwrap_or(text),
1796 "text": text,
1797 }),
1798 );
1799 if let Some(f) = formatting {
1800 item_json["formatting"] = formatting_json(f);
1801 }
1802 if let Some(h) = hyperlink {
1803 item_json["hyperlink"] = json!(h);
1804 }
1805 merge(
1806 &mut item_json,
1807 json!({
1808 "captions": captions.iter().map(|&c| ref_of(c)).collect::<Vec<_>>(),
1809 "references": [],
1810 "footnotes": [],
1811 "code_language": code_language(language.as_deref()),
1812 }),
1813 );
1814 self.texts.push(Item::json(item_json));
1815 r
1816 }
1817 TreeKind::Group { label, name } => {
1818 self.pending_exact = None;
1819 let r = format!("#/groups/{}", self.groups.len());
1820 self.groups.push(json!({
1821 "self_ref": r,
1822 "parent": { "$ref": parent },
1823 "children": children,
1824 "content_layer": layer,
1825 "name": name,
1826 "label": label,
1827 }));
1828 r
1829 }
1830 TreeKind::Table {
1831 table,
1832 rich_cells,
1833 captions,
1834 } => {
1835 let r = self.add_table_with(table, parent, true);
1837 let idx = ref_index(&r).expect("table ref");
1838 let t = &mut self.tables[idx];
1839 t.item["children"] = Value::Array(children);
1840 t.item["content_layer"] = json!(layer);
1841 t.item["captions"] =
1842 Value::Array(captions.iter().map(|&c| ref_of(c)).collect());
1843 for &(row, col, group) in rich_cells {
1847 let cell_ref = ref_of(group);
1848 for c in t.data.cells.iter_mut() {
1849 if c.start_row == row && c.start_col == col {
1850 c.cell_ref = Some(Box::new(cell_ref.clone()));
1851 }
1852 }
1853 }
1854 r
1855 }
1856 TreeKind::Picture {
1857 captions,
1858 image,
1859 classification,
1860 extra_classes,
1861 description,
1862 confidence,
1863 chart,
1864 dpi,
1865 } => {
1866 let mut meta = classification.as_ref().map(|c| {
1871 let mut pred = serde_json::Map::new();
1872 if let Some(conf) = confidence {
1873 pred.insert("confidence".into(), json!(conf));
1874 }
1875 pred.insert("class_name".into(), json!(c));
1876 let mut preds = vec![Value::Object(pred)];
1877 preds.extend(extra_classes.iter().map(|c| json!({ "class_name": c })));
1878 json!({ "classification": { "predictions": preds } })
1879 });
1880 if let (Some(m), Some(t)) = (meta.as_mut(), chart) {
1881 if !t.rows.is_empty() {
1882 m["tabular_chart"] =
1883 object([("chart_data", table_data(t).into_value())]);
1884 }
1885 }
1886 if let Some(desc) = description {
1890 let mut d = description_meta(desc);
1891 let mut merged = Map::new();
1892 merged.insert("description".into(), d["description"].take());
1893 if let Some(Value::Object(rest)) = meta {
1894 merged.extend(rest);
1895 }
1896 merged.insert("annotations".into(), d["annotations"].take());
1897 meta = Some(Value::Object(merged));
1898 }
1899 if let Some((text, language)) = &item.code_meta {
1902 let code = json!({ "text": text, "language": language });
1903 match meta.as_mut() {
1904 Some(Value::Object(m)) => {
1905 m.insert("code".into(), code);
1906 }
1907 _ => meta = Some(json!({ "code": code })),
1908 }
1909 }
1910 let prov = self.take_prov(0);
1911 let r = self.push_picture(
1912 prov,
1913 captions.iter().map(|&c| ref_of(c)).collect(),
1914 children,
1915 image.as_ref(),
1916 meta,
1917 parent,
1918 );
1919 if let Some(idx) = ref_index(&r) {
1920 self.pictures[idx]["content_layer"] = json!(layer);
1921 if let (Some(dpi), Some(img)) = (dpi, self.pictures[idx].get_mut("image")) {
1924 img["dpi"] = json!(dpi);
1925 }
1926 }
1927 r
1928 }
1929 TreeKind::FieldRegion { items } => {
1930 self.pending_exact = None;
1931 let r = self.add_field_region(items, parent);
1932 if let Some(region) = self.field_regions.last_mut() {
1933 region["content_layer"] = json!(layer);
1934 }
1935 r
1936 }
1937 TreeKind::KeyValueGraph { cells, links } => {
1938 self.pending_exact = None;
1939 let r = self.add_key_value_graph(cells, links, parent);
1940 if let Some(item) = self.key_value_items.last_mut() {
1941 item["content_layer"] = json!(layer);
1942 }
1943 r
1944 }
1945 };
1946 debug_assert_eq!(self_ref, refs[id], "tree item {id} numbered out of order");
1947 }
1948 tree.body
1949 .iter()
1950 .filter(|&&c| !tree.items[c].deleted)
1951 .map(|&c| ref_of(c))
1952 .collect()
1953 }
1954
1955 fn add_node(&mut self, node: &Node, parent: &str) -> Option<String> {
1956 match node {
1957 Node::Heading { level: 1, text } => {
1958 Some(self.add_text("title", text, parent, json!({})))
1959 }
1960 Node::Heading { level, text } => Some(self.add_text(
1961 "section_header",
1962 text,
1963 parent,
1964 json!({ "level": level.saturating_sub(1) }),
1965 )),
1966 Node::Caption { text, href } => {
1967 let extra = match href {
1968 Some(url) => json!({ "hyperlink": url }),
1969 None => json!({}),
1970 };
1971 Some(self.add_text("caption", text, parent, extra))
1972 }
1973 Node::LabeledText { label, text, href } => {
1976 let extra = match href {
1977 Some(url) => json!({ "hyperlink": url }),
1978 None => json!({}),
1979 };
1980 Some(self.add_text(label, text, parent, extra))
1981 }
1982 Node::Paragraph { text } | Node::ListText { text, .. } => {
1983 let t = text.trim();
1986 match t.strip_prefix("$$").and_then(|s| s.strip_suffix("$$")) {
1987 Some(inner) if !inner.is_empty() => Some(self.add_formula(inner, parent)),
1988 _ => Some(self.add_text("text", text, parent, json!({}))),
1989 }
1990 }
1991 Node::CheckboxItem { checked, text } => {
1992 let label = if *checked {
1998 "checkbox_selected"
1999 } else {
2000 "checkbox_unselected"
2001 };
2002 Some(self.add_text(label, text, parent, json!({})))
2003 }
2004 Node::Code {
2005 language,
2006 text,
2007 orig,
2008 ..
2009 } => Some(self.add_code(text, language.as_deref(), orig.as_deref(), parent)),
2010 Node::Formula {
2013 latex,
2014 orig,
2015 location,
2016 } => {
2017 self.adopt_loc(*location);
2018 Some(self.add_formula_item(latex, orig, parent))
2019 }
2020 Node::CommentSection {
2026 name,
2027 text,
2028 refs_note_text,
2029 grouped,
2030 } => {
2031 if !*grouped {
2032 let child =
2036 self.add_text("text", text, parent, json!({ "content_layer": "notes" }));
2037 self.comment_groups.push(child.clone());
2038 return Some(child);
2039 }
2040 if let Some((_, self_ref)) = self
2041 .last_comment_section
2042 .as_ref()
2043 .filter(|(n, _)| n == name)
2044 .cloned()
2045 {
2046 let child =
2049 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
2050 if let Some(children) =
2051 self.groups[group_index(&self_ref)]["children"].as_array_mut()
2052 {
2053 children.push(ref_value(child.as_str()));
2054 }
2055 self.comment_groups.push(if *refs_note_text {
2056 child
2057 } else {
2058 self_ref.clone()
2059 });
2060 return None;
2063 }
2064 let self_ref = format!("#/groups/{}", self.groups.len());
2065 self.groups.push(Value::Null);
2066 let child =
2067 self.add_text("text", text, &self_ref, json!({ "content_layer": "notes" }));
2068 self.groups[group_index(&self_ref)] = json!({
2069 "self_ref": self_ref,
2070 "parent": { "$ref": parent },
2071 "children": [{ "$ref": child }],
2072 "content_layer": "notes",
2073 "name": name,
2074 "label": "comment_section",
2075 });
2076 self.last_comment_section = Some((name.clone(), self_ref.clone()));
2077 self.comment_groups.push(if *refs_note_text {
2078 child
2079 } else {
2080 self_ref.clone()
2081 });
2082 Some(self_ref)
2083 }
2084 Node::Commented { comments, inner } => {
2087 let item = self.add_node(inner, parent)?;
2088 if !comments.is_empty() {
2089 self.pending_comments.push((item.clone(), comments.clone()));
2090 }
2091 Some(item)
2092 }
2093 Node::Table(t) => Some(self.add_table(t, parent)),
2094 Node::Picture {
2095 caption,
2096 caption_href,
2097 image,
2098 classification,
2099 description,
2100 caption_parent,
2101 caption_location,
2102 } => Some(self.add_picture(
2103 caption.as_deref(),
2104 caption_href.as_deref(),
2105 image.as_ref(),
2106 picture_meta(classification.as_deref(), description.as_ref()),
2107 parent,
2108 (*caption_parent, *caption_location),
2109 )),
2110 Node::Chart {
2115 kind,
2116 table,
2117 caption,
2118 location,
2119 } => {
2120 self.adopt_loc(*location);
2121 let mut meta = json!({
2122 "classification": { "predictions": [{ "class_name": kind }] },
2123 });
2124 if !table.rows.is_empty() {
2125 meta["tabular_chart"] =
2126 object([("chart_data", table_data(table).into_value())]);
2127 }
2128 let mut captions = Vec::new();
2134 if let Some(cap) = caption.as_deref().filter(|c| !c.is_empty()) {
2135 let prov = self.prov_slot(unescape_text(cap).chars().count(), true);
2136 let cap_ref = self.add_text_with("caption", cap, parent, json!({}), prov);
2137 self.pending_siblings.push(ref_value(cap_ref.as_str()));
2138 captions.push(ref_value(cap_ref));
2139 }
2140 let prov = self.take_prov(0);
2141 Some(self.push_picture(prov, captions, Vec::new(), None, Some(meta), parent))
2142 }
2143 Node::DoclangOnly(_) => None,
2145 Node::Group {
2146 label,
2147 name,
2148 layer,
2149 children,
2150 } => Some(self.add_group(label, name.as_deref(), *layer, children, parent)),
2151 Node::FieldRegion { items } => Some(self.add_field_region(items, parent)),
2152 Node::KeyValueGraph { cells, links } => {
2153 Some(self.add_key_value_graph(cells, links, parent))
2154 }
2155 Node::InlineGroup { md_text, .. } => {
2158 Some(self.add_text("text", md_text, parent, json!({})))
2159 }
2160 Node::TextDump(text) => Some(self.add_text("text", text, parent, json!({}))),
2162 Node::Furniture {
2168 layer: ContentLayer::Notes,
2169 inner,
2170 } => {
2171 let item = self.add_node(inner, parent)?;
2172 self.set_layer(&item, "notes");
2173 Some(item)
2174 }
2175 Node::Furniture { .. } => None,
2176 Node::PageFurniture {
2180 footer,
2181 location,
2182 text,
2183 } => {
2184 if self.cur_page > 0 {
2185 self.pending_loc = Some(*location);
2186 }
2187 let label = if *footer {
2188 "page_footer"
2189 } else {
2190 "page_header"
2191 };
2192 let item = self.add_text(label, text, parent, json!({}));
2193 self.pending_loc = None;
2194 self.set_layer(&item, "furniture");
2195 Some(item)
2196 }
2197 Node::FurnitureText { label, text } => {
2200 let item = self.add_text(label, text, parent, json!({}));
2201 self.set_layer(&item, "furniture");
2202 Some(item)
2203 }
2204 Node::PictureChildren(children) => {
2208 let pic = self.pictures.len().checked_sub(1)?;
2209 let pic_ref = format!("#/pictures/{pic}");
2210 let refs = self.walk_into(children, &pic_ref);
2211 if let Some(list) = self.pictures[pic]["children"].as_array_mut() {
2212 list.extend(refs.into_iter().map(Child::into_value));
2213 }
2214 None
2215 }
2216 Node::Located { location, inner } => {
2221 if self.cur_page > 0 {
2222 self.pending_loc = Some(*location);
2223 }
2224 let r = self.add_node(inner, parent);
2225 self.pending_loc = None;
2226 r
2227 }
2228 Node::Anchor { id } => {
2229 self.pending_anchors.push(id.clone());
2230 None
2231 }
2232 Node::Track { track, cue, inner } => {
2233 self.pending_track = Some((track.clone(), cue.clone()));
2234 let r = self.add_node(inner, parent);
2235 self.pending_track = None;
2236 r
2237 }
2238 Node::Prov {
2239 page_no,
2240 bbox,
2241 charspan,
2242 inner,
2243 ..
2244 } => {
2245 self.pending_exact = Some(ExactProv {
2246 page_no: *page_no,
2247 bbox: bbox.map(f64::from),
2248 bottom_left: false,
2249 charspan: *charspan,
2250 });
2251 let r = self.add_node(inner, parent);
2252 self.pending_exact = None;
2253 r
2254 }
2255 Node::PageBreak => None,
2257 Node::PageInfo {
2260 page_no,
2261 width,
2262 height,
2263 } => {
2264 self.cur_page = *page_no;
2265 self.cur_w = *width as f64;
2266 self.cur_h = *height as f64;
2267 if *page_no > 0 {
2268 self.pages.push((*page_no, self.cur_w, self.cur_h));
2269 }
2270 None
2271 }
2272 Node::ListItem { .. } => None,
2274 }
2275 }
2276
2277 fn add_field_region(&mut self, items: &[crate::FieldItem], parent: &str) -> String {
2281 let self_ref = format!("#/field_regions/{}", self.field_regions.len());
2282 self.field_regions.push(Value::Null);
2283 let region_index = self.field_regions.len() - 1;
2284 let mut item_refs = Vec::new();
2285 for item in items {
2286 item_refs.push(ref_value(self.add_field_item(item, &self_ref)));
2287 }
2288 self.field_regions[region_index] = json!({
2289 "self_ref": self_ref,
2290 "parent": { "$ref": parent },
2291 "children": item_refs,
2292 "content_layer": "body",
2293 "label": "field_region",
2294 "prov": [],
2295 });
2296 self_ref
2297 }
2298
2299 fn add_key_value_graph(
2303 &mut self,
2304 cells: &[crate::GraphCell],
2305 links: &[crate::GraphLink],
2306 parent: &str,
2307 ) -> String {
2308 let self_ref = format!("#/key_value_items/{}", self.key_value_items.len());
2309 let cells: Vec<Value> = cells
2310 .iter()
2311 .map(|c| {
2312 json!({
2313 "label": c.label,
2314 "cell_id": c.cell_id,
2315 "text": c.text,
2316 "orig": c.orig,
2317 })
2318 })
2319 .collect();
2320 let links: Vec<Value> = links
2321 .iter()
2322 .map(|l| {
2323 json!({
2324 "label": l.label,
2325 "source_cell_id": l.source_cell_id,
2326 "target_cell_id": l.target_cell_id,
2327 })
2328 })
2329 .collect();
2330 self.key_value_items.push(json!({
2331 "self_ref": self_ref,
2332 "parent": { "$ref": parent },
2333 "children": [],
2334 "content_layer": "body",
2335 "label": "key_value_region",
2336 "prov": [],
2337 "captions": [],
2338 "references": [],
2339 "footnotes": [],
2340 "graph": { "cells": cells, "links": links },
2341 }));
2342 self_ref
2343 }
2344
2345 fn add_field_item(&mut self, item: &crate::FieldItem, parent: &str) -> String {
2346 let self_ref = format!("#/field_items/{}", self.field_items.len());
2347 self.field_items.push(Value::Null);
2348 let item_index = self.field_items.len() - 1;
2349 let mut child_refs = Vec::new();
2350 for (label, text) in [
2351 ("marker", &item.marker),
2352 ("field_key", &item.key),
2353 ("field_value", &item.value),
2354 ] {
2355 if let Some(text) = text {
2356 let extra = match (label, &item.value_kind) {
2359 ("field_value", Some(kind)) => json!({ "kind": kind }),
2360 _ => json!({}),
2361 };
2362 child_refs.push(ref_value(self.add_text(label, text, &self_ref, extra)));
2363 }
2364 }
2365 self.field_items[item_index] = json!({
2366 "self_ref": self_ref,
2367 "parent": { "$ref": parent },
2368 "children": child_refs,
2369 "content_layer": "body",
2370 "label": "field_item",
2371 "prov": [],
2372 });
2373 self_ref
2374 }
2375
2376 fn set_layer(&mut self, self_ref: &str, layer: &str) {
2380 if let Some(item) = self.item_mut(self_ref) {
2381 item["content_layer"] = json!(layer);
2382 }
2383 }
2384
2385 fn add_text(&mut self, label: &str, text: &str, parent: &str, extra: Value) -> String {
2386 let prov = self.take_prov_slot(unescape_text(text).chars().count());
2387 self.add_text_with(label, text, parent, extra, prov)
2388 }
2389
2390 fn add_text_with(
2393 &mut self,
2394 label: &str,
2395 text: &str,
2396 parent: &str,
2397 extra: Value,
2398 prov: ProvSlot,
2399 ) -> String {
2400 let index = self.texts.len();
2401 let self_ref = format!("#/texts/{index}");
2402 let mut item = match self.pending_track.take() {
2407 None => Item::Text(Box::new(TextItem {
2408 parent: parent.into(),
2409 label: label.into(),
2410 prov,
2411 text: unescape_text(text).into(),
2412 })),
2413 Some((track, cue)) => {
2414 let mut m = serde_json::Map::with_capacity(9);
2415 m.insert("self_ref".into(), Value::String(self_ref.clone()));
2416 m.insert("parent".into(), ref_value(parent));
2417 m.insert("children".into(), Value::Array(Vec::new()));
2418 m.insert("content_layer".into(), "body".into());
2419 m.insert("label".into(), label.into());
2420 m.insert("prov".into(), prov.to_value());
2421 m.insert("source".into(), Value::Array(vec![track_json(&track)]));
2423 m.insert("orig".into(), Value::String(cue.clone()));
2424 m.insert("text".into(), Value::String(cue));
2425 Item::json(Value::Object(m))
2426 }
2427 };
2428 if extra.as_object().is_some_and(|e| !e.is_empty()) {
2429 merge(item.value_mut(index), extra);
2430 }
2431 let anchors = std::mem::take(&mut self.pending_anchors);
2434 if self.note_keys && !anchors.is_empty() {
2435 merge(item.value_mut(index), json!({ "_anchors": anchors }));
2436 }
2437 self.texts.push(item);
2438 self_ref
2439 }
2440
2441 fn add_formula(&mut self, latex: &str, parent: &str) -> String {
2444 let self_ref = format!("#/texts/{}", self.texts.len());
2445 let prov = self.take_prov(latex.chars().count());
2446 self.texts.push(Item::json(json!({
2447 "self_ref": self_ref,
2448 "parent": { "$ref": parent },
2449 "children": [],
2450 "content_layer": "body",
2451 "label": "formula",
2452 "prov": prov,
2453 "orig": latex,
2454 "text": latex,
2455 })));
2456 self_ref
2457 }
2458
2459 fn add_formula_item(&mut self, latex: &str, orig: &str, parent: &str) -> String {
2463 let self_ref = format!("#/texts/{}", self.texts.len());
2464 let prov = self.take_prov(latex.chars().count());
2465 self.texts.push(Item::json(json!({
2466 "self_ref": self_ref,
2467 "parent": { "$ref": parent },
2468 "children": [],
2469 "content_layer": "body",
2470 "label": "formula",
2471 "prov": prov,
2472 "orig": orig,
2473 "text": latex,
2474 })));
2475 self_ref
2476 }
2477
2478 fn add_code(
2479 &mut self,
2480 text: &str,
2481 language: Option<&str>,
2482 orig: Option<&str>,
2483 parent: &str,
2484 ) -> String {
2485 let self_ref = format!("#/texts/{}", self.texts.len());
2486 let raw = unescape_text(text);
2487 let prov = self.take_prov(raw.chars().count());
2488 self.texts.push(Item::json(json!({
2489 "self_ref": self_ref,
2490 "parent": { "$ref": parent },
2491 "children": [],
2492 "content_layer": "body",
2493 "label": "code",
2494 "prov": prov,
2495 "orig": orig.map(unescape_text).unwrap_or_else(|| raw.clone()),
2498 "text": raw,
2499 "captions": [],
2500 "references": [],
2501 "footnotes": [],
2502 "code_language": code_language(language),
2503 })));
2504 self_ref
2505 }
2506
2507 fn add_list(&mut self, items: &[Node], parent: &str) -> String {
2510 let self_ref = format!("#/groups/{}", self.groups.len());
2511 self.groups.push(Value::Null);
2513 let base = level_of(&items[0]);
2514 let mut children = Vec::new();
2515 let mut i = 0;
2516 while i < items.len() {
2517 if !matches!(items[i], Node::ListItem { .. }) {
2520 i += 1;
2521 continue;
2522 }
2523 let lvl = level_of(&items[i]);
2524 if lvl > base {
2525 i += 1;
2527 continue;
2528 }
2529 let item_ref = self.add_list_item(&items[i], &self_ref);
2530 let mut j = i + 1;
2532 while j < items.len() && level_of(&items[j]) > base {
2533 j += 1;
2534 }
2535 if j > i + 1 {
2536 let mut nested = Vec::new();
2537 self.add_sibling_lists(&items[i + 1..j], &item_ref, &mut nested);
2538 if let Some(idx) = ref_index(&item_ref) {
2540 self.texts[idx].value_mut(idx)["children"]
2541 .as_array_mut()
2542 .unwrap()
2543 .extend(nested);
2544 }
2545 }
2546 children.push(ref_value(item_ref));
2547 i = j;
2548 }
2549 self.groups[group_index(&self_ref)] = json!({
2550 "self_ref": self_ref,
2551 "parent": { "$ref": parent },
2552 "children": children,
2553 "content_layer": "body",
2554 "name": "list",
2555 "label": "list",
2556 });
2557 self_ref
2558 }
2559
2560 fn add_list_item(&mut self, node: &Node, parent: &str) -> String {
2561 let Node::ListItem {
2562 ordered,
2563 number,
2564 text,
2565 location,
2566 ..
2567 } = node
2568 else {
2569 unreachable!()
2570 };
2571 self.adopt_loc(*location);
2572 let self_ref = format!("#/texts/{}", self.texts.len());
2573 let raw = unescape_text(text);
2574 let prov = self.take_prov(raw.chars().count());
2575 let marker = if *ordered {
2576 format!("{number}.")
2577 } else {
2578 "-".to_string()
2579 };
2580 self.texts.push(Item::json(json!({
2581 "self_ref": self_ref,
2582 "parent": { "$ref": parent },
2583 "children": [],
2584 "content_layer": "body",
2585 "label": "list_item",
2586 "prov": prov,
2587 "orig": raw,
2588 "text": raw,
2589 "enumerated": ordered,
2590 "marker": marker,
2591 })));
2592 self_ref
2593 }
2594
2595 fn add_table(&mut self, t: &Table, parent: &str) -> String {
2596 self.add_table_with(t, parent, false)
2597 }
2598
2599 fn add_table_with(&mut self, t: &Table, parent: &str, raw: bool) -> String {
2602 let self_ref = format!("#/tables/{}", self.tables.len());
2603 self.adopt_loc(t.location);
2604 let prov = self.take_prov(0);
2605 let (captions, children) = match t.caption.as_deref().filter(|c| !c.is_empty()) {
2609 Some(cap) => self.add_caption(
2610 cap,
2611 json!({}),
2612 &self_ref,
2613 parent,
2614 (t.caption_parent, t.caption_location),
2615 ),
2616 None => (Vec::new(), Vec::new()),
2617 };
2618 let data = table_data_with(t, raw);
2619 let item = json!({
2620 "self_ref": self_ref,
2621 "parent": { "$ref": parent },
2622 "children": children,
2623 "content_layer": "body",
2624 "label": "table",
2625 "prov": prov,
2626 "captions": captions,
2627 "references": [],
2628 "footnotes": [],
2629 "data": null,
2631 "annotations": [],
2632 });
2633 self.tables.push(TableItem { item, data });
2634 self_ref
2635 }
2636
2637 fn add_caption(
2646 &mut self,
2647 text: &str,
2648 extra: Value,
2649 self_ref: &str,
2650 parent: &str,
2651 (choice, location): (CaptionParent, Option<[u16; 4]>),
2652 ) -> (Vec<Value>, Vec<Value>) {
2653 let cap_parent = match choice {
2658 CaptionParent::Item => self_ref,
2659 CaptionParent::Container | CaptionParent::ContainerAfter => parent,
2660 CaptionParent::Body => "#/body",
2661 };
2662 if self.cur_page > 0 && location.is_some() {
2663 self.pending_loc = location;
2664 }
2665 let cap_ref = ref_value(self.add_text("caption", text, cap_parent, extra));
2666 match choice {
2667 CaptionParent::Item => return (vec![cap_ref.clone()], vec![cap_ref]),
2668 CaptionParent::Container => self.pending_siblings.push(cap_ref.clone()),
2671 CaptionParent::Body if parent == "#/body" => {
2672 self.pending_siblings.push(cap_ref.clone())
2673 }
2674 CaptionParent::ContainerAfter => self.pending_after.push(cap_ref.clone()),
2675 CaptionParent::Body => self.pending_body.push(cap_ref.clone()),
2678 }
2679 (vec![cap_ref], Vec::new())
2680 }
2681
2682 fn add_picture(
2685 &mut self,
2686 caption: Option<&str>,
2687 caption_href: Option<&str>,
2688 image: Option<&crate::PictureImage>,
2689 meta: Option<Value>,
2690 parent: &str,
2691 caption_at: (CaptionParent, Option<[u16; 4]>),
2692 ) -> String {
2693 let self_ref = format!("#/pictures/{}", self.pictures.len());
2694 let prov = self.take_prov(0);
2697 let (captions, children) = match caption.filter(|c| !c.is_empty()) {
2698 Some(cap) => {
2699 let extra = match caption_href {
2703 Some(href) => json!({ "hyperlink": href }),
2704 None => json!({}),
2705 };
2706 self.add_caption(cap, extra, &self_ref, parent, caption_at)
2707 }
2708 None => (Vec::new(), Vec::new()),
2709 };
2710 self.push_picture(prov, captions, children, image, meta, parent)
2711 }
2712
2713 fn push_picture(
2716 &mut self,
2717 prov: Value,
2718 captions: Vec<Value>,
2719 children: Vec<Value>,
2720 image: Option<&crate::PictureImage>,
2721 meta: Option<Value>,
2722 parent: &str,
2723 ) -> String {
2724 let self_ref = format!("#/pictures/{}", self.pictures.len());
2725 let annotations = meta
2729 .as_ref()
2730 .and_then(|m| m.get("annotations").cloned())
2731 .unwrap_or_else(|| json!([]));
2732 let meta = meta.map(|mut m| {
2733 if let Some(obj) = m.as_object_mut() {
2734 obj.remove("annotations");
2735 }
2736 m
2737 });
2738 let mut item = match meta {
2742 Some(meta) => json!({
2743 "self_ref": self_ref,
2744 "parent": { "$ref": parent },
2745 "children": children,
2746 "content_layer": "body",
2747 "meta": meta,
2748 "label": "picture",
2749 "prov": prov,
2750 "captions": captions,
2751 "references": [],
2752 "footnotes": [],
2753 "annotations": annotations,
2754 }),
2755 None => json!({
2756 "self_ref": self_ref,
2757 "parent": { "$ref": parent },
2758 "children": children,
2759 "content_layer": "body",
2760 "label": "picture",
2761 "prov": prov,
2762 "captions": captions,
2763 "references": [],
2764 "footnotes": [],
2765 "annotations": annotations,
2766 }),
2767 };
2768 if let Some(img) = image {
2773 let mut image = image_ref_json(img);
2774 if self.note_keys {
2777 if let Some([w, h]) = img.display_size {
2778 image["_display"] = json!([w, h]);
2779 }
2780 }
2781 if let Some(obj) = item.as_object_mut() {
2782 let annotations = obj.remove("annotations").unwrap_or_else(|| json!([]));
2783 obj.insert("image".into(), image);
2784 obj.insert("annotations".into(), annotations);
2785 }
2786 }
2787 self.pictures.push(item);
2788 self_ref
2789 }
2790
2791 fn add_group(
2792 &mut self,
2793 label: &str,
2794 name: Option<&str>,
2795 layer: Option<ContentLayer>,
2796 nodes: &[Node],
2797 parent: &str,
2798 ) -> String {
2799 let self_ref = format!("#/groups/{}", self.groups.len());
2800 self.groups.push(Value::Null);
2801 let mark = (
2805 self.texts.len(),
2806 self.tables.len(),
2807 self.pictures.len(),
2808 self.groups.len(),
2809 );
2810 let children: Vec<Value> = self
2811 .walk_into(nodes, &self_ref)
2812 .into_iter()
2813 .map(Child::into_value)
2814 .collect();
2815 let name = name.unwrap_or(if label == "inline" { "group" } else { label });
2816 let content_layer = layer.map_or("body", |l| l.value());
2817 self.groups[group_index(&self_ref)] = json!({
2818 "self_ref": self_ref,
2819 "parent": { "$ref": parent },
2820 "children": children,
2821 "content_layer": content_layer,
2822 "name": name,
2823 "label": label,
2824 });
2825 if layer.is_some() {
2826 let (t, tb, p, g) = mark;
2827 for item in self.texts[t..]
2828 .iter_mut()
2829 .enumerate()
2830 .map(|(k, item)| item.value_mut(t + k))
2831 .chain(self.tables[tb..].iter_mut().map(|t| &mut t.item))
2832 .chain(self.pictures[p..].iter_mut())
2833 .chain(self.groups[g..].iter_mut())
2834 {
2835 if let Some(obj) = item.as_object_mut() {
2836 obj.insert("content_layer".into(), json!(content_layer));
2837 }
2838 }
2839 }
2840 self_ref
2841 }
2842
2843 fn walk_into(&mut self, nodes: &[Node], parent: &str) -> Vec<Child> {
2846 let seqs: Option<Vec<usize>> = nodes
2851 .iter()
2852 .map(|n| match n {
2853 Node::Prov { seq: Some(s), .. } => Some(*s),
2854 _ => None,
2855 })
2856 .collect();
2857 if let Some(seqs) = seqs.filter(|s| !s.is_empty()) {
2858 let mut order: Vec<usize> = (0..nodes.len()).collect();
2859 order.sort_by_key(|&i| seqs[i]);
2860 let mut slots: Vec<Vec<Child>> = (0..nodes.len()).map(|_| Vec::new()).collect();
2861 for i in order {
2862 if let Some(r) = self.add_node(&nodes[i], parent) {
2863 slots[i].extend(self.pending_siblings.drain(..).map(Child::json));
2864 slots[i].push(Child::Ref(r.into()));
2865 slots[i].extend(self.pending_after.drain(..).map(Child::json));
2866 }
2867 if parent == "#/body" {
2868 slots[i].extend(self.pending_body.drain(..).map(Child::json));
2869 }
2870 }
2871 return slots.into_iter().flatten().collect();
2872 }
2873 let mut children = Vec::new();
2874 let mut i = 0;
2875 while i < nodes.len() {
2876 if matches!(nodes[i], Node::ListItem { .. }) {
2877 let start = i;
2878 i += 1;
2879 loop {
2880 match nodes.get(i) {
2881 Some(Node::ListItem { .. }) => i += 1,
2882 Some(Node::Paragraph { text })
2885 if text.is_empty()
2886 && matches!(nodes.get(i + 1), Some(Node::ListItem { .. })) =>
2887 {
2888 i += 1
2889 }
2890 _ => break,
2891 }
2892 }
2893 let mut lists = Vec::new();
2894 self.add_sibling_lists(&nodes[start..i], parent, &mut lists);
2895 children.extend(lists.into_iter().map(Child::json));
2896 } else {
2897 if let Some(r) = self.add_node(&nodes[i], parent) {
2898 children.extend(self.pending_siblings.drain(..).map(Child::json));
2899 children.push(Child::Ref(r.into()));
2900 children.extend(self.pending_after.drain(..).map(Child::json));
2901 }
2902 i += 1;
2903 }
2904 if parent == "#/body" {
2907 children.extend(self.pending_body.drain(..).map(Child::json));
2908 }
2909 }
2910 children
2911 }
2912
2913 fn add_sibling_lists(&mut self, run: &[Node], parent: &str, out: &mut Vec<Value>) {
2918 let base = level_of(&run[0]);
2919 let mut seg = 0;
2920 for k in 0..run.len() {
2921 let Node::ListItem {
2922 first_in_list,
2923 level,
2924 ..
2925 } = &run[k]
2926 else {
2927 continue;
2928 };
2929 if *level != base {
2930 continue; }
2932 if k > seg && *first_in_list {
2933 out.push(ref_value(self.add_list(&run[seg..k], parent)));
2934 seg = k;
2935 }
2936 }
2937 out.push(ref_value(self.add_list(&run[seg..], parent)));
2938 }
2939}
2940
2941fn level_of(node: &Node) -> u8 {
2942 match node {
2943 Node::ListItem { level, .. } => *level,
2944 _ => 0,
2945 }
2946}
2947
2948fn group_index(self_ref: &str) -> usize {
2949 self_ref.rsplit('/').next().unwrap().parse().unwrap()
2950}
2951
2952fn ref_index(self_ref: &str) -> Option<usize> {
2953 self_ref.rsplit('/').next()?.parse().ok()
2954}
2955
2956fn merge(target: &mut Value, extra: Value) {
2958 if let (Some(t), Value::Object(e)) = (target.as_object_mut(), extra) {
2959 t.extend(e);
2960 }
2961}
2962
2963fn object<const N: usize>(entries: [(&str, Value); N]) -> Value {
2969 let mut m = serde_json::Map::with_capacity(N);
2970 for (k, v) in entries {
2971 m.insert(k.to_owned(), v);
2972 }
2973 Value::Object(m)
2974}
2975
2976fn ref_value(r: impl Into<String>) -> Value {
2978 object([("$ref", Value::String(r.into()))])
2979}
2980
2981fn unescape_text(s: &str) -> String {
2983 s.replace("<", "<")
2984 .replace(">", ">")
2985 .replace("&", "&")
2986 .replace("\\_", "_")
2987}
2988
2989fn fnv1a(s: &str) -> u64 {
2992 let mut h: u64 = 0xcbf29ce484222325;
2993 for b in s.bytes() {
2994 h ^= b as u64;
2995 h = h.wrapping_mul(0x100000001b3);
2996 }
2997 h
2998}
2999
3000#[cfg(test)]
3001mod tests {
3002 use crate::{
3003 CaptionParent, ContentLayer, DoclingDocument, ImageMode, Node, PictureImage, Table,
3004 };
3005 use serde_json::Value;
3006
3007 fn cell_at(text: &str, row: usize, col: usize, col_span: usize) -> crate::TableCell {
3008 crate::TableCell {
3009 text: text.into(),
3010 bbox: None,
3011 start_row: row,
3012 start_col: col,
3013 row_span: 1,
3014 col_span,
3015 column_header: row == 0,
3016 row_header: false,
3017 row_section: false,
3018 }
3019 }
3020
3021 #[test]
3027 fn ragged_rows_take_their_grid_from_first_class_cells() {
3028 let cells = vec![
3029 cell_at("h0", 0, 0, 1),
3030 cell_at("h2", 0, 2, 1),
3031 cell_at("wide", 1, 0, 3),
3032 ];
3033 let export = |rows: Vec<Vec<String>>| -> Value {
3034 let mut doc = DoclingDocument::new("t");
3035 doc.push(Node::Table(Table {
3036 rows,
3037 cells: Some(cells.clone()),
3038 ..Table::default()
3039 }));
3040 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3041 v["tables"][0]["data"].clone()
3042 };
3043 let data = export(vec![
3045 vec!["h0".into(), "h2".into()],
3046 vec!["wide".into(), "wide".into(), "wide".into()],
3047 ]);
3048 assert_eq!(data["num_rows"], 2);
3049 assert_eq!(data["num_cols"], 3);
3050 assert_eq!(data["table_cells"].as_array().unwrap().len(), 3);
3051 let row0: Vec<&str> = data["grid"][0]
3052 .as_array()
3053 .unwrap()
3054 .iter()
3055 .map(|c| c["text"].as_str().unwrap())
3056 .collect();
3057 assert_eq!(row0, ["h0", "", "h2"]);
3058 assert_eq!(data["table_cells"][2]["col_span"], 3);
3059 let data = export(vec![
3062 vec!["h0".into(), "h2".into()],
3063 vec!["wide".into(), "wide".into()],
3064 ]);
3065 assert_eq!(data["num_cols"], 2);
3066 assert_eq!(data["grid"][0].as_array().unwrap().len(), 2);
3067 }
3068
3069 fn doc_with_image() -> DoclingDocument {
3070 let mut doc = DoclingDocument::new("t");
3071 doc.push(Node::Picture {
3072 caption: Some("Fig 1".into()),
3073 caption_href: None,
3074 image: Some(PictureImage {
3075 dpi: crate::PictureImage::DEFAULT_DPI,
3076 mimetype: "image/png".into(),
3077 width: 4,
3078 height: 2,
3079 data: b"foobar".to_vec(),
3080 display_size: None,
3081 }),
3082 classification: None,
3083 description: None,
3084 caption_parent: Default::default(),
3085 caption_location: None,
3086 });
3087 doc
3088 }
3089
3090 #[test]
3095 fn notes_layer_items_reach_the_json_but_furniture_does_not() {
3096 let mut doc = DoclingDocument::new("t");
3097 doc.push(Node::Heading {
3098 level: 1,
3099 text: "Slide One".into(),
3100 });
3101 doc.push(Node::Furniture {
3102 layer: ContentLayer::Notes,
3103 inner: Box::new(Node::Located {
3104 location: [0, 0, 0, 0],
3105 inner: Box::new(Node::Paragraph {
3106 text: "Speaker note for slide 1.".into(),
3107 }),
3108 }),
3109 });
3110 doc.push(Node::Furniture {
3111 layer: ContentLayer::Furniture,
3112 inner: Box::new(Node::Paragraph {
3113 text: "page header".into(),
3114 }),
3115 });
3116
3117 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3118 let texts = v["texts"].as_array().unwrap();
3119 assert_eq!(
3120 texts
3121 .iter()
3122 .map(|t| (
3123 t["label"].as_str().unwrap(),
3124 t["content_layer"].as_str().unwrap(),
3125 t["text"].as_str().unwrap()
3126 ))
3127 .collect::<Vec<_>>(),
3128 vec![
3129 ("title", "body", "Slide One"),
3130 ("text", "notes", "Speaker note for slide 1."),
3131 ],
3132 "the note is carried on its own layer; the furniture is not carried"
3133 );
3134 assert_eq!(doc.export_to_markdown(), "# Slide One\n");
3136 }
3137
3138 #[test]
3144 fn continuation_flags_become_spanning_cells() {
3145 let mut doc = DoclingDocument::new("t");
3146 let rows = vec![
3148 vec!["merged".to_string(), "merged".into(), "merged".into()],
3149 vec!["merged".to_string(), "merged".into(), "merged".into()],
3150 vec!["a".to_string(), "b".into(), "c".into()],
3151 ];
3152 doc.push(Node::Table(crate::Table {
3153 rows,
3154 location: None,
3155 structure: Some(crate::TableStructure {
3156 header_row: vec![true, false, false],
3157 col_continuation: vec![
3158 vec![false, true, true],
3159 vec![false, true, true],
3160 vec![false, false, false],
3161 ],
3162 row_continuation: vec![
3163 vec![false, false, false],
3164 vec![true, true, true],
3165 vec![false, false, false],
3166 ],
3167 row_header: Vec::new(),
3168 col_header: Vec::new(),
3169 }),
3170 cell_blocks: None,
3171 cells: None,
3172 caption: None,
3173 caption_parent: Default::default(),
3174 caption_location: None,
3175 }));
3176 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3177 let data = &v["tables"][0]["data"];
3178 assert_eq!(data["num_rows"], 3);
3179 assert_eq!(data["num_cols"], 3);
3180 let cells = data["table_cells"].as_array().unwrap();
3181 assert_eq!(
3182 cells.len(),
3183 4,
3184 "one cell for the range, three for the plain row"
3185 );
3186 assert_eq!(
3187 cells[0],
3188 serde_json::json!({
3189 "row_span": 2, "col_span": 3,
3190 "start_row_offset_idx": 0, "end_row_offset_idx": 2,
3191 "start_col_offset_idx": 0, "end_col_offset_idx": 3,
3192 "text": "merged", "column_header": true, "row_header": false,
3193 "row_section": false, "fillable": false,
3194 })
3195 );
3196 assert_eq!(cells[1]["text"], "a");
3197 assert_eq!(cells[1]["row_span"], 1);
3198 assert_eq!(cells[1]["column_header"], false);
3199 let grid = data["grid"].as_array().unwrap();
3201 assert_eq!(grid.len(), 3);
3202 for (r, row) in grid.iter().take(2).enumerate() {
3203 for (c, cell) in row.as_array().unwrap().iter().enumerate() {
3204 assert_eq!(*cell, cells[0], "grid[{r}][{c}]");
3205 }
3206 }
3207 assert_eq!(grid[2][2]["text"], "c");
3208 }
3209
3210 #[test]
3216 fn exact_provenance_pages_and_chart_captions_follow_docling() {
3217 let mut doc = DoclingDocument::new("t");
3218 doc.push(Node::PageInfo {
3219 page_no: 1,
3220 width: 3.0,
3221 height: 4.0,
3222 });
3223 let table = crate::Table {
3224 rows: vec![vec!["a".to_string(), "b".into()]],
3225 ..Default::default()
3226 };
3227 doc.push(Node::Group {
3228 label: "sheet".into(),
3229 name: Some("Data".into()),
3230 layer: None,
3231 children: vec![
3232 Node::Prov {
3237 page_no: 1,
3238 bbox: [0.0, 0.0, 3.0, 4.0],
3239 charspan: [0, 0],
3240 seq: Some(1),
3241 inner: Box::new(Node::Table(table.clone())),
3242 },
3243 Node::Prov {
3244 page_no: 1,
3245 bbox: [0.0, 1.0, 1.0, 1.0],
3246 charspan: [0, 0],
3247 seq: Some(0),
3248 inner: Box::new(Node::Chart {
3249 kind: "bar_chart".into(),
3250 table,
3251 caption: Some("Sales".into()),
3252 location: Some([0, 128, 170, 128]),
3253 }),
3254 },
3255 ],
3256 });
3257 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3258 assert_eq!(
3259 v["pages"],
3260 serde_json::json!({"1": {"size": {"width": 3.0, "height": 4.0}, "page_no": 1}})
3261 );
3262 assert_eq!(
3263 v["tables"][0]["prov"],
3264 serde_json::json!([{
3265 "page_no": 1,
3266 "bbox": {"l": 0.0, "t": 0.0, "r": 3.0, "b": 4.0, "coord_origin": "TOPLEFT"},
3267 "charspan": [0, 0],
3268 }])
3269 );
3270 assert_eq!(v["tables"][0]["data"]["orientation"], "rot_0");
3271 let sheet = &v["groups"][0];
3274 assert_eq!(
3275 sheet["children"],
3276 serde_json::json!([
3277 {"$ref": "#/tables/0"}, {"$ref": "#/texts/0"}, {"$ref": "#/pictures/0"}
3278 ])
3279 );
3280 let cap = &v["texts"][0];
3281 assert_eq!(cap["label"], "caption");
3282 assert_eq!(cap["parent"], serde_json::json!({"$ref": "#/groups/0"}));
3283 assert_eq!(cap["prov"][0]["charspan"], serde_json::json!([0, 5]));
3284 assert_eq!(cap["prov"][0]["bbox"]["b"], 1.0);
3285 let pic = &v["pictures"][0];
3286 assert_eq!(pic["captions"], serde_json::json!([{"$ref": "#/texts/0"}]));
3287 assert_eq!(pic["prov"][0]["charspan"], serde_json::json!([0, 0]));
3288 assert_eq!(pic["prov"][0]["bbox"]["coord_origin"], "TOPLEFT");
3289 assert_eq!(
3290 pic["meta"]["classification"]["predictions"][0]["class_name"],
3291 "bar_chart"
3292 );
3293 assert_eq!(pic["meta"]["tabular_chart"]["chart_data"]["num_cols"], 2);
3294 }
3295
3296 #[test]
3300 fn a_zero_location_is_a_zero_bbox_not_the_whole_page() {
3301 let mut doc = DoclingDocument::new("t");
3302 doc.push(Node::PageInfo {
3303 page_no: 1,
3304 width: 12192000.0,
3305 height: 6858000.0,
3306 });
3307 doc.push(Node::Furniture {
3308 layer: ContentLayer::Notes,
3309 inner: Box::new(Node::Located {
3310 location: [0, 0, 0, 0],
3311 inner: Box::new(Node::Paragraph {
3312 text: "a note".into(),
3313 }),
3314 }),
3315 });
3316 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3317 let prov = &v["texts"][0]["prov"][0];
3318 assert_eq!(prov["page_no"], 1);
3319 assert_eq!(prov["charspan"], serde_json::json!([0, 6]));
3320 assert_eq!(
3321 prov["bbox"],
3322 serde_json::json!({"l": 0.0, "t": 0.0, "r": 0.0, "b": 0.0, "coord_origin": "BOTTOMLEFT"})
3323 );
3324 assert_eq!(
3326 v["pages"]["1"]["size"],
3327 serde_json::json!({"width": 12192000.0, "height": 6858000.0})
3328 );
3329 }
3330
3331 #[test]
3341 fn captions_and_checkboxes_carry_their_own_prov() {
3342 let mut doc = DoclingDocument::new("t");
3343 doc.push(Node::PageInfo {
3344 page_no: 1,
3345 width: 512.0,
3346 height: 512.0,
3347 });
3348 doc.push(Node::Located {
3349 location: [64, 64, 448, 256],
3350 inner: Box::new(Node::Picture {
3351 caption: Some("Figure 1".into()),
3352 caption_href: None,
3353 image: None,
3354 classification: None,
3355 description: None,
3356 caption_parent: CaptionParent::Item,
3357 caption_location: Some([64, 264, 448, 280]),
3358 }),
3359 });
3360 doc.push(Node::PageInfo {
3361 page_no: 2,
3362 width: 512.0,
3363 height: 512.0,
3364 });
3365 doc.push(Node::Located {
3366 location: [64, 120, 448, 300],
3367 inner: Box::new(Node::Table(Table {
3368 rows: vec![vec!["a".into()]],
3369 caption: Some("Table 1".into()),
3370 caption_parent: CaptionParent::Item,
3371 caption_location: Some([64, 100, 300, 112]),
3372 ..Table::default()
3373 })),
3374 });
3375 doc.push(Node::Located {
3376 location: [64, 320, 200, 332],
3377 inner: Box::new(Node::CheckboxItem {
3378 checked: false,
3379 text: "First option".into(),
3380 }),
3381 });
3382 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3383 let texts = v["texts"].as_array().unwrap();
3384 let by_text = |t: &str| texts.iter().find(|x| x["text"] == t).expect(t).clone();
3385 let bbox = |p: &Value| {
3386 let b = &p["prov"][0]["bbox"];
3387 [&b["l"], &b["t"], &b["r"], &b["b"]].map(|x| x.as_f64().unwrap())
3388 };
3389 let fig = by_text("Figure 1");
3392 assert_eq!(fig["prov"][0]["page_no"], 1);
3393 assert_eq!(bbox(&fig), [64.0, 248.0, 448.0, 232.0]);
3394 assert_eq!(fig["prov"][0]["charspan"], serde_json::json!([0, 8]));
3395 assert_eq!(fig["parent"]["$ref"], "#/pictures/0");
3396 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
3397 let tab = by_text("Table 1");
3399 assert_eq!(tab["prov"][0]["page_no"], 2);
3400 assert_eq!(bbox(&tab), [64.0, 412.0, 300.0, 400.0]);
3401 assert_eq!(v["tables"][0]["prov"][0]["bbox"]["t"], 392.0);
3402 let cb = by_text("First option");
3404 assert_eq!(cb["label"], "checkbox_unselected");
3405 assert_eq!(cb["prov"][0]["page_no"], 2);
3406 assert_eq!(bbox(&cb), [64.0, 192.0, 200.0, 180.0]);
3407 assert!(texts
3408 .iter()
3409 .all(|t| !t["prov"].as_array().unwrap().is_empty()));
3410
3411 let mut plain = DoclingDocument::new("t");
3414 plain.push(Node::PageInfo {
3415 page_no: 1,
3416 width: 512.0,
3417 height: 512.0,
3418 });
3419 plain.push(Node::Located {
3420 location: [64, 64, 448, 256],
3421 inner: Box::new(Node::Picture {
3422 caption: Some("Figure 1".into()),
3423 caption_href: None,
3424 image: None,
3425 classification: None,
3426 description: None,
3427 caption_parent: CaptionParent::Item,
3428 caption_location: None,
3429 }),
3430 });
3431 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
3432 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
3433 assert_eq!(v["pictures"][0]["prov"][0]["bbox"]["t"], 448.0);
3434 }
3435
3436 #[test]
3437 fn page_markers_produce_pages_and_prov() {
3438 let mut doc = DoclingDocument::new("t");
3439 doc.push(Node::PageInfo {
3440 page_no: 1,
3441 width: 512.0,
3442 height: 1024.0,
3443 });
3444 doc.push(Node::Located {
3445 location: [128, 64, 256, 128], inner: Box::new(Node::Paragraph {
3447 text: "hello".into(),
3448 }),
3449 });
3450 doc.push(Node::Table(Table {
3451 rows: vec![vec!["a".into()]],
3452 location: Some([0, 0, 512, 512]),
3453 ..Table::default()
3454 }));
3455 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3456 assert_eq!(v["pages"]["1"]["page_no"], 1);
3457 assert_eq!(v["pages"]["1"]["size"]["width"], 512.0);
3458 assert_eq!(v["pages"]["1"]["size"]["height"], 1024.0);
3459 let prov = &v["texts"][0]["prov"][0];
3462 assert_eq!(prov["page_no"], 1);
3463 assert_eq!(prov["bbox"]["l"], 128.0);
3464 assert_eq!(prov["bbox"]["t"], 896.0);
3465 assert_eq!(prov["bbox"]["r"], 256.0);
3466 assert_eq!(prov["bbox"]["b"], 768.0);
3467 assert_eq!(prov["bbox"]["coord_origin"], "BOTTOMLEFT");
3468 assert_eq!(prov["charspan"][1], 5);
3469 let tprov = &v["tables"][0]["prov"][0];
3471 assert_eq!(tprov["bbox"]["t"], 1024.0);
3472 assert_eq!(tprov["bbox"]["b"], 0.0);
3473 assert_eq!(tprov["charspan"][1], 0);
3474
3475 let mut plain = DoclingDocument::new("t");
3477 plain.push(Node::Located {
3478 location: [1, 2, 3, 4],
3479 inner: Box::new(Node::Paragraph { text: "x".into() }),
3480 });
3481 let v: Value = serde_json::from_str(&plain.export_to_json()).unwrap();
3482 assert_eq!(v["pages"], serde_json::json!({}));
3483 assert_eq!(v["texts"][0]["prov"], serde_json::json!([]));
3484 }
3485
3486 #[test]
3489 fn page_furniture_becomes_furniture_layer_text() {
3490 let mut doc = DoclingDocument::new("t");
3491 doc.push(Node::PageInfo {
3492 page_no: 1,
3493 width: 512.0,
3494 height: 512.0,
3495 });
3496 doc.push(Node::PageFurniture {
3497 footer: false,
3498 location: [10, 0, 100, 20],
3499 text: "Chapter 1".into(),
3500 });
3501 doc.push(Node::Paragraph {
3502 text: "body".into(),
3503 });
3504 doc.push(Node::PageFurniture {
3505 footer: true,
3506 location: [400, 490, 500, 512],
3507 text: "1.10.2".into(),
3508 });
3509 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3510 let texts = v["texts"].as_array().unwrap();
3511 assert_eq!(texts.len(), 3);
3512 assert_eq!(texts[0]["label"], "page_header");
3513 assert_eq!(texts[0]["content_layer"], "furniture");
3514 assert_eq!(texts[0]["parent"]["$ref"], "#/body");
3515 assert_eq!(texts[0]["prov"][0]["bbox"]["l"], 10.0);
3516 assert_eq!(texts[1]["content_layer"], "body");
3517 assert_eq!(texts[1]["prov"], serde_json::json!([]));
3518 assert_eq!(texts[2]["label"], "page_footer");
3519 assert_eq!(texts[2]["text"], "1.10.2");
3520 assert_eq!(texts[2]["content_layer"], "furniture");
3521 }
3522
3523 #[test]
3524 fn picture_image_in_markdown_modes_and_json() {
3525 let doc = doc_with_image();
3526 assert!(doc.export_to_markdown().contains("<!-- image -->"));
3528 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
3530 assert!(
3531 md.contains(""),
3532 "got:\n{md}"
3533 );
3534 assert!(files.is_empty());
3535 let (md, files) = doc.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
3537 assert!(
3538 md.contains(""),
3539 "got:\n{md}"
3540 );
3541 assert_eq!(
3542 files,
3543 vec![("artifacts/image_000000.png".to_string(), b"foobar".to_vec())]
3544 );
3545 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3548 assert_eq!(v["pictures"][0]["image"]["mimetype"], "image/png");
3549 assert_eq!(v["pictures"][0]["image"]["size"]["width"], 4.0);
3550 let keys: Vec<&str> = v["pictures"][0]
3551 .as_object()
3552 .unwrap()
3553 .keys()
3554 .map(String::as_str)
3555 .collect();
3556 assert_eq!(&keys[keys.len() - 2..], ["image", "annotations"]);
3557 assert_eq!(
3558 v["pictures"][0]["image"]["uri"],
3559 "data:image/png;base64,Zm9vYmFy"
3560 );
3561 }
3562
3563 #[test]
3564 fn exports_docling_schema() {
3565 let mut doc = DoclingDocument::new("t");
3566 doc.push(Node::Heading {
3567 level: 1,
3568 text: "Title".into(),
3569 });
3570 doc.push(Node::Heading {
3571 level: 2,
3572 text: "Sec".into(),
3573 });
3574 doc.push(Node::Paragraph {
3575 text: "Body & more".into(),
3576 }); doc.push(Node::ListItem {
3578 ordered: false,
3579 number: 0,
3580 first_in_list: true,
3581 text: "one".into(),
3582 level: 0,
3583 marker: None,
3584 location: None,
3585 dclx: None,
3586 href: None,
3587 layer: None,
3588 });
3589 doc.push(Node::ListItem {
3590 ordered: false,
3591 number: 0,
3592 first_in_list: false,
3593 text: "two".into(),
3594 level: 0,
3595 marker: None,
3596 location: None,
3597 dclx: None,
3598 href: None,
3599 layer: None,
3600 });
3601 doc.push(Node::Table(Table {
3602 rows: vec![vec!["A".into(), "B".into()]],
3603 location: None,
3604 structure: None,
3605 cell_blocks: None,
3606 cells: None,
3607 caption: None,
3608 caption_parent: Default::default(),
3609 caption_location: None,
3610 }));
3611
3612 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3613 assert_eq!(v["schema_name"], "DoclingDocument");
3614 assert_eq!(v["version"], "1.10.0");
3615 assert_eq!(v["texts"][0]["label"], "title");
3616 assert_eq!(v["texts"][1]["label"], "section_header");
3617 assert_eq!(v["texts"][1]["level"], 1); assert_eq!(v["texts"][2]["text"], "Body & more"); assert_eq!(v["groups"][0]["label"], "list");
3621 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
3622 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
3623 assert_eq!(v["texts"][3]["marker"], "-");
3624 assert_eq!(v["tables"][0]["data"]["num_cols"], 2);
3626 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["column_header"], true);
3627 }
3628 #[test]
3632 fn a_layered_group_stamps_its_whole_subtree() {
3633 let doc = DoclingDocument {
3634 name: "s".into(),
3635 nodes: vec![
3636 Node::Group {
3637 label: "sheet".into(),
3638 name: Some("Sheet1".into()),
3639 layer: None,
3640 children: vec![Node::Paragraph {
3641 text: "visible".into(),
3642 }],
3643 },
3644 Node::Group {
3645 label: "sheet".into(),
3646 name: Some("Sheet2".into()),
3647 layer: Some(ContentLayer::Invisible),
3648 children: vec![Node::Paragraph {
3649 text: "hidden".into(),
3650 }],
3651 },
3652 ],
3653 ..DoclingDocument::new("s")
3654 };
3655 let v = crate::json::to_json(&doc);
3656 assert_eq!(v["groups"][0]["label"], "sheet");
3657 assert_eq!(v["groups"][0]["name"], "Sheet1");
3658 assert_eq!(v["groups"][0]["content_layer"], "body");
3659 assert_eq!(v["texts"][0]["content_layer"], "body");
3660 assert_eq!(v["groups"][1]["name"], "Sheet2");
3661 assert_eq!(v["groups"][1]["content_layer"], "invisible");
3662 assert_eq!(v["texts"][1]["content_layer"], "invisible");
3663 assert_eq!(v["groups"][1]["children"][0]["$ref"], "#/texts/1");
3665 assert_eq!(v["body"]["children"][1]["$ref"], "#/groups/1");
3666 }
3667
3668 #[test]
3678 fn deleted_items_comment_refs_and_chart_meta_in_the_tree() {
3679 use crate::tree::{ItemTree, TreeKind};
3680 let mut t = ItemTree::default();
3681 let text = |txt: &str| TreeKind::Text {
3682 label: "text".into(),
3683 text: txt.into(),
3684 orig: None,
3685 formatting: None,
3686 hyperlink: None,
3687 level: None,
3688 list: None,
3689 };
3690 let a = t.add(None, None, text("a"));
3691 let blank = t.add(None, None, text(""));
3692 let b = t.add(None, None, text("b"));
3693 t.delete(blank);
3694 let group = t.add(
3695 None,
3696 Some(ContentLayer::Notes),
3697 TreeKind::Group {
3698 label: "comment_section".into(),
3699 name: "comment-0".into(),
3700 },
3701 );
3702 t.add(Some(group), Some(ContentLayer::Notes), text("note"));
3703 t.items[a].comments.push(group);
3704 t.add(
3705 None,
3706 None,
3707 TreeKind::Picture {
3708 captions: Vec::new(),
3709 image: None,
3710 extra_classes: Vec::new(),
3711 classification: Some("bar_chart".into()),
3712 description: None,
3713 confidence: None,
3714 chart: Some(Table {
3715 rows: vec![vec!["".into(), "s".into()], vec!["c".into(), "1".into()]],
3716 ..Table::default()
3717 }),
3718 dpi: None,
3719 },
3720 );
3721 assert_eq!(t.last_text(), Some(4), "the note; the blank is skipped");
3722 assert_eq!(t.bucket_index(b), 1, "numbered past the deleted item");
3723 let mut doc = DoclingDocument::new("t");
3724 doc.tree = Some(t);
3725 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3726 let texts = v["texts"].as_array().unwrap();
3727 assert_eq!(texts.len(), 3);
3728 assert_eq!(texts[1]["text"], "b");
3729 assert_eq!(texts[1]["self_ref"], "#/texts/1");
3730 assert_eq!(
3731 v["body"]["children"],
3732 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}, {"$ref": "#/groups/0"}, {"$ref": "#/pictures/0"}])
3733 );
3734 let keys: Vec<&str> = texts[0]
3735 .as_object()
3736 .unwrap()
3737 .keys()
3738 .map(String::as_str)
3739 .collect();
3740 assert_eq!(
3741 keys,
3742 vec![
3743 "self_ref",
3744 "parent",
3745 "children",
3746 "content_layer",
3747 "label",
3748 "prov",
3749 "comments",
3750 "orig",
3751 "text"
3752 ]
3753 );
3754 assert_eq!(
3755 texts[0]["comments"],
3756 serde_json::json!([{"$ref": "#/groups/0"}])
3757 );
3758 assert!(texts[1].get("comments").is_none());
3759 let meta = &v["pictures"][0]["meta"];
3760 assert_eq!(
3761 meta["classification"]["predictions"][0]["class_name"],
3762 "bar_chart"
3763 );
3764 assert_eq!(meta["tabular_chart"]["chart_data"]["num_rows"], 2);
3765 }
3766
3767 #[test]
3773 fn tree_items_carry_exact_provenance_and_dpi() {
3774 use crate::tree::{ItemTree, TreeKind, TreeProv};
3775 let text = |label: &str, t: &str| TreeKind::Text {
3776 label: label.into(),
3777 text: t.into(),
3778 orig: None,
3779 formatting: None,
3780 hyperlink: None,
3781 level: None,
3782 list: None,
3783 };
3784 let mut t = ItemTree::default();
3785 let slide = t.add(
3786 None,
3787 None,
3788 TreeKind::Group {
3789 label: "chapter".into(),
3790 name: "slide-0".into(),
3791 },
3792 );
3793 t.add_with_prov(
3794 Some(slide),
3795 None,
3796 text("paragraph", "héllo"),
3797 TreeProv {
3798 page_no: 1,
3799 bbox: [914400.0, 1828800.0, 2743200.0, 457200.0],
3800 bottom_left: true,
3801 charspan: [0, 5],
3802 },
3803 );
3804 t.add_with_prov(
3805 Some(slide),
3806 None,
3807 TreeKind::Picture {
3808 captions: Vec::new(),
3809 image: Some(crate::PictureImage {
3810 dpi: crate::PictureImage::DEFAULT_DPI,
3811 mimetype: "image/png".into(),
3812 width: 2,
3813 height: 2,
3814 data: vec![0],
3815 display_size: None,
3816 }),
3817 extra_classes: Vec::new(),
3818 classification: None,
3819 description: None,
3820 confidence: None,
3821 chart: None,
3822 dpi: Some(300),
3823 },
3824 TreeProv {
3825 page_no: 1,
3826 bbox: [0.0; 4],
3827 bottom_left: false,
3828 charspan: [0, 0],
3829 },
3830 );
3831 t.add(
3832 Some(slide),
3833 Some(ContentLayer::Notes),
3834 text("text", "no geometry"),
3835 );
3836 let mut doc = DoclingDocument::new("t");
3837 doc.push(Node::PageInfo {
3838 page_no: 1,
3839 width: 9144000.0,
3840 height: 6858000.0,
3841 });
3842 doc.tree = Some(t);
3843 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3844 assert_eq!(
3845 v["texts"][0]["prov"],
3846 serde_json::json!([{
3847 "page_no": 1,
3848 "bbox": { "l": 914400.0, "t": 1828800.0, "r": 2743200.0, "b": 457200.0, "coord_origin": "BOTTOMLEFT" },
3849 "charspan": [0, 5],
3850 }])
3851 );
3852 assert_eq!(v["texts"][0]["label"], "paragraph");
3853 assert_eq!(
3854 v["pictures"][0]["prov"][0]["bbox"]["coord_origin"],
3855 "TOPLEFT"
3856 );
3857 assert_eq!(v["pictures"][0]["image"]["dpi"], 300);
3858 assert_eq!(v["texts"][1]["prov"], serde_json::json!([]));
3859 assert_eq!(v["texts"][1]["content_layer"], "notes");
3860 assert_eq!(v["pages"]["1"]["size"]["width"], 9144000.0);
3861 assert_eq!(v["pages"]["1"]["page_no"], 1);
3862 }
3863
3864 #[test]
3869 fn provenance_boxes_are_clamped_to_their_page() {
3870 let mut doc = DoclingDocument::new("t");
3871 doc.push(Node::PageInfo {
3872 page_no: 1,
3873 width: 10.0,
3874 height: 8.0,
3875 });
3876 doc.push(Node::Prov {
3877 page_no: 1,
3878 bbox: [-1.0, 2.0, 12.0, 9.5],
3879 charspan: [0, 1],
3880 seq: None,
3881 inner: Box::new(Node::Paragraph { text: "x".into() }),
3882 });
3883 let mut table = Table {
3884 rows: vec![vec!["a".into()]],
3885 ..Table::default()
3886 };
3887 table.cells = Some(vec![crate::TableCell {
3888 text: "a".into(),
3889 bbox: Some([1.0, 1.0, 11.0, 9.0]),
3890 start_row: 0,
3891 start_col: 0,
3892 row_span: 1,
3893 col_span: 1,
3894 column_header: false,
3895 row_header: false,
3896 row_section: false,
3897 }]);
3898 doc.push(Node::Prov {
3899 page_no: 1,
3900 bbox: [0.0, 0.0, 10.0, 8.0],
3901 charspan: [0, 0],
3902 seq: None,
3903 inner: Box::new(Node::Table(table)),
3904 });
3905 doc.push(Node::Prov {
3906 page_no: 7,
3907 bbox: [-5.0, 0.0, 50.0, 50.0],
3908 charspan: [0, 1],
3909 seq: None,
3910 inner: Box::new(Node::Paragraph { text: "y".into() }),
3911 });
3912 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3913 assert_eq!(
3914 v["texts"][0]["prov"][0]["bbox"],
3915 serde_json::json!({ "l": 0.0, "t": 2.0, "r": 10.0, "b": 8.0, "coord_origin": "TOPLEFT" })
3916 );
3917 let cell = &v["tables"][0]["data"]["table_cells"][0]["bbox"];
3918 assert_eq!(
3919 (cell["l"].as_f64(), cell["r"].as_f64(), cell["b"].as_f64()),
3920 (Some(1.0), Some(10.0), Some(8.0))
3921 );
3922 assert_eq!(v["tables"][0]["data"]["grid"][0][0]["bbox"]["r"], 10.0);
3923 assert_eq!(
3924 v["texts"][1]["prov"][0]["bbox"]["r"], 50.0,
3925 "page 7 is not described"
3926 );
3927 }
3928
3929 #[test]
3936 fn misplaced_list_items_are_wrapped_like_docling_core() {
3937 use crate::tree::{ItemTree, ListMeta, TreeKind};
3938 let mut t = ItemTree::default();
3939 let li = |txt: &str| TreeKind::Text {
3940 label: "list_item".into(),
3941 text: txt.into(),
3942 orig: None,
3943 formatting: None,
3944 hyperlink: None,
3945 level: None,
3946 list: Some(ListMeta {
3947 enumerated: false,
3948 marker: "-".into(),
3949 }),
3950 };
3951 let group = |label: &str, name: &str| TreeKind::Group {
3952 label: label.into(),
3953 name: name.into(),
3954 };
3955 t.add(None, None, li("A"));
3956 t.add(None, None, li("B"));
3957 t.add(
3958 None,
3959 None,
3960 TreeKind::Text {
3961 label: "text".into(),
3962 text: "plain".into(),
3963 orig: None,
3964 formatting: None,
3965 hyperlink: None,
3966 level: None,
3967 list: None,
3968 },
3969 );
3970 let cell = t.add(None, None, group("unspecified", "cell"));
3971 t.add(Some(cell), None, li("C"));
3972 t.add(Some(cell), None, li("D"));
3973 let list = t.add(None, None, group("list", "list"));
3974 t.add(Some(list), None, li("E"));
3975 let doc = DoclingDocument {
3976 tree: Some(t),
3977 ..DoclingDocument::new("t")
3978 };
3979 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
3980 let refs = |x: &Value| -> Vec<String> {
3981 x.as_array()
3982 .unwrap()
3983 .iter()
3984 .map(|r| r["$ref"].as_str().unwrap().to_string())
3985 .collect()
3986 };
3987 assert_eq!(
3988 refs(&v["body"]["children"]),
3989 ["#/groups/4", "#/texts/0", "#/groups/0", "#/groups/1"]
3990 );
3991 assert_eq!(
3992 refs(&v["groups"][0]["children"]),
3993 ["#/groups/3", "#/groups/2"]
3994 );
3995 for (g, name, kids) in [
3996 (2, "group", vec!["#/texts/2"]),
3997 (3, "group", vec!["#/texts/3"]),
3998 (4, "group", vec!["#/texts/4", "#/texts/5"]),
3999 (1, "list", vec!["#/texts/1"]),
4000 ] {
4001 assert_eq!(v["groups"][g]["label"], "list");
4002 assert_eq!(v["groups"][g]["name"], name);
4003 assert_eq!(refs(&v["groups"][g]["children"]), kids);
4004 }
4005 let texts: Vec<&str> = v["texts"]
4006 .as_array()
4007 .unwrap()
4008 .iter()
4009 .map(|t| t["text"].as_str().unwrap())
4010 .collect();
4011 assert_eq!(texts, ["plain", "E", "D", "C", "A", "B"]);
4012 assert_eq!(v["texts"][2]["parent"]["$ref"], "#/groups/2");
4013 }
4014
4015 #[test]
4018 fn a_deleted_child_is_not_written_as_an_empty_ref() {
4019 use crate::tree::{ItemTree, TreeKind};
4020 let mut t = ItemTree::default();
4021 let text = |txt: &str| TreeKind::Text {
4022 label: "text".into(),
4023 text: txt.into(),
4024 orig: None,
4025 formatting: None,
4026 hyperlink: None,
4027 level: None,
4028 list: None,
4029 };
4030 let g = t.add(
4031 None,
4032 None,
4033 TreeKind::Group {
4034 label: "unspecified".into(),
4035 name: "g".into(),
4036 },
4037 );
4038 let gone = t.add(Some(g), None, text(""));
4039 t.add(Some(g), None, text("kept"));
4040 t.items[gone].deleted = true; let doc = DoclingDocument {
4042 tree: Some(t),
4043 ..DoclingDocument::new("t")
4044 };
4045 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4046 assert_eq!(
4047 v["groups"][0]["children"],
4048 serde_json::json!([{"$ref": "#/texts/0"}])
4049 );
4050 }
4051
4052 #[test]
4053 fn a_backend_item_tree_is_written_verbatim() {
4054 use crate::tree::{Formatting, ItemTree, ListMeta, TreeKind};
4055 let mut t = ItemTree::default();
4056 let text = |label: &str, txt: &str| TreeKind::Text {
4057 label: label.into(),
4058 text: txt.into(),
4059 orig: None,
4060 formatting: None,
4061 hyperlink: None,
4062 level: None,
4063 list: None,
4064 };
4065 let title = t.add(None, Some(ContentLayer::Furniture), text("title", "Page"));
4066 let h = t.add(None, None, text("title", "Heading"));
4067 let group = t.add(
4068 Some(h),
4069 None,
4070 TreeKind::Group {
4071 label: "inline".into(),
4072 name: "group".into(),
4073 },
4074 );
4075 t.add(
4076 Some(group),
4077 None,
4078 TreeKind::Text {
4079 label: "text".into(),
4080 text: "bold".into(),
4081 orig: None,
4082 formatting: Some(Formatting {
4083 bold: true,
4084 ..Formatting::default()
4085 }),
4086 hyperlink: Some("https://example.com/".into()),
4087 level: None,
4088 list: None,
4089 },
4090 );
4091 t.add(
4092 Some(group),
4093 None,
4094 TreeKind::Code {
4095 text: "x = 1".into(),
4096 orig: None,
4097 language: Some("python".into()),
4098 formatting: None,
4099 hyperlink: None,
4100 captions: Vec::new(),
4101 },
4102 );
4103 let sub = t.add(
4104 Some(h),
4105 None,
4106 TreeKind::Text {
4107 label: "section_header".into(),
4108 text: "Sub".into(),
4109 orig: Some("Sub\u{2019}".into()),
4110 formatting: None,
4111 hyperlink: None,
4112 level: Some(1),
4113 list: None,
4114 },
4115 );
4116 let item = t.add(
4117 Some(sub),
4118 None,
4119 TreeKind::Text {
4120 label: "list_item".into(),
4121 text: "item".into(),
4122 orig: None,
4123 formatting: None,
4124 hyperlink: None,
4125 level: None,
4126 list: Some(ListMeta {
4127 enumerated: true,
4128 marker: "3.".into(),
4129 }),
4130 },
4131 );
4132 let _region = t.add(
4133 Some(sub),
4134 None,
4135 TreeKind::FieldRegion {
4136 items: vec![crate::FieldItem {
4137 marker: None,
4138 key: Some("Name".into()),
4139 value: Some("Duck".into()),
4140 value_kind: Some("read_only".into()),
4141 }],
4142 },
4143 );
4144 let table = t.add(
4145 Some(sub),
4146 None,
4147 TreeKind::Table {
4148 table: Table {
4149 rows: vec![vec!["a \n<".into(), "b".into()]],
4150 cells: Some(vec![
4151 crate::TableCell {
4152 text: "a \n<".into(),
4153 bbox: None,
4154 start_row: 0,
4155 start_col: 0,
4156 row_span: 3,
4157 col_span: 1,
4158 column_header: false,
4159 row_header: true,
4160 row_section: false,
4161 },
4162 crate::TableCell {
4163 text: "b".into(),
4164 bbox: None,
4165 start_row: 0,
4166 start_col: 1,
4167 row_span: 1,
4168 col_span: 1,
4169 column_header: false,
4170 row_header: false,
4171 row_section: false,
4172 },
4173 ]),
4174 ..Table::default()
4175 },
4176 rich_cells: vec![(0, 1, 0)], captions: Vec::new(),
4178 },
4179 );
4180 let cell_group = t.add(
4181 Some(table),
4182 None,
4183 TreeKind::Group {
4184 label: "unspecified".into(),
4185 name: "rich_cell_group_1_0_0".into(),
4186 },
4187 );
4188 if let TreeKind::Table { rich_cells, .. } = &mut t.items[table].kind {
4189 *rich_cells = vec![(0, 1, cell_group)];
4190 }
4191 let after = t.add(Some(sub), None, text("text", "after the region"));
4192 let _ = (title, after);
4193 let list = t.add(
4197 Some(sub),
4198 None,
4199 TreeKind::Group {
4200 label: "list".into(),
4201 name: "list".into(),
4202 },
4203 );
4204 t.reparent(item, Some(list));
4205
4206 let doc = DoclingDocument {
4207 tree: Some(t),
4208 ..DoclingDocument::new("t")
4209 };
4210 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4211 let texts: Vec<&str> = v["texts"]
4213 .as_array()
4214 .unwrap()
4215 .iter()
4216 .map(|t| t["text"].as_str().unwrap())
4217 .collect();
4218 assert_eq!(
4219 texts,
4220 [
4221 "Page",
4222 "Heading",
4223 "bold",
4224 "x = 1",
4225 "Sub",
4226 "item",
4227 "Name",
4228 "Duck",
4229 "after the region"
4230 ]
4231 );
4232 assert_eq!(
4233 v["body"]["children"],
4234 serde_json::json!([{"$ref": "#/texts/0"}, {"$ref": "#/texts/1"}])
4235 );
4236 assert_eq!(v["texts"][0]["content_layer"], "furniture");
4237 assert_eq!(
4238 v["texts"][1]["children"],
4239 serde_json::json!([{"$ref": "#/groups/0"}, {"$ref": "#/texts/4"}])
4240 );
4241 let bold = &v["texts"][2];
4242 assert_eq!(bold["parent"]["$ref"], "#/groups/0");
4243 let keys: Vec<&str> = bold
4244 .as_object()
4245 .unwrap()
4246 .keys()
4247 .map(String::as_str)
4248 .collect();
4249 assert_eq!(
4250 keys,
4251 [
4252 "self_ref",
4253 "parent",
4254 "children",
4255 "content_layer",
4256 "label",
4257 "prov",
4258 "orig",
4259 "text",
4260 "formatting",
4261 "hyperlink"
4262 ]
4263 );
4264 assert_eq!(
4265 bold["formatting"],
4266 serde_json::json!({"bold": true, "italic": false, "underline": false, "strikethrough": false, "script": "baseline"})
4267 );
4268 let code = &v["texts"][3];
4269 assert_eq!(code["label"], "code");
4270 assert_eq!(code["code_language"], "Python");
4271 let sub = &v["texts"][4];
4272 assert_eq!(sub["orig"], "Sub\u{2019}");
4273 assert_eq!(sub["level"], 1);
4274 let item = &v["texts"][5];
4275 let keys: Vec<&str> = item
4276 .as_object()
4277 .unwrap()
4278 .keys()
4279 .map(String::as_str)
4280 .collect();
4281 assert_eq!(
4282 keys,
4283 [
4284 "self_ref",
4285 "parent",
4286 "children",
4287 "content_layer",
4288 "label",
4289 "prov",
4290 "orig",
4291 "text",
4292 "enumerated",
4293 "marker"
4294 ]
4295 );
4296 assert_eq!(item["marker"], "3.");
4297 assert_eq!(v["texts"][7]["kind"], "read_only");
4298 assert_eq!(v["field_regions"][0]["parent"]["$ref"], "#/texts/4");
4299 let table = &v["tables"][0];
4300 assert_eq!(
4301 table["children"],
4302 serde_json::json!([{"$ref": "#/groups/1"}])
4303 );
4304 let cells = table["data"]["table_cells"].as_array().unwrap();
4305 assert_eq!(
4306 cells[0]["text"], "a \n<",
4307 "raw cell text is written verbatim"
4308 );
4309 assert_eq!(
4310 cells[0]["end_row_offset_idx"], 3,
4311 "declared spans are not clamped"
4312 );
4313 assert_eq!(cells[1]["ref"], serde_json::json!({"$ref": "#/groups/1"}));
4314 assert!(cells[0].get("ref").is_none());
4315 assert!(
4316 table["data"]["grid"][0][1].get("ref").is_none(),
4317 "the grid shows plain cells"
4318 );
4319 assert_eq!(v["groups"][1]["name"], "rich_cell_group_1_0_0");
4320 }
4321
4322 #[test]
4326 fn a_comment_section_can_be_referenced_by_its_note_text() {
4327 let doc = DoclingDocument {
4328 name: "c".into(),
4329 nodes: vec![
4330 Node::Commented {
4331 comments: vec![0],
4332 inner: Box::new(Node::Paragraph {
4333 text: "annotated".into(),
4334 }),
4335 },
4336 Node::CommentSection {
4337 name: "comment-Sheet1-A1".into(),
4338 text: "[author: A]: note".into(),
4339 refs_note_text: true,
4340 grouped: true,
4341 },
4342 ],
4343 ..DoclingDocument::new("c")
4344 };
4345 let v = crate::json::to_json(&doc);
4346 assert_eq!(v["groups"][0]["name"], "comment-Sheet1-A1");
4347 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/texts/1");
4348 }
4349
4350 #[test]
4353 fn a_comment_thread_is_one_child_of_its_parent() {
4354 let note = |text: &str| Node::CommentSection {
4355 name: "comment-Sheet1-F7".into(),
4356 text: text.into(),
4357 refs_note_text: true,
4358 grouped: true,
4359 };
4360 let doc = DoclingDocument {
4361 name: "c".into(),
4362 nodes: vec![note("[author: A]: first"), note("[author: B]: reply")],
4363 ..DoclingDocument::new("c")
4364 };
4365 let v = crate::json::to_json(&doc);
4366 assert_eq!(v["body"]["children"].as_array().unwrap().len(), 1);
4367 assert_eq!(v["groups"][0]["children"].as_array().unwrap().len(), 2);
4368 }
4369
4370 #[test]
4374 fn comment_sections_link_back_to_their_items() {
4375 let doc = DoclingDocument {
4376 name: "c".into(),
4377 nodes: vec![
4378 Node::Commented {
4379 comments: vec![0],
4380 inner: Box::new(Node::Paragraph {
4381 text: "annotated".into(),
4382 }),
4383 },
4384 Node::Paragraph {
4385 text: "plain".into(),
4386 },
4387 Node::CommentSection {
4388 name: "comment-7".into(),
4389 text: "[time: t]: note".into(),
4390 refs_note_text: false,
4391 grouped: true,
4392 },
4393 ],
4394 ..DoclingDocument::new("c")
4395 };
4396 let v = crate::json::to_json(&doc);
4397 assert_eq!(v["groups"][0]["label"], "comment_section");
4399 assert_eq!(v["groups"][0]["name"], "comment-7");
4400 assert_eq!(v["groups"][0]["content_layer"], "notes");
4401 assert_eq!(v["groups"][0]["children"][0]["$ref"], "#/texts/2");
4402 assert_eq!(v["texts"][2]["content_layer"], "notes");
4403 assert_eq!(v["texts"][0]["comments"][0]["$ref"], "#/groups/0");
4405 assert!(v["texts"][1].get("comments").is_none());
4406 let keys: Vec<&str> = v["texts"][0]
4408 .as_object()
4409 .unwrap()
4410 .keys()
4411 .map(String::as_str)
4412 .collect();
4413 assert_eq!(
4414 &keys[keys.len() - 4..],
4415 &["prov", "comments", "orig", "text"]
4416 );
4417 }
4418
4419 fn picture(caption: &str, caption_parent: CaptionParent) -> Node {
4420 Node::Picture {
4421 caption: Some(caption.into()),
4422 caption_href: None,
4423 image: None,
4424 classification: None,
4425 description: None,
4426 caption_parent,
4427 caption_location: None,
4428 }
4429 }
4430
4431 fn group(children: Vec<Node>) -> Node {
4432 Node::Group {
4433 label: "section".into(),
4434 name: None,
4435 layer: None,
4436 children,
4437 }
4438 }
4439
4440 fn refs(v: &Value) -> Vec<&str> {
4441 v.as_array()
4442 .unwrap()
4443 .iter()
4444 .map(|r| r["$ref"].as_str().unwrap())
4445 .collect()
4446 }
4447
4448 #[test]
4453 fn a_body_caption_follows_the_enclosing_top_level_item() {
4454 let mut doc = DoclingDocument::new("t");
4455 doc.push(picture("top", CaptionParent::Body));
4456 doc.push(group(vec![
4457 Node::Paragraph { text: "p".into() },
4458 picture("nested", CaptionParent::Body),
4459 ]));
4460 doc.push(Node::Paragraph {
4461 text: "after".into(),
4462 });
4463 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4464 assert_eq!(
4465 refs(&v["body"]["children"]),
4466 [
4467 "#/texts/0",
4468 "#/pictures/0",
4469 "#/groups/0",
4470 "#/texts/2",
4471 "#/texts/3"
4472 ]
4473 );
4474 assert_eq!(
4475 refs(&v["groups"][0]["children"]),
4476 ["#/texts/1", "#/pictures/1"]
4477 );
4478 for (cap, pic) in [(0, 0), (2, 1)] {
4479 assert_eq!(v["texts"][cap]["label"], "caption");
4480 assert_eq!(v["texts"][cap]["parent"]["$ref"], "#/body");
4481 assert_eq!(
4482 refs(&v["pictures"][pic]["captions"]),
4483 [format!("#/texts/{cap}")]
4484 );
4485 assert_eq!(v["pictures"][pic]["children"], serde_json::json!([]));
4486 }
4487 }
4488
4489 #[test]
4492 fn an_item_caption_is_the_items_first_child() {
4493 let mut doc = DoclingDocument::new("t");
4494 doc.push(picture("fig", CaptionParent::Item));
4495 doc.push(Node::Table(Table {
4496 rows: vec![vec!["a".into()]],
4497 caption: Some("tab".into()),
4498 caption_parent: CaptionParent::Item,
4499 ..Table::default()
4500 }));
4501 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4502 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/tables/0"]);
4503 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/pictures/0");
4504 assert_eq!(refs(&v["pictures"][0]["children"]), ["#/texts/0"]);
4505 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
4506 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/tables/0");
4507 assert_eq!(refs(&v["tables"][0]["children"]), ["#/texts/1"]);
4508 assert_eq!(refs(&v["tables"][0]["captions"]), ["#/texts/1"]);
4509 }
4510
4511 #[test]
4517 fn picture_children_hang_off_the_picture_after_its_caption() {
4518 let mut doc = DoclingDocument::new("t");
4519 doc.push(picture("fig", CaptionParent::Item));
4520 doc.push(Node::PictureChildren(vec![
4521 Node::Heading {
4522 level: 2,
4523 text: "in-figure title".into(),
4524 },
4525 Node::Paragraph {
4526 text: "axis label".into(),
4527 },
4528 Node::ListItem {
4529 ordered: false,
4530 number: 0,
4531 first_in_list: true,
4532 text: "callout".into(),
4533 level: 0,
4534 marker: None,
4535 location: None,
4536 dclx: None,
4537 href: None,
4538 layer: None,
4539 },
4540 ]));
4541 doc.push(Node::Paragraph {
4542 text: "after".into(),
4543 });
4544 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4545 assert_eq!(refs(&v["body"]["children"]), ["#/pictures/0", "#/texts/4"]);
4546 assert_eq!(
4547 refs(&v["pictures"][0]["children"]),
4548 ["#/texts/0", "#/texts/1", "#/texts/2", "#/groups/0"]
4549 );
4550 assert_eq!(refs(&v["pictures"][0]["captions"]), ["#/texts/0"]);
4551 assert_eq!(v["texts"][1]["label"], "section_header");
4552 assert_eq!(v["texts"][2]["label"], "text");
4553 for t in 1..=2 {
4554 assert_eq!(v["texts"][t]["parent"]["$ref"], "#/pictures/0");
4555 assert_eq!(v["texts"][t]["content_layer"], "body");
4556 }
4557 assert_eq!(v["groups"][0]["parent"]["$ref"], "#/pictures/0");
4558 assert_eq!(v["texts"][3]["parent"]["$ref"], "#/groups/0");
4559 let md = doc.export_to_markdown();
4560 assert!(
4561 !md.contains("axis label") && !md.contains("callout"),
4562 "{md}"
4563 );
4564 assert!(md.contains("after"), "{md}");
4565 }
4566
4567 #[test]
4571 fn a_container_caption_is_the_items_sibling() {
4572 let mut doc = DoclingDocument::new("t");
4573 doc.push(group(vec![
4574 picture("chart", CaptionParent::Container),
4575 Node::Table(Table {
4576 rows: vec![vec!["a".into()]],
4577 caption: Some("figcaption".into()),
4578 caption_parent: CaptionParent::ContainerAfter,
4579 ..Table::default()
4580 }),
4581 ]));
4582 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4583 assert_eq!(refs(&v["body"]["children"]), ["#/groups/0"]);
4584 assert_eq!(
4585 refs(&v["groups"][0]["children"]),
4586 ["#/texts/0", "#/pictures/0", "#/tables/0", "#/texts/1"]
4587 );
4588 assert_eq!(v["texts"][0]["parent"]["$ref"], "#/groups/0");
4589 assert_eq!(v["texts"][1]["parent"]["$ref"], "#/groups/0");
4590 assert_eq!(v["pictures"][0]["children"], serde_json::json!([]));
4591 assert_eq!(v["tables"][0]["children"], serde_json::json!([]));
4592 }
4593
4594 #[test]
4599 fn a_labeled_text_keeps_its_label_and_hyperlink_out_of_the_text() {
4600 let mut doc = DoclingDocument::new("t");
4601 doc.push(Node::LabeledText {
4602 label: "footnote".into(),
4603 text: "1 https://example.com/a\\_b".into(),
4604 href: Some("https://example.com/a_b".into()),
4605 });
4606 doc.push(Node::LabeledText {
4607 label: "footnote".into(),
4608 text: "© 2022 the authors".into(),
4609 href: None,
4610 });
4611 let v: Value = serde_json::from_str(&doc.export_to_json()).unwrap();
4612 let t = &v["texts"][0];
4613 assert_eq!(t["label"], "footnote");
4614 assert_eq!(t["text"], "1 https://example.com/a_b");
4615 assert_eq!(t["orig"], "1 https://example.com/a_b");
4616 assert_eq!(t["hyperlink"], "https://example.com/a_b");
4617 assert_eq!(v["texts"][1]["label"], "footnote");
4618 assert!(v["texts"][1].get("hyperlink").is_none());
4619 assert_eq!(
4620 doc.export_to_markdown(),
4621 "[1 https://example.com/a\\_b](https://example.com/a_b)\n\n© 2022 the authors\n"
4622 );
4623 assert_eq!(
4624 doc.export_to_text(),
4625 "1 https://example.com/a_b\n\n© 2022 the authors"
4626 );
4627 }
4628}