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