pub fn extract_img_markers(text: &str) -> (String, Vec<String>) {
extract_markers_with_prefix(text, "<<IMG:")
}
pub fn extract_vid_markers(text: &str) -> (String, Vec<String>) {
extract_markers_with_prefix(text, "<<VID:")
}
pub fn extract_react_marker(text: &str) -> (String, Option<String>) {
extract_react_marker_inner(text, true)
}
pub fn extract_react_marker_lenient(text: &str) -> (String, Option<String>) {
extract_react_marker_inner(text, false)
}
fn extract_react_marker_inner(
text_arg: &str,
respect_code_spans: bool,
) -> (String, Option<String>) {
let plain = scan_react_text(text_arg, respect_code_spans);
if plain.1.is_some() || !text_arg.contains('`') {
return plain;
}
match recover_orphan_fenced_directive(text_arg) {
Some((cleaned, emoji)) => (cleaned.trim().to_string(), Some(emoji)),
None => plain,
}
}
fn scan_react_text(text: &str, respect_code_spans: bool) -> (String, Option<String>) {
let mut out = String::with_capacity(text.len());
let mut emoji: Option<String> = None;
let mut in_code = false;
let mut i = 0;
while i < text.len() {
let ch = text[i..].chars().next().expect("i lies on a char boundary");
if ch == '`' {
in_code = !in_code;
out.push(ch);
i += 1;
continue;
}
if (!respect_code_spans || !in_code)
&& let Some(open_len) = match_react_open(&text[i..])
&& let Some((rel_end, term_len)) = find_react_close(&text[i + open_len..])
{
let payload = text[i + open_len..i + open_len + rel_end].trim();
if is_reaction_emoji(payload) {
if emoji.is_none() {
emoji = Some(payload.to_string());
}
i += open_len + rel_end + term_len; continue;
}
}
out.push(ch);
i += ch.len_utf8();
}
(out.trim().to_string(), emoji)
}
fn is_fence_junk_prefix(prefix: &str) -> bool {
let mut rest = prefix.trim_start_matches([' ', '\t', '\r', '\n']);
if let Some(r) = rest.strip_prefix('<').or_else(|| rest.strip_prefix('\\')) {
rest = r.trim_start_matches([' ', '\t', '\r', '\n']);
}
if let Some(r) = rest.strip_prefix("```") {
rest = r;
let tag_len = rest.chars().take_while(|c| c.is_alphanumeric()).count();
if tag_len > 12 {
return false;
}
rest = &rest[tag_len..];
}
rest.bytes()
.all(|b| matches!(b, b'`' | b'<' | b'\\' | b' ' | b'\t' | b'\r' | b'\n'))
}
fn recover_orphan_fenced_directive(text: &str) -> Option<(String, String)> {
if !text.contains('`') {
return None;
}
let scan_end = text
.char_indices()
.map(|(i, _)| i)
.chain(std::iter::once(text.len()))
.find(|&i| i >= 240)
.unwrap_or(text.len());
let mut found: Option<(usize, String)> = None;
for (i, ch) in text[..scan_end].char_indices() {
if ch != '<' {
continue;
}
if !is_fence_junk_prefix(&text[..i]) {
continue;
}
if let Some(open_len) = match_react_open(&text[i..])
&& let Some((rel_end, term_len)) = find_react_close(&text[i + open_len..])
&& is_reaction_emoji(text[i + open_len..i + open_len + rel_end].trim())
{
let emoji = text[i + open_len..i + open_len + rel_end]
.trim()
.to_string();
found = Some((i + open_len + rel_end + term_len, emoji));
break;
}
}
let (after, emoji) = found?;
let mut owned = text[after..].to_string();
if let Some(pos) = owned.rfind('\n')
&& owned[pos + 1..].trim_start().starts_with("```")
{
owned.truncate(pos);
}
Some((owned, emoji))
}
fn match_react_open(s: &str) -> Option<usize> {
let bytes = s.as_bytes();
if bytes.first() != Some(&b'<') {
return None;
}
let mut j = 1;
let mut angle_brackets = 1usize; while let Some(c) = bytes.get(j) {
match c {
b'<' => {
angle_brackets += 1;
j += 1;
}
b'\\' => j += 1,
_ => break,
}
}
const TAG: &str = "react:";
if s[j..].starts_with(TAG) {
Some(j + TAG.len())
} else if angle_brackets >= 2 {
Some(j)
} else {
None
}
}
fn find_react_close(s: &str) -> Option<(usize, usize)> {
const TERMS: [&str; 3] = [">>", "</react>", ">"];
let mut best: Option<(usize, usize)> = None;
for term in TERMS {
if let Some(pos) = s.find(term) {
let better = match best {
Some((bpos, blen)) => pos < bpos || (pos == bpos && term.len() > blen),
None => true,
};
if better {
best = Some((pos, term.len()));
}
}
}
best
}
fn is_reaction_emoji(payload: &str) -> bool {
!payload.is_empty() && payload.chars().count() <= 8 && payload.chars().all(|c| !c.is_ascii())
}
fn extract_markers_with_prefix(text: &str, prefix: &str) -> (String, Vec<String>) {
let mut out = text.to_string();
let mut paths = Vec::new();
let prefix_len = prefix.len();
while let Some(start) = out.find(prefix) {
let Some(rel_end) = out[start..].find(">>") else {
break;
};
let end = start + rel_end + 2; let path = out[start + prefix_len..start + rel_end].trim().to_string();
if !path.is_empty() {
paths.push(path);
}
out.replace_range(start..end, "");
}
(out.trim().to_string(), paths)
}