//! One paragraph of prose: code spans, escapes, images, links and URLs, left to right. use super::{BREAKS, Out, runs}; /// What marked lets a backslash escape. Other characters after a backslash are plain text. const ESCAPABLE: &str = "\\`*{}[]()#+-.!_>|~"; /// What a URL loses at its end: marked leaves these out of a link when they end it. const TRAILING: [char; 11] = ['.', ',', ':', ';', '!', '?', '*', '_', '~', '\'', '"']; /// How many characters a search for the end of a construct looks at, so that a reply full of /// brackets costs time in proportion to its length. An image longer than this still has its `![` /// put in a code span; a link longer than this keeps only its URL in one. const WINDOW: usize = 2048; /// What a paragraph has shown so far that changes how its later code spans may pair. #[derive(Default)] struct Seen { /// Backtick run lengths that may pair differently in the two parsers from here on. hazards: Vec, /// What would close what a plain character opened: marked lets emphasis (`*`, `_`, `~`), a /// tag (`<`) and inline LaTeX (`$`) take a backtick before a code span starts. closers: Vec, } impl Seen { /// Add to `hazards`, once each, every length from 1 to that of each backtick run in `text`: /// marked may read part of a run as part of a URL, and the rest as a run of its own. fn runs(&mut self, text: &str) { for (_, len) in runs(text) { for part in 1..=len { if !self.hazards.contains(&part) { self.hazards.push(part); } } } } /// A plain character: `*`, `_`, `~` and `$` are their own closers, and `<` is closed by `>`; /// push the closer onto `closers` if it is not there yet. Any other character: nothing. fn plain(&mut self, c: char) { todo!() } } impl Out { /// Write `para` with its images, and when `untrusted` its links and URLs, in code spans. pub(super) fn paragraph(&mut self, para: &str, untrusted: bool) { let mut seen = Seen::default(); let mut prev = None; let mut at = 0; while let Some(rest) = para.get(at..).filter(|rest| !rest.is_empty()) { let line_start = at == 0 || para.get(..at).is_some_and(|done| done.ends_with(BREAKS)); match self.token(rest, prev, line_start, untrusted, &mut seen) { Some(used) if used > 0 => { at += used; prev = None; } _ => { let c = rest.chars().next().unwrap_or_default(); self.text.push(c); seen.plain(c); at += c.len_utf8(); prev = Some(c); } } } } /// Write the token `rest` starts with and give its length in bytes, or `None` when `rest` /// starts with a plain character, which the caller writes. `prev` is the plain character /// before, `None` after a token or at the start. fn token( &mut self, rest: &str, prev: Option, line_start: bool, untrusted: bool, seen: &mut Seen, ) -> Option { if untrusted && line_start && definition(rest) { let end = rest.find(BREAKS).unwrap_or(rest.len()); return Some(self.link(rest, end, seen)); } match rest.chars().next()? { '\\' => escape_len(rest).map(|len| self.copy(rest, len)), '`' => Some(self.backticks(rest, seen)), '!' if rest.starts_with("![") => { // No `]` in reach: the `![` alone is enough to stop the image. let len = construct_len(rest.get(1..)?, true).unwrap_or(1); Some(self.image(rest, len + 1, seen)) } '[' => self.bracket(rest, untrusted, seen), '<' => self.autolink(rest, untrusted, seen), _ if url_start(rest, prev) => self.url(rest, untrusted, seen), _ => None, } } /// A run of backticks: a code span both parsers read as code is written as it is; any other /// run is written as plain text, and its length becomes a hazard. fn backticks(&mut self, rest: &str, seen: &mut Seen) -> usize { if let Some(len) = span_len(rest, seen) { return self.copy(rest, len); } let run = rest.chars().take_while(|&c| c == '`').count(); seen.runs(rest.get(..run).unwrap_or_default()); self.copy(rest, run) } /// A `[` that starts a link, `[text](…)` or `[text][…]`: see `construct`. Any other `[` is /// written escaped if `](` follows within `WINDOW`: marked may still read a link from it, with /// a label that takes in new code spans. fn bracket(&mut self, rest: &str, untrusted: bool, seen: &mut Seen) -> Option { let Some(len) = construct_len(rest, false) else { if !window(rest).contains("](") { return None; } self.text.push_str("\\["); return Some(1); }; self.construct(rest, len, untrusted, seen) } /// A `<` that starts an autolink: see `construct`. fn autolink(&mut self, rest: &str, untrusted: bool, seen: &mut Seen) -> Option { let len = autolink_len(rest)?; self.construct(rest, len, untrusted, seen) } /// A URL: see `construct`. In a clean reply it is the most marked may read as the URL /// (`url_len` with `whole`), backticks included, since marked reads them as part of it. fn url(&mut self, rest: &str, untrusted: bool, seen: &mut Seen) -> Option { let len = url_len(rest, !untrusted); self.construct(rest, len, untrusted, seen) } /// The first `len` bytes of `rest`, a link, go in code spans when `untrusted`, and also in a /// clean reply when they hold `![` (CommonMark may read the image where marked reads a link). /// Otherwise they are written as they are. Marked may end the link sooner than this, so its /// backtick runs become hazards, and each of its characters counts as `Seen::plain`. fn construct( &mut self, rest: &str, len: usize, untrusted: bool, seen: &mut Seen, ) -> Option { let construct = rest.get(..len)?; if untrusted { return Some(self.link(rest, len, seen)); } if construct.contains("![") { return Some(self.image(rest, len, seen)); } seen.runs(construct); construct.chars().for_each(|c| seen.plain(c)); Some(self.copy(rest, len)) } fn copy(&mut self, rest: &str, len: usize) -> usize { self.text.push_str(rest.get(..len).unwrap_or_default()); len } fn image(&mut self, rest: &str, len: usize, seen: &Seen) -> usize { self.wrap( rest.get(..len).unwrap_or_default(), rest.get(len..).unwrap_or_default(), &seen.closers, ); len } fn link(&mut self, rest: &str, len: usize, seen: &Seen) -> usize { self.links = true; self.image(rest, len, seen) } } /// `rest` starts with a backslash: `Some(2)` if the next character is one of `ESCAPABLE`, else /// `None` (the backslash is plain). fn escape_len(rest: &str) -> Option { todo!() } /// `rest` starts with a run of `run` backticks. The span's length, `run + close + run`, if both /// parsers read it as code: `run` is not in `seen.hazards`; `closing(after, run)` gives `close` /// in the text `after` the run; and the content between holds no line end (`BREAKS`), no `|`, /// none of `seen.closers`, and no run (`runs`) whose length is in `seen.hazards`. fn span_len(rest: &str, seen: &Seen) -> Option { todo!() } /// The byte offset in `text` of the first run of exactly `run` backticks, if there is one. fn closing(text: &str, run: usize) -> Option { let mut start = 0; let mut len = 0; for (i, c) in text.char_indices() { if c == '`' { if len == 0 { start = i; } len += 1; } else if len > 0 { if len == run { return Some(start); } len = 0; } } (len == run).then_some(start) } /// At most the first `WINDOW` characters of `text`. fn window(text: &str) -> &str { match text.char_indices().nth(WINDOW) { Some((end, _)) => text.get(..end).unwrap_or(text), None => text, } } /// `text` starts with `[`; only `window(text)` is looked at. The label ends at /// `bracket_end(text, true)`, or if that is `None`, `bracket_end(text, false)`. Right after it: a /// `(` closed by `paren_end` gives the length through that `)`; or else, after white space /// (`trim_start`), a `[` closed by `reference_end` gives it through that `]`; or else, only when /// `bare` (an image may be `![alt]`), through the label's `]`. `None` if nothing fits. fn construct_len(text: &str, bare: bool) -> Option { todo!() } /// `text` starts with `[`: the byte offset of the `]` that closes it. Walk it counting `[` up and /// `]` down (to 0: found), skipping the character after each backslash. When `code`, a backtick /// run jumps past the next run of the same length (`closing`), as a CommonMark code span; a run /// with none is skipped as plain. `None` if no `]` closes it. fn bracket_end(text: &str, code: bool) -> Option { todo!() } /// `text` starts with `(`: the byte offset of the `)` that closes it, counting `(` up and `)` down /// (to 0: found). A backslash escapes nothing here, as in marked's link destinations. `None` if /// no `)` closes it. fn paren_end(text: &str) -> Option { todo!() } /// `text` starts with `[`: the byte index of the next `]`, skipping the character after each /// backslash, if no `[` comes first (a reference label does not nest). `None` otherwise, and when /// `text` does not start with `[`. fn reference_end(text: &str) -> Option { let mut chars = text.strip_prefix('[')?.char_indices(); while let Some((i, c)) = chars.next() { match c { '\\' => { chars.next(); } '[' => return None, ']' => return Some(i + 1), _ => {} } } None } /// `rest` starts with `<`: the length of the autolink it starts, `<` through the first `>` in /// `window(rest)`, if the text between is not empty, holds no space, tab or no-break space (a line /// end is allowed, as in marked), and holds `:` or `@` or starts with `www` in any case. fn autolink_len(rest: &str) -> Option { todo!() } /// Whether the line `rest` starts (up to the first of `BREAKS`) is a link reference definition: /// after white space (`trim_start`), `[`, then a label of at least one character with no `]` in /// it, then `]:`. fn definition(rest: &str) -> bool { todo!() } /// Whether a URL starts at `rest`, after the plain character `prev`: a `scheme` or `www` where /// `prev` is not an ASCII letter or digit, or a `scheme_less` domain where `prev` is not a /// letter, digit, `.` or `-`. `prev` is `None` at the start and after a token. fn url_start(rest: &str, prev: Option) -> bool { let word = prev.is_none_or(|p| !p.is_ascii_alphanumeric()); let run = prev.is_none_or(|p| !(p.is_alphanumeric() || p == '.' || p == '-')); (word && (scheme(rest) || www(rest))) || (run && scheme_less(rest)) } /// Whether `rest` starts with a scheme: an ASCII letter, then ASCII letters, digits, `+`, `.` /// or `-`, then `:` and a `/`, `%`, or letter or digit of any script. fn scheme(rest: &str) -> bool { rest.starts_with(|c: char| c.is_ascii_alphabetic()) && rest .trim_start_matches(|c: char| c.is_ascii_alphanumeric() || matches!(c, '+' | '.' | '-')) .strip_prefix(':') .and_then(|after| after.chars().next()) .is_some_and(|c| c == '/' || c == '%' || c.is_alphanumeric()) } /// Whether `rest` starts with `www` in any case, then ASCII digits (none or more), then `.` and a /// character that is not white space. fn www(rest: &str) -> bool { rest.get(..3).is_some_and(|w| w.eq_ignore_ascii_case("www")) && rest .get(3..) .unwrap_or_default() .trim_start_matches(|c: char| c.is_ascii_digit()) .strip_prefix('.') .and_then(|after| after.chars().next()) .is_some_and(|c| !c.is_whitespace()) } /// Whether `rest` starts with a domain and a `/` (marked links `example.com/x` bare): a run of /// letters, digits, `.` and `-`, whose part after its last `.` is 2 to 4 letters with something /// before it, then `/`. fn scheme_less(rest: &str) -> bool { let end = rest .find(|c: char| !(c.is_alphanumeric() || c == '.' || c == '-')) .unwrap_or(rest.len()); let (Some(host), Some(after)) = (rest.get(..end), rest.get(end..)) else { return false; }; let Some((name, tld)) = host.rsplit_once('.') else { return false; }; after.starts_with('/') && !name.is_empty() && (2..=4).contains(&tld.chars().count()) && tld.chars().all(char::is_alphabetic) } /// The length of the URL `rest` starts with: up to white space, a `)` that closes no `(` of the /// URL (count them), or a `<` before white space, `(`, `)`, `<`, `>` or the end (marked leaves it /// out: it may start an autolink); unless `whole`, up to any `<`, `>`, `|` or backtick too. Then /// less the characters of `TRAILING` at its end. `whole` is the most marked may read as the URL. fn url_len(rest: &str, whole: bool) -> usize { todo!() }