use crate::entity::decode_entity;
use crate::event::{Event, Inline, InlineStyle, Link, LinkDef, Span};
use crate::linkref;
use std::collections::HashMap;
type Refs = HashMap<String, LinkDef>;
pub fn parse(text: &str, style: &InlineStyle, refs: &Refs, gfm: bool, out: &mut Vec<Event>) {
let mut tokens = scan(text, refs, gfm, &mut None);
process_emphasis(&mut tokens, 0);
flatten(&tokens, style, out);
}
pub fn parse_collect_unresolved(
text: &str,
style: &InlineStyle,
refs: &Refs,
gfm: bool,
out: &mut Vec<Event>,
unresolved: &mut Vec<String>,
) {
let mut sink = Some(std::mem::take(unresolved));
let mut tokens = scan(text, refs, gfm, &mut sink);
process_emphasis(&mut tokens, 0);
flatten(&tokens, style, out);
*unresolved = sink.unwrap_or_default();
}
#[derive(Debug, Clone)]
enum Token {
Text(String),
Code(String),
SoftBreak,
HardBreak,
Link { link: Link, inner: Vec<Token> },
Autolink { text: String, href: String },
RawHtml(String),
Delim(Delim),
Node { kind: NodeKind, inner: Vec<Token> },
Consumed(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum NodeKind {
Emphasis,
Strong,
Strikethrough,
}
#[derive(Debug, Clone)]
struct Delim {
ch: u8,
len: usize,
orig_len: usize,
can_open: bool,
can_close: bool,
}
fn scan(text: &str, refs: &Refs, gfm: bool, unresolved: &mut Option<Vec<String>>) -> Vec<Token> {
let b = text.as_bytes();
let mut tokens: Vec<Token> = Vec::new();
let mut buf = String::new();
let mut i = 0;
macro_rules! flush {
() => {
if !buf.is_empty() {
tokens.push(Token::Text(std::mem::take(&mut buf)));
}
};
}
while i < b.len() {
let c = b[i];
if gfm && is_gfm_autolink_start(b, i) && gfm_autolink_boundary_ok(text, i) {
if let Some((tok, end)) = try_gfm_autolink(text, i) {
flush!();
tokens.push(tok);
i = end;
continue;
}
}
match c {
b'\\' if i + 1 < b.len() && is_ascii_punct(b[i + 1]) => {
buf.push(b[i + 1] as char);
i += 2;
}
b'\\' if i + 1 < b.len() && b[i + 1] == b'\n' => {
flush!();
tokens.push(Token::HardBreak);
i += 2;
i = skip_line_lead(b, i);
}
b'\n' => {
let hard = buf.ends_with(" ");
while buf.ends_with(' ') {
buf.pop();
}
flush!();
tokens.push(if hard {
Token::HardBreak
} else {
Token::SoftBreak
});
i += 1;
i = skip_line_lead(b, i);
}
b'`' => {
let n = run_len(b, i, b'`');
if let Some(close) = find_code_close(b, i + n, n) {
flush!();
tokens.push(Token::Code(code_span_text(&text[i + n..close])));
i = close + n;
} else {
for _ in 0..n {
buf.push('`');
}
i += n;
}
}
b'!' if i + 1 < b.len() && b[i + 1] == b'[' => {
if let Some((tok, consumed)) = try_link(text, i + 1, true, refs, gfm, unresolved) {
flush!();
tokens.push(tok);
i += 1 + consumed;
} else {
buf.push('!');
i += 1;
}
}
b'[' => {
if let Some((tok, consumed)) = try_link(text, i, false, refs, gfm, unresolved) {
flush!();
tokens.push(tok);
i += consumed;
} else {
buf.push('[');
i += 1;
}
}
b'<' => {
if let Some((end, tok)) = try_autolink(text, i) {
flush!();
tokens.push(tok);
i = end;
} else if let Some(end) = try_raw_html(b, i) {
flush!();
tokens.push(Token::RawHtml(text[i..end].to_string()));
i = end;
} else {
buf.push('<');
i += 1;
}
}
b'&' => {
if let Some((decoded, len)) = decode_entity(&text[i..]) {
buf.push_str(&decoded);
i += len;
} else {
buf.push('&');
i += 1;
}
}
b'*' | b'_' | b'~' => {
let n = run_len(b, i, c);
if c == b'~' && n >= 3 {
for _ in 0..n {
buf.push('~');
}
i += n;
continue;
}
let before = char_before(text, i);
let after = char_after(text, i + n);
let (can_open, can_close) = flanking(c, before, after);
flush!();
tokens.push(Token::Delim(Delim {
ch: c,
len: n,
orig_len: n,
can_open,
can_close,
}));
i += n;
}
_ => {
let ch_len = utf8_len(c);
buf.push_str(&text[i..i + ch_len]);
i += ch_len;
}
}
}
flush!();
tokens
}
fn skip_line_lead(b: &[u8], mut i: usize) -> usize {
while i < b.len() && (b[i] == b' ' || b[i] == b'\t') {
i += 1;
}
i
}
fn flanking(c: u8, before: Option<char>, after: Option<char>) -> (bool, bool) {
let before_ws = before.is_none_or(is_unicode_whitespace);
let after_ws = after.is_none_or(is_unicode_whitespace);
let before_punct = before.is_some_and(is_punct);
let after_punct = after.is_some_and(is_punct);
let left = !after_ws && (!after_punct || before_ws || before_punct);
let right = !before_ws && (!before_punct || after_ws || after_punct);
match c {
b'_' => {
let can_open = left && (!right || before_punct);
let can_close = right && (!left || after_punct);
(can_open, can_close)
}
_ => (left, right),
}
}
fn process_emphasis(tokens: &mut Vec<Token>, stack_bottom: usize) {
let mut closer = stack_bottom;
while closer < tokens.len() {
let (ch, closer_can_open, closer_len, closer_orig) = match &tokens[closer] {
Token::Delim(d) if d.can_close => (d.ch, d.can_open, d.len, d.orig_len),
_ => {
closer += 1;
continue;
}
};
let mut opener = closer;
let mut found = false;
while opener > stack_bottom {
opener -= 1;
if let Token::Delim(d) = &tokens[opener] {
if d.ch == ch
&& d.can_open
&& rule_of_three(d.orig_len, d.can_close, closer_orig, closer_can_open)
{
found = true;
break;
}
}
}
if !found {
if !closer_can_open {
make_literal(&mut tokens[closer]);
}
closer += 1;
continue;
}
let use_strong = delim_len(&tokens[opener]) >= 2 && closer_len >= 2;
let take = if use_strong { 2 } else { 1 };
let kind = if ch == b'~' {
NodeKind::Strikethrough
} else if use_strong {
NodeKind::Strong
} else {
NodeKind::Emphasis
};
let mut inner: Vec<Token> = tokens.drain(opener + 1..closer).collect();
for t in &mut inner {
if matches!(t, Token::Delim(_)) {
make_literal(t);
}
}
consume_delim(&mut tokens[opener], take);
consume_delim(&mut tokens[opener + 1], take);
tokens.insert(opener + 1, Token::Node { kind, inner });
let closer_idx = opener + 2;
if matches!(&tokens[closer_idx], Token::Consumed(s) if s.is_empty()) {
closer = closer_idx + 1;
} else {
closer = closer_idx;
}
}
}
fn rule_of_three(
opener_len: usize,
opener_can_close: bool,
closer_len: usize,
closer_can_open: bool,
) -> bool {
if opener_can_close || closer_can_open {
let sum = opener_len + closer_len;
if sum.is_multiple_of(3) && !(opener_len.is_multiple_of(3) && closer_len.is_multiple_of(3))
{
return false;
}
}
true
}
fn delim_len(t: &Token) -> usize {
match t {
Token::Delim(d) => d.len,
_ => 0,
}
}
fn consume_delim(t: &mut Token, take: usize) {
if let Token::Delim(d) = t {
d.len -= take;
if d.len == 0 {
*t = Token::Consumed(String::new());
}
}
}
fn make_literal(t: &mut Token) {
if let Token::Delim(d) = t {
let s: String = std::iter::repeat_n(d.ch as char, d.len).collect();
*t = Token::Consumed(s);
}
}
fn flatten(tokens: &[Token], base: &InlineStyle, out: &mut Vec<Event>) {
for t in tokens {
match t {
Token::Text(s) => push_text(s, base, out),
Token::Consumed(s) => {
if !s.is_empty() {
push_text(s, base, out);
}
}
Token::Code(s) => {
let mut st = base.clone();
st.code = true;
out.push(Event::EnterInline {
inline: Inline::Code,
span: Span::default(),
});
out.push(Event::Text {
text: s.clone(),
style: st,
span: Span::default(),
});
out.push(Event::ExitInline {
inline: Inline::Code,
});
}
Token::SoftBreak => out.push(Event::SoftBreak),
Token::HardBreak => out.push(Event::LineBreak),
Token::Node { kind, inner } => {
let mut st = base.clone();
let inline = match kind {
NodeKind::Emphasis => {
st.emphasis = true;
Inline::Emphasis
}
NodeKind::Strong => {
st.strong = true;
Inline::Strong
}
NodeKind::Strikethrough => {
st.strikethrough = true;
Inline::Strikethrough
}
};
out.push(Event::EnterInline {
inline: inline.clone(),
span: Span::default(),
});
flatten(inner, &st, out);
out.push(Event::ExitInline { inline });
}
Token::Link { link, inner } => {
let mut st = base.clone();
st.link = Some(link.clone());
let inline = if link.image {
Inline::Image(link.clone())
} else {
Inline::Link(link.clone())
};
out.push(Event::EnterInline {
inline: inline.clone(),
span: Span::default(),
});
flatten(inner, &st, out);
out.push(Event::ExitInline { inline });
}
Token::RawHtml(s) => {
let mut st = base.clone();
st.raw_html = true;
out.push(Event::Text {
text: s.clone(),
style: st,
span: Span::default(),
});
}
Token::Autolink { text, href } => {
let link = Link {
href: href.clone(),
title: String::new(),
image: false,
};
let mut st = base.clone();
st.link = Some(link.clone());
out.push(Event::EnterInline {
inline: Inline::Link(link.clone()),
span: Span::default(),
});
out.push(Event::Text {
text: text.clone(),
style: st,
span: Span::default(),
});
out.push(Event::ExitInline {
inline: Inline::Link(link),
});
}
Token::Delim(d) => {
let s: String = std::iter::repeat_n(d.ch as char, d.len).collect();
push_text(&s, base, out);
}
}
}
}
fn push_text(s: &str, style: &InlineStyle, out: &mut Vec<Event>) {
if s.is_empty() {
return;
}
if let Some(Event::Text {
text, style: prev, ..
}) = out.last_mut()
{
if prev == style {
text.push_str(s);
return;
}
}
out.push(Event::Text {
text: s.to_string(),
style: style.clone(),
span: Span::default(),
});
}
fn is_ascii_punct(c: u8) -> bool {
c.is_ascii_punctuation()
}
fn run_len(b: &[u8], i: usize, c: u8) -> usize {
let mut n = 0;
while i + n < b.len() && b[i + n] == c {
n += 1;
}
n
}
fn utf8_len(b: u8) -> usize {
match b {
0x00..=0x7f => 1,
0xc0..=0xdf => 2,
0xe0..=0xef => 3,
_ => 4,
}
}
fn char_before(text: &str, i: usize) -> Option<char> {
text[..i].chars().next_back()
}
fn char_after(text: &str, i: usize) -> Option<char> {
text[i..].chars().next()
}
fn is_unicode_whitespace(c: char) -> bool {
c.is_whitespace()
}
fn is_punct(c: char) -> bool {
if c.is_ascii() {
return c.is_ascii_punctuation();
}
is_unicode_punct(c)
}
fn find_code_close(b: &[u8], from: usize, n: usize) -> Option<usize> {
let mut i = from;
while i < b.len() {
if b[i] == b'`' {
let run = run_len(b, i, b'`');
if run == n {
return Some(i);
}
i += run;
} else {
i += 1;
}
}
None
}
fn code_span_text(s: &str) -> String {
let collapsed: String = s.chars().map(|c| if c == '\n' { ' ' } else { c }).collect();
if collapsed.len() >= 2
&& collapsed.starts_with(' ')
&& collapsed.ends_with(' ')
&& !collapsed.trim().is_empty()
{
collapsed[1..collapsed.len() - 1].to_string()
} else {
collapsed
}
}
fn try_link(
text: &str,
open: usize,
image: bool,
refs: &Refs,
gfm: bool,
unresolved: &mut Option<Vec<String>>,
) -> Option<(Token, usize)> {
let b = text.as_bytes();
let close = matching_bracket(b, open)?;
let label_raw = &text[open + 1..close];
let inner = scan_inner(label_raw, refs, gfm, unresolved);
let nested_link = contains_link(&inner);
let make = |link: Link, end: usize| {
if !image && nested_link {
None
} else {
Some((
Token::Link {
link,
inner: inner.clone(),
},
end - open,
))
}
};
if b.get(close + 1) == Some(&b'(') {
if let Some((link, end)) = parse_inline_target(text, close + 2, image) {
return make(link, end);
}
}
if b.get(close + 1) == Some(&b'[') {
if let Some(ref_close) = find_byte(b, close + 2, b']') {
let ref_label = &text[close + 2..ref_close];
if ref_label.is_empty() {
if let Some(link) = resolve_ref(label_raw, image, refs) {
return make(link, ref_close + 1);
}
note_unresolved(label_raw, unresolved);
} else if let Some(link) = resolve_ref(ref_label, image, refs) {
return make(link, ref_close + 1);
} else {
note_unresolved(ref_label, unresolved);
}
return None;
}
}
if let Some(link) = resolve_ref(label_raw, image, refs) {
return make(link, close + 1);
}
note_unresolved(label_raw, unresolved);
None
}
fn note_unresolved(label: &str, unresolved: &mut Option<Vec<String>>) {
if let Some(sink) = unresolved {
if let Some(norm) = linkref::normalize_label(label) {
sink.push(norm);
}
}
}
fn scan_inner(
label: &str,
refs: &Refs,
gfm: bool,
unresolved: &mut Option<Vec<String>>,
) -> Vec<Token> {
let mut inner = scan(label, refs, gfm, unresolved);
process_emphasis(&mut inner, 0);
inner
}
fn contains_link(tokens: &[Token]) -> bool {
tokens.iter().any(|t| match t {
Token::Link { link, inner } => !link.image || contains_link(inner),
Token::Node { inner, .. } => contains_link(inner),
_ => false,
})
}
fn resolve_ref(label: &str, image: bool, refs: &Refs) -> Option<Link> {
let norm = linkref::normalize_label(label)?;
let def = refs.get(&norm)?;
Some(Link {
href: def.dest.clone(),
title: def.title.clone(),
image,
})
}
fn parse_inline_target(text: &str, start: usize, image: bool) -> Option<(Link, usize)> {
let b = text.as_bytes();
let mut i = linkref::skip_ws(b, start);
let (raw_dest, after_dest) = if b.get(i) == Some(&b')') {
(String::new(), i)
} else {
linkref::parse_destination(b, i)?
};
i = after_dest;
let ws_end = linkref::skip_ws(b, i);
let (raw_title, after_title) = if ws_end > i && b.get(ws_end).is_some_and(|&c| c != b')') {
linkref::parse_title(b, ws_end)?
} else {
(String::new(), i)
};
i = linkref::skip_ws(b, after_title);
if b.get(i) != Some(&b')') {
return None;
}
let link = Link {
href: linkref::normalize_dest(&raw_dest),
title: linkref::normalize_title(&raw_title),
image,
};
Some((link, i + 1))
}
fn matching_bracket(b: &[u8], open: usize) -> Option<usize> {
let mut depth = 0i32;
let mut i = open;
while i < b.len() {
match b[i] {
b'\\' => i += 2,
b'[' => {
depth += 1;
i += 1;
}
b']' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
i += 1;
}
b'`' => {
let n = run_len(b, i, b'`');
i = find_code_close(b, i + n, n).map(|c| c + n).unwrap_or(i + n);
}
_ => i += 1,
}
}
None
}
fn find_byte(b: &[u8], from: usize, target: u8) -> Option<usize> {
(from..b.len()).find(|&i| b[i] == target)
}
fn try_raw_html(b: &[u8], lt: usize) -> Option<usize> {
if b.get(lt) != Some(&b'<') {
return None;
}
if b.get(lt + 1) == Some(&b'/') {
return scan_closing_tag(b, lt);
}
if let Some((end, _)) = scan_open_tag(b, lt) {
return Some(end);
}
match b.get(lt + 1) {
Some(b'!') => scan_comment(b, lt)
.or_else(|| scan_cdata(b, lt))
.or_else(|| scan_declaration(b, lt)),
Some(b'?') => scan_processing_instruction(b, lt),
_ => None,
}
}
pub(crate) fn scan_open_tag(b: &[u8], lt: usize) -> Option<(usize, String)> {
if b.get(lt) != Some(&b'<') {
return None;
}
let mut i = lt + 1;
let name_start = i;
if !b.get(i).is_some_and(|c| c.is_ascii_alphabetic()) {
return None;
}
i += 1;
while b
.get(i)
.is_some_and(|&c| c.is_ascii_alphanumeric() || c == b'-')
{
i += 1;
}
let name = String::from_utf8_lossy(&b[name_start..i]).into_owned();
loop {
let ws_end = skip_html_ws(b, i);
if let Some(next) = scan_attribute(b, ws_end) {
if ws_end == i {
break;
}
i = next;
} else {
i = ws_end;
break;
}
}
let i = skip_html_ws(b, i);
let i = if b.get(i) == Some(&b'/') { i + 1 } else { i };
if b.get(i) == Some(&b'>') {
Some((i + 1, name))
} else {
None
}
}
pub(crate) fn scan_closing_tag(b: &[u8], lt: usize) -> Option<usize> {
if b.get(lt) != Some(&b'<') || b.get(lt + 1) != Some(&b'/') {
return None;
}
let mut i = lt + 2;
if !b.get(i).is_some_and(|c| c.is_ascii_alphabetic()) {
return None;
}
i += 1;
while b
.get(i)
.is_some_and(|&c| c.is_ascii_alphanumeric() || c == b'-')
{
i += 1;
}
let i = skip_html_ws(b, i);
if b.get(i) == Some(&b'>') {
Some(i + 1)
} else {
None
}
}
fn scan_attribute(b: &[u8], i: usize) -> Option<usize> {
if !b
.get(i)
.is_some_and(|&c| c.is_ascii_alphabetic() || c == b'_' || c == b':')
{
return None;
}
let mut j = i + 1;
while b
.get(j)
.is_some_and(|&c| c.is_ascii_alphanumeric() || matches!(c, b'_' | b'.' | b':' | b'-'))
{
j += 1;
}
let eq = skip_html_ws(b, j);
if b.get(eq) != Some(&b'=') {
return Some(j);
}
let val = skip_html_ws(b, eq + 1);
let after = scan_attr_value(b, val)?;
Some(after)
}
fn scan_attr_value(b: &[u8], i: usize) -> Option<usize> {
match b.get(i) {
Some(&q @ (b'"' | b'\'')) => {
let mut j = i + 1;
while let Some(&c) = b.get(j) {
if c == q {
return Some(j + 1);
}
j += 1;
}
None
}
Some(_) => {
let mut j = i;
while b.get(j).is_some_and(|&c| {
!matches!(
c,
b' ' | b'\t' | b'\r' | b'\n' | b'"' | b'\'' | b'=' | b'<' | b'>' | b'`'
)
}) {
j += 1;
}
if j > i {
Some(j)
} else {
None
}
}
None => None,
}
}
fn scan_comment(b: &[u8], lt: usize) -> Option<usize> {
if !b[lt..].starts_with(b"<!--") {
return None;
}
let body = lt + 4;
if b[body..].starts_with(b">") {
return Some(body + 1); }
if b[body..].starts_with(b"->") {
return Some(body + 2); }
let mut i = body;
while i + 3 <= b.len() {
if &b[i..i + 3] == b"-->" {
if i > body && b[i - 1] == b'-' {
return None;
}
return Some(i + 3);
}
i += 1;
}
None
}
fn scan_processing_instruction(b: &[u8], lt: usize) -> Option<usize> {
if !b[lt..].starts_with(b"<?") {
return None;
}
let mut i = lt + 2;
while i + 2 <= b.len() {
if &b[i..i + 2] == b"?>" {
return Some(i + 2);
}
i += 1;
}
None
}
fn scan_declaration(b: &[u8], lt: usize) -> Option<usize> {
if !b[lt..].starts_with(b"<!") {
return None;
}
let mut i = lt + 2;
if !b.get(i).is_some_and(|c| c.is_ascii_alphabetic()) {
return None;
}
while b.get(i).is_some_and(|c| c.is_ascii_alphabetic()) {
i += 1;
}
while let Some(&c) = b.get(i) {
if c == b'>' {
return Some(i + 1);
}
i += 1;
}
None
}
fn scan_cdata(b: &[u8], lt: usize) -> Option<usize> {
if !b[lt..].starts_with(b"<![CDATA[") {
return None;
}
let mut i = lt + 9;
while i + 3 <= b.len() {
if &b[i..i + 3] == b"]]>" {
return Some(i + 3);
}
i += 1;
}
None
}
fn skip_html_ws(b: &[u8], mut i: usize) -> usize {
while matches!(b.get(i), Some(b' ' | b'\t' | b'\r' | b'\n')) {
i += 1;
}
i
}
fn try_autolink(text: &str, lt: usize) -> Option<(usize, Token)> {
let b = text.as_bytes();
let gt = find_byte(b, lt + 1, b'>')?;
let inner = &text[lt + 1..gt];
if is_uri_autolink(inner) {
Some((
gt + 1,
Token::Autolink {
text: inner.to_string(),
href: linkref::normalize_autolink(inner),
},
))
} else if is_email_autolink(inner) {
Some((
gt + 1,
Token::Autolink {
text: inner.to_string(),
href: format!("mailto:{inner}"),
},
))
} else {
None
}
}
fn gfm_autolink_boundary_ok(text: &str, i: usize) -> bool {
match char_before(text, i) {
None => true,
Some(c) => c.is_whitespace() || matches!(c, '*' | '_' | '~' | '('),
}
}
fn is_gfm_autolink_start(b: &[u8], i: usize) -> bool {
matches!(b[i], b'w' | b'W' | b'h' | b'H' | b'f' | b'F') || is_email_local_char(b[i])
}
fn try_gfm_autolink(text: &str, i: usize) -> Option<(Token, usize)> {
if let Some((end, scheme_len)) = gfm_url_extent(text, i) {
let raw = &text[i..end];
let href = if scheme_len == 0 {
format!("http://{}", linkref::normalize_autolink(raw))
} else {
linkref::normalize_autolink(raw)
};
return Some((
Token::Autolink {
text: raw.to_string(),
href,
},
end,
));
}
if let Some(end) = gfm_email_extent(text, i) {
let raw = &text[i..end];
return Some((
Token::Autolink {
text: raw.to_string(),
href: format!("mailto:{raw}"),
},
end,
));
}
None
}
fn gfm_url_extent(text: &str, i: usize) -> Option<(usize, usize)> {
let rest = &text[i..];
let lower = rest.to_ascii_lowercase();
let scheme_len = if lower.starts_with("http://") {
7
} else if lower.starts_with("https://") {
8
} else if lower.starts_with("ftp://") {
6
} else if lower.starts_with("www.") {
0
} else {
return None;
};
let domain_start = scheme_len;
let b = rest.as_bytes();
let mut k = domain_start;
while k < b.len() && !b[k].is_ascii_whitespace() && b[k] != b'<' {
k += 1;
}
let end = trim_gfm_url_tail(rest, domain_start, k);
if end <= domain_start {
return None;
}
let after = &rest[domain_start..end];
let domain = after.split('/').next().unwrap_or(after);
if !is_valid_gfm_domain(domain) {
return None;
}
Some((i + end, scheme_len))
}
fn trim_gfm_url_tail(rest: &str, path_start: usize, mut end: usize) -> usize {
let b = rest.as_bytes();
let min = path_start;
loop {
let before = end;
while end > min
&& matches!(
b[end - 1],
b'?' | b'!' | b'.' | b',' | b':' | b'*' | b'_' | b'~'
)
{
end -= 1;
}
while end > min && b[end - 1] == b')' {
let opens = rest[..end].bytes().filter(|&c| c == b'(').count();
let closes = rest[..end].bytes().filter(|&c| c == b')').count();
if closes > opens {
end -= 1;
} else {
break;
}
}
if end > min && b[end - 1] == b';' {
if let Some(amp) = rest[..end].rfind('&') {
if amp >= min {
let candidate = &rest[amp..end];
if candidate[1..candidate.len() - 1]
.bytes()
.all(|c| c.is_ascii_alphanumeric() || c == b'#')
&& candidate.len() > 2
{
end = amp;
}
}
}
}
if end == before {
break;
}
}
end
}
fn is_valid_gfm_domain(domain: &str) -> bool {
let labels: Vec<&str> = domain.trim_end_matches('.').split('.').collect();
if labels.len() < 2 || labels.iter().any(|l| l.is_empty()) {
return false;
}
if labels.iter().any(|l| {
!l.bytes()
.all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
}) {
return false;
}
let n = labels.len();
!labels[n - 1].contains('_') && !labels[n - 2].contains('_')
}
fn gfm_email_extent(text: &str, i: usize) -> Option<usize> {
let b = text.as_bytes();
let mut j = i;
while j < b.len() && is_email_local_char(b[j]) {
j += 1;
}
if j == i || b.get(j) != Some(&b'@') {
return None;
}
j += 1;
let dom_start = j;
while j < b.len() && (b[j].is_ascii_alphanumeric() || matches!(b[j], b'-' | b'_' | b'.')) {
j += 1;
}
while j > dom_start && b[j - 1] == b'.' {
j -= 1;
}
let domain = &text[dom_start..j];
if domain.is_empty() {
return None;
}
let labels: Vec<&str> = domain.split('.').collect();
if labels.len() < 2 || labels.iter().any(|l| l.is_empty()) {
return None;
}
if labels.iter().any(|l| {
!l.bytes()
.all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
}) {
return None;
}
let n = labels.len();
let last = labels[n - 1].as_bytes();
if matches!(last[last.len() - 1], b'-' | b'_')
|| labels[n - 1].contains('_')
|| labels[n - 2].contains('_')
{
return None;
}
Some(j)
}
fn is_email_local_char(c: u8) -> bool {
c.is_ascii_alphanumeric() || matches!(c, b'.' | b'-' | b'_' | b'+')
}
fn is_uri_autolink(inner: &str) -> bool {
let b = inner.as_bytes();
if b.first().is_none_or(|c| !c.is_ascii_alphabetic()) {
return false;
}
let mut i = 1;
while i < b.len() && (b[i].is_ascii_alphanumeric() || matches!(b[i], b'+' | b'.' | b'-')) {
i += 1;
}
if !(2..=32).contains(&i) || b.get(i) != Some(&b':') {
return false;
}
inner[i + 1..]
.bytes()
.all(|c| c > 0x20 && c != b'<' && c != 0x7f)
}
fn is_email_autolink(inner: &str) -> bool {
let Some((local, domain)) = inner.split_once('@') else {
return false;
};
if local.is_empty()
|| !local.bytes().all(|c| {
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'-'
)
})
{
return false;
}
if domain.is_empty() {
return false;
}
domain.split('.').all(is_email_label)
}
fn is_email_label(label: &str) -> bool {
let b = label.as_bytes();
if b.is_empty() || b.len() > 63 || b[0] == b'-' || b[b.len() - 1] == b'-' {
return false;
}
b.iter().all(|&c| c.is_ascii_alphanumeric() || c == b'-')
}
fn is_unicode_punct(c: char) -> bool {
matches!(c,
'\u{00A1}'..='\u{00BF}' | '\u{2000}'..='\u{206F}' | '\u{20A0}'..='\u{20CF}' | '\u{2E00}'..='\u{2E7F}' | '\u{3000}'..='\u{303F}' | '\u{FF00}'..='\u{FF0F}' | '\u{FF1A}'..='\u{FF20}'
| '\u{FF3B}'..='\u{FF40}'
| '\u{FF5B}'..='\u{FF65}'
)
}