use crate::parser::utils::helpers::strip_newline;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Target {
pub indent_len: usize,
pub label: (usize, usize),
pub marker_end: usize,
}
pub(crate) fn try_parse_target(content: &str) -> Option<Target> {
let (line, _newline) = strip_newline(content);
let indent_len = line.bytes().take_while(|&b| b == b' ').count();
if indent_len > 3 {
return None;
}
let rest = &line[indent_len..];
if !rest.starts_with('(') {
return None;
}
let close = rest.rfind(")=")?;
if close < 2 {
return None; }
let after = &rest[close + 2..];
if !after.trim().is_empty() {
return None;
}
let label_start = indent_len + 1;
let label_end = indent_len + close;
Some(Target {
indent_len,
label: (label_start, label_end),
marker_end: indent_len + close + 2,
})
}
pub(crate) fn is_comment_line(content: &str) -> bool {
let (line, _newline) = strip_newline(content);
let indent_len = line.bytes().take(4).take_while(|&b| b == b' ').count();
indent_len <= 3 && line[indent_len..].starts_with('%')
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct BlockBreak {
pub indent_len: usize,
pub marker_end: usize,
pub metadata: (usize, usize),
}
pub(crate) fn try_parse_block_break(content: &str) -> Option<BlockBreak> {
let (line, _newline) = strip_newline(content);
let indent_len = line.bytes().take(4).take_while(|&b| b == b' ').count();
if indent_len > 3 {
return None;
}
let rest = line.as_bytes();
if rest.get(indent_len) != Some(&b'+') {
return None;
}
let mut count = 0usize;
let mut last_plus = indent_len;
let mut pos = indent_len;
while pos < rest.len() {
match rest[pos] {
b'+' => {
count += 1;
last_plus = pos + 1;
}
b' ' | b'\t' => {}
_ => break,
}
pos += 1;
}
if count < 3 {
return None;
}
let marker_end = last_plus;
let after = &line[marker_end..];
let trimmed = after.trim();
let metadata = if trimmed.is_empty() {
(marker_end, marker_end)
} else {
let lead = after.len() - after.trim_start().len();
let start = marker_end + lead;
(start, start + trimmed.len())
};
Some(BlockBreak {
indent_len,
marker_end,
metadata,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_target() {
let t = try_parse_target("(my-label)=\n").unwrap();
assert_eq!(t.indent_len, 0);
assert_eq!(t.label, (1, 9));
assert_eq!(t.marker_end, 11);
}
#[test]
fn target_allows_trailing_whitespace_and_indent() {
let t = try_parse_target(" (sec:intro)= \n").unwrap();
assert_eq!(t.indent_len, 2);
let src = " (sec:intro)= \n";
assert_eq!(&src[t.label.0..t.label.1], "sec:intro");
}
#[test]
fn rejects_non_targets() {
assert!(try_parse_target("()=\n").is_none()); assert!(try_parse_target("(label)= trailing\n").is_none()); assert!(try_parse_target("not a target\n").is_none());
assert!(try_parse_target(" (x)=\n").is_none()); }
#[test]
fn comment_detection() {
assert!(is_comment_line("% a comment\n"));
assert!(is_comment_line(" % indented comment\n"));
assert!(is_comment_line("%\n"));
assert!(!is_comment_line(" % four-space indent\n"));
assert!(!is_comment_line("not % a comment\n"));
}
#[test]
fn basic_block_break() {
let b = try_parse_block_break("+++\n").unwrap();
assert_eq!(b.indent_len, 0);
assert_eq!(b.marker_end, 3);
assert_eq!(b.metadata, (3, 3)); }
#[test]
fn block_break_with_metadata() {
let src = "+++ {\"key\": 1}\n";
let b = try_parse_block_break(src).unwrap();
assert_eq!(b.marker_end, 3);
assert_eq!(&src[b.metadata.0..b.metadata.1], "{\"key\": 1}");
}
#[test]
fn block_break_allows_indent_and_spaced_markers() {
let src = " + + +\n";
let b = try_parse_block_break(src).unwrap();
assert_eq!(b.indent_len, 2);
assert_eq!(&src[b.indent_len..b.marker_end], "+ + +");
assert_eq!(b.metadata, (b.marker_end, b.marker_end));
}
#[test]
fn rejects_non_block_breaks() {
assert!(try_parse_block_break("++\n").is_none()); assert!(try_parse_block_break("+ +\n").is_none()); assert!(try_parse_block_break(" +++\n").is_none()); assert!(try_parse_block_break("text\n").is_none());
assert!(try_parse_block_break("+++text\n").is_some()); }
}