use std::{fmt, ops::Deref}; use line_span::{LineSpan, LineSpans}; fn process<'raw>(raw: &'raw str) -> Document<'raw> { let tokens: Vec> = raw .line_spans() .map(|span| TokenLine { tok: Token::detect(span.as_str()), span, }) .collect(); let mut blocks = vec![]; let mut curr = &tokens[..]; loop { if curr.is_empty() { break; } let tok = curr[0]; match tok.tok { Token::Text => blocks.push(Block::Paragraph(tok.span.as_str())), Token::Blank => (), Token::Header { level, text } => blocks.push(Block::header(level, text)), Token::Escaped { text } => blocks.push(Block::Paragraph(text)), Token::HorizontalLine => blocks.push(Block::HorizontalLine), Token::ListItem { text } => { let mut mur = map_until(&curr[1..], extract_listitem); mur.items.insert(0, text); blocks.push(Block::list(mur.items)); curr = mur.remaining; continue; } Token::CodeFence { lang } => { let mur = map_until(&curr[1..], extract_codeblock_body); let body = StrOrMany::from_spans(raw, &mur.items); blocks.push(Block::code(lang, body)); // Advance remaining by one to skip the close curr = &mur.remaining[1..]; continue; } } curr = &curr[1..]; } Document { blocks } } fn maybe_header<'raw>(line: &'raw str) -> Option> { if line.starts_with('#') { if let Some((level_str, text)) = line.split_once(|c| c == ' ') { let level = level_str .chars() .fold(0, |acc, c| if c == '#' { acc + 1 } else { acc }); // If the level (which is the count of '#' in the level_str) is not // equal to level_str.len(), not every character was a hash and this // header is invalid if level == level_str.len() { return Some(Token::Header { level: level as u8, text, }); } } } None } fn extract_listitem<'raw>(tok: &TokenLine<'raw>) -> Option<&'raw str> { if let Token::ListItem { text } = tok.tok { Some(text) } else { None } } fn extract_codeblock_body<'raw>(tok: &TokenLine<'raw>) -> Option> { match tok.tok { Token::CodeFence { .. } => None, _ => Some(tok.as_code_body()), } } fn map_until<'raw, 'tok, F, T>(toks: &'tok [TokenLine<'raw>], f: F) -> MapUntilReturn<'raw, 'tok, T> where F: Fn(&TokenLine<'raw>) -> Option, { let mut items = vec![]; for (idx, tok) in toks.iter().enumerate() { match f(tok) { Some(item) => items.push(item), None => { return MapUntilReturn { items, remaining: &toks[idx..], }; } } } MapUntilReturn { items, remaining: &[], } } struct MapUntilReturn<'raw, 'tok, T> { items: Vec, remaining: &'tok [TokenLine<'raw>], } #[derive(Copy, Clone, Debug)] struct TokenLine<'raw> { span: LineSpan<'raw>, tok: Token<'raw>, } impl<'raw> Deref for TokenLine<'raw> { type Target = Token<'raw>; fn deref(&self) -> &Self::Target { &self.tok } } impl<'raw> TokenLine<'raw> { pub fn as_code_body(&self) -> SpanOrStr<'raw> { if let Token::Escaped { text } = self.tok { return text.into(); } self.span.into() } } #[derive(Copy, Clone, Debug)] enum Token<'raw> { Text, Blank, Escaped { text: &'raw str }, Header { level: u8, text: &'raw str }, CodeFence { lang: Option<&'raw str> }, HorizontalLine, ListItem { text: &'raw str }, } impl<'raw> Token<'raw> { pub fn detect(line: &'raw str) -> Self { if line.is_empty() { return Token::Blank; } let mut escaped = None; let working = if let Some(stripped) = line.strip_prefix('\\') { escaped = Some(stripped); stripped } else { line }; macro_rules! maybe_escaped { () => { if let Some(text) = escaped { return Token::Escaped { text }; } }; } if let Some(header_tok) = maybe_header(working) { maybe_escaped!(); return header_tok; } if let Some(stripped) = working.strip_prefix("```") { maybe_escaped!(); return Token::CodeFence { lang: Self::str_empty_option(stripped), }; } if working.trim_end().eq("---") { maybe_escaped!(); return Token::HorizontalLine; } if let Some(stripped) = working.strip_prefix('-') { maybe_escaped!(); return Token::ListItem { text: stripped }; } Token::Text } pub fn str_empty_option(s: &str) -> Option<&str> { match s.is_empty() { true => None, false => Some(s), } } } enum SpanOrStr<'raw> { Span(LineSpan<'raw>), Str(&'raw str), } impl<'raw> SpanOrStr<'raw> { pub fn slice_has_str(slice: &[SpanOrStr<'raw>]) -> bool { for sos in slice { if let SpanOrStr::Str(_) = sos { return true; } } false } pub fn as_str(&self) -> &'raw str { match self { Self::Span(span) => span.as_str(), Self::Str(s) => s, } } } impl<'raw> From> for SpanOrStr<'raw> { fn from(span: LineSpan<'raw>) -> Self { Self::Span(span) } } impl<'raw> From<&'raw str> for SpanOrStr<'raw> { fn from(s: &'raw str) -> Self { Self::Str(s) } } #[derive(Clone, Debug, PartialEq)] pub struct Document<'raw> { blocks: Vec>, } impl<'raw> Document<'raw> { pub fn parse(raw: &'raw str) -> Self { process(raw) } pub fn blocks(&self) -> &[Block<'raw>] { &self.blocks } } #[derive(Clone, Debug, PartialEq)] pub enum StrOrMany<'raw> { One(&'raw str), Many(Vec<&'raw str>), } impl<'raw> From<&'raw str> for StrOrMany<'raw> { fn from(s: &'raw str) -> Self { StrOrMany::One(s) } } impl<'raw> StrOrMany<'raw> { fn from_spans(raw: &'raw str, slice: &[SpanOrStr<'raw>]) -> Self { if SpanOrStr::slice_has_str(slice) { StrOrMany::Many(slice.iter().map(|sp| sp.as_str()).collect()) } else { match [slice.first(), slice.last()] { // Slice is empty [None, _] => return StrOrMany::One(""), // These can't happen because we just checked if the slice has a Str variant [Some(SpanOrStr::Str(_)), _] => unreachable!(), [_, Some(SpanOrStr::Str(_))] => unreachable!(), // This can't happen because the last would be the first [Some(SpanOrStr::Span(_)), None] => unreachable!(), // Return one large slice encompassing all the Span in the slice [Some(SpanOrStr::Span(first)), Some(SpanOrStr::Span(last))] => { StrOrMany::One(&raw[first.start()..last.end()]) } } } } } impl<'raw> fmt::Display for StrOrMany<'raw> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::One(s) => write!(f, "{s}"), Self::Many(strs) => match strs.len() { 0 => write!(f, ""), 1 => write!(f, "{}", strs[1]), len => { let last = strs.last().unwrap(); for s in &strs[..len - 1] { writeln!(f, "{s}")?; } write!(f, "{last}") } }, } } } #[derive(Clone, Debug, PartialEq)] pub enum Block<'raw> { Header { level: u8, text: &'raw str, }, Paragraph(&'raw str), HorizontalLine, List { items: Vec<&'raw str>, }, Code { language: Option<&'raw str>, lines: StrOrMany<'raw>, }, } impl<'raw> Block<'raw> { pub fn header(level: u8, text: &'raw str) -> Self { Self::Header { level, text } } pub fn para(text: &'raw str) -> Self { Self::Paragraph(text) } pub fn list(items: Vec<&'raw str>) -> Self { Self::List { items } } pub fn code>>(language: Option<&'raw str>, lines: L) -> Self { Self::Code { language, lines: lines.into(), } } } #[cfg(test)] mod test { use super::{Block, Document, process}; macro_rules! doc { (1p $text:literal) => { Document { blocks: vec![Block::Paragraph($text)], } }; (1 $someblock:expr) => { Document { blocks: vec![$someblock], } }; } #[test] pub fn header() { let raw = "# Level one"; let actual = process(raw); let expected = doc!(1 Block::header(1, "Level one")); assert_eq!(actual, expected) } #[test] pub fn many_headers() { let raw = "# One\n## Two\n### Three"; let actual = process(raw); let expected = Document { blocks: vec![ Block::header(1, "One"), Block::header(2, "Two"), Block::header(3, "Three"), ], }; assert_eq!(actual, expected) } #[test] pub fn paragraph() { let raw = "paragraph"; let actual = process(raw); let expected = doc!(1p "paragraph"); assert_eq!(actual, expected) } #[test] pub fn horizontal_line() { let raw = "---"; let actual = process(raw); let expected = doc!(1 Block::HorizontalLine); assert_eq!(actual, expected) } #[test] pub fn list_item() { let raw = "- list item"; let actual = process(raw); let expected = doc!(1 Block::list(vec![" list item"])); assert_eq!(actual, expected) } #[test] pub fn empty_code_block_no_lang() { let raw = "```\n```"; let actual = process(raw); let expected = doc!(1 Block::code(None, "")); assert_eq!(actual, expected) } #[test] pub fn empty_code_block_with_lang() { let raw = "```lang\n```"; let actual = process(raw); let expected = doc!(1 Block::code(Some("lang"), "")); assert_eq!(actual, expected) } #[test] pub fn code_block_no_lang() { let raw = "```\nsome code\n```"; let actual = process(raw); let expected = doc!(1 Block::code(None, "some code")); assert_eq!(actual, expected) } #[test] pub fn code_block_with_lang() { let raw = "```html\n

paragraph

\n```"; let actual = process(raw); let expected = doc!(1 Block::code(Some("html"), "

paragraph

")); assert_eq!(actual, expected) } const ALL: &'static str = " # Header h1 Paragraph! - Item 1 - Item 2 ## Header h2 ```html

some html

``` "; #[test] pub fn all_together_now() { let actual = process(ALL); let expected_blocks = vec![ Block::header(1, "Header h1"), Block::para("Paragraph!"), Block::list(vec![" Item 1", " Item 2"]), Block::header(2, "Header h2"), Block::code(Some("html"), "

some html

"), ]; assert_eq!( actual, Document { blocks: expected_blocks } ) } }