use std::borrow::Cow;
pub(crate) fn decode_mermaid_entity_placeholders(input: &str) -> Cow<'_, str> {
if !input.contains('\u{fb02}') && !input.contains('\u{00b6}') {
return Cow::Borrowed(input);
}
Cow::Owned(
input
.replace("fl°°", "&#")
.replace("fl°", "&")
.replace("¶ß", ";"),
)
}
pub fn decode_mermaid_entities_to_unicode(input: &str) -> Cow<'_, str> {
let entities = restore_mermaid_entity_spelling(input);
if !entities.contains('&') {
return entities;
}
Cow::Owned(decode_html_entities_to_unicode(entities.as_ref()).into_owned())
}
pub fn restore_mermaid_entity_spelling(input: &str) -> Cow<'_, str> {
if !input.contains('#') && !input.contains('fl') && !input.contains('¶') {
return Cow::Borrowed(input);
}
let mut s = decode_mermaid_entity_placeholders(input).into_owned();
if s.contains('#') {
let mut out = String::with_capacity(s.len());
let mut it = s.chars().peekable();
let mut prev: Option<char> = None;
while let Some(ch) = it.next() {
if ch != '#' {
out.push(ch);
prev = Some(ch);
continue;
}
if prev == Some('&') {
out.push('#');
prev = Some('#');
continue;
}
let mut entity = String::new();
let mut ok = false;
for _ in 0..64 {
match it.peek().copied() {
Some(';') => {
it.next();
ok = true;
break;
}
Some(c) if c.is_ascii_alphanumeric() || c == '_' || c == '+' => {
entity.push(c);
it.next();
}
_ => break,
}
}
if !ok {
out.push('#');
out.push_str(&entity);
continue;
}
let is_int = entity.chars().all(|c| c.is_ascii_digit() || c == '+')
&& entity.chars().any(|c| c.is_ascii_digit());
if is_int {
out.push('&');
out.push('#');
out.push_str(&entity);
out.push(';');
} else {
out.push('&');
out.push_str(&entity);
out.push(';');
}
prev = Some(';');
}
s = out;
}
Cow::Owned(s)
}
pub fn decode_html_entities_to_unicode(input: &str) -> Cow<'_, str> {
if !input.contains('&') {
return Cow::Borrowed(input);
}
htmlize::unescape(input)
}
#[cfg(test)]
mod tests {
use super::{
decode_html_entities_to_unicode, decode_mermaid_entities_to_unicode,
decode_mermaid_entity_placeholders, restore_mermaid_entity_spelling,
};
#[test]
fn html_entity_decode_does_not_apply_mermaid_shorthand() {
assert_eq!(
decode_html_entities_to_unicode("Tom & Jerry <ok> 'x'"),
"Tom & Jerry <ok> 'x'"
);
assert_eq!(decode_html_entities_to_unicode("#quot;"), "#quot;");
}
#[test]
fn mermaid_entity_decode_keeps_shorthand_and_placeholder_semantics() {
assert_eq!(decode_mermaid_entities_to_unicode("#quot;"), "\"");
assert_eq!(decode_mermaid_entities_to_unicode("fl°quot¶ß"), "\"");
assert_eq!(decode_mermaid_entities_to_unicode("fl°°39¶ß"), "'");
}
#[test]
fn mermaid_entity_spelling_restoration_stops_before_browser_decode() {
assert_eq!(restore_mermaid_entity_spelling("#nbsp;"), " ");
assert_eq!(restore_mermaid_entity_spelling("fl°nbsp¶ß"), " ");
assert_eq!(restore_mermaid_entity_spelling("fl°°160¶ß"), " ");
assert_eq!(restore_mermaid_entity_spelling(" "), " ");
}
#[test]
fn placeholder_decode_preserves_the_serialized_entity_layer() {
assert_eq!(
decode_mermaid_entity_placeholders("javascriptfl°colon¶ßalert(1)"),
"javascript:alert(1)"
);
assert_eq!(
decode_mermaid_entity_placeholders("ticket&value"),
"ticket&value"
);
}
}