Compare commits

...

7 Commits

Author SHA1 Message Date
Tom Alexander
dd009498dd
Switch to using coalesce_whitespace_escaped for macro args.
Some checks failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-10-08 16:19:25 -04:00
Tom Alexander
17c745ee71
Improve coalesce_whitespace_escaped to borrow when single spaces are used. 2023-10-08 16:15:49 -04:00
Tom Alexander
41aa0349a0
Add tests for coalesce_whitespace_escaped. 2023-10-08 16:06:52 -04:00
Tom Alexander
a6adeee40b
Handle escaping the characters in org macro arguments. 2023-10-08 15:54:56 -04:00
Tom Alexander
a32cea8139
Coalesce whitespace in macro args. 2023-10-08 15:08:21 -04:00
Tom Alexander
37bc5ef712
Do not include whitespace at the end of value. 2023-10-08 14:48:29 -04:00
Tom Alexander
1a2f0856da
Compare macro properties. 2023-10-08 14:40:01 -04:00
7 changed files with 425 additions and 3 deletions

View File

@ -0,0 +1,7 @@
{{{foo}}}
{{{fo\o}}}
{{{foo(b\ar)}}}
{{{foo(b\,r)}}}

View File

@ -0,0 +1,4 @@
{{{foo(bar baz)}}}
{{{foo(bar
baz)}}}

View File

@ -1,6 +1,7 @@
use std::fmt::Debug;
use super::diff::DiffStatus;
use super::sexp::unquote;
use super::sexp::Token;
use super::util::get_property;
use super::util::get_property_quoted_string;
@ -102,3 +103,59 @@ pub(crate) fn compare_property_unquoted_atom<'b, 's, 'x, R, RG: Fn(R) -> Option<
Ok(None)
}
}
pub(crate) fn compare_property_list_of_quoted_string<
'b,
's,
'x,
R,
RV: AsRef<str> + std::fmt::Debug,
RI: Iterator<Item = RV>,
RG: Fn(R) -> Option<RI>,
>(
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)?
.map(Token::as_list)
.map_or(Ok(None), |r| r.map(Some))?;
let rust_value = rust_value_getter(rust_node);
// TODO: Seems we are needlessly coverting to a vec here.
let rust_value: Option<Vec<RV>> = rust_value.map(|it| it.collect());
match (value, &rust_value) {
(None, None) => {}
(None, Some(_)) | (Some(_), None) => {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, value, rust_value
));
return Ok(Some((this_status, message)));
}
(Some(el), Some(rl)) if el.len() != rl.len() => {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, value, rust_value
));
return Ok(Some((this_status, message)));
}
(Some(el), Some(rl)) => {
for (e, r) in el.iter().zip(rl) {
let e = unquote(e.as_atom()?)?;
let r = r.as_ref();
if e != r {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}. Full list: {:?} != {:?}",
emacs_field, e, r, value, rust_value
));
return Ok(Some((this_status, message)));
}
}
}
}
Ok(None)
}

View File

