use std::path::Path; use tokio::process::Command; use crate::settings::GlobalSettings; use crate::settings::HeadlineLevelFilter; pub async fn print_versions() -> Result<(), Box> { eprintln!("Using emacs version: {}", get_emacs_version().await?.trim()); eprintln!( "Using org-mode version: {}", get_org_mode_version().await?.trim() ); Ok(()) } pub(crate) async fn get_emacs_version() -> Result> { let elisp_script = r#"(progn (message "%s" (version)) )"#; let mut cmd = Command::new("emacs"); let cmd = cmd .arg("-q") .arg("--no-site-file") .arg("--no-splash") .arg("--batch") .arg("--eval") .arg(elisp_script); let out = cmd.output().await?; out.status.exit_ok()?; Ok(String::from_utf8(out.stderr)?) } pub(crate) async fn get_org_mode_version() -> Result> { let elisp_script = r#"(progn (org-mode) (message "%s" (org-version nil t nil)) )"#; let mut cmd = Command::new("emacs"); let cmd = cmd .arg("-q") .arg("--no-site-file") .arg("--no-splash") .arg("--batch") .arg("--eval") .arg(elisp_script); let out = cmd.output().await?; out.status.exit_ok()?; Ok(String::from_utf8(out.stderr)?) } pub(crate) async fn emacs_parse_anonymous_org_document<'g, 's, C>( file_contents: C, global_settings: &GlobalSettings<'g, 's>, ) -> Result> where C: AsRef, { let escaped_file_contents = escape_elisp_string(file_contents); let elisp_script = format!( r#"(progn (erase-buffer) (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, global_settings = global_settings_elisp(global_settings) ); let mut cmd = Command::new("emacs"); let cmd = cmd .arg("-q") .arg("--no-site-file") .arg("--no-splash") .arg("--batch") .arg("--eval") .arg(elisp_script); let out = cmd.output().await?; let status = out.status.exit_ok(); if status.is_err() { eprintln!( "Emacs errored out: {}\n{}", String::from_utf8(out.stdout)?, String::from_utf8(out.stderr)? ); status?; unreachable!(); } let org_sexp = out.stderr; Ok(String::from_utf8(org_sexp)?) } pub(crate) async fn emacs_parse_file_org_document<'g, 's, P>( file_path: P, global_settings: &GlobalSettings<'g, 's>, ) -> Result> where P: AsRef, { let file_path = file_path.as_ref().canonicalize()?; let containing_directory = file_path.parent().ok_or(format!( "Failed to get containing directory for path {}", file_path.display() ))?; let elisp_script = format!( r#"(progn (require 'org) (defun org-table-align () t) (setq vc-handled-backends nil) {global_settings} (find-file-read-only "{file_path}") (org-mode) (message "%s" (pp-to-string (org-element-parse-buffer))) )"#, global_settings = global_settings_elisp(global_settings), file_path = file_path .as_os_str() .to_str() .expect("File name should be valid utf-8.") ); let mut cmd = Command::new("emacs"); let cmd = cmd .current_dir(containing_directory) .arg("-q") .arg("--no-site-file") .arg("--no-splash") .arg("--batch") .arg("--eval") .arg(elisp_script); let out = cmd.output().await?; let status = out.status.exit_ok(); if status.is_err() { eprintln!( "Emacs errored out: {}\n{}", String::from_utf8(out.stdout)?, String::from_utf8(out.stderr)? ); status?; unreachable!(); } let org_sexp = out.stderr; Ok(String::from_utf8(org_sexp)?) } fn escape_elisp_string(file_contents: C) -> String where C: AsRef, { let source = file_contents.as_ref(); let source_len = source.len(); // We allocate a string 10% larger than the source to account for escape characters. Without this, we would have more allocations during processing. let mut output = String::with_capacity(source_len + (source_len / 10)); for c in source.chars() { match c { '"' | '\\' => { output.push('\\'); output.push(c); } _ => { output.push(c); } } } output } /// 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 }