How do you wish to handle:
=== abc === def ===
?
Is the heading "abc === def"? Are you requiring spaces after the initial === and before the final ===? Are you disallowing trailing spaces (i.e. requiring the final marker to be precisely at the end of the line)?
Assuming yes to all, given your constraints to avoid your own ifs, this felt surprisingly messy and took me, a nom newbie, way too long to write. That being said, I learned a lot here, especially with how to use not() effectively. I've compiled and tested this code. I wonder whether anyone has a simpler solution.
fn parse_heading_and_level(s: &str) -> IResult<&str, (&str, usize)> {
let (rest, marker) = terminated(take_while(|c| c == '='), space1)(s)?;
let (rest, heading) = recognize(many0_count(tuple((
// This is a zero-width matcher that _fails_
// if the passed-in combinator succeeds. So
// it serves as a negative lookahead.
not(tuple((
space1, // trailing space
tag(marker), // trailing marker
alt((line_ending, eof)), // end of line or input
))),
// Otherwise, consume spaces and match contiguous
// non-space/tab/ending characters before we need
// to do the check for the tag again.
tuple((space0, take_till(|c| " \t\r\n".contains(c)))),
))))(rest)?;
// Move past the final space, marker, and newline.
let (rest, _) = tuple((take(marker.len() + 1),
alt((line_ending, eof))))(rest)?;
// Return not only the heading, but also its level.
Ok((rest, (heading, marker.len())))
}
Honestly, I'd rather write something more straightforward with if by pulling out a line up front and checking just the beginning and end. It's probably more efficient in the worst case as well since we only ever check the closing marker at the end of the line. Also, it's a shorter and was easier for me to write:
fn parse_heading_and_level2(s: &str) -> IResult<&str, (&str, usize)> {
// Pull out a line up front.
let (rest, line) =
terminated(take_till(|c| "\r\n".contains(c)), alt((line_ending, eof)))(s)?;
// Match the start.
let (heading, marker) =
terminated(take_while(|c| c == '='), space1)(line)?;
// Match the end.
// (Note that `split_ascii_whitespace()` ignores trailing spaces.)
if heading.ends_with('=') &&
heading.split_ascii_whitespace().next_back() == Some(marker) {
let heading = heading[..heading.len() - marker.len()].trim_end();
Ok((rest, (heading, marker.len())))
} else {
Err(nom::Err::Error(Error::new(heading, ErrorKind::Tag)))
}
}