@ -6,6 +6,7 @@ use std::collections::HashSet;
use super::compare_field::compare_identity;
use super::compare_field::compare_property_always_nil;
use super::compare_field::compare_property_list_of_quoted_string;
use super::compare_field::compare_property_quoted_string;
use super::compare_field::compare_property_unquoted_atom;
use super::elisp_fact::ElispFact;
@ -3068,10 +3069,35 @@ fn compare_org_macro<'b, 's>(
emacs: &'b Token<'s>,
rust: &'b OrgMacro<'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 :key :value :args
if let Some((new_status, new_message)) = compare_properties!(
emacs,
rust,
(
EmacsField::Required(":key"),
|r| Some(r.macro_name),
compare_property_quoted_string
),
(
EmacsField::Required(":value"),
|r| Some(r.macro_value),
compare_property_quoted_string
),
(
EmacsField::Required(":args"),
|r| if r.macro_args.is_empty() {
None
} else {
Some(r.get_macro_args())
},
compare_property_list_of_quoted_string
)
)? {
this_status = new_status;
message = new_message;
}
Ok(DiffResult {
status: this_status,

View File

@ -26,6 +26,7 @@ pub(crate) fn org_macro<'b, 'g, 'r, 's>(
let (remaining, macro_name) = org_macro_name(context, remaining)?;
let (remaining, macro_args) = opt(parser_with_context!(org_macro_args)(context))(remaining)?;
let (remaining, _) = tag("}}}")(remaining)?;
let macro_value = get_consumed(input, remaining);
let (remaining, _trailing_whitespace) =
maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?;
@ -40,6 +41,7 @@ pub(crate) fn org_macro<'b, 'g, 'r, 's>(
.into_iter()
.map(|arg| arg.into())
.collect(),
macro_value: Into::<&str>::into(macro_value),
},
))
}

View File

@ -1,6 +1,7 @@
use std::borrow::Borrow;
use std::borrow::Cow;
use super::util::coalesce_whitespace_escaped;
use super::util::coalesce_whitespace_if_line_break;
use super::util::remove_line_break;
use super::util::remove_whitespace_if_line_break;
@ -149,7 +150,11 @@ pub struct AngleLink<'s> {
pub struct OrgMacro<'s> {
pub source: &'s str,
pub macro_name: &'s str,
/// The macro args from the source.
///
/// This does not take into account the post-processing that you would get from the upstream emacs org-mode AST. Use `get_macro_args` for an equivalent value.
pub macro_args: Vec<&'s str>,
pub macro_value: &'s str,
}
#[derive(Debug, PartialEq)]
@ -732,3 +737,11 @@ impl<'s> AngleLink<'s> {
self.search_option.map(remove_whitespace_if_line_break)
}
}
impl<'s> OrgMacro<'s> {
pub fn get_macro_args<'b>(&'b self) -> impl Iterator<Item = Cow<'s, str>> + 'b {
self.macro_args
.iter()
.map(|arg| coalesce_whitespace_escaped('\\', |c| ",".contains(c))(*arg))
}
}

View File

