Merge branch 'regular_link_properties'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded

This commit is contained in:
Tom Alexander 2023-10-07 00:34:14 -04:00
commit dfad7b7888
Signed by: talexander
GPG Key ID: D3A179C9A53C0EDE
17 changed files with 617 additions and 49 deletions

View File

@ -0,0 +1,13 @@
[[(foo)]]
[[((bar))]]
[[((baz)]]
[[(lo
rem)]]
# These become fuzzy
[[(foo) ]]
[[ (foo)]]
[[(foo)::3]]

View File

@ -0,0 +1,6 @@
[[#foo]]
[[#fo
o]]
[[#foo::3]]

View File

@ -0,0 +1,21 @@
[[./simple.org]]
[[../simple.org]]
[[/simple.org]]
[[file:simple.org]]
[[file:sim ple.org]]
[[file:simp
le.org]]
[[file:simple.org::3]]
[[file:simple.org::foo]]
[[file:simple.org::#foo]]
[[file:simple.org::foo bar]]
[[file:simple.org::foo
bar]]
[[file:simple.org::foo
bar]]
[[file:simple.org::foo
bar]]
[[file:simple.org::foo::bar]]
[[file:simple.org::/foo/]]

View File

@ -0,0 +1,6 @@
[[elisp.org]]
[[eli
sp.org]]
[[elisp.org::3]]

View File

@ -0,0 +1,6 @@
[[id:83986bdf-987c-465d-8851-44cb4c02a86c]]
[[id:83986bdf-987c-465d
-8851-44cb4c02a86c]]
[[id:83986bdf-987c-465d-8851-44cb4c02a86c::foo]]

View File

@ -0,0 +1,6 @@
[[shell:foo]]
[[shell:fo
o]]
[[shell:foo::3]]

View File

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

View File

@ -2,7 +2,9 @@ use std::fmt::Debug;
use super::diff::DiffStatus;
use super::sexp::Token;
use super::util::get_property;
use super::util::get_property_quoted_string;
use super::util::get_property_unquoted_atom;
#[derive(Debug)]
pub(crate) enum EmacsField<'s> {
@ -32,7 +34,36 @@ pub(crate) fn compare_identity() -> () {
()
}
pub(crate) fn compare_property_quoted_string<'b, 's, 'x, R, RG: Fn(R) -> Option<&'s str>>(
/// Assert that the emacs value is always nil or absent.
///
/// This is usually used for fields which, in my testing, are always nil. Using this compare function instead of simply doing a compare_noop will enable us to be alerted when we finally come across an org-mode document that has a value other than nil for the property.
pub(crate) fn compare_property_always_nil<'b, 's, 'x, R, RG>(
emacs: &'b Token<'s>,
_rust_node: R,
emacs_field: &'x str,
_rust_value_getter: RG,
) -> Result<Option<(DiffStatus, Option<String>)>, Box<dyn std::error::Error>> {
let value = get_property(emacs, emacs_field)?;
if value.is_some() {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} was expected to always be nil: {:?}",
emacs_field, value
));
Ok(Some((this_status, message)))
} else {
Ok(None)
}
}
pub(crate) fn compare_property_quoted_string<
'b,
's,
'x,
R,
RV: AsRef<str> + std::fmt::Debug,
RG: Fn(R) -> Option<RV>,
>(
emacs: &'b Token<'s>,
rust_node: R,
emacs_field: &'x str,
@ -40,7 +71,27 @@ pub(crate) fn compare_property_quoted_string<'b, 's, 'x, R, RG: Fn(R) -> Option<
) -> Result<Option<(DiffStatus, Option<String>)>, Box<dyn std::error::Error>> {
let value = get_property_quoted_string(emacs, emacs_field)?;
let rust_value = rust_value_getter(rust_node);
if !rust_value.eq(&value.as_ref().map(String::as_str)) {
if rust_value.as_ref().map(|s| s.as_ref()) != value.as_ref().map(String::as_str) {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, value, rust_value
));
Ok(Some((this_status, message)))
} else {
Ok(None)
}
}
pub(crate) fn compare_property_unquoted_atom<'b, 's, 'x, R, RG: Fn(R) -> Option<&'s str>>(
emacs: &'b Token<'s>,
rust_node: R,
emacs_field: &'x str,
rust_value_getter: RG,
) -> Result<Option<(DiffStatus, Option<String>)>, Box<dyn std::error::Error>> {
let value = get_property_unquoted_atom(emacs, emacs_field)?;
let rust_value = rust_value_getter(rust_node);
if rust_value != value {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",

View File

@ -3,7 +3,10 @@ use std::borrow::Cow;
use std::collections::BTreeSet;
use std::collections::HashSet;
use super::compare_field::compare_identity;
use super::compare_field::compare_property_always_nil;
use super::compare_field::compare_property_quoted_string;
use super::compare_field::compare_property_unquoted_atom;
use super::elisp_fact::ElispFact;
use super::elisp_fact::GetElispFact;
use super::sexp::unquote;
@ -58,6 +61,7 @@ use crate::types::LatexEnvironment;
use crate::types::LatexFragment;
use crate::types::LineBreak;
use crate::types::LineNumber;
use crate::types::LinkType;
use crate::types::Minute;
use crate::types::MinuteInner;
use crate::types::Month;
@ -2758,10 +2762,55 @@ fn compare_regular_link<'b, 's>(
emacs: &'b Token<'s>,
rust: &'b RegularLink<'s>,
) -> Result<DiffEntry<'b, 's>, Box<dyn std::error::Error>> {
let this_status = DiffStatus::Good;
let message = None;
let mut this_status = DiffStatus::Good;
let mut message = None;
// TODO: Compare :type :path :format :raw-link :application :search-option
if let Some((new_status, new_message)) = compare_properties!(
emacs,
rust,
(
EmacsField::Required(":type"),
|r| {
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
),
(
EmacsField::Required(":path"),
|r| Some(r.get_path()),
compare_property_quoted_string
),
(
EmacsField::Required(":format"),
|_| Some("bracket"),
compare_property_unquoted_atom
),
(
EmacsField::Required(":raw-link"),
|r| Some(r.get_raw_link()),
compare_property_quoted_string
),
(
EmacsField::Required(":application"),
compare_identity,
compare_property_always_nil
),
(
EmacsField::Required(":search-option"),
|r| r.get_search_option(),
compare_property_quoted_string
)
)? {
this_status = new_status;
message = new_message;
}
Ok(DiffResult {
status: this_status,

View File

@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use super::FileAccessInterface;
@ -37,6 +38,18 @@ pub struct GlobalSettings<'g, 's> {
///
/// Corresponds to org-coderef-label-format elisp variable.
pub coderef_label_format: &'g str,
/// The allowed protocols for links (for example, the "https" in "https://foo.bar/").
///
/// Corresponds to org-link-parameters elisp variable.
pub link_parameters: &'g [&'g str],
/// Link templates where the key is the document text and the value is the replacement.
///
/// 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<String, String>,
}
pub const DEFAULT_TAB_WIDTH: IndentationLevel = 8;
@ -55,6 +68,8 @@ impl<'g, 's> GlobalSettings<'g, 's> {
odd_levels_only: HeadlineLevelFilter::default(),
footnote_section: "Footnotes",
coderef_label_format: "(ref:%s)",
link_parameters: &DEFAULT_ORG_LINK_PARAMETERS,
link_templates: BTreeMap::new(),
}
}
}
@ -76,3 +91,29 @@ impl Default for HeadlineLevelFilter {
HeadlineLevelFilter::OddEven
}
}
const DEFAULT_ORG_LINK_PARAMETERS: [&'static str; 23] = [
"id",
"eww",
"rmail",
"mhe",
"irc",
"info",
"gnus",
"docview",
"bibtex",
"bbdb",
"w3m",
"doi",
"file+sys",
"file+emacs",
"shell",
"news",
"mailto",
"https",
"http",
"ftp",
"help",
"file",
"elisp",
];

View File

@ -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(),

View File

@ -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::*;

View File

@ -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>,

View File

@ -32,33 +32,6 @@ use crate::parser::util::get_consumed;
use crate::parser::util::WORD_CONSTITUENT_CHARACTERS;
use crate::types::PlainLink;
// TODO: Make this a user-provided variable corresponding to elisp's org-link-parameters
const ORG_LINK_PARAMETERS: [&'static str; 23] = [
"id",
"eww",
"rmail",
"mhe",
"irc",
"info",
"gnus",
"docview",
"bibtex",
"bbdb",
"w3m",
"doi",
"file+sys",
"file+emacs",
"shell",
"news",
"mailto",
"https",
"http",
"ftp",
"help",
"file",
"elisp",
];
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn plain_link<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
@ -113,12 +86,11 @@ fn post<'b, 'g, 'r, 's>(
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn protocol<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
// TODO: This should be defined by org-link-parameters
for link_parameter in ORG_LINK_PARAMETERS {
let result = tag_no_case::<_, _, CustomError<_>>(link_parameter)(input);
for link_parameter in context.get_global_settings().link_parameters {
let result = tag_no_case::<_, _, CustomError<_>>(*link_parameter)(input);
match result {
Ok((remaining, ent)) => {
return Ok((remaining, ent));

View File

@ -1,13 +1,28 @@
use std::borrow::Cow;
use nom::branch::alt;
use nom::bytes::complete::escaped;
use nom::bytes::complete::is_a;
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;
use nom::combinator::map;
use nom::combinator::map_parser;
use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize;
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;
use super::plain_link::protocol;
use super::util::exit_matcher_parser;
use super::util::get_consumed;
use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting;
@ -16,7 +31,10 @@ 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;
use crate::types::RegularLink;
@ -37,7 +55,7 @@ fn regular_link_without_description<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, RegularLink<'s>> {
let (remaining, _opening_bracket) = tag("[[")(input)?;
let (remaining, _path) = pathreg(context, remaining)?;
let (remaining, path) = pathreg(context, remaining)?;
let (remaining, _closing_bracket) = tag("]]")(remaining)?;
let (remaining, _trailing_whitespace) =
maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?;
@ -46,6 +64,10 @@ fn regular_link_without_description<'b, 'g, 'r, 's>(
remaining,
RegularLink {
source: source.into(),
link_type: path.link_type,
path: path.path,
raw_link: path.raw_link,
search_option: path.search_option,
},
))
}
@ -56,7 +78,7 @@ fn regular_link_with_description<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, RegularLink<'s>> {
let (remaining, _opening_bracket) = tag("[[")(input)?;
let (remaining, _path) = pathreg(context, remaining)?;
let (remaining, path) = pathreg(context, remaining)?;
let (remaining, _closing_bracket) = tag("][")(remaining)?;
let (remaining, _description) = description(context, remaining)?;
let (remaining, _closing_bracket) = tag("]]")(remaining)?;
@ -67,26 +89,251 @@ fn regular_link_with_description<'b, 'g, 'r, 's>(
remaining,
RegularLink {
source: source.into(),
link_type: path.link_type,
path: path.path,
raw_link: path.raw_link,
search_option: path.search_option,
},
))
}
#[derive(Debug)]
struct PathReg<'s> {
link_type: LinkType<'s>,
path: Cow<'s, str>,
raw_link: Cow<'s, str>,
search_option: Option<Cow<'s, str>>,
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn pathreg<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, path) = escaped(
take_till1(|c| match c {
'\\' | '[' | ']' => true,
_ => false,
}),
'\\',
anychar,
) -> Res<OrgSource<'s>, PathReg<'s>> {
let (remaining, path) = map_parser(
escaped(
take_till1(|c| match c {
'\\' | '[' | ']' => true,
_ => false,
}),
'\\',
anychar,
),
parser_with_context!(parse_path_reg)(context),
)(input)?;
Ok((remaining, path))
}
fn parse_path_reg<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'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((
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<_>>(":")),
is_a(":"),
)),
|(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;
let mut injected_value = false;
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);
injected_value = true;
ParserState::Normal
}
(ParserState::Percent, _) => {
panic!("Unhandled percent value: {}", c)
}
};
}
// Handle lingering state
match state {
ParserState::Percent => {
ret.push('%');
}
_ => {}
}
if !injected_value {
ret.push_str(inject_value);
}
Some(ret)
}
fn file_path_reg<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, PathReg<'s>> {
let (remaining, (raw_link, (_, path, search_option))) = consumed(tuple((
alt((tag("file:"), peek(tag(".")), peek(tag("/")))),
recognize(many_till(anychar, alt((peek(tag("::")), eof)))),
opt(map(tuple((tag("::"), rest)), |(_, search_option)| {
search_option
})),
)))(input)?;
Ok((
remaining,
PathReg {
link_type: LinkType::File,
path: path.into(),
raw_link: raw_link.into(),
search_option: search_option
.map(Into::<&str>::into)
.map(Into::<Cow<str>>::into),
},
))
}
fn id_path_reg<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, PathReg<'s>> {
let (remaining, (raw_link, (_, path))) = consumed(tuple((tag("id:"), rest)))(input)?;
Ok((
remaining,
PathReg {
link_type: LinkType::Id,
path: path.into(),
raw_link: raw_link.into(),
search_option: None,
},
))
}
fn custom_id_path_reg<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, PathReg<'s>> {
let (remaining, (raw_link, (_, path))) = consumed(tuple((tag("#"), rest)))(input)?;
Ok((
remaining,
PathReg {
link_type: LinkType::CustomId,
path: path.into(),
raw_link: raw_link.into(),
search_option: None,
},
))
}
fn code_ref_path_reg<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, PathReg<'s>> {
let (remaining, (raw_link, (_, path, _))) = consumed(tuple((
tag("("),
recognize(many_till(anychar, peek(tuple((tag(")"), eof))))),
tag(")"),
)))(input)?;
Ok((
remaining,
PathReg {
link_type: LinkType::CodeRef,
path: path.into(),
raw_link: raw_link.into(),
search_option: None,
},
))
}
fn protocol_path_reg<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, PathReg<'s>> {
let (remaining, (raw_link, (protocol, _, path))) = consumed(tuple((
parser_with_context!(protocol)(context),
tag(":"),
rest,
)))(input)?;
Ok((
remaining,
PathReg {
link_type: LinkType::Protocol(protocol.into()),
path: path.into(),
raw_link: raw_link.into(),
search_option: None,
},
))
}
fn fuzzy_path_reg<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, PathReg<'s>> {
let (remaining, body) = rest(input)?;
Ok((
remaining,
PathReg {
link_type: LinkType::Fuzzy,
path: body.into(),
raw_link: body.into(),
search_option: None,
},
))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn description<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,

