use crate::{ContentLayer, FieldItem, PictureImage, Script, Table};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Formatting {
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
pub script: Script,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ListMeta {
pub enumerated: bool,
pub marker: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TreeKind {
Text {
label: String,
text: String,
orig: Option<String>,
formatting: Option<Formatting>,
hyperlink: Option<String>,
level: Option<u8>,
list: Option<ListMeta>,
},
Code {
text: String,
orig: Option<String>,
language: Option<String>,
formatting: Option<Formatting>,
hyperlink: Option<String>,
},
Group { label: String, name: String },
Table {
table: Table,
rich_cells: Vec<(usize, usize, usize)>,
captions: Vec<usize>,
},
Picture {
captions: Vec<usize>,
image: Option<PictureImage>,
classification: Option<String>,
chart: Option<Table>,
dpi: Option<u32>,
},
FieldRegion { items: Vec<FieldItem> },
}
#[derive(Debug, Clone, PartialEq)]
pub struct TreeProv {
pub page_no: usize,
pub bbox: [f64; 4],
pub bottom_left: bool,
pub charspan: [usize; 2],
}
#[derive(Debug, Clone, PartialEq)]
pub struct TreeTrack {
pub start_time: f64,
pub end_time: f64,
pub identifier: Option<String>,
pub voice: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TreeItem {
pub parent: Option<usize>,
pub children: Vec<usize>,
pub layer: Option<ContentLayer>,
pub kind: TreeKind,
pub prov: Option<TreeProv>,
pub comments: Vec<usize>,
pub source: Option<TreeTrack>,
pub deleted: bool,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ItemTree {
pub items: Vec<TreeItem>,
pub body: Vec<usize>,
}
impl ItemTree {
pub fn add(
&mut self,
parent: Option<usize>,
layer: Option<ContentLayer>,
kind: TreeKind,
) -> usize {
let id = self.items.len();
self.items.push(TreeItem {
parent,
children: Vec::new(),
layer,
kind,
prov: None,
comments: Vec::new(),
source: None,
deleted: false,
});
match parent {
Some(p) => self.items[p].children.push(id),
None => self.body.push(id),
}
id
}
pub fn add_with_prov(
&mut self,
parent: Option<usize>,
layer: Option<ContentLayer>,
kind: TreeKind,
prov: TreeProv,
) -> usize {
let id = self.add(parent, layer, kind);
self.items[id].prov = Some(prov);
id
}
pub fn append(&mut self, other: ItemTree) {
let off = self.items.len();
let shift = |i: usize| i + off;
for mut item in other.items {
item.parent = item.parent.map(shift);
for c in item.children.iter_mut().chain(item.comments.iter_mut()) {
*c = shift(*c);
}
match &mut item.kind {
TreeKind::Table {
rich_cells,
captions,
..
} => {
for (_, _, g) in rich_cells.iter_mut() {
*g = shift(*g);
}
for c in captions.iter_mut() {
*c = shift(*c);
}
}
TreeKind::Picture { captions, .. } => {
for c in captions.iter_mut() {
*c = shift(*c);
}
}
_ => {}
}
self.items.push(item);
}
self.body.extend(other.body.into_iter().map(shift));
}
pub fn reparent(&mut self, id: usize, new_parent: Option<usize>) {
let old = self.items[id].parent;
let siblings = match old {
Some(p) => &mut self.items[p].children,
None => &mut self.body,
};
siblings.retain(|&c| c != id);
self.items[id].parent = new_parent;
match new_parent {
Some(p) => self.items[p].children.push(id),
None => self.body.push(id),
}
}
pub fn delete(&mut self, id: usize) {
match self.items[id].parent {
Some(p) => self.items[p].children.retain(|&c| c != id),
None => self.body.retain(|&c| c != id),
}
self.items[id].deleted = true;
}
pub fn last_text(&self) -> Option<usize> {
self.items.iter().rposition(|it| {
!it.deleted && matches!(it.kind, TreeKind::Text { .. } | TreeKind::Code { .. })
})
}
pub fn bucket_index(&self, id: usize) -> usize {
let same = |k: &TreeKind| {
std::mem::discriminant(k) == std::mem::discriminant(&self.items[id].kind)
|| matches!(
(k, &self.items[id].kind),
(TreeKind::Text { .. }, TreeKind::Code { .. })
| (TreeKind::Code { .. }, TreeKind::Text { .. })
)
};
self.items[..id]
.iter()
.filter(|it| !it.deleted && same(&it.kind))
.count()
}
pub fn table_count(&self) -> usize {
self.items
.iter()
.filter(|it| !it.deleted && matches!(it.kind, TreeKind::Table { .. }))
.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn text(t: &str) -> TreeKind {
TreeKind::Text {
label: "text".into(),
text: t.into(),
orig: None,
formatting: None,
hyperlink: None,
level: None,
list: None,
}
}
#[test]
fn add_and_reparent_keep_docling_children_order() {
let mut t = ItemTree::default();
let title = t.add(None, None, text("Title"));
let a = t.add(Some(title), None, text("a"));
let b = t.add(Some(title), None, text("b"));
let table = t.add(
Some(title),
None,
TreeKind::Table {
table: Table::default(),
rich_cells: Vec::new(),
captions: Vec::new(),
},
);
let group = t.add(
Some(table),
None,
TreeKind::Group {
label: "unspecified".into(),
name: "rich_cell_group_1_0_0".into(),
},
);
assert_eq!(t.body, vec![title]);
assert_eq!(t.items[title].children, vec![a, b, table]);
t.reparent(a, Some(group));
assert_eq!(t.items[title].children, vec![b, table]);
assert_eq!(t.items[group].children, vec![a]);
assert_eq!(t.items[a].parent, Some(group));
assert_eq!(t.table_count(), 1);
let code = t.add(
None,
None,
TreeKind::Code {
text: "x".into(),
orig: None,
language: None,
formatting: None,
hyperlink: None,
},
);
assert_eq!(t.bucket_index(code), 3, "title, a, b precede it in `texts`");
assert_eq!(t.bucket_index(group), 0);
assert_eq!(t.body, vec![title, code]);
}
#[test]
fn append_renumbers_a_fragment_into_creation_order() {
let mut whole = ItemTree::default();
let slide0 = whole.add(
None,
None,
TreeKind::Group {
label: "chapter".into(),
name: "slide-0".into(),
},
);
whole.add(Some(slide0), None, text("first"));
let mut frag = ItemTree::default();
let slide1 = frag.add(
None,
None,
TreeKind::Group {
label: "chapter".into(),
name: "slide-1".into(),
},
);
let cap = frag.add_with_prov(
Some(slide1),
None,
TreeKind::Text {
label: "caption".into(),
text: "Title".into(),
orig: None,
formatting: None,
hyperlink: None,
level: None,
list: None,
},
TreeProv {
page_no: 2,
bbox: [1.0, 2.0, 3.0, 4.0],
bottom_left: true,
charspan: [0, 5],
},
);
let pic = frag.add(
Some(slide1),
None,
TreeKind::Picture {
captions: vec![cap],
image: None,
classification: Some("bar_chart".into()),
chart: None,
dpi: None,
},
);
let note = frag.add(
None,
Some(ContentLayer::Notes),
TreeKind::Group {
label: "comment_section".into(),
name: "comment-slide2-1".into(),
},
);
frag.items[pic].comments.push(note);
whole.append(frag);
assert_eq!(whole.body, vec![slide0, 2, 5]);
assert_eq!(whole.items[2].children, vec![3, 4]);
assert_eq!(whole.items[3].parent, Some(2));
assert_eq!(whole.items[3].prov.as_ref().map(|p| p.page_no), Some(2));
assert!(
matches!(&whole.items[4].kind, TreeKind::Picture { captions, .. } if captions == &[3])
);
assert_eq!(whole.items[4].comments, vec![5]);
assert_eq!(whole.items[5].parent, None);
assert_eq!(
whole.bucket_index(4),
0,
"the fragment's picture is #/pictures/0"
);
assert_eq!(
whole.bucket_index(5),
2,
"slide-0, slide-1 precede it in `groups`"
);
}
}