use crate::*; macro_rules! opt { ($v:expr) => {|s| Some($v(s)) }; } pub struct Line { pub elements: Vec, } impl Line { pub fn from_str(raw_string: &str) -> Self { fn unlabeled_extern_link(target: String) -> Option { target.contains("/").then( || LineElement::ExternalLink(ExternalLink { target, label:String::new() }) ) } fn labelled_extern_link(s: String) -> Option { let (label, target) = match s.split_once("](") { Some((l, t)) => (l.to_string(), t.to_string()), None => return None }; if label.contains("]") || target.contains("]") { return None } Some(LineElement::ExternalLink(ExternalLink { label, target })) } const DELIMITERS: [(fn(String)->Option, &str, &str, &str); 7] = [ ( opt!(LineElement::Bold), "**", "**", "*" ), ( opt!(LineElement::Italic), "_", "_", "_" ), ( opt!(LineElement::Monospace), "`", "`", "`" ), ( opt!(LineElement::Math), "$", "$", "$" ), ( opt!(LineElement::InternalLink), "[[", "]]", "[]" ), ( labelled_extern_link, "[", ")", "[]()" ), ( unlabeled_extern_link, "[", "]", "[]" ), ]; let chars: Vec = raw_string.chars().collect(); let mut elements = Vec::new(); let mut cached_chars = String::new(); let mut i = 0; let starts_with = |i, p:&str| std::iter::zip(&chars[i..], p.chars()).all(|(a, b)| *a == b); 'outer: while let Some(c) = chars.get(i) { // Only check for opening delimiters that directly follow a whitespace character. let follows_whitespace = match chars.get(i.wrapping_sub(1)) { Some(w) => is_whitespace(w), None => true, }; if follows_whitespace { // Try to parse an opening delimiter. for (variant, start_delim, end_delim, delim_chars) in DELIMITERS { // Try to match an opening delimiter with a terminating delimiter. if starts_with(i, start_delim) { let s_end = i + start_delim.chars().count(); let mut e_start = s_end; let mut e_end = e_start + end_delim.chars().count(); while e_end <= chars.len() { e_start += 1; e_end += 1; let end_is_whitespace = if let Some(end_char) = chars.get(e_end) { is_whitespace(end_char) } else { e_end == chars.len() }; // If the terminating delimiter is found, store the normal // text and the styled text, and continue to the next character. if end_is_whitespace && starts_with(e_start, end_delim) { // Check that there is content within the styled string. let styled_string: String = chars[s_end..e_start].iter().collect(); let non_content_chars: Vec<_> = delim_chars.chars().collect(); if !is_contentful(&styled_string, &non_content_chars) { continue } if styled_string.len() != styled_string.trim().len() { continue } let line_element = match variant(styled_string) { Some(e) => e, None => continue, }; // Commit the normal and styled strings. if !cached_chars.is_empty() { let normal_string = std::mem::take(&mut cached_chars); elements.push(LineElement::Normal(normal_string)); } elements.push(line_element); i = e_end; continue 'outer; } } } } } cached_chars.push(*c); i += 1; } if !cached_chars.is_empty() { let normal_string = std::mem::take(&mut cached_chars); elements.push(LineElement::Normal(normal_string)); } Self { elements } } /// Return only the character content, with none of the styling information. pub fn as_plain_text(&self) -> String { let mut string = String::new(); for line_element in &self.elements { string.push_str(line_element.as_plain_text()) } return string; } } impl std::fmt::Display for Line { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { for line_element in &self.elements { write!(f, "{line_element}")?; } Ok(()) } } impl std::fmt::Debug for Line { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { for line_element in &self.elements { write!(f, "{line_element:?}\n")?; } Ok(()) } }