210 lines
7.4 KiB
Rust
210 lines
7.4 KiB
Rust
use indextree::NodeId;
|
|
use std::ops::RangeInclusive;
|
|
|
|
use crate::elements::{Element, Table, TableCell, TableRow};
|
|
use crate::Org;
|
|
|
|
/// Validation Error
|
|
#[derive(Debug)]
|
|
pub enum ValidationError {
|
|
/// Expected at least one child
|
|
ExpectedChildren {
|
|
at: NodeId,
|
|
},
|
|
/// Expected no children
|
|
UnexpectedChildren {
|
|
at: NodeId,
|
|
},
|
|
UnexpectedElement {
|
|
expected: &'static str,
|
|
at: NodeId,
|
|
},
|
|
/// Expected a detached element
|
|
ExpectedDetached {
|
|
at: NodeId,
|
|
},
|
|
/// Expected headline level in specify range
|
|
HeadlineLevelMismatch {
|
|
range: RangeInclusive<usize>,
|
|
at: NodeId,
|
|
},
|
|
}
|
|
|
|
impl ValidationError {
|
|
pub fn element<'a, 'b>(&self, org: &'a Org<'b>) -> &'a Element<'b> {
|
|
match self {
|
|
ValidationError::ExpectedChildren { at }
|
|
| ValidationError::UnexpectedChildren { at }
|
|
| ValidationError::UnexpectedElement { at, .. }
|
|
| ValidationError::ExpectedDetached { at }
|
|
| ValidationError::HeadlineLevelMismatch { at, .. } => &org[*at],
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type ValidationResult<T> = Result<T, ValidationError>;
|
|
|
|
impl Org<'_> {
|
|
/// Validates an `Org` struct.
|
|
pub fn validate(&self) -> Vec<ValidationError> {
|
|
let mut errors = Vec::new();
|
|
|
|
macro_rules! expect_element {
|
|
($node:ident, $expect:expr, $($pattern:pat)|+) => {
|
|
match self[$node] {
|
|
$($pattern)|+ => (),
|
|
_ => errors.push(ValidationError::UnexpectedElement {
|
|
expected: $expect,
|
|
at: $node
|
|
}),
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! expect_children {
|
|
($node:ident) => {
|
|
if self.arena[$node].first_child().is_none() {
|
|
errors.push(ValidationError::ExpectedChildren { at: $node });
|
|
}
|
|
};
|
|
}
|
|
|
|
for node_id in self.root.descendants(&self.arena) {
|
|
let node = &self.arena[node_id];
|
|
match node.get() {
|
|
Element::Document { .. } => {
|
|
let mut children = node_id.children(&self.arena);
|
|
if let Some(child) = children.next() {
|
|
expect_element!(
|
|
child,
|
|
"Headline|Section",
|
|
Element::Headline { .. } | Element::Section
|
|
);
|
|
}
|
|
|
|
for child in children {
|
|
expect_element!(child, "Headline", Element::Headline { .. });
|
|
}
|
|
}
|
|
Element::Headline { .. } => {
|
|
expect_children!(node_id);
|
|
|
|
let mut children = node_id.children(&self.arena);
|
|
if let Some(child) = children.next() {
|
|
expect_element!(child, "Title", Element::Title(_));
|
|
}
|
|
|
|
if let Some(child) = children.next() {
|
|
expect_element!(
|
|
child,
|
|
"Headline|Section",
|
|
Element::Headline { .. } | Element::Section
|
|
);
|
|
}
|
|
|
|
for child in children {
|
|
expect_element!(child, "Headline", Element::Headline { .. });
|
|
}
|
|
}
|
|
Element::Title(title) => {
|
|
if !title.raw.is_empty() && node.first_child().is_none() {
|
|
errors.push(ValidationError::ExpectedChildren { at: node_id });
|
|
}
|
|
}
|
|
Element::List(_) => {
|
|
expect_children!(node_id);
|
|
for child in node_id.children(&self.arena) {
|
|
expect_element!(child, "ListItem", Element::ListItem(_));
|
|
}
|
|
}
|
|
Element::Table(Table::Org { .. }) => {
|
|
for child in node_id.children(&self.arena) {
|
|
expect_element!(child, "TableRow", Element::TableRow(_));
|
|
}
|
|
}
|
|
Element::TableRow(TableRow::Header) => {
|
|
for child in node_id.children(&self.arena) {
|
|
expect_element!(
|
|
child,
|
|
"TableCell::Header",
|
|
Element::TableCell(TableCell::Header)
|
|
);
|
|
}
|
|
}
|
|
Element::TableRow(TableRow::Body) => {
|
|
for child in node_id.children(&self.arena) {
|
|
expect_element!(
|
|
child,
|
|
"TableCell::Body",
|
|
Element::TableCell(TableCell::Body)
|
|
);
|
|
}
|
|
}
|
|
Element::CommentBlock(_)
|
|
| Element::ExampleBlock(_)
|
|
| Element::ExportBlock(_)
|
|
| Element::SourceBlock(_)
|
|
| Element::BabelCall(_)
|
|
| Element::InlineSrc(_)
|
|
| Element::Code { .. }
|
|
| Element::FnRef(_)
|
|
| Element::InlineCall(_)
|
|
| Element::Link(_)
|
|
| Element::Macros(_)
|
|
| Element::RadioTarget
|
|
| Element::Snippet(_)
|
|
| Element::Target(_)
|
|
| Element::Text { .. }
|
|
| Element::Timestamp(_)
|
|
| Element::Verbatim { .. }
|
|
| Element::FnDef(_)
|
|
| Element::Clock(_)
|
|
| Element::Comment { .. }
|
|
| Element::FixedWidth { .. }
|
|
| Element::Keyword(_)
|
|
| Element::Rule(_)
|
|
| Element::Cookie(_)
|
|
| Element::TableRow(TableRow::BodyRule)
|
|
| Element::TableRow(TableRow::HeaderRule) => {
|
|
if node.first_child().is_some() {
|
|
errors.push(ValidationError::UnexpectedChildren { at: node_id });
|
|
}
|
|
}
|
|
Element::SpecialBlock(_)
|
|
| Element::QuoteBlock(_)
|
|
| Element::CenterBlock(_)
|
|
| Element::VerseBlock(_)
|
|
| Element::Paragraph { .. }
|
|
| Element::Section
|
|
| Element::Bold
|
|
| Element::Italic
|
|
| Element::Underline
|
|
| Element::Strike
|
|
| Element::DynBlock(_) => {
|
|
expect_children!(node_id);
|
|
}
|
|
Element::ListItem(_)
|
|
| Element::Drawer(_)
|
|
| Element::TableCell(_)
|
|
| Element::Table(_) => (),
|
|
}
|
|
}
|
|
errors
|
|
}
|
|
|
|
pub(crate) fn debug_validate(&self) {
|
|
if cfg!(debug_assertions) {
|
|
let errors = self.validate();
|
|
if !errors.is_empty() {
|
|
eprintln!("Org validation failed. {} error(s) found:", errors.len());
|
|
for err in errors {
|
|
eprintln!("{:?} at {:?}", err, err.element(self));
|
|
}
|
|
panic!(
|
|
"Looks like there's a bug in orgize! Please report it with your org-mode content at https://github.com/PoiScript/orgize/issues."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|