View File

@ -75,6 +75,7 @@ pub use object::InlineSourceBlock;
pub use object::Italic;
pub use object::LatexFragment;
pub use object::LineBreak;
pub use object::LinkType;
pub use object::Minute;
pub use object::MinuteInner;
pub use object::Month;

View File

@ -1,3 +1,6 @@
use std::borrow::Borrow;
use std::borrow::Cow;
use super::GetStandardProperties;
use super::StandardProperties;
@ -77,6 +80,10 @@ pub struct PlainText<'s> {
#[derive(Debug, PartialEq)]
pub struct RegularLink<'s> {
pub source: &'s str,
pub link_type: LinkType<'s>,
pub path: Cow<'s, str>,
pub raw_link: Cow<'s, str>,
pub search_option: Option<Cow<'s, str>>,
}
#[derive(Debug, PartialEq)]
@ -635,3 +642,77 @@ impl<'s> Timestamp<'s> {
self.source.trim_end()
}
}
#[derive(Debug, PartialEq)]
pub enum LinkType<'s> {
File,
Protocol(Cow<'s, str>),
Id,
CustomId,
CodeRef,
Fuzzy,
}
#[derive(Debug)]
enum ParserState {
Normal,
InWhitespace,
}
/// 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<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() {
state = match (&state, c) {
(ParserState::Normal, _) if " \t\r\n".contains(c) => {
ret.push(' ');
ParserState::InWhitespace
}
(ParserState::InWhitespace, _) if " \t\r\n".contains(c) => ParserState::InWhitespace,
(ParserState::Normal, _) => {
ret.push(c);
ParserState::Normal
}
(ParserState::InWhitespace, _) => {
ret.push(c);
ParserState::Normal
}
};
}
ret
}
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(Borrow::<str>::borrow(&self.raw_link))
} else {
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(Borrow::<str>::borrow(&self.path))
} else {
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.as_ref().map(|search_option| {
if search_option.contains('\n') {
orgify_text(search_option)
} else {
search_option.clone().into_owned()
}
})
}
}