use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub const MAX_CONTENT_DEPTH: usize = 256;
pub const MAX_CONTENT_LOCATION_BYTES: usize = 8 * 1024;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum ContentBlockStep {
ListItem {
index: u32,
},
DefinitionItem {
index: u32,
},
TableCell {
row: u32,
column: u32,
},
Block {
index: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, JsonSchema)]
#[serde(
tag = "kind",
rename_all = "kebab-case",
rename_all_fields = "camelCase",
deny_unknown_fields
)]
pub enum ContentInlineRoot {
Inlines,
DefinitionTerm {
item_index: u32,
term_index: u32,
},
}
#[derive(Deserialize)]
#[serde(
tag = "kind",
rename_all = "kebab-case",
rename_all_fields = "camelCase",
deny_unknown_fields
)]
enum ClosedInlineRoot {
Inlines {},
DefinitionTerm { item_index: u32, term_index: u32 },
}
impl<'de> Deserialize<'de> for ContentInlineRoot {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(match ClosedInlineRoot::deserialize(deserializer)? {
ClosedInlineRoot::Inlines {} => Self::Inlines,
ClosedInlineRoot::DefinitionTerm {
item_index,
term_index,
} => Self::DefinitionTerm {
item_index,
term_index,
},
})
}
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum ContentLocation {
DocumentHeading {
path: Vec<u32>,
},
SectionHeading {
sections: Vec<u32>,
path: Vec<u32>,
},
Content {
sections: Vec<u32>,
blocks: Vec<ContentBlockStep>,
root: ContentInlineRoot,
path: Vec<u32>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentLocationRef<'a> {
DocumentHeading {
path: &'a [u32],
},
SectionHeading {
sections: &'a [u32],
path: &'a [u32],
},
Content {
sections: &'a [u32],
blocks: &'a [ContentBlockStep],
root: ContentInlineRoot,
path: &'a [u32],
},
}
impl ContentLocation {
#[must_use]
pub fn as_ref(&self) -> ContentLocationRef<'_> {
match self {
Self::DocumentHeading { path } => ContentLocationRef::DocumentHeading { path },
Self::SectionHeading { sections, path } => {
ContentLocationRef::SectionHeading { sections, path }
}
Self::Content {
sections,
blocks,
root,
path,
} => ContentLocationRef::Content {
sections,
blocks,
root: *root,
path,
},
}
}
}
impl ContentLocationRef<'_> {
#[must_use]
pub fn depth(self) -> usize {
match self {
Self::DocumentHeading { path } => path.len(),
Self::SectionHeading { sections, path } => sections.len().saturating_add(path.len()),
Self::Content {
sections,
blocks,
path,
..
} => sections
.len()
.saturating_add(blocks.len())
.saturating_add(path.len()),
}
}
#[must_use]
pub fn encoded_len(self) -> usize {
fn indices(values: &[u32]) -> usize {
2 + values.iter().map(|n| digits(*n)).sum::<usize>() + values.len().saturating_sub(1)
}
fn steps(values: &[ContentBlockStep]) -> usize {
2 + values
.iter()
.map(|step| match step {
ContentBlockStep::Block { index } => {
"{\"kind\":\"block\",\"index\":}".len() + digits(*index)
}
ContentBlockStep::ListItem { index } => {
"{\"kind\":\"list-item\",\"index\":}".len() + digits(*index)
}
ContentBlockStep::DefinitionItem { index } => {
"{\"kind\":\"definition-item\",\"index\":}".len() + digits(*index)
}
ContentBlockStep::TableCell { row, column } => {
"{\"kind\":\"table-cell\",\"row\":,\"column\":}".len()
+ digits(*row)
+ digits(*column)
}
})
.sum::<usize>()
+ values.len().saturating_sub(1)
}
match self {
Self::DocumentHeading { path } => {
"{\"kind\":\"document-heading\",\"path\":}".len() + indices(path)
}
Self::SectionHeading { sections, path } => {
"{\"kind\":\"section-heading\",\"sections\":,\"path\":}".len()
+ indices(sections)
+ indices(path)
}
Self::Content {
sections,
blocks,
root,
path,
} => {
let root = match root {
ContentInlineRoot::Inlines => "{\"kind\":\"inlines\"}".len(),
ContentInlineRoot::DefinitionTerm {
item_index,
term_index,
} => {
"{\"kind\":\"definition-term\",\"itemIndex\":,\"termIndex\":}".len()
+ digits(item_index)
+ digits(term_index)
}
};
"{\"kind\":\"content\",\"sections\":,\"blocks\":,\"root\":,\"path\":}".len()
+ indices(sections)
+ steps(blocks)
+ root
+ indices(path)
}
}
}
#[must_use]
pub fn to_owned(self) -> Option<ContentLocation> {
if !self.within_limits() {
return None;
}
Some(match self {
Self::DocumentHeading { path } => ContentLocation::DocumentHeading {
path: path.to_vec(),
},
Self::SectionHeading { sections, path } => ContentLocation::SectionHeading {
sections: sections.to_vec(),
path: path.to_vec(),
},
Self::Content {
sections,
blocks,
root,
path,
} => ContentLocation::Content {
sections: sections.to_vec(),
blocks: blocks.to_vec(),
root,
path: path.to_vec(),
},
})
}
pub(super) fn within_limits(self) -> bool {
self.depth() <= MAX_CONTENT_DEPTH && self.encoded_len() <= MAX_CONTENT_LOCATION_BYTES
}
}
fn digits(value: u32) -> usize {
value.checked_ilog10().unwrap_or(0) as usize + 1
}