| 1 | //! Structure similar to `` `code span` `` with configurable markers of variable length. |
| 2 | //! |
| 3 | //! It allows you to define a custom structure with variable number of markers |
| 4 | //! (e.g. with `%` defined as a marker, user can write `%foo%` or `%%%foo%%%` |
| 5 | //! resulting in the same node). |
| 6 | //! |
| 7 | //! You add a custom structure by using [add_with] function, which takes following arguments: |
| 8 | //! - `MARKER` - marker character |
| 9 | //! - `md` - parser instance |
| 10 | //! - `f` - function that should return your custom [Node] |
| 11 | //! |
| 12 | //! Here is an example of a rule turning `%foo%` into `🦀foo🦀`: |
| 13 | //! |
| 14 | //! ```rust |
| 15 | //! use markdown_it::generics::inline::code_pair; |
| 16 | //! use markdown_it::{MarkdownIt, Node, NodeValue, Renderer}; |
| 17 | //! |
| 18 | //! #[derive(Debug)] |
| 19 | //! struct Ferris; |
| 20 | //! impl NodeValue for Ferris { |
| 21 | //! fn render(&self, node: &Node, fmt: &mut dyn Renderer) { |
| 22 | //! fmt.text("🦀"); |
| 23 | //! fmt.contents(&node.children); |
| 24 | //! fmt.text("🦀"); |
| 25 | //! } |
| 26 | //! } |
| 27 | //! |
| 28 | //! let md = &mut MarkdownIt::new(); |
| 29 | //! code_pair::add_with::<'%'>(md, |_| Node::new(Ferris)); |
| 30 | //! let html = md.parse("hello %world%").render(); |
| 31 | //! assert_eq!(html.trim(), "hello 🦀world🦀"); |
| 32 | //! ``` |
| 33 | //! |
| 34 | //! This generic structure follows exact rules of code span in CommonMark: |
| 35 | //! |
| 36 | //! 1. Literal marker character sequence can be used inside of structure if its length |
| 37 | //! doesn't match length of the opening/closing sequence (e.g. with `%` defined |
| 38 | //! as a marker, `%%foo%bar%%` gets parsed as `Node("foo%bar")`). |
| 39 | //! |
| 40 | //! 2. Single space inside is trimmed to allow you to write `% %%foo %` to be parsed as |
| 41 | //! `Node("%%foo")`. |
| 42 | //! |
| 43 | //! If you define two structures with the same marker, only the first one will work. |
| 44 | //! |
| 45 | use crate::parser::extset::{InlineRootExt, MarkdownItExt}; |
| 46 | use crate::parser::inline::{InlineRule, InlineState, Text}; |
| 47 | use crate::{MarkdownIt, Node}; |
| 48 | |
| 49 | #[derive(Debug, Default)] |
| 50 | struct CodePairCache<const MARKER: char> { |
| 51 | scanned: bool, |
| 52 | max: Vec<usize>, |
| 53 | } |
| 54 | impl<const MARKER: char> InlineRootExt for CodePairCache<MARKER> {} |
| 55 | |
| 56 | #[derive(Debug)] |
| 57 | struct CodePairConfig<const MARKER: char>(fn (usize) -> Node); |
| 58 | impl<const MARKER: char> MarkdownItExt for CodePairConfig<MARKER> {} |
| 59 | |
| 60 | pub fn add_with<const MARKER: char>(md: &mut MarkdownIt, f: fn (length: usize) -> Node) { |
| 61 | md.ext.insert(CodePairConfig::<MARKER>(f)); |
| 62 | |
| 63 | md.inline.add_rule::<CodePairScanner<MARKER>>(); |
| 64 | } |
| 65 | |
| 66 | #[doc(hidden)] |
| 67 | pub struct CodePairScanner<const MARKER: char>; |
| 68 | impl<const MARKER: char> InlineRule for CodePairScanner<MARKER> { |
| 69 | const MARKER: char = MARKER; |
| 70 | |
| 71 | fn run(state: &mut InlineState) -> Option<(Node, usize)> { |
| 72 | let mut chars = state.src[state.pos..state.pos_max].chars(); |
| 73 | if chars.next().unwrap() != MARKER { return None; } |
| 74 | if state.trailing_text_get().ends_with(MARKER) { return None; } |
| 75 | |
| 76 | let mut pos = state.pos + 1; |
| 77 | |
| 78 | // scan marker length |
| 79 | while Some(MARKER) == chars.next() { |
| 80 | pos += 1; |
| 81 | } |
| 82 | |
| 83 | // backtick length => last seen position |
| 84 | let backticks = state.inline_ext.get_or_insert_default::<CodePairCache<MARKER>>(); |
| 85 | let opener_len = pos - state.pos; |
| 86 | |
| 87 | if backticks.scanned && backticks.max.get(opener_len).copied().unwrap_or(0) <= state.pos { |
| 88 | // performance note: adding entire sequence into pending is 5x faster, |
| 89 | // but it will interfere with other rules working on the same char; |
| 90 | // and it is extremely rare that user would put a thousand "`" in text |
| 91 | return None; |
| 92 | } |
| 93 | |
| 94 | let mut match_start; |
| 95 | let mut match_end = pos; |
| 96 | |
| 97 | // Nothing found in the cache, scan until the end of the line (or until marker is found) |
| 98 | while let Some(p) = state.src[match_end..state.pos_max].find(MARKER) { |
| 99 | match_start = match_end + p; |
| 100 | |
| 101 | // scan marker length |
| 102 | match_end = match_start + 1; |
| 103 | chars = state.src[match_end..state.pos_max].chars(); |
| 104 | |
| 105 | while Some(MARKER) == chars.next() { |
| 106 | match_end += 1; |
| 107 | } |
| 108 | |
| 109 | let closer_len = match_end - match_start; |
| 110 | |
| 111 | if closer_len == opener_len { |
| 112 | // Found matching closer length. |
| 113 | let mut content = state.src[pos..match_start].to_owned().replace('\n', " "); |
| 114 | if content.starts_with(' ') && content.ends_with(' ') && content.len() > 2 { |
| 115 | content[1..content.len() - 1].to_owned().clone_into(&mut content); |
| 116 | pos += 1; |
| 117 | match_start -= 1; |
| 118 | } |
| 119 | |
| 120 | let f = state.md.ext.get::<CodePairConfig<MARKER>>().unwrap().0; |
| 121 | let mut node = f(opener_len); |
| 122 | |
| 123 | let mut inner_node = Node::new(Text { content }); |
| 124 | inner_node.srcmap = state.get_map(pos, match_start); |
| 125 | node.children.push(inner_node); |
| 126 | |
| 127 | return Some((node, match_end - state.pos)); |
| 128 | } |
| 129 | |
| 130 | // Some different length found, put it in cache as upper limit of where closer can be found |
| 131 | let backticks = state.inline_ext.get_mut::<CodePairCache<MARKER>>().unwrap(); |
| 132 | while backticks.max.len() <= closer_len { backticks.max.push(0); } |
| 133 | backticks.max[closer_len] = match_start; |
| 134 | } |
| 135 | |
| 136 | // Scanned through the end, didn't find anything |
| 137 | let backticks = state.inline_ext.get_mut::<CodePairCache<MARKER>>().unwrap(); |
| 138 | backticks.scanned = true; |
| 139 | |
| 140 | None |
| 141 | } |
| 142 | } |