Apply the link templates.
This commit is contained in:
		
							parent
							
								
									2ba5156ee1
								
							
						
					
					
						commit
						4c8828b91b
					
				| @ -1,3 +1,6 @@ | ||||
| #+LINK:       foo https://foo.bar/baz#%s | ||||
| [[foo::lorem]] | ||||
| [[foo::ipsum][dolar]] | ||||
| 
 | ||||
| [[cat::bat]] | ||||
| #+LINK:       cat dog%s | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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<String, String>, | ||||
| } | ||||
| 
 | ||||
| pub const DEFAULT_TAB_WIDTH: IndentationLevel = 8; | ||||
|  | ||||
| @ -131,7 +131,8 @@ fn document_org_source<'b, 'g, 'r, 's>( | ||||
|         .collect::<Result<Vec<_>, _>>()?; | ||||
|     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(), | ||||
|  | ||||
| @ -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<OrgSource<'s>, OrgSou | ||||
|     ))(input) | ||||
| } | ||||
| 
 | ||||
| #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] | ||||
| pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>( | ||||
|     keywords: Vec<Keyword<'sf>>, | ||||
|     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::*; | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| use std::borrow::Cow; | ||||
| use std::ops::RangeBounds; | ||||
| 
 | ||||
| use nom::Compare; | ||||
| @ -184,6 +185,18 @@ impl<'s> From<OrgSource<'s>> 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<OrgSource<'s>> for Cow<'s, str> { | ||||
|     fn from(value: OrgSource<'s>) -> Self { | ||||
|         (&value.full_source[value.start..value.end]).into() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl<'s, R> Slice<R> for OrgSource<'s> | ||||
| where | ||||
|     R: RangeBounds<usize>, | ||||
|  | ||||
| @ -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<Cow<'s, str>>, | ||||
| } | ||||
| 
 | ||||
| #[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<OrgSource<'s>, 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::<OrgSource<'_>>::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<String> { | ||||
|     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<OrgSource<'s>, PathReg<'s>> { | ||||
| @ -144,7 +245,9 @@ fn file_path_reg<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, 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::<Cow<str>>::into), | ||||
|         }, | ||||
|     )) | ||||
| } | ||||
|  | ||||
| @ -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<Cow<'s, str>>, | ||||
| } | ||||
| 
 | ||||
| #[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<T: AsRef<str>>(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::<str>::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::<str>::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<String> { | ||||
|         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() | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Tom Alexander
						Tom Alexander