markdown_linkify/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use link_aggregator::LinkTools;
4pub use transform::*;
5
6use pulldown_cmark::{BrokenLink, CowStr, Event, Options, Parser};
7
8use crate::aggregation::Aggregation;
9
10pub mod aggregation;
11pub mod link;
12pub mod link_aggregator;
13mod transform;
14
15pub fn broken_link_callback_with_replacers<'a>(
16    replacers: Vec<Box<dyn LinkTransformer>>,
17) -> impl Fn(BrokenLink<'a>) -> Option<(CowStr<'a>, CowStr<'a>)> {
18    move |link: BrokenLink<'a>| {
19        for replacer in &replacers {
20            if replacer.pattern().is_match(&link.reference) {
21                let mut link = link::Link {
22                    link_type: link.link_type,
23                    destination: link.reference,
24                    title: "".into(),
25                    text: vec![],
26                };
27                replacer.apply(&mut link).unwrap();
28                return Some((link.destination, link.title));
29            }
30        }
31        None
32    }
33}
34
35pub fn process_broken_links<'a>(
36    input: &'a str,
37    replacers: Vec<Box<dyn LinkTransformer>>,
38    cb: &'a mut impl Fn(BrokenLink<'a>) -> Option<(CowStr<'a>, CowStr<'a>)>,
39) -> impl Iterator<Item = Event<'a>> {
40    // Add missing references via the broken link callback (the replacers are registered there).
41    let parser = Parser::new_with_broken_link_callback(input, Options::empty(), Some(cb));
42
43    // transform `rust:Vec` to `Vec` (remove the tag text, if present).
44    parser.aggregate_links().flat_map(move |mut aggregation| {
45        // return links untouched.
46        let Aggregation::Link(ref mut link) = aggregation else {
47            return aggregation;
48        };
49        // return empty text and code events untouched.
50        let Some(Event::Text(first)) = link.text.get_mut(0) else {
51            return aggregation;
52        };
53        // turn broken links into full links
54        for replacer in &replacers {
55            // remove initial replacer tag (such as `rust:`)
56            // TODO why `continue`?
57            if replacer.strip_tag() {
58                if first.starts_with(&replacer.tag()) {
59                    let new_text = first.replace(&replacer.tag(), "");
60                    *first = new_text.into();
61                    return Aggregation::Link(link.clone());
62                }
63            }
64        }
65        aggregation
66    })
67}
68
69pub fn process_links<'a>(
70    input: impl Iterator<Item = Event<'a>>,
71    replacers: &'a [Box<dyn LinkTransformer>],
72) -> impl Iterator<Item = Event<'a>> {
73    input
74        .aggregate_links()
75        .flat_map(move |aggregation| {
76            let Aggregation::Link(mut link) = aggregation else {
77                return anyhow::Ok(aggregation);
78            };
79            for replacement in replacers {
80                if replacement.pattern().is_match(&link.destination) {
81                    replacement.apply(&mut link)?;
82                    break;
83                }
84            }
85            Ok(Aggregation::Link(link))
86        })
87        .flatten()
88}