markdown_it/generics/inline/code_pair.rs
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//!
45use crate::parser::extset::{InlineRootExt, MarkdownItExt};
46use crate::parser::inline::{InlineRule, InlineState, Text};
47use crate::{MarkdownIt, Node};
48
49#[derive(Debug, Default)]
50struct CodePairCache<const MARKER: char> {
51 scanned: bool,
52 max: Vec<usize>,
53}
54impl<const MARKER: char> InlineRootExt for CodePairCache<MARKER> {}
55
56#[derive(Debug)]
57struct CodePairConfig<const MARKER: char>(fn (usize) -> Node);
58impl<const MARKER: char> MarkdownItExt for CodePairConfig<MARKER> {}
59
60pub 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)]
67pub struct CodePairScanner<const MARKER: char>;
68impl<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}