use derive_more::{Debug, Display};
use mlua::IntoLua;
use std::str::CharIndices;
#[derive(Display, Debug)]
#[display("{content}")]
#[debug("\"{content}\"")]
pub struct MdHeading {
content: String,
name_span: (usize, usize),
level: usize,
}
impl MdHeading {
pub fn content(&self) -> &str {
&self.content
}
pub fn name(&self) -> &str {
&self.content[self.name_span.0..self.name_span.1]
}
pub fn level(&self) -> usize {
self.level
}
#[allow(unused)]
pub fn matches(&self, level: usize, name: &str) -> bool {
self.level == level && self.name() == name.trim()
}
}
pub enum ParseResponse<T> {
Item(T),
#[allow(unused)]
Other(String),
}
impl MdHeading {
pub fn peek_line(line: &str) -> Option<(usize, &str)> {
let mut chars = line.char_indices();
let level = MdHeading::parse_level_inner(&mut chars)?;
let name_span = MdHeading::continue_parse_for_name_span(&mut chars)?;
Some((level, &line[name_span.0..name_span.1]))
}
pub fn parse_line(line: impl Into<String>) -> ParseResponse<MdHeading> {
let line: String = line.into();
let mut chars = line.char_indices();
let Some(level) = MdHeading::parse_level_inner(&mut chars) else {
return ParseResponse::Other(line);
};
let Some(name_span) = MdHeading::continue_parse_for_name_span(&mut chars) else {
return ParseResponse::Other(line);
};
let content = line;
ParseResponse::Item(MdHeading {
content,
name_span,
level,
})
}
#[cfg(test)]
fn new(line: impl Into<String>) -> Option<Self> {
let line: String = line.into();
let mut chars = line.char_indices();
let level = MdHeading::parse_level_inner(&mut chars)?;
let name_span = MdHeading::continue_parse_for_name_span(&mut chars)?;
Some(MdHeading {
content: line,
name_span,
level,
})
}
}
impl MdHeading {
pub fn into_content(self) -> String {
self.content
}
}
impl IntoLua for MdHeading {
fn into_lua(self, lua: &mlua::Lua) -> mlua::Result<mlua::Value> {
let table = lua.create_table()?;
let name = self.name().to_string();
table.set("content", self.content)?;
table.set("level", self.level)?;
table.set("name", name)?;
Ok(mlua::Value::Table(table))
}
}
impl MdHeading {
fn parse_level_inner(chars: &mut CharIndices<'_>) -> Option<usize> {
let mut level = 0;
for (_, c) in chars.by_ref() {
if level == 0 && c.is_whitespace() {
return None;
} else if c == '#' {
level += 1;
}
else if !c.is_whitespace() {
return None;
} else {
break;
}
}
if level == 0 || level > 6 {
return None; }
Some(level)
}
fn continue_parse_for_name_span(chars: &mut CharIndices<'_>) -> Option<(usize, usize)> {
let mut start_pos = None;
let mut end_pos = None;
for (i, c) in chars {
if c == '\n' {
break;
}
if start_pos.is_none() && !c.is_whitespace() {
start_pos = Some(i); }
if start_pos.is_some() {
end_pos = Some(i + 1); }
}
Some((start_pos?, end_pos?))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_md_heading_simple_ok() {
let headings = vec![
("# Heading One", 1, "Heading One"),
("## Heading Two", 2, "Heading Two"),
("### Heading Three", 3, "Heading Three"),
("##### Heading Five", 5, "Heading Five"),
("###### Heading Six", 6, "Heading Six"),
];
for (line, expected_level, expected_name) in headings {
let heading = MdHeading::new(line);
assert!(heading.is_some(), "Failed to parse valid heading: '{}'", line);
let heading = heading.unwrap();
assert_eq!(heading.level, expected_level, "Incorrect level for heading: '{}'", line);
assert_eq!(heading.name(), expected_name, "Incorrect name for heading: '{}'", line);
}
}
#[test]
fn test_md_heading_simple_none() {
let invalid_headings = vec![
" Heading without hash", " #### Heading with leading space", "####### Too many hashes", "#NoSpaceAfterHash", "", " ", "###", " ##No space after leading whitespace and hash", ];
for line in invalid_headings {
let heading = MdHeading::new(line.to_string());
assert!(
heading.is_none(),
"Parsed an invalid heading that should be None: '{}'",
line
);
}
}
}