#[cfg(test)]
mod tests;
pub type Matcher = for<'source> fn(&'source str) -> Option<MarkdownToken<'source>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarkdownToken<'source> {
Story {
story: Option<&'source str>,
len: usize,
controls: bool,
},
Markdown {
text: &'source str,
len: usize,
},
Header {
level: usize,
text: &'source str,
len: usize,
},
}
impl<'source> MarkdownToken<'source> {
pub fn len(&self) -> usize {
use MarkdownToken::*;
match self {
Story { len, .. } | Markdown { len, .. } | Header { len, .. } => *len,
}
}
}
const MAX_TAG_SEQUENCE_SIZE_CODE_FENCE: usize = 1024;
const MAX_TAG_SEQUENCE_SIZE_HEADER: usize = 8;
pub struct MarkdownParser {
matchers: Vec<Matcher>,
}
impl MarkdownParser {
pub fn new() -> Self {
Self {
matchers: vec![|source| header(source, false), story, sink],
}
}
pub fn parse<'source>(&self, source: &'source str) -> Vec<MarkdownToken<'source>> {
let mut result = Vec::new();
let mut rest = source;
if let Some(header) = header(rest, true) {
result.push(header);
rest = &rest[header.len()..];
}
while !rest.is_empty() {
for matcher in &self.matchers {
if let Some(token) = matcher(rest) {
rest = &rest[token.len()..];
result.push(token);
break;
}
}
}
result
}
}
struct TagSequence<'source> {
len: usize,
guard: &'source str,
}
impl<'source> TagSequence<'source> {
#[allow(unsafe_code)]
unsafe fn parse(source: &'_ str, max: usize, skip: usize) -> TagSequence<'_> {
let mut len = 1;
let chars = source.chars();
let mut chars = chars.skip(skip);
let guard = chars.next().unwrap();
for char in chars {
if char != guard {
break;
}
len += 1;
if len >= max {
break;
}
}
TagSequence {
len,
guard: &source[..len],
}
}
}
fn sink(source: &'_ str) -> Option<MarkdownToken<'_>> {
let mut current = 1; let length = source.len();
while current < length {
if source.is_char_boundary(current) {
let slice = &source[current..];
if !slice.starts_with("\n")
&& !slice.starts_with("<Story ")
&& !slice.starts_with("<Story\n")
{
current += 1;
continue;
}
if slice.starts_with("\n```") || slice.starts_with("\n~~~")
{
#[allow(unsafe_code)]
let open_guard = unsafe {
TagSequence::parse(slice, MAX_TAG_SEQUENCE_SIZE_CODE_FENCE, 1) };
let markdown_slice = &slice[open_guard.len + 1..];
if let Some(end) = markdown_slice.find(open_guard.guard) {
current += 2*(open_guard.len) + 1 + end;
continue;
} else {
return Some(MarkdownToken::Markdown {
text: source,
len: source.len(),
});
}
}
if slice.starts_with("\n#") || slice.starts_with("<Story ") ||
slice.starts_with("<Story\n")
{
return Some(MarkdownToken::Markdown {
text: &source[..current],
len: current,
});
}
}
current += 1;
}
Some(MarkdownToken::Markdown {
text: source,
len: source.len(),
})
}
fn header(source: &'_ str, initial: bool) -> Option<MarkdownToken<'_>> {
if source.starts_with("\n#") || initial && source.starts_with("#") {
let new_line_len = if source.starts_with("\n#") { 1 } else { 0 };
#[allow(unsafe_code)]
let header_guard =
unsafe { TagSequence::parse(source, MAX_TAG_SEQUENCE_SIZE_HEADER, new_line_len) };
let header_value = &source[header_guard.len + new_line_len..];
if header_value.starts_with(" ") {
if let Some(new_line) = header_value.find("\n") {
return Some(MarkdownToken::Header {
level: header_guard.len,
text: &header_value[..new_line],
len: header_guard.len + new_line_len + new_line,
});
} else {
return Some(MarkdownToken::Header {
level: header_guard.len,
text: header_value,
len: header_guard.len + new_line_len + header_value.len(),
});
}
}
if header_value.starts_with("\n") {
return Some(MarkdownToken::Header {
level: header_guard.len,
text: "",
len: header_guard.len + new_line_len,
});
}
}
None
}
#[derive(Debug, PartialEq, Eq)]
struct OfAttribute<'source> {
subpath: &'source str,
}
fn parse_of_attribute(source: &'_ str) -> Option<OfAttribute<'_>> {
if source[..2].to_ascii_lowercase().starts_with("of") {
let mut rest = source[2..].chars().enumerate();
let mut current = rest.next();
while let Some((_, char)) = current
&& char.is_whitespace()
{
current = rest.next();
}
if let Some((_, eq)) = current
&& eq != '='
{
return None;
}
current = rest.next();
while let Some((_, char)) = current
&& char.is_whitespace()
{
current = rest.next();
}
if let Some((idx, quote)) = current
&& (quote == '\'' || quote == '"')
{
let value = &source[idx + 2 + 1..];
if let Some(close) = value.find(quote) {
let subpath = &value[..close];
return Some(OfAttribute { subpath });
}
}
return None;
}
None
}
fn parse_bool_attribute(attribute: &'_ str, source: &'_ str) -> bool {
let mut source = source;
while let Some(idx) = source.find(attribute) {
let start: usize = if idx == 0 {
0
} else {
idx - 1
};
let end = if idx + attribute.len() == source.len() {
source.len()
} else {
idx + attribute.len() + 1
};
let attribute_name = &source[start..end];
let mut attribute_chars = attribute_name.chars();
if let Some(first_char) = attribute_chars.next()
&& (idx == 0 || first_char.is_whitespace())
{
let mut attribute_chars = attribute_chars.skip(attribute_name.len() - 1 - 1);
let last_char = attribute_chars.next();
if let Some(last_char) = last_char
&& last_char.is_whitespace()
{
return true;
} else if end == source.len() {
return true;
}
}
source = &source[idx + attribute.len()..];
while source.starts_with(attribute) {
source = &source[attribute.len()..];
}
}
false
}
fn find_of_attribute(source: &'_ str) -> Option<&'_ str> {
if let Some(attribute) = parse_of_attribute(source) {
return Some(attribute.subpath);
}
let mut rest = source;
while !rest.is_empty() {
let of_pos = rest.find("of");
if let Some(idx) = of_pos {
if idx == 0 {
rest = &rest[1..]; continue;
} else {
let check = &rest[idx - 1..idx].chars().next().unwrap();
if !check.is_whitespace() {
rest = &rest[1..];
continue;
} else {
let of = &rest[idx..];
if let Some(of) = parse_of_attribute(of) {
return Some(of.subpath);
}
}
}
} else {
return None;
}
}
None
}
fn story<'source>(source: &'source str) -> Option<MarkdownToken<'source>> {
if source.starts_with("<Story") {
if let Some(end) = source.find("/>") {
let tag = &source["<Story".len()..end];
let story = find_of_attribute(tag);
let controls = parse_bool_attribute("controls", tag);
return Some(MarkdownToken::Story {
story,
len: end + 2,
controls,
});
} else {
return None; }
}
None
}