use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::{LinkOccurrenceRef, ReferenceOwnerRef};
use crate::{
ContentLocation, ContentLocationRef, DocumentReference, EntryOwnerLocationRef, FragmentAlias,
LinkTarget, NodeId, SourceSpan,
};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "kebab-case")]
pub enum ReferenceTargetType {
Document,
Manual,
Local,
External,
Email,
}
impl ReferenceTargetType {
#[must_use]
pub const fn of(target: &LinkTarget) -> Self {
match target {
LinkTarget::Document { .. } => Self::Document,
LinkTarget::Manual { .. } => Self::Manual,
LinkTarget::Section { .. } => Self::Local,
LinkTarget::External { .. } => Self::External,
LinkTarget::Email { .. } => Self::Email,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReferenceLinkFilter(u8);
impl ReferenceLinkFilter {
pub const NONE: Self = Self(0);
pub const ALL: Self = Self(31);
pub const DOCUMENTS: Self = Self(3);
#[must_use]
pub fn from_types(types: &[ReferenceTargetType]) -> Self {
Self(types.iter().fold(0, |bits, kind| bits | (1 << *kind as u8)))
}
#[must_use]
pub const fn contains(self, target: &LinkTarget) -> bool {
self.0 & (1 << ReferenceTargetType::of(target) as u8) != 0
}
}
#[derive(Debug, Clone, Copy)]
pub struct NavigationScanOptions {
pub links: ReferenceLinkFilter,
pub targets: bool,
pub entry_sets: bool,
}
impl Default for NavigationScanOptions {
fn default() -> Self {
Self {
links: ReferenceLinkFilter::ALL,
targets: false,
entry_sets: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(
tag = "kind",
rename_all = "kebab-case",
rename_all_fields = "camelCase",
deny_unknown_fields
)]
pub enum ContentReveal {
Document {},
Section {
sections: Vec<u32>,
},
Owner {
sections: Vec<u32>,
blocks: Vec<crate::ContentBlockStep>,
item_index: u32,
},
Inline {
location: ContentLocation,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentRevealRef<'a> {
Document,
Section(&'a [u32]),
Owner(EntryOwnerLocationRef<'a>),
Inline(ContentLocationRef<'a>),
}
impl ContentReveal {
#[must_use]
pub fn as_ref(&self) -> ContentRevealRef<'_> {
match self {
Self::Document {} => ContentRevealRef::Document,
Self::Section { sections } => ContentRevealRef::Section(sections),
Self::Owner {
sections,
blocks,
item_index,
} => ContentRevealRef::Owner(EntryOwnerLocationRef {
sections,
blocks,
item_index: *item_index,
}),
Self::Inline { location } => ContentRevealRef::Inline(location.as_ref()),
}
}
}
impl ContentRevealRef<'_> {
#[must_use]
pub fn depth(self) -> usize {
match self {
Self::Document => 0,
Self::Section(sections) => sections.len(),
Self::Owner(owner) => owner
.sections
.len()
.saturating_add(owner.blocks.len())
.saturating_add(1),
Self::Inline(location) => location.depth(),
}
}
#[must_use]
pub fn encoded_size_bound(self) -> usize {
match self {
Self::Document => r#"{"kind":"document"}"#.len(),
Self::Section(sections) => {
ContentLocationRef::SectionHeading {
sections,
path: &[],
}
.encoded_len()
- "-heading".len()
- ",\"path\":[]".len()
}
Self::Owner(owner) => {
let raw = ContentLocationRef::Content {
sections: owner.sections,
blocks: owner.blocks,
root: crate::ContentInlineRoot::Inlines,
path: &[],
}
.encoded_len();
raw - "content".len() + "owner".len()
- ",\"root\":{\"kind\":\"inlines\"},\"path\":[]".len()
+ ",\"itemIndex\":".len()
+ owner
.item_index
.checked_ilog10()
.map_or(1, |n| n as usize + 1)
}
Self::Inline(location) => location
.encoded_len()
.saturating_add(r#"{"kind":"inline","location":}"#.len()),
}
}
#[must_use]
pub fn to_owned(self) -> Option<ContentReveal> {
if self.depth() > crate::MAX_CONTENT_DEPTH
|| self.encoded_size_bound() > crate::MAX_CONTENT_LOCATION_BYTES
{
return None;
}
Some(match self {
Self::Document => ContentReveal::Document {},
Self::Section(sections) => ContentReveal::Section {
sections: sections.to_vec(),
},
Self::Owner(owner) => ContentReveal::Owner {
sections: owner.sections.to_vec(),
blocks: owner.blocks.to_vec(),
item_index: owner.item_index,
},
Self::Inline(location) => ContentReveal::Inline {
location: location.to_owned()?,
},
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct NavigationTargetRef<'ir, 'path> {
pub id: &'ir NodeId,
pub aliases: &'ir [FragmentAlias],
pub reveal: ContentRevealRef<'path>,
}
#[derive(Debug, Clone, Copy)]
pub struct EntrySetReferenceRef<'ir, 'path> {
pub reference: &'ir DocumentReference,
pub owner: ReferenceOwnerRef<'ir, 'path>,
pub source: Option<SourceSpan>,
}
#[derive(Debug, Clone, Copy)]
pub enum NavigationEvent<'ir, 'path> {
Link(LinkOccurrenceRef<'ir, 'path>),
Target(NavigationTargetRef<'ir, 'path>),
EntrySet(EntrySetReferenceRef<'ir, 'path>),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reveal_sizes_match_closed_wire_and_do_not_reject_deep_sections() {
let sections = vec![u32::MAX; 200];
let blocks = [crate::ContentBlockStep::Block { index: 123 }];
for reveal in [
ContentRevealRef::Document,
ContentRevealRef::Section(§ions),
ContentRevealRef::Owner(EntryOwnerLocationRef {
sections: &[2],
blocks: &blocks,
item_index: 12,
}),
ContentRevealRef::Inline(ContentLocationRef::DocumentHeading { path: &[1] }),
] {
let owned = reveal.to_owned().unwrap();
assert_eq!(
reveal.encoded_size_bound(),
serde_json::to_vec(&owned).unwrap().len()
);
}
}
}