use super::super::range::{Position, Range};
use super::super::traits::{AstNode, Container, Visitor, VisualStructure};
use super::container::GeneralContainer;
use super::content_item::ContentItem;
use super::data::Data;
use super::label::Label;
use super::parameter::Parameter;
use super::typed_content::ContentElement;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct Annotation {
pub data: Data,
pub children: GeneralContainer,
pub location: Range,
}
impl Annotation {
pub const RESERVED_NAMESPACE_PREFIX: &'static str = "lex.";
pub const INCLUDE_LABEL: &'static str = "lex.include";
fn default_location() -> Range {
Range::new(0..0, Position::new(0, 0), Position::new(0, 0))
}
pub fn new(label: Label, parameters: Vec<Parameter>, children: Vec<ContentElement>) -> Self {
let data = Data::new(label, parameters);
Self::from_data(data, children)
}
pub fn marker(label: Label) -> Self {
Self::from_data(Data::new(label, Vec::new()), Vec::new())
}
pub fn with_parameters(label: Label, parameters: Vec<Parameter>) -> Self {
Self::from_data(Data::new(label, parameters), Vec::new())
}
pub fn from_data(data: Data, children: Vec<ContentElement>) -> Self {
Self {
data,
children: GeneralContainer::from_typed(children),
location: Self::default_location(),
}
}
pub fn at(mut self, location: Range) -> Self {
self.location = location;
self
}
pub fn header_location(&self) -> &Range {
&self.data.location
}
pub fn body_location(&self) -> Option<Range> {
Range::bounding_box(self.children.iter().map(|item| item.range()))
}
pub fn is_reserved(&self) -> bool {
self.data
.label
.value
.starts_with(Self::RESERVED_NAMESPACE_PREFIX)
}
pub fn is_include(&self) -> bool {
self.data.label.value == Self::INCLUDE_LABEL
}
pub fn include_src(&self) -> Option<String> {
self.data
.parameters
.iter()
.find(|p| p.key == "src")
.map(|p| p.unquoted_value())
}
}
impl AstNode for Annotation {
fn node_type(&self) -> &'static str {
"Annotation"
}
fn display_label(&self) -> String {
if self.data.parameters.is_empty() {
self.data.label.value.clone()
} else {
format!(
"{} ({} params)",
self.data.label.value,
self.data.parameters.len()
)
}
}
fn range(&self) -> &Range {
&self.location
}
fn accept(&self, visitor: &mut dyn Visitor) {
visitor.visit_annotation(self);
super::super::traits::visit_children(visitor, &self.children);
visitor.leave_annotation(self);
}
}
impl VisualStructure for Annotation {
fn is_source_line_node(&self) -> bool {
true
}
fn has_visual_header(&self) -> bool {
true
}
}
impl Container for Annotation {
fn label(&self) -> &str {
&self.data.label.value
}
fn children(&self) -> &[ContentItem] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<ContentItem> {
self.children.as_mut_vec()
}
}
impl fmt::Display for Annotation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Annotation('{}', {} params, {} items)",
self.data.label.value,
self.data.parameters.len(),
self.children.len()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lex::ast::elements::paragraph::Paragraph;
use crate::lex::ast::elements::typed_content::ContentElement;
#[test]
fn test_annotation_header_and_body_locations() {
let header_range = Range::new(0..4, Position::new(0, 0), Position::new(0, 4));
let child_range = Range::new(10..20, Position::new(1, 0), Position::new(2, 0));
let label = Label::new("note".to_string()).at(header_range.clone());
let data = Data::new(label, Vec::new()).at(header_range.clone());
let child = ContentElement::Paragraph(
Paragraph::from_line("body".to_string()).at(child_range.clone()),
);
let annotation = Annotation::from_data(data, vec![child]).at(Range::new(
0..25,
Position::new(0, 0),
Position::new(2, 0),
));
assert_eq!(annotation.header_location().span, header_range.span);
assert_eq!(annotation.body_location().unwrap().span, child_range.span);
}
fn ann(label: &str, params: Vec<(&str, &str)>) -> Annotation {
let parameters = params
.into_iter()
.map(|(k, v)| Parameter::new(k.to_string(), v.to_string()))
.collect();
Annotation::with_parameters(Label::new(label.to_string()), parameters)
}
#[test]
fn test_is_reserved() {
assert!(ann("lex.include", vec![]).is_reserved());
assert!(ann("lex.foo.bar", vec![]).is_reserved());
assert!(!ann("lexicon", vec![]).is_reserved());
assert!(!ann("review", vec![]).is_reserved());
assert!(!ann("mycompany.include", vec![]).is_reserved());
}
#[test]
fn test_is_include() {
assert!(ann("lex.include", vec![("src", "x.lex")]).is_include());
assert!(!ann("lex.something_else", vec![]).is_include());
assert!(!ann("include", vec![("src", "x.lex")]).is_include());
}
#[test]
fn test_include_src() {
let with_src = ann("lex.include", vec![("src", "chapters/01.lex")]);
assert_eq!(with_src.include_src().as_deref(), Some("chapters/01.lex"));
let other_with_src = ann("image", vec![("src", "diagram.png")]);
assert_eq!(other_with_src.include_src().as_deref(), Some("diagram.png"));
assert_eq!(ann("lex.include", vec![]).include_src(), None);
assert_eq!(
ann("lex.include", vec![("title", "Chapter 1")]).include_src(),
None
);
}
#[test]
fn test_include_src_strips_quotes_from_parsed_value() {
let with_quoted = ann("lex.include", vec![("src", "\"chapters/01.lex\"")]);
assert_eq!(
with_quoted.include_src().as_deref(),
Some("chapters/01.lex")
);
}
#[test]
fn test_constants_match_documented_values() {
assert_eq!(Annotation::RESERVED_NAMESPACE_PREFIX, "lex.");
assert_eq!(Annotation::INCLUDE_LABEL, "lex.include");
assert!(Annotation::INCLUDE_LABEL.starts_with(Annotation::RESERVED_NAMESPACE_PREFIX));
}
}