use crate::linkify::Linkifier;
use crate::span::{Attrs, ParsedText, SpanBuilder, Style};
use std::ops::Range;
const MAX_DEPTH: u8 = 8;
pub fn scheme_allowed(url: &str) -> bool {
Linkifier::default().allows(url)
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum RawBlock {
Paragraph(String),
Code {
text: String,
language: Option<String>,
},
Quote { text: String, depth: u8 },
}
pub fn split_blocks(body: &str) -> Vec<RawBlock> {
let mut out: Vec<RawBlock> = Vec::new();
let mut para = String::new();
let mut lines = body.split('\n').peekable();
let flush = |para: &mut String, out: &mut Vec<RawBlock>| {
if !para.is_empty() {
out.push(RawBlock::Paragraph(std::mem::take(para)));
}
};
while let Some(line) = lines.next() {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("```") {
flush(&mut para, &mut out);
if let Some(inner) = rest.strip_suffix("```") {
out.push(RawBlock::Code {
text: inner.to_string(),
language: None,
});
continue;
}
let language = {
let l = rest.trim();
if l.is_empty() {
None
} else {
Some(l.to_string())
}
};
let mut code = String::new();
for l in lines.by_ref() {
if l.trim_start().starts_with("```") {
break;
}
if !code.is_empty() {
code.push('\n');
}
code.push_str(l);
}
out.push(RawBlock::Code {
text: code,
language,
});
continue;
}
if trimmed.starts_with('>') {
flush(&mut para, &mut out);
let mut depth = 0u8;
let mut rest = trimmed;
while let Some(r) = rest.strip_prefix('>') {
depth = depth.saturating_add(1);
rest = r.trim_start();
}
let mut quoted = rest.to_string();
while let Some(next) = lines.peek() {
let nt = next.trim_start();
if !nt.starts_with('>') {
break;
}
let mut d = 0u8;
let mut r = nt;
while let Some(s) = r.strip_prefix('>') {
d = d.saturating_add(1);
r = s.trim_start();
}
if d != depth {
break;
}
quoted.push('\n');
quoted.push_str(r);
lines.next();
}
out.push(RawBlock::Quote {
text: quoted,
depth,
});
continue;
}
if !para.is_empty() {
para.push('\n');
}
para.push_str(line);
}
flush(&mut para, &mut out);
out
}
pub fn parse_inline(src: &str) -> ParsedText {
parse_inline_with(src, &Linkifier::default())
}
pub fn parse_inline_with(src: &str, links: &Linkifier) -> ParsedText {
let mut b = SpanBuilder::new();
scan(src, Style::default(), 0, links, &mut b);
let out = b.finish();
out.debug_assert_well_formed();
out
}
fn scan(src: &str, base: Style, depth: u8, links: &Linkifier, out: &mut SpanBuilder) {
let bytes = src.as_bytes();
let mut i = 0usize;
let mut lit = 0usize;
macro_rules! flush_lit {
($upto:expr) => {
if $upto > lit {
out.push(&src[lit..$upto], base);
}
};
}
while i < bytes.len() {
let c = bytes[i];
if c == b'\\' && i + 1 < bytes.len() {
let next = i + 1;
let ch_end = next + utf8_len(bytes[next]);
let ch_end = ch_end.min(bytes.len());
if is_escapable(&src[next..ch_end]) {
flush_lit!(i);
out.push(&src[next..ch_end], base);
i = ch_end;
lit = i;
continue;
}
i += 1;
continue;
}
if c == b'`' {
if let Some((inner, end)) = code_span(bytes, i) {
flush_lit!(i);
out.push(strip_code_pad(&src[inner]), base.with_attrs(Attrs::CODE));
i = end;
lit = i;
continue;
}
i += backtick_run(bytes, i);
continue;
}
if depth < MAX_DEPTH {
let two = match c {
b'*' if bytes.get(i + 1) == Some(&b'*') => Some((Attrs::BOLD, "**")),
b'~' if bytes.get(i + 1) == Some(&b'~') => Some((Attrs::STRIKETHROUGH, "~~")),
_ => None,
};
if let Some((attr, delim)) = two {
if can_open(bytes, i + 2) {
if let Some(close) = find_delim(src, i + 2, delim) {
flush_lit!(i);
scan(
&src[i + 2..close],
base.with_attrs(attr),
depth + 1,
links,
out,
);
i = close + 2;
lit = i;
continue;
}
}
i += 2;
continue;
}
if (c == b'*' && can_open(bytes, i + 1))
|| (c == b'_' && can_open(bytes, i + 1) && intraword_ok(bytes, i))
{
if let Some(close) = find_italic_close(src, i + 1, c) {
flush_lit!(i);
scan(
&src[i + 1..close],
base.with_attrs(Attrs::ITALIC),
depth + 1,
links,
out,
);
i = close + 1;
lit = i;
continue;
}
i += 1;
continue;
}
if c == b'[' {
if let Some((label, href, end)) = parse_link(src, i) {
if links.allows(href) {
flush_lit!(i);
let id = out.reserve_link(href.to_string());
let start = out.len();
let mut label_style = base.with_attrs(Attrs::UNDERLINE);
label_style.link = Some(id);
scan(label, label_style, MAX_DEPTH, links, out);
out.set_link_range(id, start..out.len());
i = end;
lit = i;
continue;
}
}
i += 1;
continue;
}
}
i += 1;
}
flush_lit!(bytes.len());
}
fn is_escapable(s: &str) -> bool {
matches!(
s,
"\\" | "*" | "_" | "`" | "~" | "[" | "]" | "(" | ")" | ">" | "#"
)
}
fn utf8_len(b: u8) -> usize {
if b < 0x80 {
1
} else if b >> 5 == 0b110 {
2
} else if b >> 4 == 0b1110 {
3
} else if b >> 3 == 0b11110 {
4
} else {
1
}
}
fn backtick_run(bytes: &[u8], at: usize) -> usize {
let mut n = 0usize;
while bytes.get(at + n) == Some(&b'`') {
n += 1;
}
n
}
fn code_span(bytes: &[u8], at: usize) -> Option<(Range<usize>, usize)> {
let n = backtick_run(bytes, at);
let mut j = at + n;
while j < bytes.len() {
if bytes[j] == b'`' {
let m = backtick_run(bytes, j);
if m == n {
return Some((at + n..j, j + m));
}
j += m;
continue;
}
j += 1;
}
None
}
fn strip_code_pad(s: &str) -> &str {
let b = s.as_bytes();
if b.len() >= 2 && b[0] == b' ' && b[b.len() - 1] == b' ' && b.iter().any(|&c| c != b' ') {
&s[1..s.len() - 1]
} else {
s
}
}
fn find_delim(src: &str, from: usize, delim: &str) -> Option<usize> {
let bytes = src.as_bytes();
let d = delim.as_bytes();
let mut i = from;
while i + d.len() <= bytes.len() {
if bytes[i] == b'\\' {
i += 2;
continue;
}
if bytes[i] == b'`' {
match code_span(bytes, i) {
Some((_, end)) => {
i = end;
continue;
}
None => return None,
}
}
if bytes[i..].starts_with(d) {
if i == from || !can_close(bytes, i) {
i += d.len();
continue;
}
return Some(i);
}
i += 1;
}
None
}
fn find_italic_close(src: &str, from: usize, open: u8) -> Option<usize> {
let bytes = src.as_bytes();
let mut i = from;
while i < bytes.len() {
if bytes[i] == b'\\' {
i += 2;
continue;
}
if bytes[i] == b'`' {
match code_span(bytes, i) {
Some((_, end)) => {
i = end;
continue;
}
None => return None,
}
}
if bytes[i] == open {
if bytes.get(i + 1) == Some(&open) {
i += 2;
continue;
}
if i == from || !can_close(bytes, i) {
i += 1;
continue;
}
if open == b'_' && !intraword_close_ok(bytes, i) {
i += 1;
continue;
}
return Some(i);
}
i += 1;
}
None
}
fn can_open(bytes: &[u8], after: usize) -> bool {
bytes.get(after).is_some_and(|b| !b.is_ascii_whitespace())
}
fn can_close(bytes: &[u8], at: usize) -> bool {
at > 0 && !bytes[at - 1].is_ascii_whitespace()
}
fn intraword_ok(bytes: &[u8], i: usize) -> bool {
let before_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric();
let after_ok = bytes.get(i + 1).is_some_and(|b| *b != b'_');
before_ok && after_ok
}
fn intraword_close_ok(bytes: &[u8], i: usize) -> bool {
bytes.get(i + 1).is_none_or(|b| !b.is_ascii_alphanumeric())
}
fn parse_link(src: &str, open: usize) -> Option<(&str, &str, usize)> {
let bytes = src.as_bytes();
let mut i = open + 1;
let mut depth = 1usize;
while i < bytes.len() {
match bytes[i] {
b'\\' => {
i += 2;
continue;
}
b'[' => depth += 1,
b']' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
i += 1;
}
if depth != 0 || i >= bytes.len() {
return None;
}
let label_end = i;
if bytes.get(label_end + 1) != Some(&b'(') {
return None;
}
let url_start = label_end + 2;
let mut j = url_start;
while j < bytes.len() && bytes[j] != b')' {
if bytes[j] == b'\\' {
j += 2;
continue;
}
if bytes[j].is_ascii_whitespace() {
return None;
}
j += 1;
}
if j >= bytes.len() {
return None;
}
let label = &src[open + 1..label_end];
let href = &src[url_start..j];
if label.is_empty() || href.is_empty() {
return None;
}
Some((label, href, j + 1))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceSpan {
pub start: usize,
pub end: usize,
pub attrs: Attrs,
pub delim: bool,
}
pub fn scan_delims(src: &str) -> Vec<SourceSpan> {
let bytes = src.as_bytes();
let mut out = Vec::new();
let mut i = 0usize;
while i < bytes.len() {
if bytes[i] == b'\\' && i + 1 < bytes.len() {
i += 2;
continue;
}
let (run, attrs) = match bytes[i] {
b'*' if bytes.get(i + 1) == Some(&b'*') => (2usize, Attrs::BOLD),
b'*' => (1, Attrs::ITALIC),
b'_' if intraword_ok(bytes, i) => (1, Attrs::ITALIC),
b'`' => (backtick_run(bytes, i), Attrs::CODE),
_ => {
i += 1;
continue;
}
};
let literal = attrs == Attrs::CODE;
if !literal && !can_open(bytes, i + run) {
i += run;
continue;
}
let mut j = i + run;
let close = loop {
if j >= bytes.len() {
break None;
}
if !literal && bytes[j] == b'\\' {
j += 2;
continue;
}
if literal {
if bytes[j] == b'`' {
let m = backtick_run(bytes, j);
if m == run {
break Some(j);
}
j += m;
continue;
}
j += 1;
continue;
}
let hit = match run {
2 => bytes[j] == b'*' && bytes.get(j + 1) == Some(&b'*'),
_ => bytes[j] == bytes[i] && (bytes[i] != b'_' || intraword_close_ok(bytes, j)),
};
if hit && can_close(bytes, j) {
break Some(j);
}
j += 1;
};
let Some(close) = close else {
i += run;
continue;
};
out.push(SourceSpan {
start: i,
end: i + run,
attrs,
delim: true,
});
out.push(SourceSpan {
start: i + run,
end: close,
attrs,
delim: false,
});
out.push(SourceSpan {
start: close,
end: close + run,
attrs,
delim: true,
});
i = close + run;
}
out
}