pub fn parse_destination(b: &[u8], i: usize) -> Option<(String, usize)> {
if i < b.len() && b[i] == b'<' {
let mut j = i + 1;
let mut raw = String::new();
while j < b.len() {
match b[j] {
b'>' => return Some((raw, j + 1)),
b'\n' | b'<' => return None,
b'\\' if j + 1 < b.len() && b[j + 1].is_ascii_punctuation() => {
raw.push('\\');
raw.push(b[j + 1] as char);
j += 2;
}
c => {
raw.push(c as char);
j += 1;
}
}
}
return None;
}
let start = i;
let mut j = i;
let mut depth: i32 = 0;
while j < b.len() {
let c = b[j];
match c {
b'\\' if j + 1 < b.len() && b[j + 1].is_ascii_punctuation() => {
j += 2;
continue;
}
b'(' => depth += 1,
b')' => {
if depth == 0 {
break;
}
depth -= 1;
}
0x00..=0x20 | 0x7f => break,
_ => {}
}
j += 1;
}
if j == start || depth != 0 {
return None;
}
Some((String::from_utf8_lossy(&b[start..j]).into_owned(), j))
}
pub fn parse_title(b: &[u8], i: usize) -> Option<(String, usize)> {
let open = *b.get(i)?;
let close = match open {
b'"' => b'"',
b'\'' => b'\'',
b'(' => b')',
_ => return None,
};
let mut j = i + 1;
let mut raw = String::new();
while j < b.len() {
let c = b[j];
if c == b'\\' && j + 1 < b.len() && b[j + 1].is_ascii_punctuation() {
raw.push('\\');
raw.push(b[j + 1] as char);
j += 2;
continue;
}
if c == close {
return Some((raw, j + 1));
}
if open == b'(' && c == b'(' {
return None;
}
raw.push(c as char);
j += 1;
}
None
}
pub fn skip_ws(b: &[u8], mut i: usize) -> usize {
while i < b.len() && matches!(b[i], b' ' | b'\t' | b'\n' | b'\r') {
i += 1;
}
i
}
pub fn normalize_dest(raw: &str) -> String {
let decoded = unescape_string(raw);
percent_encode_uri(&decoded)
}
pub fn normalize_title(raw: &str) -> String {
unescape_string(raw)
}
pub fn unescape_string(s: &str) -> String {
let b = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < b.len() {
match b[i] {
b'\\' if i + 1 < b.len() && b[i + 1].is_ascii_punctuation() => {
out.push(b[i + 1] as char);
i += 2;
}
b'&' => {
if let Some((ch, len)) = crate::entity::decode_entity(&s[i..]) {
out.push_str(&ch);
i += len;
} else {
out.push('&');
i += 1;
}
}
c if c < 0x80 => {
out.push(c as char);
i += 1;
}
_ => {
let ch = s[i..].chars().next().unwrap();
out.push(ch);
i += ch.len_utf8();
}
}
}
out
}
pub fn normalize_autolink(raw: &str) -> String {
let mut decoded = String::with_capacity(raw.len());
let b = raw.as_bytes();
let mut i = 0;
while i < b.len() {
if b[i] == b'&' {
if let Some((ch, len)) = crate::entity::decode_entity(&raw[i..]) {
decoded.push_str(&ch);
i += len;
continue;
}
}
let ch = raw[i..].chars().next().unwrap();
decoded.push(ch);
i += ch.len_utf8();
}
percent_encode_uri(&decoded)
}
fn percent_encode_uri(s: &str) -> String {
let b = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < b.len() {
let c = b[i];
if c == b'%'
&& i + 2 < b.len()
&& b[i + 1].is_ascii_hexdigit()
&& b[i + 2].is_ascii_hexdigit()
{
out.push('%');
out.push(b[i + 1] as char);
out.push(b[i + 2] as char);
i += 3;
} else if is_uri_safe(c) {
out.push(c as char);
i += 1;
} else {
out.push_str(&format!("%{c:02X}"));
i += 1;
}
}
out
}
fn is_uri_safe(c: u8) -> bool {
c.is_ascii_alphanumeric()
|| matches!(
c,
b'-' | b'_'
| b'.'
| b'~'
| b'!'
| b'#'
| b'$'
| b'&'
| b'\''
| b'('
| b')'
| b'*'
| b'+'
| b','
| b'/'
| b':'
| b';'
| b'='
| b'?'
| b'@'
)
}
pub fn normalize_label(label: &str) -> Option<String> {
let mut out = String::with_capacity(label.len());
let mut last_ws = false;
for ch in label.trim().chars() {
if ch.is_whitespace() {
if !last_ws {
out.push(' ');
last_ws = true;
}
} else {
for folded in ch.to_uppercase().flat_map(|u| u.to_lowercase()) {
out.push(folded);
}
last_ws = false;
}
}
let out = out.trim_end().to_string();
if out.is_empty() {
None
} else {
Some(out)
}
}