diff options
Diffstat (limited to 'src/elements/line.rs')
-rw-r--r-- | src/elements/line.rs | 117 |
1 files changed, 117 insertions, 0 deletions
diff --git a/src/elements/line.rs b/src/elements/line.rs new file mode 100644 index 0000000..d5c078e --- /dev/null +++ b/src/elements/line.rs @@ -0,0 +1,117 @@ +use crate::*; + +macro_rules! opt { + ($v:expr) => {|s| Some($v(s)) }; +} + +pub struct Line { + pub elements: Vec<LineElement>, +} + +impl Line { + pub fn from_str(raw_string: &str) -> Self { + fn unlabeled_extern_link(target: String) -> Option<LineElement> { + target.contains("/").then( || + LineElement::ExternalLink(ExternalLink { target, label:String::new() }) + ) + } + fn labelled_extern_link(s: String) -> Option<LineElement> { + 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<LineElement>, &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<char> = 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(()) + } +} |