use std::ops::Range;
use crate::syntax::{run_len, skip_ws};
use super::{is_blank, trim_range};
pub fn thematic_break(tail: &str) -> bool {
let mut marker = 0u8;
let mut count = 0usize;
for b in tail.bytes() {
match b {
b' ' | b'\t' => {}
b'-' | b'_' | b'*' => {
if marker == 0 {
marker = b;
} else if b != marker {
return false;
}
count += 1;
}
_ => return false,
}
}
count >= 3
}
#[expect(clippy::cast_possible_truncation)] pub fn atx_heading(tail: &str) -> Option<(u8, Range<usize>)> {
let bytes = tail.as_bytes();
let level = run_len(bytes, b'#');
if level == 0 || level > 6 {
return None;
}
match bytes.get(level) {
None => return Some((level as u8, level..level)),
Some(b' ' | b'\t') => {}
Some(_) => return None,
}
let start = skip_ws(bytes, level, 0);
let mut end = bytes.len();
while end > start && matches!(bytes[end - 1], b' ' | b'\t') {
end -= 1;
}
let closing = (start..end).rev().take_while(|&i| bytes[i] == b'#').count();
if closing > 0 {
let before = end - closing;
if before == start {
end = start;
} else if matches!(bytes[before - 1], b' ' | b'\t') {
end = before;
while end > start && matches!(bytes[end - 1], b' ' | b'\t') {
end -= 1;
}
}
}
Some((level as u8, start..end))
}
pub fn setext_underline(tail: &str) -> Option<u8> {
let bytes = tail.as_bytes();
let marker = *bytes.first()?;
if marker != b'=' && marker != b'-' {
return None;
}
let run = run_len(bytes, marker);
if !is_blank(&tail[run..]) {
return None;
}
Some(if marker == b'=' { 1 } else { 2 })
}
#[expect(clippy::cast_possible_truncation)] pub fn fence_open(tail: &str) -> Option<(u8, u32, Range<usize>)> {
let bytes = tail.as_bytes();
let marker = *bytes.first()?;
if marker != b'`' && marker != b'~' {
return None;
}
let run = run_len(bytes, marker);
if run < 3 {
return None;
}
let info = trim_range(tail, run..tail.len());
if marker == b'`' && tail[info.clone()].contains('`') {
return None;
}
Some((marker, run as u32, info))
}
pub fn fence_close(tail: &str, marker: u8, min_len: u32) -> bool {
let bytes = tail.as_bytes();
let run = run_len(bytes, marker);
run >= min_len as usize && is_blank(&tail[run..])
}
pub struct ListMarkerScan {
pub marker: u8,
pub ordered: bool,
pub len: usize,
pub starts_at_one: bool,
}
pub fn list_marker(tail: &str) -> Option<ListMarkerScan> {
let bytes = tail.as_bytes();
let first = *bytes.first()?;
let (scan, after) = if matches!(first, b'-' | b'*' | b'+') {
(ListMarkerScan { marker: first, ordered: false, len: 1, starts_at_one: false }, 1)
} else if first.is_ascii_digit() {
let digits = bytes.iter().take_while(|b| b.is_ascii_digit()).count();
if digits > 9 {
return None;
}
let delimiter = *bytes.get(digits)?;
if delimiter != b'.' && delimiter != b')' {
return None;
}
let starts_at_one = tail[..digits].parse::<u64>() == Ok(1);
(
ListMarkerScan { marker: delimiter, ordered: true, len: digits + 1, starts_at_one },
digits + 1,
)
} else {
return None;
};
match bytes.get(after) {
None | Some(b' ' | b'\t') => Some(scan),
Some(_) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thematic_breaks() {
assert!(thematic_break("***"));
assert!(thematic_break("- - -"));
assert!(thematic_break("__\t_ "));
assert!(!thematic_break("--"));
assert!(!thematic_break("-*-"));
assert!(!thematic_break("--- a"));
}
#[test]
fn atx_headings() {
assert_eq!(atx_heading("# h"), Some((1, 2..3)));
assert_eq!(atx_heading("###"), Some((3, 3..3)));
assert_eq!(atx_heading("#\tx "), Some((1, 2..3)));
assert_eq!(atx_heading("## h ##"), Some((2, 3..4)));
assert_eq!(atx_heading("## ##"), Some((2, 3..3)));
assert_eq!(atx_heading("# h#"), Some((1, 2..4)));
assert_eq!(atx_heading("#hashtag"), None);
assert_eq!(atx_heading("####### seven"), None);
}
#[test]
fn setext_underlines() {
assert_eq!(setext_underline("==="), Some(1));
assert_eq!(setext_underline("- "), Some(2));
assert_eq!(setext_underline("= ="), None);
assert_eq!(setext_underline("~~~"), None);
}
#[test]
fn fences() {
assert_eq!(fence_open("```rust "), Some((b'`', 3, 3..7)));
assert_eq!(fence_open("~~~~"), Some((b'~', 4, 4..4)));
assert_eq!(fence_open("``"), None);
assert_eq!(fence_open("``` a`b"), None);
assert_eq!(fence_open("~~~ a`b"), Some((b'~', 3, 4..7)));
assert!(fence_close("```` ", b'`', 3));
assert!(!fence_close("``", b'`', 3));
assert!(!fence_close("``` x", b'`', 3));
assert!(!fence_close("~~~", b'`', 3));
}
#[test]
fn list_markers() {
let m = list_marker("- a").unwrap();
assert_eq!((m.marker, m.ordered, m.len), (b'-', false, 1));
assert!(list_marker("+").is_some(), "marker at end of line");
let m = list_marker("1. a").unwrap();
assert_eq!((m.marker, m.ordered, m.len, m.starts_at_one), (b'.', true, 2, true));
let m = list_marker("001)\tx").unwrap();
assert_eq!((m.marker, m.len, m.starts_at_one), (b')', 4, true));
assert!(!list_marker("2.").unwrap().starts_at_one);
assert!(list_marker("-a").is_none());
assert!(list_marker("1234567890. ten digits").is_none());
assert!(list_marker("1: x").is_none());
}
}