diff --git a/org_mode_samples/sections_and_headings/footnote_section/as_target.org b/org_mode_samples/sections_and_headings/footnote_section/as_target.org new file mode 100644 index 0000000..b4cf1fd --- /dev/null +++ b/org_mode_samples/sections_and_headings/footnote_section/as_target.org @@ -0,0 +1,2 @@ +<<>> and stuff +* Footnotes diff --git a/org_mode_samples/sections_and_headings/footnote_section/different_case.org b/org_mode_samples/sections_and_headings/footnote_section/different_case.org new file mode 100644 index 0000000..117f44d --- /dev/null +++ b/org_mode_samples/sections_and_headings/footnote_section/different_case.org @@ -0,0 +1,3 @@ +* FOOTNOTES +* Footnotes +* footnotes diff --git a/org_mode_samples/sections_and_headings/footnote_section/multiple.org b/org_mode_samples/sections_and_headings/footnote_section/multiple.org new file mode 100644 index 0000000..cd0a968 --- /dev/null +++ b/org_mode_samples/sections_and_headings/footnote_section/multiple.org @@ -0,0 +1,2 @@ +* Footnotes +* Footnotes diff --git a/org_mode_samples/sections_and_headings/footnote_section/simple.org b/org_mode_samples/sections_and_headings/footnote_section/simple.org new file mode 100644 index 0000000..cc5c989 --- /dev/null +++ b/org_mode_samples/sections_and_headings/footnote_section/simple.org @@ -0,0 +1,3 @@ +* Foo +* Footnotes :foo:bar: +* Footnotes and stuff diff --git a/src/compare/diff.rs b/src/compare/diff.rs index 5d68d32..25cf74d 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -9,6 +9,7 @@ use super::sexp::unquote; use super::sexp::Token; use super::util::compare_standard_properties; use super::util::get_property; +use super::util::get_property_boolean; use super::util::get_property_quoted_string; use super::util::get_property_unquoted_atom; use crate::types::AngleLink; @@ -714,7 +715,29 @@ fn compare_heading<'s>( (None, false) | (Some(_), true) => {} } - // TODO: Compare :pre-blank :raw-value :footnote-section-p :scheduled :closed + // Compare raw-value + let raw_value = get_property_quoted_string(emacs, ":raw-value")? + .ok_or("Headlines should have :raw-value.")?; + let rust_raw_value = rust.get_raw_value(); + if raw_value != rust_raw_value { + this_status = DiffStatus::Bad; + message = Some(format!( + "raw-value mismatch (emacs != rust) {:?} != {:?}", + raw_value, rust_raw_value + )); + } + + // Compare footnote-section-p + let footnote_section = get_property_boolean(emacs, ":footnote-section-p")?; + if footnote_section != rust.is_footnote_section { + this_status = DiffStatus::Bad; + message = Some(format!( + "footnote section mismatch (emacs != rust) {:?} != {:?}", + footnote_section, rust.is_footnote_section + )); + } + + // TODO: Compare :pre-blank :scheduled :closed // // :scheduled and :closed seem to only appear when the headline has a planning diff --git a/src/compare/util.rs b/src/compare/util.rs index a706804..c770abd 100644 --- a/src/compare/util.rs +++ b/src/compare/util.rs @@ -217,3 +217,19 @@ pub(crate) fn get_property_quoted_string<'s, 'x>( .map(unquote) .map_or(Ok(None), |r| r.map(Some))?) } + +/// Get a named property containing a boolean value. +/// +/// This uses the elisp convention of nil == false, non-nil == true. +/// +/// Returns false if key is not found. +pub(crate) fn get_property_boolean<'s, 'x>( + emacs: &'s Token<'s>, + key: &'x str, +) -> Result> { + Ok(get_property(emacs, key)? + .map(Token::as_atom) + .map_or(Ok(None), |r| r.map(Some))? + .unwrap_or("nil") + != "nil") +} diff --git a/src/context/global_settings.rs b/src/context/global_settings.rs index c12bbcf..f730c58 100644 --- a/src/context/global_settings.rs +++ b/src/context/global_settings.rs @@ -27,6 +27,11 @@ pub struct GlobalSettings<'g, 's> { /// /// Corresponds to org-odd-levels-only elisp variable. pub odd_levels_only: HeadlineLevelFilter, + + /// If a headline title matches this string exactly, then that section will become a "footnote section". + /// + /// Corresponds to org-footnote-section elisp variable. + pub footnote_section: &'g str, } pub const DEFAULT_TAB_WIDTH: IndentationLevel = 8; @@ -43,6 +48,7 @@ impl<'g, 's> GlobalSettings<'g, 's> { list_allow_alphabetical: false, tab_width: DEFAULT_TAB_WIDTH, odd_levels_only: HeadlineLevelFilter::default(), + footnote_section: "Footnotes", } } } diff --git a/src/parser/headline.rs b/src/parser/headline.rs index d6e57bb..753c72d 100644 --- a/src/parser/headline.rs +++ b/src/parser/headline.rs @@ -4,6 +4,7 @@ use nom::bytes::complete::tag; use nom::character::complete::anychar; use nom::character::complete::space0; use nom::character::complete::space1; +use nom::combinator::consumed; use nom::combinator::map; use nom::combinator::not; use nom::combinator::opt; @@ -55,20 +56,9 @@ fn _heading<'b, 'g, 'r, 's>( parent_star_count: HeadlineLevel, ) -> Res, Heading<'s>> { not(|i| context.check_exit_matcher(i))(input)?; - let ( - remaining, - ( - headline_level, - star_count, - maybe_todo_keyword, - maybe_priority, - maybe_comment, - title, - heading_tags, - ), - ) = headline(context, input, parent_star_count)?; + let (remaining, pre_headline) = headline(context, input, parent_star_count)?; let section_matcher = parser_with_context!(section)(context); - let heading_matcher = parser_with_context!(heading(star_count))(context); + let heading_matcher = parser_with_context!(heading(pre_headline.star_count))(context); let (remaining, maybe_section) = opt(map(section_matcher, DocumentElement::Section))(remaining)?; let (remaining, _ws) = opt(tuple((start_of_line, many0(blank_line))))(remaining)?; @@ -84,23 +74,26 @@ fn _heading<'b, 'g, 'r, 's>( } else { remaining }; - let is_archived = heading_tags.contains(&"ARCHIVE"); + let is_archived = pre_headline.tags.contains(&"ARCHIVE"); let source = get_consumed(input, remaining); Ok(( remaining, Heading { source: source.into(), - level: headline_level, - todo_keyword: maybe_todo_keyword.map(|(todo_keyword_type, todo_keyword)| { - (todo_keyword_type, Into::<&str>::into(todo_keyword)) - }), - priority_cookie: maybe_priority.map(|(_, priority)| priority), - title, - tags: heading_tags, + level: pre_headline.headline_level, + todo_keyword: pre_headline + .todo_keyword + .map(|(todo_keyword_type, todo_keyword)| { + (todo_keyword_type, Into::<&str>::into(todo_keyword)) + }), + priority_cookie: pre_headline.priority_cookie.map(|(_, priority)| priority), + title: pre_headline.title, + tags: pre_headline.tags, children, - is_comment: maybe_comment.is_some(), + is_comment: pre_headline.comment.is_some(), is_archived, + is_footnote_section: pre_headline.is_footnote_section, }, )) } @@ -111,23 +104,26 @@ pub(crate) fn detect_headline<'s>(input: OrgSource<'s>) -> Res, () Ok((input, ())) } +/// Fields from a not-yet-fully-parsed Headline. +/// +/// This struct exists to give names to the fields of a partially-parsed Headline to avoid returning a large tuple of nameless fields. +struct PreHeadline<'s> { + headline_level: HeadlineLevel, + star_count: HeadlineLevel, + todo_keyword: Option<(TodoKeywordType, OrgSource<'s>)>, + priority_cookie: Option<(OrgSource<'s>, PriorityCookie)>, + comment: Option>, + title: Vec>, + tags: Vec<&'s str>, + is_footnote_section: bool, +} + #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn headline<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, parent_star_count: HeadlineLevel, -) -> Res< - OrgSource<'s>, - ( - HeadlineLevel, - HeadlineLevel, - Option<(TodoKeywordType, OrgSource<'s>)>, - Option<(OrgSource<'s>, PriorityCookie)>, - Option>, - Vec>, - Vec<&'s str>, - ), -> { +) -> Res, PreHeadline<'s>> { let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode { class: ExitClass::Document, exit_matcher: &headline_title_end, @@ -159,30 +155,43 @@ fn headline<'b, 'g, 'r, 's>( let (remaining, maybe_title) = opt(tuple(( space1, - many1(parser_with_context!(standard_set_object)(&parser_context)), + consumed(many1(parser_with_context!(standard_set_object)( + &parser_context, + ))), )))(remaining)?; let (remaining, maybe_tags) = opt(tuple((space0, tags)))(remaining)?; let (remaining, _) = tuple((space0, org_line_ending))(remaining)?; + let is_footnote_section = maybe_title + .as_ref() + .map(|(_, (raw_title, _))| raw_title) + .map(|raw_title| { + Into::<&str>::into(raw_title) == context.get_global_settings().footnote_section + }) + .unwrap_or(false); + Ok(( remaining, - ( + PreHeadline { headline_level, star_count, - maybe_todo_keyword.map(|(_, todo, _)| todo), - maybe_priority, - maybe_comment.map(|(_, comment, _)| comment), - maybe_title.map(|(_, title)| title).unwrap_or(Vec::new()), - maybe_tags + todo_keyword: maybe_todo_keyword.map(|(_, todo, _)| todo), + priority_cookie: maybe_priority, + comment: maybe_comment.map(|(_, comment, _)| comment), + title: maybe_title + .map(|(_, (_, title))| title) + .unwrap_or(Vec::new()), + tags: maybe_tags .map(|(_ws, tags)| { tags.into_iter() .map(|single_tag| Into::<&str>::into(single_tag)) .collect() }) .unwrap_or(Vec::new()), - ), + is_footnote_section, + }, )) } diff --git a/src/types/document.rs b/src/types/document.rs index b5c9e97..6066873 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -28,6 +28,7 @@ pub struct Heading<'s> { pub children: Vec>, pub is_comment: bool, pub is_archived: bool, + pub is_footnote_section: bool, } #[derive(Debug)] @@ -74,3 +75,15 @@ impl<'s> StandardProperties<'s> for Heading<'s> { self.source } } + +impl<'s> Heading<'s> { + pub fn get_raw_value(&self) -> String { + // TODO: I think this could just return a string slice instead of an owned string. + let title_source: String = self + .title + .iter() + .map(|obj| obj.get_standard_properties().get_source()) + .collect(); + title_source + } +}