@ -197,3 +197,316 @@ enum CoalesceWhitespaceIfLineBreakState {
ret: String,
},
}
/// Removes all whitespace from a string.
///
/// Example: "foo bar" => "foobar" and "foo \n bar" => "foobar".
#[allow(dead_code)]
pub(crate) fn coalesce_whitespace<'s>(input: &'s str) -> Cow<'s, str> {
let mut state = CoalesceWhitespace::Normal;
for (offset, c) in input.char_indices() {
match (&mut state, c) {
(CoalesceWhitespace::Normal, ' ' | '\t' | '\r' | '\n') => {
let mut ret = String::with_capacity(input.len());
ret.push_str(&input[..offset]);
ret.push(' ');
state = CoalesceWhitespace::HasWhitespace {
in_whitespace: true,
ret,
};
}
(CoalesceWhitespace::Normal, _) => {}
(
CoalesceWhitespace::HasWhitespace { in_whitespace, ret },
' ' | '\t' | '\r' | '\n',
) => {
if !*in_whitespace {
*in_whitespace = true;
ret.push(' ');
}
}
(CoalesceWhitespace::HasWhitespace { in_whitespace, ret }, _) => {
*in_whitespace = false;
ret.push(c);
}
}
}
match state {
CoalesceWhitespace::Normal => Cow::Borrowed(input),
CoalesceWhitespace::HasWhitespace {
in_whitespace: _,
ret,
} => Cow::Owned(ret),
}
}
enum CoalesceWhitespace {
Normal,
HasWhitespace { in_whitespace: bool, ret: String },
}
/// Removes all whitespace from a string and handle escaping characters.
///
/// Example: "foo bar" => "foobar" and "foo \n bar" => "foobar" but if the escape character is backslash and comma is an escapable character than "foo\,bar" becomes "foo,bar".
pub(crate) fn coalesce_whitespace_escaped<'c, C: Fn(char) -> bool>(
escape_character: char,
escapable_characters: C,
) -> impl for<'s> Fn(&'s str) -> Cow<'s, str> {
move |input| impl_coalesce_whitespace_escaped(input, escape_character, &escapable_characters)
}
fn impl_coalesce_whitespace_escaped<'s, C: Fn(char) -> bool>(
input: &'s str,
escape_character: char,
escapable_characters: C,
) -> Cow<'s, str> {
let mut state = CoalesceWhitespaceEscaped::Normal {
in_whitespace: false,
};
for (offset, c) in input.char_indices() {
state = match (state, c) {
(CoalesceWhitespaceEscaped::Normal { in_whitespace: _ }, c)
if c == escape_character =>
{
CoalesceWhitespaceEscaped::NormalEscaping {
escape_offset: offset,
}
}
(CoalesceWhitespaceEscaped::Normal { in_whitespace }, ' ') => {
if in_whitespace {
let mut ret = String::with_capacity(input.len());
ret.push_str(&input[..offset]);
CoalesceWhitespaceEscaped::RequiresMutation {
in_whitespace: true,
ret,
}
} else {
CoalesceWhitespaceEscaped::Normal {
in_whitespace: true,
}
}
}
(CoalesceWhitespaceEscaped::Normal { in_whitespace: _ }, '\t' | '\r' | '\n') => {
let mut ret = String::with_capacity(input.len());
ret.push_str(&input[..offset]);
ret.push(' ');
CoalesceWhitespaceEscaped::RequiresMutation {
in_whitespace: true,
ret,
}
}
(CoalesceWhitespaceEscaped::Normal { in_whitespace: _ }, _) => {
CoalesceWhitespaceEscaped::Normal {
in_whitespace: false,
}
}
(CoalesceWhitespaceEscaped::NormalEscaping { escape_offset }, c)
if escapable_characters(c) =>
{
// We escaped a character so we need mutation
let mut ret = String::with_capacity(input.len());
ret.push_str(&input[..escape_offset]);
ret.push(c);
CoalesceWhitespaceEscaped::RequiresMutation {
in_whitespace: false,
ret,
}
}
(CoalesceWhitespaceEscaped::NormalEscaping { escape_offset: _ }, ' ') => {
// We didn't escape the character so continue as normal.
CoalesceWhitespaceEscaped::Normal {
in_whitespace: true,
}
}
(
CoalesceWhitespaceEscaped::NormalEscaping { escape_offset: _ },
'\t' | '\r' | '\n',
) => {
// We didn't escape the character but we hit whitespace anyway.
let mut ret = String::with_capacity(input.len());
ret.push_str(&input[..offset]);
ret.push(' ');
CoalesceWhitespaceEscaped::RequiresMutation {
in_whitespace: true,
ret,
}
}
(CoalesceWhitespaceEscaped::NormalEscaping { escape_offset: _ }, _) => {
// We didn't escape the character so continue as normal.
CoalesceWhitespaceEscaped::Normal {
in_whitespace: false,
}
}
(
CoalesceWhitespaceEscaped::RequiresMutation {
in_whitespace: _,
ret,
},
c,
) if c == escape_character => CoalesceWhitespaceEscaped::RequiresMutationEscaping {
ret,
matched_escape_character: c,
},
(
CoalesceWhitespaceEscaped::RequiresMutation {
mut in_whitespace,
mut ret,
},
' ' | '\t' | '\r' | '\n',
) => {
if !in_whitespace {
in_whitespace = true;
ret.push(' ');
}
CoalesceWhitespaceEscaped::RequiresMutation { in_whitespace, ret }
}
(
CoalesceWhitespaceEscaped::RequiresMutation {
in_whitespace: _,
mut ret,
},
_,
) => {
ret.push(c);
CoalesceWhitespaceEscaped::RequiresMutation {
in_whitespace: false,
ret,
}
}
(
CoalesceWhitespaceEscaped::RequiresMutationEscaping {
mut ret,
matched_escape_character: _,
},
c,
) if escapable_characters(c) => {
ret.push(c);
CoalesceWhitespaceEscaped::RequiresMutation {
in_whitespace: false,
ret,
}
}
(
CoalesceWhitespaceEscaped::RequiresMutationEscaping {
mut ret,
matched_escape_character: _,
},
' ' | '\t' | '\r' | '\n',
) => {
ret.push(' ');
CoalesceWhitespaceEscaped::RequiresMutation {
in_whitespace: true,
ret,
}
}
(
CoalesceWhitespaceEscaped::RequiresMutationEscaping {
mut ret,
matched_escape_character,
},
c,
) => {
ret.push(matched_escape_character);
ret.push(c);
CoalesceWhitespaceEscaped::RequiresMutation {
in_whitespace: false,
ret,
}
}
}
}
match state {
CoalesceWhitespaceEscaped::Normal { in_whitespace: _ } => Cow::Borrowed(input),
CoalesceWhitespaceEscaped::NormalEscaping { escape_offset: _ } => Cow::Borrowed(input),
CoalesceWhitespaceEscaped::RequiresMutation {
in_whitespace: _,
ret,
} => Cow::Owned(ret),
CoalesceWhitespaceEscaped::RequiresMutationEscaping {
mut ret,
matched_escape_character,
} => {
ret.push(matched_escape_character);
Cow::Owned(ret)
}
}
}
enum CoalesceWhitespaceEscaped {
Normal {
in_whitespace: bool,
},
NormalEscaping {
escape_offset: usize,
},
RequiresMutation {
in_whitespace: bool,
ret: String,
},
RequiresMutationEscaping {
ret: String,
matched_escape_character: char,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coalesce_whitespace_escaped_default() -> Result<(), Box<dyn std::error::Error>> {
let input = "foobarbaz";
let output = coalesce_whitespace_escaped('&', |c| "".contains(c))(input);
assert_eq!(output, "foobarbaz");
assert!(matches!(output, Cow::Borrowed(_)));
Ok(())
}
#[test]
fn coalesce_whitespace_escaped_whitespace_single() -> Result<(), Box<dyn std::error::Error>> {
let input = "foo bar baz";
let output = coalesce_whitespace_escaped('&', |c| "".contains(c))(input);
assert_eq!(output, "foo bar baz");
assert!(matches!(output, Cow::Borrowed(_)));
Ok(())
}
#[test]
fn coalesce_whitespace_escaped_whitespace_double() -> Result<(), Box<dyn std::error::Error>> {
let input = "foo bar baz";
let output = coalesce_whitespace_escaped('&', |c| "".contains(c))(input);
assert_eq!(output, "foo bar baz");
assert!(matches!(output, Cow::Owned(_)));
Ok(())
}
#[test]
fn coalesce_whitespace_escaped_escape_match() -> Result<(), Box<dyn std::error::Error>> {
let input = "foo &bar baz";
let output = coalesce_whitespace_escaped('&', |c| "b".contains(c))(input);
assert_eq!(output, "foo bar baz");
assert!(matches!(output, Cow::Owned(_)));
Ok(())
}
#[test]
fn coalesce_whitespace_escaped_escape_mismatch() -> Result<(), Box<dyn std::error::Error>> {
let input = "foo b&ar baz";
let output = coalesce_whitespace_escaped('&', |c| "b".contains(c))(input);
assert_eq!(output, "foo b&ar baz");
assert!(matches!(output, Cow::Owned(_)));
Ok(())
}
#[test]
fn coalesce_whitespace_escaped_escape_mismatch_around_whitespace(
) -> Result<(), Box<dyn std::error::Error>> {
let input = "foo& bar &baz";
let output = coalesce_whitespace_escaped('&', |c| "z".contains(c))(input);
assert_eq!(output, "foo& bar &baz");
assert!(matches!(output, Cow::Borrowed(_)));
Ok(())
}
}