markdown_it/plugins/cmark/block/
reference.rs1use derivative::Derivative;
11use derive_more::{Deref, DerefMut};
12use downcast_rs::{impl_downcast, Downcast};
13use std::collections::HashMap;
14use std::fmt::Debug;
15
16use crate::common::utils::normalize_reference;
17use crate::generics::inline::full_link;
18use crate::parser::block::{BlockRule, BlockState};
19use crate::parser::extset::RootExt;
20use crate::{MarkdownIt, Node, NodeValue};
21
22#[derive(Debug, Deref, DerefMut)]
99#[deref(forward)]
100#[deref_mut(forward)]
101pub struct ReferenceMap(Box<dyn CustomReferenceMap>);
102
103impl ReferenceMap {
104 pub fn new(custom_map: impl CustomReferenceMap + 'static) -> Self {
105 Self(Box::new(custom_map))
106 }
107}
108
109impl Default for ReferenceMap {
110 fn default() -> Self {
111 Self::new(DefaultReferenceMap::new())
112 }
113}
114
115impl RootExt for ReferenceMap {}
116
117pub trait CustomReferenceMap : Debug + Downcast + Send + Sync {
118 fn insert(&mut self, label: String, destination: String, title: Option<String>) -> bool;
120
121 fn get(&self, label: &str) -> Option<(&str, Option<&str>)>;
123}
124
125impl_downcast!(CustomReferenceMap);
126
127#[derive(Default, Debug)]
128pub struct DefaultReferenceMap(HashMap<ReferenceMapKey, ReferenceMapEntry>);
129
130impl DefaultReferenceMap {
131 pub fn new() -> Self {
132 Self::default()
133 }
134
135 pub fn iter(&self) -> impl Iterator<Item = (&str, &str, Option<&str>)> {
136 Box::new(self.0.iter().map(|(a, b)| {
137 (a.label.as_str(), b.destination.as_str(), b.title.as_deref())
138 }))
139 }
140}
141
142impl CustomReferenceMap for DefaultReferenceMap {
143 fn insert(&mut self, label: String, destination: String, title: Option<String>) -> bool {
144 let Some(key) = ReferenceMapKey::new(label) else { return false; };
145 self.0.entry(key)
146 .or_insert(ReferenceMapEntry::new(destination, title));
147 true
148 }
149
150 fn get(&self, label: &str) -> Option<(&str, Option<&str>)> {
151 let key = ReferenceMapKey::new(label.to_owned())?;
152 self.0.get(&key)
153 .map(|r| (r.destination.as_str(), r.title.as_deref()))
154 }
155}
156
157#[derive(Derivative)]
158#[derivative(Debug, Default, Hash, PartialEq, Eq)]
159struct ReferenceMapKey {
161 #[derivative(PartialEq = "ignore")]
162 #[derivative(Hash = "ignore")]
163 pub label: String,
164 normalized: String,
165}
166
167impl ReferenceMapKey {
168 pub fn new(label: String) -> Option<Self> {
169 let normalized = normalize_reference(&label);
170
171 if normalized.is_empty() {
172 return None;
174 }
175
176 Some(Self { label, normalized })
177 }
178}
179
180#[derive(Debug, Default)]
181struct ReferenceMapEntry {
183 pub destination: String,
184 pub title: Option<String>,
185}
186
187impl ReferenceMapEntry {
188 pub fn new(destination: String, title: Option<String>) -> Self {
189 Self { destination, title }
190 }
191}
192
193pub fn add(md: &mut MarkdownIt) {
195 md.block.add_rule::<ReferenceScanner>();
196}
197
198#[derive(Debug)]
199pub struct Definition {
200 pub label: String,
201 pub destination: String,
202 pub title: Option<String>,
203}
204impl NodeValue for Definition {
205 fn render(&self, _: &Node, _: &mut dyn crate::Renderer) {}
206}
207
208#[doc(hidden)]
209pub struct ReferenceScanner;
210impl BlockRule for ReferenceScanner {
211 fn check(_: &mut BlockState) -> Option<()> {
212 None }
214
215 fn run(state: &mut BlockState) -> Option<(Node, usize)> {
216
217 if state.line_indent(state.line) >= state.md.max_indent { return None; }
218
219 let mut chars = state.get_line(state.line).chars();
220
221 let Some('[') = chars.next() else { return None; };
222
223 loop {
226 match chars.next() {
227 Some('\\') => { chars.next(); },
228 Some(']') => {
229 if let Some(':') = chars.next() {
230 break;
231 } else {
232 return None;
233 }
234 }
235 Some(_) => {},
236 None => break,
237 }
238 }
239
240 let start_line = state.line;
241 let mut next_line = start_line;
242
243 'outer: loop {
245 next_line += 1;
246
247 if next_line >= state.line_max || state.is_empty(next_line) { break; }
248
249 if state.line_indent(next_line) >= state.md.max_indent { continue; }
252
253 if state.line_offsets[next_line].indent_nonspace < 0 { continue; }
255
256 let old_state_line = state.line;
258 state.line = next_line;
259 if state.test_rules_at_line() {
260 state.line = old_state_line;
261 break 'outer;
262 }
263 state.line = old_state_line;
264 }
265
266 let (str_before_trim, _) = state.get_lines(start_line, next_line, state.blk_indent, false);
267 let str = str_before_trim.trim();
268 let mut chars = str.char_indices();
269 chars.next(); let label_end;
271 let mut lines = 0;
272
273 loop {
274 match chars.next() {
275 Some((_, '[')) => return None,
276 Some((p, ']')) => {
277 label_end = p;
278 break;
279 }
280 Some((_, '\n')) => lines += 1,
281 Some((_, '\\')) => {
282 if let Some((_, '\n')) = chars.next() {
283 lines += 1;
284 }
285 }
286 Some(_) => {},
287 None => return None,
288 }
289 }
290
291 let Some((_, ':')) = chars.next() else { return None; };
292
293 let mut pos = label_end + 2;
296 while let Some((_, ch @ (' ' | '\t' | '\n'))) = chars.next() {
297 if ch == '\n' { lines += 1; }
298 pos += 1;
299 }
300
301 let href;
304 if let Some(res) = full_link::parse_link_destination(str, pos, str.len()) {
305 if pos == res.pos { return None; }
306 href = state.md.link_formatter.normalize_link(&res.str);
307 state.md.link_formatter.validate_link(&href)?;
308 pos = res.pos;
309 lines += res.lines;
310 } else {
311 return None;
312 }
313
314 let dest_end_pos = pos;
316 let dest_end_lines = lines;
317
318 let start = pos;
321 let mut chars = str[pos..].chars();
322 while let Some(ch @ (' ' | '\t' | '\n')) = chars.next() {
323 if ch == '\n' { lines += 1; }
324 pos += 1;
325 }
326
327 let mut title = None;
330 if pos != start {
331 if let Some(res) = full_link::parse_link_title(str, pos, str.len()) {
332 title = Some(res.str);
333 pos = res.pos;
334 lines += res.lines;
335 } else {
336 pos = dest_end_pos;
337 lines = dest_end_lines;
338 }
339 }
340
341 let mut chars = str[pos..].chars();
343 loop {
344 match chars.next() {
345 Some(' ' | '\t') => pos += 1,
346 Some('\n') | None => break,
347 Some(_) if title.is_some() => {
348 title = None;
351 pos = dest_end_pos;
352 lines = dest_end_lines;
353 chars = str[pos..].chars();
354 }
355 Some(_) => {
356 return None;
358 }
359 }
360 }
361
362 let references = state.root_ext.get_or_insert_default::<ReferenceMap>();
363 if !references.insert(str[1..label_end].to_owned(), href.clone(), title.clone()) { return None; }
364
365 Some((Node::new(
366 Definition {
367 label: str[1..label_end].to_owned(),
368 destination: href,
369 title
370 }),
371 lines + 1
372 ))
373 }
374}