Apply the link templates.

This commit is contained in:
Tom Alexander 2023-10-06 22:08:26 -04:00
parent 2ba5156ee1
commit 4c8828b91b
Signed by: talexander
GPG Key ID: D3A179C9A53C0EDE
8 changed files with 203 additions and 32 deletions

View File

@ -1,3 +1,6 @@
#+LINK: foo https://foo.bar/baz#%s #+LINK: foo https://foo.bar/baz#%s
[[foo::lorem]] [[foo::lorem]]
[[foo::ipsum][dolar]] [[foo::ipsum][dolar]]
[[cat::bat]]
#+LINK: cat dog%s

View File

@ -2771,13 +2771,13 @@ fn compare_regular_link<'b, 's>(
( (
EmacsField::Required(":type"), EmacsField::Required(":type"),
|r| { |r| {
match r.link_type { match &r.link_type {
LinkType::File => Some("file"), LinkType::File => Some(Cow::Borrowed("file")),
LinkType::Protocol(protocol) => Some(protocol), LinkType::Protocol(protocol) => Some(protocol.clone()),
LinkType::Id => Some("id"), LinkType::Id => Some(Cow::Borrowed("id")),
LinkType::CustomId => Some("custom-id"), LinkType::CustomId => Some(Cow::Borrowed("custom-id")),
LinkType::CodeRef => Some("coderef"), LinkType::CodeRef => Some(Cow::Borrowed("coderef")),
LinkType::Fuzzy => Some("fuzzy"), LinkType::Fuzzy => Some(Cow::Borrowed("fuzzy")),
} }
}, },
compare_property_quoted_string compare_property_quoted_string

View File

@ -49,7 +49,7 @@ pub struct GlobalSettings<'g, 's> {
/// For example, `"foo": "bar%s"` will replace `[[foo::baz]]` with `[[barbaz]]` /// For example, `"foo": "bar%s"` will replace `[[foo::baz]]` with `[[barbaz]]`
/// ///
/// This is set by including #+LINK in the org-mode document. /// 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; pub const DEFAULT_TAB_WIDTH: IndentationLevel = 8;

View File

@ -131,7 +131,8 @@ fn document_org_source<'b, 'g, 'r, 's>(
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
for setup_file in setup_files.iter().map(String::as_str) { for setup_file in setup_files.iter().map(String::as_str) {
let (_, setup_file_settings) = 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( nom::Err::Error(CustomError::MyError(MyError(
"TODO: make this take an owned string so I can dump err.to_string() into it." "TODO: make this take an owned string so I can dump err.to_string() into it."
.into(), .into(),
@ -141,7 +142,8 @@ fn document_org_source<'b, 'g, 'r, 's>(
} }
final_settings.extend(document_settings); final_settings.extend(document_settings);
let new_settings = apply_in_buffer_settings(final_settings, context.get_global_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( nom::Err::Error(CustomError::MyError(MyError(
"TODO: make this take an owned string so I can dump err.to_string() into it." "TODO: make this take an owned string so I can dump err.to_string() into it."
.into(), .into(),

View File

@ -2,8 +2,17 @@ use nom::branch::alt;
use nom::bytes::complete::is_not; use nom::bytes::complete::is_not;
use nom::bytes::complete::tag_no_case; use nom::bytes::complete::tag_no_case;
use nom::bytes::complete::take_until; 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::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::multi::separated_list0;
use nom::sequence::tuple;
use super::keyword::filtered_keyword; use super::keyword::filtered_keyword;
use super::keyword_todo::todo_keywords; use super::keyword_todo::todo_keywords;
@ -75,6 +84,7 @@ fn in_buffer_settings_key<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSou
))(input) ))(input)
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>( pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
keywords: Vec<Keyword<'sf>>, keywords: Vec<Keyword<'sf>>,
original_settings: &'g GlobalSettings<'g, 's>, 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) Ok(new_settings)
} }
/// Apply in-buffer settings that do not impact parsing and therefore can be applied after parsing. /// 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>( pub(crate) fn apply_post_parse_in_buffer_settings<'g, 's, 'sf>(
document: &mut Document<'s>, document: &mut Document<'s>,
) -> Result<(), &'static str> { ) -> Result<(), &'static str> {
@ -135,6 +157,30 @@ pub(crate) fn apply_post_parse_in_buffer_settings<'g, 's, 'sf>(
Ok(()) 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::ops::RangeBounds; use std::ops::RangeBounds;
use nom::Compare; 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> impl<'s, R> Slice<R> for OrgSource<'s>
where where
R: RangeBounds<usize>, R: RangeBounds<usize>,

View File

@ -1,7 +1,10 @@
use std::borrow::Cow;
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::escaped; use nom::bytes::complete::escaped;
use nom::bytes::complete::tag; use nom::bytes::complete::tag;
use nom::bytes::complete::take_till1; use nom::bytes::complete::take_till1;
use nom::bytes::complete::take_until;
use nom::character::complete::anychar; use nom::character::complete::anychar;
use nom::combinator::consumed; use nom::combinator::consumed;
use nom::combinator::eof; use nom::combinator::eof;
@ -14,6 +17,7 @@ use nom::combinator::rest;
use nom::combinator::verify; use nom::combinator::verify;
use nom::multi::many_till; use nom::multi::many_till;
use nom::sequence::tuple; use nom::sequence::tuple;
use nom::InputTake;
use super::object_parser::regular_link_description_set_object; use super::object_parser::regular_link_description_set_object;
use super::org_source::OrgSource; use super::org_source::OrgSource;
@ -26,6 +30,8 @@ use crate::context::ContextElement;
use crate::context::ExitClass; use crate::context::ExitClass;
use crate::context::ExitMatcherNode; use crate::context::ExitMatcherNode;
use crate::context::RefContext; use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res; use crate::error::Res;
use crate::types::LinkType; use crate::types::LinkType;
use crate::types::Object; use crate::types::Object;
@ -90,11 +96,12 @@ fn regular_link_with_description<'b, 'g, 'r, 's>(
)) ))
} }
#[derive(Debug)]
struct PathReg<'s> { struct PathReg<'s> {
link_type: LinkType<'s>, link_type: LinkType<'s>,
path: &'s str, path: Cow<'s, str>,
raw_link: &'s str, raw_link: Cow<'s, str>,
search_option: Option<&'s str>, search_option: Option<Cow<'s, str>>,
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@ -120,6 +127,40 @@ fn parse_path_reg<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, PathReg<'s>> { ) -> Res<OrgSource<'s>, PathReg<'s>> {
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(( alt((
file_path_reg, file_path_reg,
id_path_reg, id_path_reg,
@ -129,6 +170,66 @@ fn parse_path_reg<'b, 'g, 'r, 's>(
fuzzy_path_reg, fuzzy_path_reg,
))(input) ))(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>> { fn file_path_reg<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, PathReg<'s>> {
let (remaining, (raw_link, (_, path, search_option))) = consumed(tuple(( let (remaining, (raw_link, (_, path, search_option))) = consumed(tuple((
@ -144,7 +245,9 @@ fn file_path_reg<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, PathReg<'s>> {
link_type: LinkType::File, link_type: LinkType::File,
path: path.into(), path: path.into(),
raw_link: raw_link.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),
}, },
)) ))
} }

View File

@ -1,3 +1,6 @@
use std::borrow::Borrow;
use std::borrow::Cow;
use super::GetStandardProperties; use super::GetStandardProperties;
use super::StandardProperties; use super::StandardProperties;
@ -78,9 +81,9 @@ pub struct PlainText<'s> {
pub struct RegularLink<'s> { pub struct RegularLink<'s> {
pub source: &'s str, pub source: &'s str,
pub link_type: LinkType<'s>, pub link_type: LinkType<'s>,
pub path: &'s str, pub path: Cow<'s, str>,
pub raw_link: &'s str, pub raw_link: Cow<'s, str>,
pub search_option: Option<&'s str>, pub search_option: Option<Cow<'s, str>>,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -643,7 +646,7 @@ impl<'s> Timestamp<'s> {
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum LinkType<'s> { pub enum LinkType<'s> {
File, File,
Protocol(&'s str), Protocol(Cow<'s, str>),
Id, Id,
CustomId, CustomId,
CodeRef, CodeRef,
@ -659,7 +662,8 @@ enum ParserState {
/// Org-mode treats multiple consecutive whitespace characters as a single space. This function performs that transformation. /// 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"` /// 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 ret = String::with_capacity(raw_text.len());
let mut state = ParserState::Normal; let mut state = ParserState::Normal;
for c in raw_text.chars() { for c in raw_text.chars() {
@ -686,28 +690,28 @@ impl<'s> RegularLink<'s> {
/// Orgify the raw_link if it contains line breaks. /// Orgify the raw_link if it contains line breaks.
pub fn get_raw_link(&self) -> String { pub fn get_raw_link(&self) -> String {
if self.raw_link.contains('\n') { if self.raw_link.contains('\n') {
orgify_text(self.raw_link) orgify_text(Borrow::<str>::borrow(&self.raw_link))
} else { } else {
self.raw_link.to_owned() self.raw_link.clone().into_owned()
} }
} }
/// Orgify the path if it contains line breaks. /// Orgify the path if it contains line breaks.
pub fn get_path(&self) -> String { pub fn get_path(&self) -> String {
if self.path.contains('\n') { if self.path.contains('\n') {
orgify_text(self.path) orgify_text(Borrow::<str>::borrow(&self.path))
} else { } else {
self.path.to_owned() self.path.clone().into_owned()
} }
} }
/// Orgify the search_option if it contains line breaks. /// Orgify the search_option if it contains line breaks.
pub fn get_search_option(&self) -> Option<String> { 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') { if search_option.contains('\n') {
orgify_text(search_option) orgify_text(search_option)
} else { } else {
search_option.to_owned() search_option.clone().into_owned()
} }
}) })
} }