#[derive(Debug, Clone, Copy)]
pub(super) struct RawListLine {
pub(super) indent: usize,
pub(super) ordered: bool,
pub(super) content_start: usize,
pub(super) content_end: usize,
pub(super) line_start: usize,
}
pub(super) fn list_marker_at(bytes: &[u8], pos: usize) -> Option<(usize, bool, usize)> {
let mut i = pos;
let mut indent = 0_usize;
while i < bytes.len() && bytes[i] == b' ' {
indent += 1;
i += 1;
}
if i >= bytes.len() || bytes[i] == b'\n' || bytes[i] == b'\r' {
return None;
}
if bytes[i] == b'-' {
let after = i + 1;
if after >= bytes.len() {
return None;
}
if bytes[after] != b' ' && bytes[after] != b'\t' {
return None;
}
let mut j = after;
while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t') {
j += 1;
}
return Some((indent, false, j));
}
if bytes[i].is_ascii_digit() {
let mut j = i;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
if j >= bytes.len() || bytes[j] != b'.' {
return None;
}
let after = j + 1;
if after >= bytes.len() {
return None;
}
if bytes[after] != b' ' && bytes[after] != b'\t' {
return None;
}
let mut k = after;
while k < bytes.len() && (bytes[k] == b' ' || bytes[k] == b'\t') {
k += 1;
}
return Some((indent, true, k));
}
None
}
pub(super) fn skip_set_ws(bytes: &[u8], from: usize, end: usize) -> usize {
let mut i = from;
while i < end && matches!(bytes[i], b' ' | b'\t' | b'\n' | b'\r') {
i += 1;
}
i
}
pub(super) fn skip_to_comma(bytes: &[u8], from: usize, end: usize) -> usize {
let mut i = from;
while i < end && bytes[i] != b',' {
i += 1;
}
i
}
pub(super) fn next_char_boundary(src: &str, from: usize) -> usize {
let mut i = from + 1;
while i < src.len() && !src.is_char_boundary(i) {
i += 1;
}
i
}
pub(super) fn find_byte(haystack: &[u8], needle: u8, from: usize) -> Option<usize> {
haystack[from..]
.iter()
.position(|&b| b == needle)
.map(|p| p + from)
}
pub(super) fn scan_label_chars(bytes: &[u8], from: usize) -> usize {
let mut i = from;
while i < bytes.len() {
let b = bytes[i];
let is_id = b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b':' | b'.');
if !is_id {
break;
}
i += 1;
}
i
}
pub(super) fn normalize_raw_text(text: &str) -> String {
let text = text
.strip_prefix("\r\n")
.or_else(|| text.strip_prefix('\n'))
.or_else(|| text.strip_prefix('\r'))
.unwrap_or(text);
text.replace("\r\n", "\n").replace('\r', "\n")
}
pub(super) struct ParsedLabel {
pub text: String,
pub start: usize,
pub end: usize,
}
pub(super) fn strip_leading_label(
src: &str,
start: usize,
end: usize,
) -> (usize, Option<ParsedLabel>) {
let bytes = src.as_bytes();
let mut i = start;
while i < end && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
if i >= end || bytes[i] != b'<' {
return (start, None);
}
let id_start = i + 1;
let id_end = scan_label_chars(bytes, id_start);
if id_end == id_start || id_end >= end || bytes[id_end] != b'>' {
return (start, None);
}
let label = ParsedLabel {
text: src[id_start..id_end].to_owned(),
start: id_start,
end: id_end,
};
let mut after = id_end + 1;
while after < end && (bytes[after] == b' ' || bytes[after] == b'\t' || bytes[after] == b'\n') {
after += 1;
}
(after, Some(label))
}
pub(super) fn strip_trailing_label(
src: &str,
start: usize,
end: usize,
) -> (usize, Option<ParsedLabel>) {
let bytes = src.as_bytes();
if end <= start || bytes[end - 1] != b'>' {
return (end, None);
}
let close = end - 1;
let mut i = close;
while i > start {
let b = bytes[i - 1];
let is_id = b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b':' | b'.');
if !is_id {
break;
}
i -= 1;
}
if i == close || i == start || bytes[i - 1] != b'<' {
return (end, None);
}
let label = ParsedLabel {
text: src[i..close].to_owned(),
start: i,
end: close,
};
let mut text_end = i - 1;
while text_end > start && (bytes[text_end - 1] == b' ' || bytes[text_end - 1] == b'\t') {
text_end -= 1;
}
(text_end, Some(label))
}