use crate::{
Block, DefinitionItem, Inline,
visit::{self, Visit},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct DeclarationGroup {
pub start_item: usize,
pub end_item: usize,
}
impl DeclarationGroup {
#[must_use]
pub fn resolve(self, items: &[DefinitionItem]) -> Option<&[DefinitionItem]> {
if self.end_item.checked_sub(self.start_item)? < 2 {
return None;
}
let members = items.get(self.start_item..self.end_item)?;
let (tail, heads) = members.split_last()?;
(members
.iter()
.all(|item| item.terms.iter().flatten().any(readable_inline))
&& blocks_have_readable_content(&tail.description)
&& heads
.iter()
.all(|head| !blocks_have_readable_content(&head.description)))
.then_some(members)
}
#[must_use]
pub fn within(self, start: usize, end: usize) -> Option<Self> {
(start <= self.start_item && self.start_item < self.end_item && self.end_item <= end).then(
|| Self {
start_item: self.start_item - start,
end_item: self.end_item - start,
},
)
}
}
fn readable_inline(inline: &Inline) -> bool {
match inline {
Inline::Text { value } | Inline::Code { value } => !value.trim().is_empty(),
Inline::Strong { children }
| Inline::Emphasis { children }
| Inline::Link { children, .. } => children.iter().any(readable_inline),
Inline::LineBreak | Inline::Anchor { .. } => false,
}
}
#[must_use]
pub fn blocks_have_readable_content(blocks: &[Block]) -> bool {
struct Readable(bool);
impl<'a> Visit<'a> for Readable {
fn visit_block(&mut self, block: &'a Block) {
if self.0 {
return;
}
match block {
Block::Equation { value, .. } => self.0 |= !value.trim().is_empty(),
Block::Unsupported { text, .. } => self.0 |= !text.trim().is_empty(),
_ => visit::walk_block(self, block),
}
}
fn visit_inline(&mut self, inline: &'a Inline) {
if self.0 {
return;
}
match inline {
Inline::Text { value } | Inline::Code { value } => {
self.0 |= !value.trim().is_empty();
}
_ => visit::walk_inline(self, inline),
}
}
}
let mut readable = Readable(false);
for block in blocks {
readable.visit_block(block);
}
readable.0
}
#[cfg(test)]
mod tests {
use super::*;
fn item(text: &str) -> DefinitionItem {
DefinitionItem {
source: None,
entry: None,
terms: vec![vec![Inline::Text {
value: "--name".into(),
}]],
description: vec![Block::Paragraph {
children: vec![Inline::Text { value: text.into() }],
layout: crate::LayoutHint::default(),
source: None,
}],
layout: crate::DefinitionLayout {
inline_term: false,
spacing_before_lines: None,
..Default::default()
},
}
}
#[test]
fn ranges_are_checked_and_partial_excerpts_cannot_borrow_context() {
let items = vec![item(""), item(""), item("body"), item("next")];
let group = DeclarationGroup {
start_item: 0,
end_item: 3,
};
assert_eq!(group.resolve(&items).unwrap().len(), 3);
for bad in [
DeclarationGroup {
start_item: 1,
end_item: 1,
},
DeclarationGroup {
start_item: 0,
end_item: usize::MAX,
},
DeclarationGroup {
start_item: 3,
end_item: 2,
},
DeclarationGroup {
start_item: 0,
end_item: 2,
},
DeclarationGroup {
start_item: 2,
end_item: 4,
},
] {
assert!(bad.resolve(&items).is_none(), "{bad:?}");
}
assert_eq!(group.within(0, 3), Some(group));
assert_eq!(group.within(1, 3), None);
assert_eq!(group.within(0, 2), None);
assert_eq!(
DeclarationGroup {
start_item: 2,
end_item: 4
}
.within(2, 5),
Some(DeclarationGroup {
start_item: 0,
end_item: 2
})
);
}
#[test]
fn anchors_and_spacing_do_not_count_as_readable_descriptions() {
let blocks = vec![
Block::VerticalSpace {
lines: 3,
source: None,
},
Block::Paragraph {
children: vec![Inline::anchor("target")],
layout: crate::LayoutHint::default(),
source: None,
},
];
assert!(!blocks_have_readable_content(&blocks));
assert!(blocks_have_readable_content(&[Block::Equation {
value: "x + y".into(),
display: true,
layout: crate::LayoutHint::default(),
source: None
}]));
}
#[test]
fn group_wire_is_closed_and_round_trip_keeps_original_items() {
let block = Block::DefinitionList {
items: vec![item(""), item("body")],
declaration_groups: vec![DeclarationGroup {
start_item: 0,
end_item: 2,
}],
compact: false,
layout: crate::LayoutHint::default(),
source: None,
};
let json = serde_json::to_value(&block).unwrap();
assert_eq!(
serde_json::from_value::<Block>(json.clone()).unwrap(),
block
);
let mut malformed = json;
malformed["declarationGroups"][0]["endItme"] = 2.into();
assert!(serde_json::from_value::<Block>(malformed).is_err());
}
}