From 4c8828b91b796fe61ff53b88a3c7c580a3539a45 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Fri, 6 Oct 2023 22:08:26 -0400 Subject: [PATCH] Apply the link templates. --- .../object/regular_link/template.org | 3 + src/compare/diff.rs | 14 +- src/context/global_settings.rs | 2 +- src/parser/document.rs | 6 +- src/parser/in_buffer_settings.rs | 46 +++++++ src/parser/org_source.rs | 13 ++ src/parser/regular_link.rs | 125 ++++++++++++++++-- src/types/object.rs | 26 ++-- 8 files changed, 203 insertions(+), 32 deletions(-) diff --git a/org_mode_samples/object/regular_link/template.org b/org_mode_samples/object/regular_link/template.org index 113de674..d98a77b2 100644 --- a/org_mode_samples/object/regular_link/template.org +++ b/org_mode_samples/object/regular_link/template.org @@ -1,3 +1,6 @@ #+LINK: foo https://foo.bar/baz#%s [[foo::lorem]] [[foo::ipsum][dolar]] + +[[cat::bat]] +#+LINK: cat dog%s diff --git a/src/compare/diff.rs b/src/compare/diff.rs index 016bb912..0ea57a88 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -2771,13 +2771,13 @@ fn compare_regular_link<'b, 's>( ( EmacsField::Required(":type"), |r| { - match r.link_type { - LinkType::File => Some("file"), - LinkType::Protocol(protocol) => Some(protocol), - LinkType::Id => Some("id"), - LinkType::CustomId => Some("custom-id"), - LinkType::CodeRef => Some("coderef"), - LinkType::Fuzzy => Some("fuzzy"), + match &r.link_type { + LinkType::File => Some(Cow::Borrowed("file")), + LinkType::Protocol(protocol) => Some(protocol.clone()), + LinkType::Id => Some(Cow::Borrowed("id")), + LinkType::CustomId => Some(Cow::Borrowed("custom-id")), + LinkType::CodeRef => Some(Cow::Borrowed("coderef")), + LinkType::Fuzzy => Some(Cow::Borrowed("fuzzy")), } }, compare_property_quoted_string diff --git a/src/context/global_settings.rs b/src/context/global_settings.rs index ebf9365a..0c3a0cfb 100644 --- a/src/context/global_settings.rs +++ b/src/context/global_settings.rs @@ -49,7 +49,7 @@ pub struct GlobalSettings<'g, 's> { /// For example, `"foo": "bar%s"` will replace `[[foo::baz]]` with `[[barbaz]]` /// /// This is set by including #+LINK in the org-mode document. - pub link_templates: BTreeMap<&'s str, &'s str>, + pub link_templates: BTreeMap, } pub const DEFAULT_TAB_WIDTH: IndentationLevel = 8; diff --git a/src/parser/document.rs b/src/parser/document.rs index b49973ec..49011731 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -131,7 +131,8 @@ fn document_org_source<'b, 'g, 'r, 's>( .collect::, _>>()?; for setup_file in setup_files.iter().map(String::as_str) { let (_, setup_file_settings) = - scan_for_in_buffer_settings(setup_file.into()).map_err(|_err| { + scan_for_in_buffer_settings(setup_file.into()).map_err(|err| { + eprintln!("{}", err); nom::Err::Error(CustomError::MyError(MyError( "TODO: make this take an owned string so I can dump err.to_string() into it." .into(), @@ -141,7 +142,8 @@ fn document_org_source<'b, 'g, 'r, 's>( } final_settings.extend(document_settings); let new_settings = apply_in_buffer_settings(final_settings, context.get_global_settings()) - .map_err(|_err| { + .map_err(|err| { + eprintln!("{}", err); nom::Err::Error(CustomError::MyError(MyError( "TODO: make this take an owned string so I can dump err.to_string() into it." .into(), diff --git a/src/parser/in_buffer_settings.rs b/src/parser/in_buffer_settings.rs index d71e413b..60ff54b7 100644 --- a/src/parser/in_buffer_settings.rs +++ b/src/parser/in_buffer_settings.rs @@ -2,8 +2,17 @@ use nom::branch::alt; use nom::bytes::complete::is_not; use nom::bytes::complete::tag_no_case; use nom::bytes::complete::take_until; +use nom::character::complete::anychar; +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::peek; +use nom::combinator::recognize; +use nom::multi::many_till; use nom::multi::separated_list0; +use nom::sequence::tuple; use super::keyword::filtered_keyword; use super::keyword_todo::todo_keywords; @@ -75,6 +84,7 @@ fn in_buffer_settings_key<'s>(input: OrgSource<'s>) -> Res, OrgSou ))(input) } +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>( keywords: Vec>, original_settings: &'g GlobalSettings<'g, 's>, @@ -113,10 +123,22 @@ pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>( } } + // Link templates + for kw in keywords + .iter() + .filter(|kw| kw.key.eq_ignore_ascii_case("link")) + { + let (_, (link_key, link_value)) = link_template(kw.value).map_err(|e| e.to_string())?; + new_settings + .link_templates + .insert(link_key.to_owned(), link_value.to_owned()); + } + Ok(new_settings) } /// Apply in-buffer settings that do not impact parsing and therefore can be applied after parsing. +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub(crate) fn apply_post_parse_in_buffer_settings<'g, 's, 'sf>( document: &mut Document<'s>, ) -> Result<(), &'static str> { @@ -135,6 +157,30 @@ pub(crate) fn apply_post_parse_in_buffer_settings<'g, 's, 'sf>( Ok(()) } +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn link_template<'s>(input: &'s str) -> Res<&'s str, (&'s str, &'s str)> { + let (remaining, key) = map( + tuple(( + space0, + recognize(many_till(anychar, peek(alt((space1, line_ending, eof))))), + )), + |(_, key)| key, + )(input)?; + + let (remaining, replacement) = map( + tuple(( + space1, + recognize(many_till( + anychar, + peek(tuple((space0, alt((line_ending, eof))))), + )), + )), + |(_, replacement)| replacement, + )(remaining)?; + + Ok((remaining, (key, replacement))) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/parser/org_source.rs b/src/parser/org_source.rs index eb827a09..b530b341 100644 --- a/src/parser/org_source.rs +++ b/src/parser/org_source.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::ops::RangeBounds; use nom::Compare; @@ -184,6 +185,18 @@ impl<'s> From> for &'s str { } } +impl<'s> From<&OrgSource<'s>> for Cow<'s, str> { + fn from(value: &OrgSource<'s>) -> Self { + (&value.full_source[value.start..value.end]).into() + } +} + +impl<'s> From> for Cow<'s, str> { + fn from(value: OrgSource<'s>) -> Self { + (&value.full_source[value.start..value.end]).into() + } +} + impl<'s, R> Slice for OrgSource<'s> where R: RangeBounds, diff --git a/src/parser/regular_link.rs b/src/parser/regular_link.rs index 76f9ec35..d74b272a 100644 --- a/src/parser/regular_link.rs +++ b/src/parser/regular_link.rs @@ -1,7 +1,10 @@ +use std::borrow::Cow; + use nom::branch::alt; use nom::bytes::complete::escaped; use nom::bytes::complete::tag; use nom::bytes::complete::take_till1; +use nom::bytes::complete::take_until; use nom::character::complete::anychar; use nom::combinator::consumed; use nom::combinator::eof; @@ -14,6 +17,7 @@ use nom::combinator::rest; use nom::combinator::verify; use nom::multi::many_till; use nom::sequence::tuple; +use nom::InputTake; use super::object_parser::regular_link_description_set_object; use super::org_source::OrgSource; @@ -26,6 +30,8 @@ use crate::context::ContextElement; use crate::context::ExitClass; use crate::context::ExitMatcherNode; use crate::context::RefContext; +use crate::error::CustomError; +use crate::error::MyError; use crate::error::Res; use crate::types::LinkType; use crate::types::Object; @@ -90,11 +96,12 @@ fn regular_link_with_description<'b, 'g, 'r, 's>( )) } +#[derive(Debug)] struct PathReg<'s> { link_type: LinkType<'s>, - path: &'s str, - raw_link: &'s str, - search_option: Option<&'s str>, + path: Cow<'s, str>, + raw_link: Cow<'s, str>, + search_option: Option>, } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] @@ -120,14 +127,108 @@ fn parse_path_reg<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, PathReg<'s>> { - alt(( - file_path_reg, - id_path_reg, - custom_id_path_reg, - code_ref_path_reg, - parser_with_context!(protocol_path_reg)(context), - fuzzy_path_reg, + if let Some(replaced_link) = apply_link_templates(context, input) { + let replaced_input = Into::>::into(replaced_link.as_str()); + let (_remaining, link) = alt(( + file_path_reg, + id_path_reg, + custom_id_path_reg, + code_ref_path_reg, + parser_with_context!(protocol_path_reg)(context), + fuzzy_path_reg, + ))(replaced_input) + .map_err(|_| { + nom::Err::Error(CustomError::MyError(MyError( + "No pathreg match after replacement.", + ))) + })?; + let remaining = input.take(input.len()); + let link_type = match link.link_type { + LinkType::Protocol(protocol) => LinkType::Protocol(protocol.into_owned().into()), + LinkType::File => LinkType::File, + LinkType::Id => LinkType::Id, + LinkType::CustomId => LinkType::CustomId, + LinkType::CodeRef => LinkType::CodeRef, + LinkType::Fuzzy => LinkType::Fuzzy, + }; + Ok(( + remaining, + PathReg { + link_type, + path: link.path.into_owned().into(), + raw_link: link.raw_link.into_owned().into(), + search_option: link.search_option.map(|s| s.into_owned().into()), + }, + )) + } else { + alt(( + file_path_reg, + id_path_reg, + custom_id_path_reg, + code_ref_path_reg, + parser_with_context!(protocol_path_reg)(context), + fuzzy_path_reg, + ))(input) + } +} + +enum ParserState { + Normal, + Percent, +} + +fn apply_link_templates<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Option { + let (remaining, key) = opt(map( + tuple(( + recognize(take_until::<_, _, nom::error::Error<_>>("::")), + tag("::"), + )), + |(key, _)| key, ))(input) + .expect("opt ensures this cannot error."); + let key = match key { + Some(key) => key, + None => { + return None; + } + }; + let replacement_template = match context.get_global_settings().link_templates.get(key.into()) { + Some(template) => template, + None => { + return None; + } + }; + + let inject_value = Into::<&str>::into(remaining); + let mut ret = String::with_capacity(replacement_template.len() + inject_value.len()); + let mut state = ParserState::Normal; + for c in replacement_template.chars() { + state = match (&state, c) { + (ParserState::Normal, '%') => ParserState::Percent, + (ParserState::Normal, _) => { + ret.push(c); + ParserState::Normal + } + (ParserState::Percent, 's') => { + ret.push_str(inject_value); + ParserState::Normal + } + (ParserState::Percent, _) => { + panic!("Unhandled percent value: {}", c) + } + }; + } + // Handle lingering state + match state { + ParserState::Percent => { + ret.push('%'); + } + _ => {} + } + Some(ret) } fn file_path_reg<'s>(input: OrgSource<'s>) -> Res, PathReg<'s>> { @@ -144,7 +245,9 @@ fn file_path_reg<'s>(input: OrgSource<'s>) -> Res, PathReg<'s>> { link_type: LinkType::File, path: path.into(), raw_link: raw_link.into(), - search_option: search_option.map(Into::<&str>::into), + search_option: search_option + .map(Into::<&str>::into) + .map(Into::>::into), }, )) } diff --git a/src/types/object.rs b/src/types/object.rs index 9185e9ec..68b5bc08 100644 --- a/src/types/object.rs +++ b/src/types/object.rs @@ -1,3 +1,6 @@ +use std::borrow::Borrow; +use std::borrow::Cow; + use super::GetStandardProperties; use super::StandardProperties; @@ -78,9 +81,9 @@ pub struct PlainText<'s> { pub struct RegularLink<'s> { pub source: &'s str, pub link_type: LinkType<'s>, - pub path: &'s str, - pub raw_link: &'s str, - pub search_option: Option<&'s str>, + pub path: Cow<'s, str>, + pub raw_link: Cow<'s, str>, + pub search_option: Option>, } #[derive(Debug, PartialEq)] @@ -643,7 +646,7 @@ impl<'s> Timestamp<'s> { #[derive(Debug, PartialEq)] pub enum LinkType<'s> { File, - Protocol(&'s str), + Protocol(Cow<'s, str>), Id, CustomId, CodeRef, @@ -659,7 +662,8 @@ enum ParserState { /// Org-mode treats multiple consecutive whitespace characters as a single space. This function performs that transformation. /// /// Example: `orgify_text("foo \t\n bar") == "foo bar"` -pub(crate) fn orgify_text<'s>(raw_text: &'s str) -> String { +pub(crate) fn orgify_text>(raw_text: T) -> String { + let raw_text = raw_text.as_ref(); let mut ret = String::with_capacity(raw_text.len()); let mut state = ParserState::Normal; for c in raw_text.chars() { @@ -686,28 +690,28 @@ impl<'s> RegularLink<'s> { /// Orgify the raw_link if it contains line breaks. pub fn get_raw_link(&self) -> String { if self.raw_link.contains('\n') { - orgify_text(self.raw_link) + orgify_text(Borrow::::borrow(&self.raw_link)) } else { - self.raw_link.to_owned() + self.raw_link.clone().into_owned() } } /// Orgify the path if it contains line breaks. pub fn get_path(&self) -> String { if self.path.contains('\n') { - orgify_text(self.path) + orgify_text(Borrow::::borrow(&self.path)) } else { - self.path.to_owned() + self.path.clone().into_owned() } } /// Orgify the search_option if it contains line breaks. pub fn get_search_option(&self) -> Option { - self.search_option.map(|search_option| { + self.search_option.as_ref().map(|search_option| { if search_option.contains('\n') { orgify_text(search_option) } else { - search_option.to_owned() + search_option.clone().into_owned() } }) }