diff --git a/build.rs b/build.rs index 6fbdbe86..307b9f46 100644 --- a/build.rs +++ b/build.rs @@ -19,8 +19,6 @@ fn main() { // Re-generate the tests if any org-mode files change println!("cargo:rerun-if-changed=org_mode_samples"); - write_header(&mut test_file); - let test_files = WalkDir::new("org_mode_samples") .into_iter() .filter(|e| match e { @@ -54,28 +52,15 @@ fn write_test(test_file: &mut File, test: &walkdir::DirEntry) { .strip_suffix(".org") .expect("Should have .org extension") .replace("/", "_"); - let test_name = format!("autogen_{}", test_name); - if let Some(_reason) = is_expect_fail(test_name.as_str()) { - write!(test_file, "#[ignore]\n").unwrap(); - } write!( test_file, include_str!("./tests/test_template"), name = test_name, - path = test.path().display() - ) - .unwrap(); -} - -#[cfg(feature = "compare")] -fn write_header(test_file: &mut File) { - write!( - test_file, - r#" -#[feature(exit_status_error)] - -"# + path = test.path().display(), + expect_fail = is_expect_fail(test_name.as_str()) + .map(|_| "#[ignore]\n") + .unwrap_or("") ) .unwrap(); } @@ -83,8 +68,8 @@ fn write_header(test_file: &mut File) { #[cfg(feature = "compare")] fn is_expect_fail(name: &str) -> Option<&str> { match name { - "autogen_greater_element_drawer_drawer_with_headline_inside" => Some("Apparently lines with :end: become their own paragraph. This odd behavior needs to be investigated more."), - "autogen_element_container_priority_footnote_definition_dynamic_block" => Some("Apparently broken begin lines become their own paragraph."), + "greater_element_drawer_drawer_with_headline_inside" => Some("Apparently lines with :end: become their own paragraph. This odd behavior needs to be investigated more."), + "element_container_priority_footnote_definition_dynamic_block" => Some("Apparently broken begin lines become their own paragraph."), _ => None, } } diff --git a/notes/test_names.org b/notes/test_names.org new file mode 100644 index 00000000..fa0dde3a --- /dev/null +++ b/notes/test_names.org @@ -0,0 +1,7 @@ +* Autogen tests +The autogen tests are the tests automatically generated to compare the output of Organic vs the upstream Emacs Org-mode parser using the sample documents in the =org_mode_samples= folder. They will have a prefix based on the settings for each test. + +- default :: The test is run with the default settings (The upstream Emacs Org-mode determines the default settings) +- la :: Short for "list alphabetic". Enables alphabetic plain lists. +- t# :: Sets the tab-width to # (as in t4 sets the tab-width to 4). +- odd :: Sets the org-odd-levels-only setting to true (meaning "odd" as opposed to "oddeven"). diff --git a/org_mode_samples/greater_element/plain_list/alphabetic_counter_set.org b/org_mode_samples/greater_element/plain_list/alphabetic_counter_set.org new file mode 100644 index 00000000..6fbf6601 --- /dev/null +++ b/org_mode_samples/greater_element/plain_list/alphabetic_counter_set.org @@ -0,0 +1,22 @@ +# An ordered list starting at 3 +1. [@3] foo + + +# An ordered list starting at 11 +1. [@D] bar + + +# An ordered list starting at 1 with the contents of "[@kk] baz" +1. [@kk] baz + + +# A paragraph when org-list-allow-alphabetical is nil +m. lorem + + +# A paragraph when org-list-allow-alphabetical is nil +m. [@k] ipsum + + +# An unordered list with :counter set to 3 +- [@3] dolar diff --git a/org_mode_samples/greater_element/plain_list/alphabetic_list_larger_than_26.org b/org_mode_samples/greater_element/plain_list/alphabetic_list_larger_than_26.org new file mode 100644 index 00000000..f8f0216c --- /dev/null +++ b/org_mode_samples/greater_element/plain_list/alphabetic_list_larger_than_26.org @@ -0,0 +1,30 @@ +# Alphabetic lists larger than 26 elements should become numbered. From M-x describe-variable org-list-allow-alphabetical: +# +# > Lists with more than 26 items will fallback to standard numbering. +a. 1 +a. 2 +a. 3 +a. 4 +a. 5 +a. 6 +a. 7 +a. 8 +a. 9 +a. 10 +a. 11 +a. 12 +a. 13 +a. 14 +a. 15 +a. 16 +a. 17 +a. 18 +a. 19 +a. 20 +a. 21 +a. 22 +a. 23 +a. 24 +a. 25 +a. 26 +a. 27 diff --git a/org_mode_samples/greater_element/plain_list/tab_width_indentation_level.org b/org_mode_samples/greater_element/plain_list/tab_width_indentation_level.org new file mode 100644 index 00000000..2dbe9003 --- /dev/null +++ b/org_mode_samples/greater_element/plain_list/tab_width_indentation_level.org @@ -0,0 +1,5 @@ +# "lorem" is prefixed by a tab instead of spaces, so the editor's tab-width value determines whether lorem is a sibling of baz (tab-width 8), a sibling of bar (tab-width < 8), or a child of baz (tab-width > 8). +1. foo + 1. bar + 1. baz + 1. lorem diff --git a/src/compare/compare.rs b/src/compare/compare.rs index 5d6cd460..02242675 100644 --- a/src/compare/compare.rs +++ b/src/compare/compare.rs @@ -6,21 +6,31 @@ use crate::compare::parse::emacs_parse_file_org_document; use crate::compare::parse::get_emacs_version; use crate::compare::parse::get_org_mode_version; use crate::compare::sexp::sexp; -use crate::parser::parse; +use crate::context::GlobalSettings; +use crate::context::LocalFileAccessInterface; use crate::parser::parse_with_settings; -use crate::GlobalSettings; -use crate::LocalFileAccessInterface; pub fn run_anonymous_compare>( org_contents: P, +) -> Result<(), Box> { + run_anonymous_compare_with_settings(org_contents, &GlobalSettings::default()) +} + +pub fn run_compare_on_file>(org_path: P) -> Result<(), Box> { + run_compare_on_file_with_settings(org_path, &GlobalSettings::default()) +} + +pub fn run_anonymous_compare_with_settings>( + org_contents: P, + global_settings: &GlobalSettings, ) -> Result<(), Box> { // TODO: This is a work-around to pretend that dos line endings do not exist. It would be better to handle the difference in line endings. let org_contents = org_contents.as_ref().replace("\r\n", "\n"); let org_contents = org_contents.as_str(); eprintln!("Using emacs version: {}", get_emacs_version()?.trim()); eprintln!("Using org-mode version: {}", get_org_mode_version()?.trim()); - let rust_parsed = parse(org_contents)?; - let org_sexp = emacs_parse_anonymous_org_document(org_contents)?; + let rust_parsed = parse_with_settings(org_contents, global_settings)?; + let org_sexp = emacs_parse_anonymous_org_document(org_contents, global_settings)?; let (_remaining, parsed_sexp) = sexp(org_sexp.as_str()).map_err(|e| e.to_string())?; println!("{}\n\n\n", org_contents); @@ -38,7 +48,10 @@ pub fn run_anonymous_compare>( Ok(()) } -pub fn run_compare_on_file>(org_path: P) -> Result<(), Box> { +pub fn run_compare_on_file_with_settings>( + org_path: P, + global_settings: &GlobalSettings, +) -> Result<(), Box> { let org_path = org_path.as_ref(); eprintln!("Using emacs version: {}", get_emacs_version()?.trim()); eprintln!("Using org-mode version: {}", get_org_mode_version()?.trim()); @@ -53,12 +66,12 @@ pub fn run_compare_on_file>(org_path: P) -> Result<(), Box( +use crate::context::HeadlineLevelFilter; +use crate::settings::GlobalSettings; + +/// Generate elisp to configure org-mode parsing settings +/// +/// Currently only org-list-allow-alphabetical is supported. +fn global_settings_elisp(global_settings: &GlobalSettings) -> String { + // This string concatenation is wildly inefficient but its only called in tests 🤷. + let mut ret = "".to_owned(); + if global_settings.list_allow_alphabetical { + ret += "(setq org-list-allow-alphabetical t)\n" + } + if global_settings.tab_width != crate::settings::DEFAULT_TAB_WIDTH { + ret += format!("(setq-default tab-width {})", global_settings.tab_width).as_str(); + } + if global_settings.odd_levels_only != HeadlineLevelFilter::default() { + ret += match global_settings.odd_levels_only { + HeadlineLevelFilter::Odd => "(setq org-odd-levels-only t)\n", + HeadlineLevelFilter::OddEven => "(setq org-odd-levels-only nil)\n", + }; + } + ret +} + +pub(crate) fn emacs_parse_anonymous_org_document( file_contents: C, + global_settings: &GlobalSettings, ) -> Result> where C: AsRef, @@ -14,10 +39,12 @@ where (require 'org) (defun org-table-align () t) (insert "{escaped_file_contents}") + {global_settings} (org-mode) (message "%s" (pp-to-string (org-element-parse-buffer))) )"#, - escaped_file_contents = escaped_file_contents + escaped_file_contents = escaped_file_contents, + global_settings = global_settings_elisp(global_settings) ); let mut cmd = Command::new("emacs"); let cmd = cmd @@ -33,7 +60,10 @@ where Ok(String::from_utf8(org_sexp)?) } -pub fn emacs_parse_file_org_document

(file_path: P) -> Result> +pub(crate) fn emacs_parse_file_org_document

( + file_path: P, + global_settings: &GlobalSettings, +) -> Result> where P: AsRef, { @@ -46,9 +76,11 @@ where r#"(progn (require 'org) (defun org-table-align () t) + {global_settings} (org-mode) (message "%s" (pp-to-string (org-element-parse-buffer))) -)"# +)"#, + global_settings = global_settings_elisp(global_settings) ); let mut cmd = Command::new("emacs"); let cmd = cmd diff --git a/src/context/global_settings.rs b/src/context/global_settings.rs index e91c93a5..c12bbcf5 100644 --- a/src/context/global_settings.rs +++ b/src/context/global_settings.rs @@ -16,7 +16,7 @@ pub struct GlobalSettings<'g, 's> { /// Set to true to allow for plain lists using single letters as the bullet in the same way that numbers are used. /// /// Corresponds to the org-list-allow-alphabetical elisp variable. - pub org_list_allow_alphabetical: bool, + pub list_allow_alphabetical: bool, /// How many spaces a tab should be equal to. /// @@ -29,6 +29,8 @@ pub struct GlobalSettings<'g, 's> { pub odd_levels_only: HeadlineLevelFilter, } +pub const DEFAULT_TAB_WIDTH: IndentationLevel = 8; + impl<'g, 's> GlobalSettings<'g, 's> { fn new() -> GlobalSettings<'g, 's> { GlobalSettings { @@ -38,9 +40,9 @@ impl<'g, 's> GlobalSettings<'g, 's> { }, in_progress_todo_keywords: BTreeSet::new(), complete_todo_keywords: BTreeSet::new(), - org_list_allow_alphabetical: false, - tab_width: 8, - odd_levels_only: HeadlineLevelFilter::OddEven, + list_allow_alphabetical: false, + tab_width: DEFAULT_TAB_WIDTH, + odd_levels_only: HeadlineLevelFilter::default(), } } } @@ -51,8 +53,14 @@ impl<'g, 's> Default for GlobalSettings<'g, 's> { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum HeadlineLevelFilter { Odd, OddEven, } + +impl Default for HeadlineLevelFilter { + fn default() -> Self { + HeadlineLevelFilter::OddEven + } +} diff --git a/src/context/mod.rs b/src/context/mod.rs index c2d4a0eb..7e45fdd2 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -26,5 +26,6 @@ pub use file_access_interface::FileAccessInterface; pub use file_access_interface::LocalFileAccessInterface; pub use global_settings::GlobalSettings; pub use global_settings::HeadlineLevelFilter; +pub use global_settings::DEFAULT_TAB_WIDTH; pub(crate) use list::List; pub(crate) use parser_with_context::parser_with_context; diff --git a/src/lib.rs b/src/lib.rs index de241252..b8a8d108 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,10 @@ mod iter; pub mod parser; pub mod types; -pub use context::FileAccessInterface; -pub use context::GlobalSettings; -pub use context::LocalFileAccessInterface; +pub mod settings { + pub use crate::context::FileAccessInterface; + pub use crate::context::GlobalSettings; + pub use crate::context::HeadlineLevelFilter; + pub use crate::context::LocalFileAccessInterface; + pub use crate::context::DEFAULT_TAB_WIDTH; +} diff --git a/src/main.rs b/src/main.rs index e6e7880d..6cdd8515 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,8 @@ use std::path::Path; use ::organic::parser::parse; use organic::parser::parse_with_settings; -use organic::GlobalSettings; -use organic::LocalFileAccessInterface; +use organic::settings::GlobalSettings; +use organic::settings::LocalFileAccessInterface; #[cfg(feature = "tracing")] use crate::init_tracing::init_telemetry; diff --git a/src/parser/in_buffer_settings.rs b/src/parser/in_buffer_settings.rs index 58976ab6..167d4284 100644 --- a/src/parser/in_buffer_settings.rs +++ b/src/parser/in_buffer_settings.rs @@ -11,8 +11,8 @@ use super::OrgSource; use crate::context::HeadlineLevelFilter; use crate::error::CustomError; use crate::error::Res; +use crate::settings::GlobalSettings; use crate::types::Keyword; -use crate::GlobalSettings; #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub(crate) fn scan_for_in_buffer_settings<'s>( diff --git a/src/parser/plain_list.rs b/src/parser/plain_list.rs index f2e43131..3b10d55d 100644 --- a/src/parser/plain_list.rs +++ b/src/parser/plain_list.rs @@ -166,12 +166,8 @@ fn plain_list_item<'b, 'g, 'r, 's>( |(_bullet_type, bull)| Into::<&str>::into(bull) != "*" || indent_level > 0, )(remaining)?; - let (remaining, _maybe_counter_set) = opt(tuple(( - space1, - tag("[@"), - parser_with_context!(counter)(context), - tag("]"), - )))(remaining)?; + let (remaining, _maybe_counter_set) = + opt(tuple((space1, tag("[@"), counter_set_value, tag("]"))))(remaining)?; let (remaining, maybe_checkbox) = opt(tuple((space1, item_checkbox)))(remaining)?; @@ -302,7 +298,7 @@ fn counter<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, OrgSource<'s>> { - if context.get_global_settings().org_list_allow_alphabetical { + if context.get_global_settings().list_allow_alphabetical { alt(( recognize(one_of( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", @@ -314,6 +310,16 @@ fn counter<'b, 'g, 'r, 's>( } } +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn counter_set_value<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { + alt(( + recognize(one_of( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + )), + digit1, + ))(input) +} + #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn plain_list_end<'b, 'g, 'r, 's>( _context: RefContext<'b, 'g, 'r, 's>, diff --git a/tests/test_loader.rs b/tests/test_loader.rs index 9d63cc11..076f4851 100644 --- a/tests/test_loader.rs +++ b/tests/test_loader.rs @@ -1,2 +1,5 @@ -#[cfg(feature = "compare")] +#![cfg(feature = "compare")] + +#[feature(exit_status_error)] + include!(concat!(env!("OUT_DIR"), "/tests.rs")); diff --git a/tests/test_template b/tests/test_template index 4454eb9b..108ac0c3 100644 --- a/tests/test_template +++ b/tests/test_template @@ -1,7 +1,66 @@ +// TODO: Investigate writing a proc macro to make specifying these combinations easier. For example, currently I am only setting 1 setting per test to keep the repetition reasonable when I should be mixing all the different combinations. + +{expect_fail} #[test] -fn {name}() -> Result<(), Box> {{ +fn autogen_default_{name}() -> Result<(), Box> {{ let org_path = "{path}"; let org_contents = std::fs::read_to_string(org_path).expect("Read org file."); organic::compare::run_anonymous_compare(org_contents.as_str())?; Ok(()) }} + +{expect_fail} +#[test] +fn autogen_la_{name}() -> Result<(), Box> {{ + let org_path = "{path}"; + let org_contents = std::fs::read_to_string(org_path).expect("Read org file."); + let global_settings = {{ + let mut global_settings = organic::settings::GlobalSettings::default(); + global_settings.list_allow_alphabetical = true; + global_settings + }}; + organic::compare::run_anonymous_compare_with_settings(org_contents.as_str(), &global_settings)?; + Ok(()) +}} + +{expect_fail} +#[test] +fn autogen_t1_{name}() -> Result<(), Box> {{ + let org_path = "{path}"; + let org_contents = std::fs::read_to_string(org_path).expect("Read org file."); + let global_settings = {{ + let mut global_settings = organic::settings::GlobalSettings::default(); + global_settings.tab_width = 1; + global_settings + }}; + organic::compare::run_anonymous_compare_with_settings(org_contents.as_str(), &global_settings)?; + Ok(()) +}} + +{expect_fail} +#[test] +fn autogen_t16_{name}() -> Result<(), Box> {{ + let org_path = "{path}"; + let org_contents = std::fs::read_to_string(org_path).expect("Read org file."); + let global_settings = {{ + let mut global_settings = organic::settings::GlobalSettings::default(); + global_settings.tab_width = 16; + global_settings + }}; + organic::compare::run_anonymous_compare_with_settings(org_contents.as_str(), &global_settings)?; + Ok(()) +}} + +{expect_fail} +#[test] +fn autogen_odd_{name}() -> Result<(), Box> {{ + let org_path = "{path}"; + let org_contents = std::fs::read_to_string(org_path).expect("Read org file."); + let global_settings = {{ + let mut global_settings = organic::settings::GlobalSettings::default(); + global_settings.odd_levels_only = organic::settings::HeadlineLevelFilter::Odd; + global_settings + }}; + organic::compare::run_anonymous_compare_with_settings(org_contents.as_str(), &global_settings)?; + Ok(()) +}}