From d78ce10a0bd298be511f8b2cd6db7c9501885876 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Sat, 30 Sep 2023 19:21:24 -0400 Subject: [PATCH 1/5] Compare raw-value. --- src/compare/diff.rs | 14 +++++++++++++- src/types/document.rs | 12 ++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/compare/diff.rs b/src/compare/diff.rs index 5d68d32..dbd3766 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -714,7 +714,19 @@ 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 + )); + } + + // TODO: Compare :pre-blank :footnote-section-p :scheduled :closed // // :scheduled and :closed seem to only appear when the headline has a planning diff --git a/src/types/document.rs b/src/types/document.rs index b5c9e97..c271e8d 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -74,3 +74,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 + } +} From 178894680ba0f050d476f564dd22cf29ed042314 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 2 Oct 2023 10:48:34 -0400 Subject: [PATCH 2/5] Compare footnote section. --- .../footnote_section/simple.org | 3 +++ src/compare/diff.rs | 13 ++++++++++++- src/compare/util.rs | 16 ++++++++++++++++ src/context/global_settings.rs | 6 ++++++ src/parser/headline.rs | 10 ++++++++-- src/types/document.rs | 1 + 6 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 org_mode_samples/sections_and_headings/footnote_section/simple.org 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..6bbce6f --- /dev/null +++ b/org_mode_samples/sections_and_headings/footnote_section/simple.org @@ -0,0 +1,3 @@ +* Foo +* Footnotes +* Footnotes and stuff diff --git a/src/compare/diff.rs b/src/compare/diff.rs index dbd3766..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; @@ -726,7 +727,17 @@ fn compare_heading<'s>( )); } - // TODO: Compare :pre-blank :footnote-section-p :scheduled :closed + // 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..4e94726 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; @@ -101,6 +102,7 @@ fn _heading<'b, 'g, 'r, 's>( children, is_comment: maybe_comment.is_some(), is_archived, + is_footnote_section: false, // TODO }, )) } @@ -159,7 +161,9 @@ 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)?; @@ -174,7 +178,9 @@ fn headline<'b, 'g, 'r, 's>( maybe_todo_keyword.map(|(_, todo, _)| todo), maybe_priority, maybe_comment.map(|(_, comment, _)| comment), - maybe_title.map(|(_, title)| title).unwrap_or(Vec::new()), + maybe_title + .map(|(_, (_, title))| title) + .unwrap_or(Vec::new()), maybe_tags .map(|(_ws, tags)| { tags.into_iter() diff --git a/src/types/document.rs b/src/types/document.rs index c271e8d..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)] From 5a254392cb726025f90718e32880540983ee82a7 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 2 Oct 2023 10:50:19 -0400 Subject: [PATCH 3/5] Add more tests. --- .../sections_and_headings/footnote_section/as_target.org | 2 ++ .../sections_and_headings/footnote_section/different_case.org | 3 +++ .../sections_and_headings/footnote_section/multiple.org | 2 ++ .../sections_and_headings/footnote_section/simple.org | 2 +- 4 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 org_mode_samples/sections_and_headings/footnote_section/as_target.org create mode 100644 org_mode_samples/sections_and_headings/footnote_section/different_case.org create mode 100644 org_mode_samples/sections_and_headings/footnote_section/multiple.org 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 index 6bbce6f..cc5c989 100644 --- a/org_mode_samples/sections_and_headings/footnote_section/simple.org +++ b/org_mode_samples/sections_and_headings/footnote_section/simple.org @@ -1,3 +1,3 @@ * Foo -* Footnotes +* Footnotes :foo:bar: * Footnotes and stuff From de5788d8f3e8bde4a9da424de95c8edbd06a8dc4 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 2 Oct 2023 11:16:05 -0400 Subject: [PATCH 4/5] Introduce a struct for the partially-parsed headline. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We are returning so many fields from that parser that managing a tuple is becoming unreadable. The struct should add some structure 😉 to the code. --- src/parser/headline.rs | 75 +++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/src/parser/headline.rs b/src/parser/headline.rs index 4e94726..6b51f2b 100644 --- a/src/parser/headline.rs +++ b/src/parser/headline.rs @@ -56,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)?; @@ -85,22 +74,24 @@ 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: false, // TODO }, @@ -113,23 +104,25 @@ 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>, +} + #[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, @@ -172,23 +165,23 @@ fn headline<'b, 'g, 'r, 's>( Ok(( remaining, - ( + PreHeadline { headline_level, star_count, - maybe_todo_keyword.map(|(_, todo, _)| todo), - maybe_priority, - maybe_comment.map(|(_, comment, _)| comment), - maybe_title + 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()), - maybe_tags + tags: maybe_tags .map(|(_ws, tags)| { tags.into_iter() .map(|single_tag| Into::<&str>::into(single_tag)) .collect() }) .unwrap_or(Vec::new()), - ), + }, )) } From 270ba53150dcba392d9fdb0a5e9c8190abb95f98 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 2 Oct 2023 11:20:43 -0400 Subject: [PATCH 5/5] Set is_footnote_section during parsing. --- src/parser/headline.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/parser/headline.rs b/src/parser/headline.rs index 6b51f2b..753c72d 100644 --- a/src/parser/headline.rs +++ b/src/parser/headline.rs @@ -93,7 +93,7 @@ fn _heading<'b, 'g, 'r, 's>( children, is_comment: pre_headline.comment.is_some(), is_archived, - is_footnote_section: false, // TODO + is_footnote_section: pre_headline.is_footnote_section, }, )) } @@ -115,6 +115,7 @@ struct PreHeadline<'s> { comment: Option>, title: Vec>, tags: Vec<&'s str>, + is_footnote_section: bool, } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] @@ -163,6 +164,14 @@ fn headline<'b, 'g, 'r, 's>( 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 { @@ -181,6 +190,7 @@ fn headline<'b, 'g, 'r, 's>( .collect() }) .unwrap_or(Vec::new()), + is_footnote_section, }, )) }