diff --git a/Cargo.toml b/Cargo.toml index 7f42a24..9e11b28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ name = "toy" version = "0.1.0" edition = "2021" license = "0BSD" -default-run = "toy" [lib] name = "organic" diff --git a/org_mode_samples/property_drawer/add_in_child.org b/org_mode_samples/property_drawer/add_in_child.org new file mode 100644 index 0000000..aa3c11f --- /dev/null +++ b/org_mode_samples/property_drawer/add_in_child.org @@ -0,0 +1,9 @@ +* foo +** bar +:PROPERTIES: +:lorem: ipsum +:END: +*** baz +:PROPERTIES: +:lorem+: dolar +:END: diff --git a/org_mode_samples/property_drawer/empty.org b/org_mode_samples/property_drawer/empty.org new file mode 100644 index 0000000..d86fd5b --- /dev/null +++ b/org_mode_samples/property_drawer/empty.org @@ -0,0 +1,3 @@ +:PROPERTIES: + +:END: diff --git a/org_mode_samples/property_drawer/zeroth_section.org b/org_mode_samples/property_drawer/zeroth_section.org index ccdae6f..2538054 100644 --- a/org_mode_samples/property_drawer/zeroth_section.org +++ b/org_mode_samples/property_drawer/zeroth_section.org @@ -6,3 +6,20 @@ :PROPERTIES: :FOO: bar :END: + + + +* Spaces turn property drawers into regular drawers + +:PROPERTIES: +:FOO: bar +:END: +* Comments turn property drawers into regular drawers +# Comment +:PROPERTIES: +:FOO: bar +:END: +* Baseline +:PROPERTIES: +:FOO: bar +:END: diff --git a/src/compare/diff.rs b/src/compare/diff.rs index a1480aa..4920fb3 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -1,7 +1,6 @@ use super::sexp::Token; use super::util::assert_bounds; use super::util::assert_name; -use crate::compare::util::get_offsets; use crate::parser::Comment; use crate::parser::Document; use crate::parser::DocumentElement; @@ -13,6 +12,7 @@ use crate::parser::Heading; use crate::parser::Paragraph; use crate::parser::PlainList; use crate::parser::PlainListItem; +use crate::parser::PropertyDrawer; use crate::parser::Section; use crate::DynamicBlock; @@ -197,6 +197,7 @@ fn compare_element<'s>( Element::FootnoteDefinition(obj) => compare_footnote_definition(source, emacs, obj), Element::Comment(obj) => compare_comment(source, emacs, obj), Element::Drawer(obj) => compare_drawer(source, emacs, obj), + Element::PropertyDrawer(obj) => compare_property_drawer(source, emacs, obj), } } @@ -428,3 +429,33 @@ fn compare_drawer<'s>( children: child_status, }) } + +fn compare_property_drawer<'s>( + source: &'s str, + emacs: &'s Token<'s>, + rust: &'s PropertyDrawer<'s>, +) -> Result> { + let children = emacs.as_list()?; + let mut child_status = Vec::new(); + let mut this_status = DiffStatus::Good; + let emacs_name = "property-drawer"; + if assert_name(emacs, emacs_name).is_err() { + this_status = DiffStatus::Bad; + } + + if assert_bounds(source, emacs, rust).is_err() { + this_status = DiffStatus::Bad; + } + + for (emacs_child, rust_child) in children.iter().skip(2).zip(rust.children.iter()) { + // TODO: What are node properties and are they the only legal child of property drawers? + // child_status.push(compare_element(source, emacs_child, rust_child)?); + } + + Ok(DiffResult { + status: this_status, + name: emacs_name.to_owned(), + message: None, + children: child_status, + }) +} diff --git a/src/parser/document.rs b/src/parser/document.rs index f2e4308..b2e3541 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -1,7 +1,18 @@ +use crate::parser::comment::comment; +use crate::parser::element::element; +use crate::parser::exiting::ExitClass; +use crate::parser::object::standard_set_object; +use crate::parser::parser_context::ContextElement; +use crate::parser::parser_context::ContextTree; +use crate::parser::parser_context::ExitMatcherNode; +use crate::parser::property_drawer::property_drawer; +use crate::parser::util::blank_line; +use crate::parser::util::maybe_consume_trailing_whitespace_if_not_exiting; use nom::branch::alt; use nom::bytes::complete::tag; use nom::character::complete::line_ending; use nom::character::complete::space1; +use nom::combinator::eof; use nom::combinator::map; use nom::combinator::not; use nom::combinator::opt; @@ -13,14 +24,6 @@ use nom::multi::many1_count; use nom::multi::many_till; use nom::sequence::tuple; -use crate::parser::element::element; -use crate::parser::exiting::ExitClass; -use crate::parser::object::standard_set_object; -use crate::parser::parser_context::ContextElement; -use crate::parser::parser_context::ContextTree; -use crate::parser::parser_context::ExitMatcherNode; -use crate::parser::util::maybe_consume_trailing_whitespace_if_not_exiting; - use super::element::Element; use super::error::Res; use super::object::Object; @@ -92,9 +95,10 @@ pub fn document(input: &str) -> Res<&str, Document> { let initial_context: ContextTree<'_, '_> = ContextTree::new(); let document_context = initial_context.with_additional_node(ContextElement::DocumentRoot(input)); - let section_matcher = parser_with_context!(section)(&document_context); + let zeroth_section_matcher = parser_with_context!(zeroth_section)(&document_context); let heading_matcher = parser_with_context!(heading)(&document_context); - let (remaining, zeroth_section) = opt(section_matcher)(input)?; + let (remaining, _blank_lines) = many0(blank_line)(input)?; + let (remaining, zeroth_section) = opt(zeroth_section_matcher)(remaining)?; let (remaining, children) = many0(heading_matcher)(remaining)?; let source = get_consumed(input, remaining); Ok(( @@ -108,7 +112,52 @@ pub fn document(input: &str) -> Res<&str, Document> { } #[tracing::instrument(ret, level = "debug")] -fn section<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Section<'s>> { +fn zeroth_section<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Section<'s>> { + // TODO: The zeroth section is specialized so it probably needs its own parser + let parser_context = context + .with_additional_node(ContextElement::ConsumeTrailingWhitespace(true)) + .with_additional_node(ContextElement::Context("section")) + .with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode { + class: ExitClass::Document, + exit_matcher: §ion_end, + })); + let without_consuming_whitespace_context = + parser_context.with_additional_node(ContextElement::ConsumeTrailingWhitespace(false)); + + let element_matcher = parser_with_context!(element)(&parser_context); + let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); + + let (remaining, comment_and_property_drawer_element) = opt(tuple(( + opt(parser_with_context!(comment)( + &without_consuming_whitespace_context, + )), + parser_with_context!(property_drawer)(&without_consuming_whitespace_context), + many0(blank_line), + )))(input)?; + + let (remaining, (mut children, _exit_contents)) = verify( + many_till(element_matcher, exit_matcher), + |(children, _exit_contents)| { + !children.is_empty() || comment_and_property_drawer_element.is_some() + }, + )(remaining)?; + + comment_and_property_drawer_element.map(|(comment, property_drawer, _ws)| { + children.insert(0, Element::PropertyDrawer(property_drawer)); + comment + .map(Element::Comment) + .map(|ele| children.insert(0, ele)); + }); + + let (remaining, _trailing_ws) = + maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?; + + let source = get_consumed(input, remaining); + Ok((remaining, Section { source, children })) +} + +#[tracing::instrument(ret, level = "debug")] +fn section<'r, 's>(context: Context<'r, 's>, mut input: &'s str) -> Res<&'s str, Section<'s>> { // TODO: The zeroth section is specialized so it probably needs its own parser let parser_context = context .with_additional_node(ContextElement::ConsumeTrailingWhitespace(true)) @@ -119,10 +168,20 @@ fn section<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Sec })); let element_matcher = parser_with_context!(element)(&parser_context); let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); - let (remaining, (children, _exit_contents)) = verify( + let (mut remaining, property_drawer_element) = + opt(parser_with_context!(property_drawer)(&parser_context))(input)?; + if property_drawer_element.is_none() { + let (remain, _ws) = many0(blank_line)(remaining)?; + remaining = remain; + input = remain; + } + let (remaining, (mut children, _exit_contents)) = verify( many_till(element_matcher, exit_matcher), - |(children, _exit_contents)| !children.is_empty(), - )(input)?; + |(children, _exit_contents)| !children.is_empty() || property_drawer_element.is_some(), + )(remaining)?; + property_drawer_element + .map(Element::PropertyDrawer) + .map(|ele| children.insert(0, ele)); let (remaining, _trailing_ws) = maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?; @@ -140,7 +199,7 @@ fn section_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, #[tracing::instrument(ret, level = "debug")] fn heading<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Heading<'s>> { not(|i| context.check_exit_matcher(i))(input)?; - let (remaining, (star_count, _ws, title, _ws2)) = headline(context, input)?; + let (remaining, (star_count, _ws, title)) = headline(context, input)?; let section_matcher = parser_with_context!(section)(context); let heading_matcher = parser_with_context!(heading)(context); let (remaining, children) = many0(alt(( @@ -166,7 +225,7 @@ fn heading<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Hea fn headline<'r, 's>( context: Context<'r, 's>, input: &'s str, -) -> Res<&'s str, (usize, &'s str, Vec>, &'s str)> { +) -> Res<&'s str, (usize, &'s str, Vec>)> { let parser_context = context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Document, @@ -175,14 +234,14 @@ fn headline<'r, 's>( let standard_set_object_matcher = parser_with_context!(standard_set_object)(&parser_context); let start_of_line_matcher = parser_with_context!(start_of_line)(&parser_context); - let (remaining, (_sol, star_count, ws, title, ws2)) = tuple(( + let (remaining, (_sol, star_count, ws, title, _line_ending)) = tuple(( start_of_line_matcher, many1_count(tag("*")), space1, many1(standard_set_object_matcher), - trailing_whitespace, + alt((line_ending, eof)), ))(input)?; - Ok((remaining, (star_count, ws, title, ws2))) + Ok((remaining, (star_count, ws, title))) } #[tracing::instrument(ret, level = "debug")] diff --git a/src/parser/drawer.rs b/src/parser/drawer.rs index d8f2f87..e79cdeb 100644 --- a/src/parser/drawer.rs +++ b/src/parser/drawer.rs @@ -1,5 +1,6 @@ use nom::branch::alt; use nom::bytes::complete::tag; +use nom::bytes::complete::tag_no_case; use nom::bytes::complete::take_while; use nom::character::complete::line_ending; use nom::character::complete::space0; @@ -92,7 +93,7 @@ fn drawer_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, start_of_line(context, input)?; recognize(tuple(( space0, - tag(":end:"), + tag_no_case(":end:"), space0, alt((line_ending, eof)), )))(input) diff --git a/src/parser/element.rs b/src/parser/element.rs index ecf9a80..e3a2c4e 100644 --- a/src/parser/element.rs +++ b/src/parser/element.rs @@ -8,6 +8,7 @@ use super::greater_element::DynamicBlock; use super::greater_element::FootnoteDefinition; use super::greater_element::GreaterBlock; use super::greater_element::PlainList; +use super::greater_element::PropertyDrawer; use super::lesser_element::Comment; use super::lesser_element::Paragraph; use super::paragraph::paragraph; @@ -29,6 +30,7 @@ pub enum Element<'s> { FootnoteDefinition(FootnoteDefinition<'s>), Comment(Comment<'s>), Drawer(Drawer<'s>), + PropertyDrawer(PropertyDrawer<'s>), } impl<'s> Source<'s> for Element<'s> { @@ -41,6 +43,7 @@ impl<'s> Source<'s> for Element<'s> { Element::FootnoteDefinition(obj) => obj.source, Element::Comment(obj) => obj.source, Element::Drawer(obj) => obj.source, + Element::PropertyDrawer(obj) => obj.source, } } } @@ -93,6 +96,12 @@ impl<'s> Source<'s> for Drawer<'s> { } } +impl<'s> Source<'s> for PropertyDrawer<'s> { + fn get_source(&'s self) -> &'s str { + self.source + } +} + #[tracing::instrument(ret, level = "debug")] pub fn element<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Element<'s>> { let non_paragraph_matcher = parser_with_context!(non_paragraph_element)(context); diff --git a/src/parser/greater_element.rs b/src/parser/greater_element.rs index 9018726..78b05c2 100644 --- a/src/parser/greater_element.rs +++ b/src/parser/greater_element.rs @@ -43,3 +43,15 @@ pub struct Drawer<'s> { pub name: &'s str, pub children: Vec>, } + +#[derive(Debug)] +pub struct PropertyDrawer<'s> { + pub source: &'s str, + pub children: Vec>, +} + +#[derive(Debug)] +pub struct NodeProperty<'s> { + pub source: &'s str, + pub value: Option<&'s str>, +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3a2eb61..6359a5c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -16,6 +16,7 @@ mod parser_context; mod parser_with_context; mod plain_list; mod plain_text; +mod property_drawer; mod source; mod util; pub use document::document; @@ -30,6 +31,7 @@ pub use greater_element::FootnoteDefinition; pub use greater_element::GreaterBlock; pub use greater_element::PlainList; pub use greater_element::PlainListItem; +pub use greater_element::PropertyDrawer; pub use lesser_element::Comment; pub use lesser_element::Paragraph; pub use source::Source; diff --git a/src/parser/property_drawer.rs b/src/parser/property_drawer.rs new file mode 100644 index 0000000..f8283c6 --- /dev/null +++ b/src/parser/property_drawer.rs @@ -0,0 +1,147 @@ +use nom::branch::alt; +use nom::bytes::complete::is_not; +use nom::bytes::complete::tag; +use nom::bytes::complete::tag_no_case; +use nom::character::complete::line_ending; +use nom::character::complete::space0; +use nom::character::complete::space1; +use nom::combinator::eof; +use nom::combinator::map; +use nom::combinator::opt; +use nom::combinator::recognize; +use nom::multi::many_till; +use nom::sequence::tuple; + +use super::Context; +use crate::parser::error::CustomError; +use crate::parser::error::MyError; +use crate::parser::error::Res; +use crate::parser::exiting::ExitClass; +use crate::parser::greater_element::NodeProperty; +use crate::parser::greater_element::PropertyDrawer; +use crate::parser::parser_context::ContextElement; +use crate::parser::parser_context::ExitMatcherNode; +use crate::parser::parser_with_context::parser_with_context; +use crate::parser::plain_text::plain_text; +use crate::parser::util::exit_matcher_parser; +use crate::parser::util::get_consumed; +use crate::parser::util::immediate_in_section; +use crate::parser::util::maybe_consume_trailing_whitespace_if_not_exiting; +use crate::parser::util::start_of_line; + +#[tracing::instrument(ret, level = "debug")] +pub fn property_drawer<'r, 's>( + context: Context<'r, 's>, + input: &'s str, +) -> Res<&'s str, PropertyDrawer<'s>> { + if immediate_in_section(context, "property-drawer") { + return Err(nom::Err::Error(CustomError::MyError(MyError( + "Cannot nest objects of the same element", + )))); + } + let ( + remaining, + (_start_of_line, _leading_whitespace, open_tag, _trailing_whitespace, _line_ending), + ) = tuple(( + parser_with_context!(start_of_line)(context), + space0, + tag_no_case(":PROPERTIES:"), + space0, + line_ending, + ))(input)?; + + let parser_context = context + .with_additional_node(ContextElement::ConsumeTrailingWhitespace(true)) + .with_additional_node(ContextElement::Context("property-drawer")) + .with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode { + class: ExitClass::Alpha, + exit_matcher: &property_drawer_end, + })); + + let node_property_matcher = parser_with_context!(node_property)(&parser_context); + let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); + let (remaining, (children, _exit_contents)) = + many_till(node_property_matcher, exit_matcher)(remaining)?; + let (remaining, _end) = property_drawer_end(&parser_context, remaining)?; + + let (remaining, _trailing_ws) = + maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?; + let source = get_consumed(input, remaining); + + Ok((remaining, PropertyDrawer { source, children })) +} + +#[tracing::instrument(ret, level = "debug")] +fn property_drawer_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> { + recognize(tuple(( + parser_with_context!(start_of_line)(context), + space0, + tag_no_case(":end:"), + space0, + alt((line_ending, eof)), + )))(input) +} + +#[tracing::instrument(ret, level = "debug")] +fn node_property<'r, 's>( + context: Context<'r, 's>, + input: &'s str, +) -> Res<&'s str, NodeProperty<'s>> { + let (remaining, (_start_of_line, _leading_whitespace, _open_colon, name, _close_colon)) = + tuple(( + parser_with_context!(start_of_line)(context), + space0, + tag(":"), + parser_with_context!(node_property_name)(context), + tag(":"), + ))(input)?; + match tuple((space0::<&str, nom::error::Error<&str>>, line_ending))(remaining) { + Ok((remaining, _ws)) => { + let source = get_consumed(input, remaining); + Ok(( + remaining, + NodeProperty { + source, + value: None, + }, + )) + } + Err(_) => { + let (remaining, (_ws, value, _line_ending)) = + tuple((space1, is_not("\r\n"), line_ending))(remaining)?; + let source = get_consumed(input, remaining); + Ok(( + remaining, + NodeProperty { + source, + value: Some(value), + }, + )) + } + } +} + +#[tracing::instrument(ret, level = "debug")] +fn node_property_name<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> { + let parser_context = + context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode { + class: ExitClass::Beta, + exit_matcher: &node_property_name_end, + })); + + let (remaining, name) = recognize(tuple(( + map(parser_with_context!(plain_text)(&parser_context), |pt| { + pt.source + }), + opt(tag("+")), + )))(input)?; + Ok((remaining, name)) +} + +#[tracing::instrument(ret, level = "debug")] +fn node_property_name_end<'r, 's>( + context: Context<'r, 's>, + input: &'s str, +) -> Res<&'s str, &'s str> { + alt((tag("+:"), tag(":")))(input) +} diff --git a/toy_language.txt b/toy_language.txt index 574a2f2..2538054 100644 --- a/toy_language.txt +++ b/toy_language.txt @@ -1,5 +1,25 @@ -foo -:drawername: -:end: + + +# Blank lines and comments can come before property drawers in the zeroth section +:PROPERTIES: +:FOO: bar +:END: + + + +* Spaces turn property drawers into regular drawers + +:PROPERTIES: +:FOO: bar +:END: +* Comments turn property drawers into regular drawers +# Comment +:PROPERTIES: +:FOO: bar +:END: +* Baseline +:PROPERTIES: +:FOO: bar +:END: