markdown_it/plugins/cmark/block/
reference.rs1use std::collections::HashMap;
11use std::fmt::Debug;
12
13use derive_more::{Deref, DerefMut};
14use downcast_rs::{impl_downcast, Downcast};
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(
137 self.0
138 .iter()
139 .map(|(a, b)| (a.label.as_str(), b.destination.as_str(), b.title.as_deref())),
140 )
141 }
142}
143
144impl CustomReferenceMap for DefaultReferenceMap {
145 fn insert(&mut self, label: String, destination: String, title: Option<String>) -> bool {
146 let Some(key) = ReferenceMapKey::new(label) else {
147 return false;
148 };
149 self.0
150 .entry(key)
151 .or_insert(ReferenceMapEntry::new(destination, title));
152 true
153 }
154
155 fn get(&self, label: &str) -> Option<(&str, Option<&str>)> {
156 let key = ReferenceMapKey::new(label.to_owned())?;
157 self.0
158 .get(&key)
159 .map(|r| (r.destination.as_str(), r.title.as_deref()))
160 }
161}
162
163#[derive(Debug, Default)]
164struct ReferenceMapKey {
166 pub label: String,
167 normalized: String,
168}
169
170impl PartialEq for ReferenceMapKey {
171 fn eq(&self, other: &Self) -> bool {
172 self.normalized == other.normalized
173 }
174}
175
176impl Eq for ReferenceMapKey {}
177
178impl std::hash::Hash for ReferenceMapKey {
179 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
180 self.normalized.hash(state)
181 }
182}
183
184impl ReferenceMapKey {
185 pub fn new(label: String) -> Option<Self> {
186 let normalized = normalize_reference(&label);
187
188 if normalized.is_empty() {
189 return None;
191 }
192
193 Some(Self { label, normalized })
194 }
195}
196
197#[derive(Debug, Default)]
198struct ReferenceMapEntry {
200 pub destination: String,
201 pub title: Option<String>,
202}
203
204impl ReferenceMapEntry {
205 pub fn new(destination: String, title: Option<String>) -> Self {
206 Self { destination, title }
207 }
208}
209
210pub fn add(md: &mut MarkdownIt) {
212 md.block.add_rule::<ReferenceScanner>();
213}
214
215#[derive(Debug)]
216pub struct Definition {
217 pub label: String,
218 pub destination: String,
219 pub title: Option<String>,
220}
221impl NodeValue for Definition {
222 fn render(&self, _: &Node, _: &mut dyn crate::Renderer) {}
223}
224
225#[doc(hidden)]
226pub struct ReferenceScanner;
227impl BlockRule for ReferenceScanner {
228 fn check(_: &mut BlockState) -> Option<()> {
229 None }
231
232 fn run(state: &mut BlockState) -> Option<(Node, usize)> {
233 if state.line_indent(state.line) >= state.md.max_indent {
234 return None;
235 }
236
237 let mut chars = state.get_line(state.line).chars();
238
239 let Some('[') = chars.next() else {
240 return None;
241 };
242
243 loop {
246 match chars.next() {
247 Some('\\') => {
248 chars.next();
249 }
250 Some(']') => {
251 if let Some(':') = chars.next() {
252 break;
253 } else {
254 return None;
255 }
256 }
257 Some(_) => {}
258 None => break,
259 }
260 }
261
262 let start_line = state.line;
263 let mut next_line = start_line;
264
265 'outer: loop {
267 next_line += 1;
268
269 if next_line >= state.line_max || state.is_empty(next_line) {
270 break;
271 }
272
273 if state.line_indent(next_line) >= state.md.max_indent {
276 continue;
277 }
278
279 if state.line_offsets[next_line].indent_nonspace < 0 {
281 continue;
282 }
283
284 let old_state_line = state.line;
286 state.line = next_line;
287 if state.test_rules_at_line() {
288 state.line = old_state_line;
289 break 'outer;
290 }
291 state.line = old_state_line;
292 }
293
294 let (str_before_trim, _) = state.get_lines(start_line, next_line, state.blk_indent, false);
295 let str = str_before_trim.trim();
296 let mut chars = str.char_indices();
297 chars.next(); let label_end;
299 let mut lines = 0;
300
301 loop {
302 match chars.next() {
303 Some((_, '[')) => return None,
304 Some((p, ']')) => {
305 label_end = p;
306 break;
307 }
308 Some((_, '\n')) => lines += 1,
309 Some((_, '\\')) => {
310 if let Some((_, '\n')) = chars.next() {
311 lines += 1;
312 }
313 }
314 Some(_) => {}
315 None => return None,
316 }
317 }
318
319 let Some((_, ':')) = chars.next() else {
320 return None;
321 };
322
323 let mut pos = label_end + 2;
326 while let Some((_, ch @ (' ' | '\t' | '\n'))) = chars.next() {
327 if ch == '\n' {
328 lines += 1;
329 }
330 pos += 1;
331 }
332
333 let href;
336 if let Some(res) = full_link::parse_link_destination(str, pos, str.len()) {
337 if pos == res.pos {
338 return None;
339 }
340 href = state.md.link_formatter.normalize_link(&res.str);
341 state.md.link_formatter.validate_link(&href)?;
342 pos = res.pos;
343 lines += res.lines;
344 } else {
345 return None;
346 }
347
348 let dest_end_pos = pos;
350 let dest_end_lines = lines;
351
352 let start = pos;
355 let mut chars = str[pos..].chars();
356 while let Some(ch @ (' ' | '\t' | '\n')) = chars.next() {
357 if ch == '\n' {
358 lines += 1;
359 }
360 pos += 1;
361 }
362
363 let mut title = None;
366 if pos != start {
367 if let Some(res) = full_link::parse_link_title(str, pos, str.len()) {
368 title = Some(res.str);
369 pos = res.pos;
370 lines += res.lines;
371 } else {
372 pos = dest_end_pos;
373 lines = dest_end_lines;
374 }
375 }
376
377 let mut chars = str[pos..].chars();
379 loop {
380 match chars.next() {
381 Some(' ' | '\t') => {} Some('\n') | None => break,
383 Some(_) if title.is_some() => {
384 title = None;
387 pos = dest_end_pos;
388 lines = dest_end_lines;
389 chars = str[pos..].chars();
390 }
391 Some(_) => {
392 return None;
394 }
395 }
396 }
397
398 let references = state.root_ext.get_or_insert_default::<ReferenceMap>();
399 if !references.insert(str[1..label_end].to_owned(), href.clone(), title.clone()) {
400 return None;
401 }
402
403 Some((
404 Node::new(Definition {
405 label: str[1..label_end].to_owned(),
406 destination: href,
407 title,
408 }),
409 lines + 1,
410 ))
411 }
412}