parse_hyperlinks/
iterator.rs

1//! Module providing an iterator over the hyperlinks found in the input text.  Consult the
2//! documentation of `parser::parse::take_link()` to see a list of supported markup languages. The
3//! iterator resolves link references.
4
5use crate::parser::parse::take_link;
6use crate::parser::Link;
7use std::borrow::Cow;
8use std::collections::HashMap;
9use std::mem::swap;
10
11#[derive(Debug, PartialEq)]
12/// A collection of `Link` objects grouped by link type.
13struct MarkupLinkCollection<'a> {
14    /// Vector storing all `Link::Text2Dest`, `Link::Text2Label` and `Link::TextLabel2Dest` links.
15    /// The tuple is defined as follows: `(link_first_byte_offset, link_len, Link)`.
16    text2dest_label: Vec<(usize, usize, Link<'a>)>,
17    /// Vector for `Link::Label2Label` links.
18    label2label: Vec<(Cow<'a, str>, Cow<'a, str>)>,
19    /// Vector for `Link::Label2Dest` and `Link::TextLabel2Dest` links.
20    /// The `HashMap`'s key is the `link_label` of the link, the value its
21    /// `(link_destination, link_title)`.
22    label2dest: HashMap<Cow<'a, str>, (Cow<'a, str>, Cow<'a, str>)>,
23}
24
25impl<'a> MarkupLinkCollection<'a> {
26    fn new() -> Self {
27        Self {
28            text2dest_label: Vec::new(),
29            label2label: Vec::new(),
30            label2dest: HashMap::new(),
31        }
32    }
33
34    /// Reads through the whole `Self::input` and extracts all hyperlinks and
35    /// stores them in `Self::HyperlinkCollection` according to their category.
36    /// One type is treated specially: `Link::TextLabel2Dest` are cloned and one
37    /// copy is stored in `HyperlinkCollection::Text2Dest` and the other copy is
38    /// stored in `HyperlinkCollection::Label2Dest`.
39    #[inline]
40    fn from(input: &'a str, render_label2dest: bool) -> Self {
41        let mut i = input;
42        let mut hc = MarkupLinkCollection::new();
43        let mut anonymous_text2label_counter = 0;
44        let mut anonymous_label2x_counter = 0;
45        // This index refers to `input`.
46        let mut input_idx = 0;
47
48        while let Ok((j, (skipped, res))) = take_link(i) {
49            match res {
50                // `Text2Dest` is stored without modification in `hc.text2dest_label`.
51                l if matches!(l, Link::Text2Dest { .. })
52                    || matches!(l, Link::Image2Dest { .. })
53                    || matches!(l, Link::Image { .. }) =>
54                {
55                    let link_offset = input_idx + skipped.len();
56                    let link_len = i.len() - j.len() - skipped.len();
57                    hc.text2dest_label.push((link_offset, link_len, l));
58                }
59
60                // `Text2label` is stored without modification in `hc.text2dest_label`.
61                Link::Text2Label(text, mut label) => {
62                    if label == "_" {
63                        anonymous_text2label_counter += 1;
64                        label = Cow::Owned(format!("_{}", anonymous_text2label_counter));
65                    }
66                    let link_offset = input_idx + skipped.len();
67                    let link_len = i.len() - j.len() - skipped.len();
68                    hc.text2dest_label
69                        .push((link_offset, link_len, Link::Text2Label(text, label)))
70                }
71                //`TextLabel2Dest` are cloned and stored in `hc.text2dest_label` as `Text2Dest`
72                // and in `hc.label2dest` (repacked in a `HashMap`).
73                Link::TextLabel2Dest(tl, d, t) => {
74                    let link_offset = input_idx + skipped.len();
75                    let link_len = i.len() - j.len() - skipped.len();
76                    hc.text2dest_label.push((
77                        link_offset,
78                        link_len,
79                        Link::Text2Dest(tl.clone(), d.clone(), t.clone()),
80                    ));
81
82                    // Silently ignore when overwriting a key that exists already.
83                    hc.label2dest.insert(tl, (d, t));
84                }
85
86                // `Label2Label` are unpacked and stored in `hc.label2label`.
87                Link::Label2Label(mut from, to) => {
88                    if from == "_" {
89                        anonymous_label2x_counter += 1;
90                        from = Cow::Owned(format!("_{}", anonymous_label2x_counter));
91                    }
92                    hc.label2label.push((from, to));
93                }
94
95                // `Label2Dest` are unpacked and stored as `HashMap` in `hc.label2dest`:
96                Link::Label2Dest(mut l, d, t) => {
97                    if l == "_" {
98                        anonymous_label2x_counter += 1;
99                        l = Cow::Owned(format!("_{}", anonymous_label2x_counter));
100                    }
101                    // Some want to have link reference definitions clickable
102                    // too. Strictly speaking they are not links, this is why
103                    // this is optional.
104                    if render_label2dest {
105                        let link_offset = input_idx + skipped.len();
106                        let link_len = i.len() - j.len() - skipped.len();
107                        hc.text2dest_label.push((
108                            link_offset,
109                            link_len,
110                            Link::Text2Dest(
111                                Cow::from(&input[link_offset..link_offset + link_len]),
112                                d.clone(),
113                                t.clone(),
114                            ),
115                        ));
116                    };
117
118                    // Silently ignore when overwriting a key that exists already.
119                    hc.label2dest.insert(l, (d, t));
120                }
121                _ => unreachable!(),
122            };
123
124            // Prepare next iteration.
125            input_idx += i.len() - j.len();
126            i = j;
127        }
128
129        hc
130    }
131
132    /// Takes one by one, one item from `HyperlinkCollection::label2label` and
133    /// searches the corresponding label in `HyperlinkCollection::label2dest`.
134    /// When found, add a new item to `HyperlinkCollection::label2dest`. Continue
135    /// until `HyperlinkCollection::label2label` is empty or no more corresponding
136    /// items can be associated.
137    #[inline]
138    fn resolve_label2label_references(&mut self) {
139        let mut nb_no_match = 0;
140        let mut idx = 0;
141        while !self.label2label.is_empty() && nb_no_match < self.label2label.len() {
142            let (key_alias, key) = &self.label2label[idx];
143            // This makes sure, that we advance in the loop.
144            if let Some(value) = self.label2dest.get(key) {
145                let found_new_key = key_alias.clone();
146                let found_value = value.clone();
147                // We advance in the loop, because we remove the element `idx` points to.
148                self.label2label.remove(idx);
149                self.label2dest.insert(found_new_key, found_value);
150                // We give up only, after a complete round without match.
151                nb_no_match = 0;
152            } else {
153                // We advance in the loop because we increment `idx`.
154                idx += 1;
155                nb_no_match += 1;
156            };
157            // Make sure, that `idx` always points to some valid index.
158            if idx >= self.label2label.len() {
159                idx = 0;
160            }
161        }
162    }
163
164    /// Takes one by one, one item of type `Link::Text2Label` from
165    /// `HyperlinkCollection::text2text_label` and searches the corresponding
166    /// label in `HyperlinkCollection::label2dest`. The associated
167    /// `Link::Text2Label` and `Link::Label2Dest` are resolved into a new
168    /// `Link::Text2Dest` object. Then the item form the fist list is replaced by
169    /// this new object. After this operation the
170    /// `HyperlinkCollection::text2text_label` list contains only
171    /// `Link::Text2Dest` and `Link::Image2Dest` objects (and some unresolvable
172    /// `Link::Text2Label` objects).
173    #[inline]
174    fn resolve_text2label_references(&mut self) {
175        let mut idx = 0;
176        while idx < self.text2dest_label.len() {
177            // If we can not resolve the label, we just skip it.
178            if let (input_offset, len, Link::Text2Label(text, label)) = &self.text2dest_label[idx] {
179                if let Some((dest, title)) = &self.label2dest.get(label) {
180                    let new_link = if text.is_empty() {
181                        (
182                            *input_offset,
183                            *len,
184                            Link::Text2Dest(dest.clone(), dest.clone(), title.clone()),
185                        )
186                    } else {
187                        (
188                            *input_offset,
189                            *len,
190                            Link::Text2Dest(text.clone(), dest.clone(), title.clone()),
191                        )
192                    };
193                    self.text2dest_label[idx] = new_link;
194                };
195            };
196            // We advance in the loop because we increment `idx`.
197            idx += 1;
198        }
199    }
200}
201
202#[derive(Debug, PartialEq)]
203/// The interator's state.
204enum Status<'a> {
205    /// Initial state. Iterator is not started.
206    Init,
207    /// So far only `Text2Dest` and `Image2Dest` links are coming, no links
208    /// need to be resolved.
209    DirectSearch(&'a str),
210    /// As soon as the first reference appears, the remaining text is read and
211    /// all links are resolved. The tuple describes a resolved link. The first
212    /// integer index points to the first byte of the link in `self.input`, the
213    /// second interger is the lenght of the link in `input` bytes. Then follows
214    /// the `Link`.
215    ResolvedLinks(Vec<(usize, usize, Link<'a>)>),
216    /// All links have been returned. From now on only `None` are returned.
217    End,
218}
219
220#[derive(Debug, PartialEq)]
221/// Iterator over all the hyperlinks in the `input` text.
222/// This struct holds the iterator's state and an advancing pointer into the
223/// `input` text. The iterator's `next()` method returns a tuple with a tuples
224/// inside: `Some(((input_split), Link))`.
225///
226/// Each tuple has the following parts:
227/// * `input_split = (skipped_characters, consumed_characters, remaining_characters)`
228/// * `Link` is `Link::Text2Dest`, `Link::Image2Dest`, or `Link::Image`.
229///
230/// # Input split
231///
232/// ```
233/// use parse_hyperlinks::iterator::MarkupLink;
234/// use std::borrow::Cow;
235///
236/// let i = "abc[text0](dest0)efg[text1](dest1)hij<foo@dest2>klm";
237///
238/// let mut iter = MarkupLink::new(i, false);
239/// assert_eq!(iter.next().unwrap().0, ("abc",
240///                                     "[text0](dest0)",
241///                                     "efg[text1](dest1)hij<foo@dest2>klm"));
242/// assert_eq!(iter.next().unwrap().0, ("efg",
243///                                     "[text1](dest1)",
244///                                      "hij<foo@dest2>klm"));
245/// assert_eq!(iter.next().unwrap().0, ("hij", "<foo@dest2>", "klm"));
246/// assert_eq!(iter.next(), None);
247/// ```
248/// # Link content
249/// ## Markdown
250/// ```
251/// use parse_hyperlinks::parser::Link;
252/// use parse_hyperlinks::iterator::MarkupLink;
253/// use std::borrow::Cow;
254///
255/// let i = r#"abc[text0](dest0 "title0")abc
256/// abc[text1][label1]abc
257/// abc[text2](dest2 "title2")
258/// [text3]: dest3 "title3"
259/// [label1]: dest1 "title1"
260/// abc[text3]abc
261/// "#;
262///
263/// let mut iter = MarkupLink::new(i, false);
264/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text0"), Cow::from("dest0"), Cow::from("title0")));
265/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text1"), Cow::from("dest1"), Cow::from("title1")));
266/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text2"), Cow::from("dest2"), Cow::from("title2")));
267/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text3"), Cow::from("dest3"), Cow::from("title3")));
268/// assert_eq!(iter.next(), None);
269/// ```
270///
271/// ## reStructuredText
272///
273/// ```
274/// use parse_hyperlinks::parser::Link;
275/// use parse_hyperlinks::iterator::MarkupLink;
276/// use std::borrow::Cow;
277///
278/// let i = r#"
279/// abc `text1 <label1_>`_abc
280/// abc text2_ abc
281/// abc text3__ abc
282/// abc text_label4_ abc
283/// abc text5__ abc
284/// .. _label1: dest1
285/// .. _text2: dest2
286/// .. __: dest3
287/// __ dest5
288/// "#;
289///
290/// let mut iter = MarkupLink::new(i, false);
291/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text1"), Cow::from("dest1"), Cow::from("")));
292/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text2"), Cow::from("dest2"), Cow::from("")));
293/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text3"), Cow::from("dest3"), Cow::from("")));
294/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text5"), Cow::from("dest5"), Cow::from("")));
295/// assert_eq!(iter.next(), None);
296///
297/// ```
298/// ## Asciidoc
299///
300/// ```
301/// use parse_hyperlinks::parser::Link;
302/// use parse_hyperlinks::iterator::MarkupLink;
303/// use std::borrow::Cow;
304///
305/// let i = r#"abc
306/// abc https://dest0[text0]abc
307/// abc link:https://dest1[text1]abc
308/// abc {label2}[text2]abc
309/// abc {label3}abc
310/// :label2: https://dest2
311/// :label3: https://dest3
312/// "#;
313///
314/// let mut iter = MarkupLink::new(i, false);
315/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text0"), Cow::from("https://dest0"), Cow::from("")));
316/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text1"), Cow::from("https://dest1"), Cow::from("")));
317/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text2"), Cow::from("https://dest2"), Cow::from("")));
318/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("https://dest3"), Cow::from("https://dest3"), Cow::from("")));
319/// assert_eq!(iter.next(), None);
320/// ```
321///
322/// # HTML
323///
324/// ```
325/// use parse_hyperlinks::parser::Link;
326/// use parse_hyperlinks::iterator::MarkupLink;
327/// use std::borrow::Cow;
328///
329/// let i = r#"abc<a href="dest1" title="title1">text1</a>abc
330/// abc<a href="dest2" title="title2">text2</a>abc
331/// abc<a href="dest3" title="title3">cde<img alt="alt3" src="src3"/>fgh</a>abc
332/// "#;
333///
334/// let mut iter = MarkupLink::new(i, false);
335/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text1"), Cow::from("dest1"), Cow::from("title1")));
336/// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text2"), Cow::from("dest2"), Cow::from("title2")));
337/// assert_eq!(iter.next().unwrap().1, Link::Image2Dest(Cow::from("cde"), Cow::from("alt3"), Cow::from("src3"), Cow::from("fgh"), Cow::from("dest3"), Cow::from("title3")));
338/// assert_eq!(iter.next(), None);
339/// ```
340pub struct MarkupLink<'a> {
341    /// The remaining text input.
342    input: &'a str,
343    /// Status of the `MarkupLink` state machine.
344    status: Status<'a>,
345    /// Index where the last output started.
346    last_output_offset: usize,
347    /// Length of the last output.
348    last_output_len: usize,
349    /// By default, `Label2Dest` link reference definitions are not rendered. If
350    /// `render_label` is true, then `Label2Dest` is rendered like an inline
351    /// link: with the full link reference definition's source as _link text_ and
352    /// the definition's destination as _link destination_.
353    render_label: bool,
354}
355
356/// Constructor for the `MarkupLink` struct.
357impl<'a> MarkupLink<'a> {
358    /// Constructor for the iterator. `input` is the text with hyperlinks to be
359    /// extracted.
360    ///
361    /// # Optional rendering of Label2Dest link reference definitions
362    ///
363    /// By default `Label2Dest` link reference definitions are not rendered:
364    ///
365    /// ```
366    /// use parse_hyperlinks::parser::Link;
367    /// use parse_hyperlinks::iterator::MarkupLink;
368    /// use std::borrow::Cow;
369    ///
370    /// let i = r#"abc[text1][label1]abc
371    /// [label1]: dest1 "title1"
372    /// "#;
373    ///
374    /// let mut iter = MarkupLink::new(i, false);
375    /// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text1"), Cow::from("dest1"), Cow::from("title1")));
376    /// assert_eq!(iter.next(), None);
377    /// ```
378    ///
379    /// If the internal variable `render_label` is true, then `Label2Dest` link
380    /// reference definitions are rendered like inline links: the full
381    /// `Label2Dest` link reference definition's source will appear as _link
382    /// text_ and its destination as _link destination_. To enable this feature,
383    /// construct `MarkupLink` with the second positional parameter set to `true`,
384    /// e.g. `MarkupLink::new(i, true)`.
385    ///
386    /// ```
387    /// use parse_hyperlinks::parser::Link;
388    /// use parse_hyperlinks::iterator::MarkupLink;
389    /// use std::borrow::Cow;
390    ///
391    /// let i = r#"abc[text1][label1]abc
392    /// [label1]: dest1 "title1"
393    /// "#;
394    ///
395    /// let mut iter = MarkupLink::new(i, true);
396    /// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("text1"), Cow::from("dest1"), Cow::from("title1")));
397    /// assert_eq!(iter.next().unwrap().1, Link::Text2Dest(Cow::from("[label1]: dest1 \"title1\""), Cow::from("dest1"), Cow::from("title1")));
398    /// assert_eq!(iter.next(), None);
399    /// ```
400    ///
401    #[inline]
402    pub fn new(input: &'a str, render_label: bool) -> Self {
403        Self {
404            input,
405            status: Status::Init,
406            last_output_offset: 0,
407            last_output_len: 0,
408            render_label,
409        }
410    }
411}
412
413/// Iterator over the hyperlinks (with markup) in the `input`-text.
414/// The iterator's `next()` method returns a tuple with 2 tuples inside:
415/// * `Some(((input_split)(link_content)))`
416///
417/// Each tuple has the following parts:
418/// * `input_split = (skipped_characters, consumed_characters, remaining_characters)`
419/// * `link_content = (link_text, link_destination, link_title)`
420///
421impl<'a> Iterator for MarkupLink<'a> {
422    #[allow(clippy::type_complexity)]
423    type Item = ((&'a str, &'a str, &'a str), Link<'a>);
424    /// The iterator operates in 2 modes:
425    /// 1. `Status::DirectSearch`: This is the starting state. So far
426    ///    the iterator has only encountered inline links so far.
427    ///    Nothing needs to be resolved and the next method can
428    ///    output the link immediately.
429    ///    The `next()` method outputs directly the result from the parser
430    ///    `parser::take_link()`.
431    /// 2. `Status::ResolvedLinks`: as soon as the iterator encounters
432    ///    some reference link, e.g. `Text2label`, `Label2Dest` or
433    ///    `Label2Label` link, it switches into `Status::ResolvedLinks` mode.
434    ///    The transition happens as follows:
435    ///    1. The `next()` method consumes all the remaining `input` and
436    ///       calls the `populate_collection()`,
437    ///       `resolve_label2label_references()` and
438    ///       `resolve_text2label_references()` methods.
439    ///       From now on,
440    ///    2. the `next()` method outputs and deletes
441    ///       `HyperlinkCollection::Dest2Text_label[0]`.
442    ///       Not resolved `Text2Label` are ignored.
443    fn next(&mut self) -> Option<Self::Item> {
444        let mut output = None;
445        let mut status = Status::Init;
446        swap(&mut status, &mut self.status);
447
448        // Advance state machine.
449        let mut again = true;
450        while again {
451            status = match status {
452                // Advance state machine and match one more time.
453                Status::Init => Status::DirectSearch(self.input),
454
455                Status::DirectSearch(input) => {
456                    // We stay in direct mode.
457                    match take_link(input) {
458                        Ok((remaining_input, (skipped, link)))
459                            if matches!(
460                                link,
461                                Link::Text2Dest(_, _, _)
462                                    | Link::Image2Dest(_, _, _, _, _, _)
463                                    | Link::Image(_, _)
464                            ) =>
465                        {
466                            let consumed =
467                                &input[skipped.len()..input.len() - remaining_input.len()];
468                            // Assinig output.
469                            output = Some(((skipped, consumed, remaining_input), link));
470                            debug_assert_eq!(input, {
471                                let mut s = "".to_string();
472                                s.push_str(skipped);
473                                s.push_str(consumed);
474                                s.push_str(remaining_input);
475                                s
476                            });
477                            // Same state, we leave the loop.
478                            again = false;
479                            Status::DirectSearch(remaining_input)
480                        }
481                        _ => {
482                            // We switch to resolving mode.
483                            self.input = input;
484                            let mut hc = MarkupLinkCollection::from(input, self.render_label);
485                            hc.resolve_label2label_references();
486                            hc.resolve_text2label_references();
487                            let mut resolved_links = Vec::new();
488                            swap(&mut hc.text2dest_label, &mut resolved_links);
489
490                            // Advance state machine and match one more time.
491                            Status::ResolvedLinks(resolved_links)
492                        }
493                    }
494                }
495
496                Status::ResolvedLinks(mut resolved_links) => {
497                    while !resolved_links.is_empty() {
498                        // if let (input_offset, len, Link::Text2Dest(te, de, ti)) =
499                        //     resolved_links.remove(0)
500                        // Ok((remaining_input, (skipped, link)))
501                        match resolved_links.remove(0) {
502                            (input_offset, len, link)
503                                if matches!(
504                                    link,
505                                    Link::Text2Dest(_, _, _)
506                                        | Link::Image2Dest(_, _, _, _, _, _)
507                                        | Link::Image(_, _)
508                                ) =>
509                            {
510                                let skipped = &self.input[(self.last_output_offset
511                                    + self.last_output_len)
512                                    ..input_offset];
513                                let consumed = &self.input[input_offset..input_offset + len];
514                                let remaining_input = &self.input[input_offset + len..];
515                                // Assign output.
516                                output = Some(((skipped, consumed, remaining_input), link));
517                                debug_assert_eq!(self.input, {
518                                    let mut s = (self.input
519                                        [..self.last_output_offset + self.last_output_len])
520                                        .to_string();
521                                    s.push_str(skipped);
522                                    s.push_str(consumed);
523                                    s.push_str(remaining_input);
524                                    s
525                                });
526                                self.last_output_offset = input_offset;
527                                self.last_output_len = len;
528                                break;
529                            }
530                            _ => {}
531                        };
532                    }
533                    again = false;
534                    if output.is_some() {
535                        Status::ResolvedLinks(resolved_links)
536                    } else {
537                        Status::End
538                    }
539                }
540
541                Status::End => {
542                    again = false;
543                    output = None;
544                    Status::End
545                }
546            }
547        }
548        swap(&mut status, &mut self.status);
549        output
550    }
551}
552
553/// Recognizes hyperlinks in all supported markup languages
554/// and returns the first hyperlink found as
555/// `Some(Link::Text2Dest` or `Some(Link::Image2Dest)`.
556/// Returns `None` if no hyperlink is found.
557///
558/// This function resolves _link references_.
559/// ```
560/// use parse_hyperlinks::parser::Link;
561/// use parse_hyperlinks::iterator::find_first;
562/// use std::borrow::Cow;
563///
564/// let i = r#"abc[t][u]abc
565///            [u]: v "w"
566///            abc"#;
567///
568/// let r = find_first(i);
569/// assert_eq!(r, Some(Link::Text2Dest(Cow::from("t"), Cow::from("v"), Cow::from("w"))));
570/// ```
571pub fn find_first(i: &str) -> Option<Link> {
572    MarkupLink::new(i, false).next().map(|(_, l)| l)
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578
579    #[test]
580    fn test_populate_collection() {
581        let i = r#"[md label1]: md_destination1 "md title1"
582abc [md text2](md_destination2 "md title2")[md text3]: abc[md text4]: abc
583   [md label5]: md_destination5 "md title5"
584abc `rst text1 <rst_destination1>`__abc
585abc `rst text2 <rst_label2_>`_ .. _norst: no .. _norst: no
586.. _rst label3: rst_destination3
587  .. _rst label4: rst_d
588     estination4
589__ rst_label5_
590__ rst_label6_
591abc `rst text5`__abc
592abc `rst text6`__abc
593abc `rst text_label7 <rst_destination7>`_abc
594abc<scheme:md_dest8>abc
595abc<local@md_email8>abc
596abc[http://text9](<http://destination9> "title9")
597abc[def![alt10](img10.png)ghi](doc10.md "title10")jkl
598"#;
599
600        let hc = MarkupLinkCollection::from(i, false);
601
602        let expected = r#"[
603    (
604        45,
605        39,
606        Text2Dest(
607            "md text2",
608            "md_destination2",
609            "md title2",
610        ),
611    ),
612    (
613        84,
614        10,
615        Text2Label(
616            "md text3",
617            "md text3",
618        ),
619    ),
620    (
621        99,
622        10,
623        Text2Label(
624            "md text4",
625            "md text4",
626        ),
627    ),
628    (
629        163,
630        32,
631        Text2Dest(
632            "rst text1",
633            "rst_destination1",
634            "",
635        ),
636    ),
637    (
638        203,
639        54,
640        Text2Label(
641            "rst text2",
642            "rst_label2",
643        ),
644    ),
645    (
646        366,
647        13,
648        Text2Label(
649            "rst text5",
650            "_1",
651        ),
652    ),
653    (
654        387,
655        13,
656        Text2Label(
657            "rst text6",
658            "_2",
659        ),
660    ),
661    (
662        408,
663        37,
664        Text2Dest(
665            "rst text_label7",
666            "rst_destination7",
667            "",
668        ),
669    ),
670    (
671        452,
672        17,
673        Text2Dest(
674            "scheme:md_dest8",
675            "scheme:md_dest8",
676            "",
677        ),
678    ),
679    (
680        500,
681        46,
682        Text2Dest(
683            "http://text9",
684            "http://destination9",
685            "title9",
686        ),
687    ),
688    (
689        550,
690        47,
691        Image2Dest(
692            "def",
693            "alt10",
694            "img10.png",
695            "ghi",
696            "doc10.md",
697            "title10",
698        ),
699    ),
700]"#;
701        let res = format!("{:#?}", hc.text2dest_label);
702        eprintln!("{}", res);
703        assert_eq!(res, expected);
704        assert_eq!(hc.text2dest_label.len(), 11);
705
706        let expected = r#"[
707    (
708        "_1",
709        "rst_label5",
710    ),
711    (
712        "_2",
713        "rst_label6",
714    ),
715]"#;
716
717        let res = format!("{:#?}", hc.label2label);
718        assert_eq!(hc.label2label.len(), 2);
719        assert_eq!(res, expected);
720
721        //eprintln!("{:#?}", c.label2dest);
722        assert_eq!(hc.label2dest.len(), 5);
723        assert_eq!(
724            *hc.label2dest.get("md label1").unwrap(),
725            (Cow::from("md_destination1"), Cow::from("md title1"))
726        );
727        assert_eq!(
728            *hc.label2dest.get("md label5").unwrap(),
729            (Cow::from("md_destination5"), Cow::from("md title5"))
730        );
731        assert_eq!(
732            *hc.label2dest.get("rst label3").unwrap(),
733            (Cow::from("rst_destination3"), Cow::from(""))
734        );
735        assert_eq!(
736            *hc.label2dest.get("rst label4").unwrap(),
737            (Cow::from("rst_destination4"), Cow::from(""))
738        );
739        assert_eq!(
740            *hc.label2dest.get("rst text_label7").unwrap(),
741            (Cow::from("rst_destination7"), Cow::from(""))
742        );
743    }
744
745    #[test]
746    fn test_resolve_label2label_references() {
747        let i = r#"label2_
748.. _label2: rst_destination2
749  .. _label5: label4_
750  .. _label1: nolabel_
751  .. _label4: label3_
752  .. _label3: label2_
753"#;
754
755        let mut hc = MarkupLinkCollection::from(i, false);
756        hc.resolve_label2label_references();
757        //eprintln!("{:#?}", hc);
758        assert_eq!(hc.label2label.len(), 1);
759        assert_eq!(
760            hc.label2label[0],
761            (Cow::from("label1"), Cow::from("nolabel"))
762        );
763
764        assert_eq!(hc.label2dest.len(), 4);
765        assert_eq!(
766            *hc.label2dest.get("label2").unwrap(),
767            (Cow::from("rst_destination2"), Cow::from(""))
768        );
769        assert_eq!(
770            *hc.label2dest.get("label3").unwrap(),
771            (Cow::from("rst_destination2"), Cow::from(""))
772        );
773        assert_eq!(
774            *hc.label2dest.get("label4").unwrap(),
775            (Cow::from("rst_destination2"), Cow::from(""))
776        );
777        assert_eq!(
778            *hc.label2dest.get("label5").unwrap(),
779            (Cow::from("rst_destination2"), Cow::from(""))
780        );
781    }
782
783    #[test]
784    fn test_resolve_text2label_references() {
785        let i = r#"abc[text1][label1]abc
786        abc [text2](destination2 "title2")
787          [label3]: destination3 "title3"
788          [label1]: destination1 "title1"
789           .. _label4: label3_
790        abc[label3]abc[label5]abc
791        label4_
792        "#;
793
794        let mut hc = MarkupLinkCollection::from(i, false);
795        //eprintln!("{:#?}", hc);
796        hc.resolve_label2label_references();
797        //eprintln!("{:#?}", hc);
798        hc.resolve_text2label_references();
799        //eprintln!("{:#?}", hc);
800
801        let expected = vec![
802            (
803                3,
804                15,
805                Link::Text2Dest(
806                    Cow::from("text1"),
807                    Cow::from("destination1"),
808                    Cow::from("title1"),
809                ),
810            ),
811            (
812                34,
813                30,
814                Link::Text2Dest(
815                    Cow::from("text2"),
816                    Cow::from("destination2"),
817                    Cow::from("title2"),
818                ),
819            ),
820            (
821                191,
822                8,
823                Link::Text2Dest(
824                    Cow::from("label3"),
825                    Cow::from("destination3"),
826                    Cow::from("title3"),
827                ),
828            ),
829            (
830                202,
831                8,
832                Link::Text2Label(Cow::from("label5"), Cow::from("label5")),
833            ),
834            (
835                222,
836                7,
837                Link::Text2Dest(
838                    Cow::from("label4"),
839                    Cow::from("destination3"),
840                    Cow::from("title3"),
841                ),
842            ),
843        ];
844        assert_eq!(hc.text2dest_label, expected);
845    }
846
847    #[test]
848    fn test_resolve_text2label_references2() {
849        let i = r#"
850abc `text1 <label1_>`_abc
851abc text_label2_ abc
852abc text3__ abc
853abc text_label4_ abc
854abc text5__ abc
855  .. _label1: destination1
856  .. _text_label2: destination2
857  .. __: destination3
858  __ destination5
859        "#;
860
861        let mut hc = MarkupLinkCollection::from(i, false);
862        //eprintln!("{:#?}", hc);
863        hc.resolve_label2label_references();
864        //eprintln!("{:#?}", hc);
865        hc.resolve_text2label_references();
866        //eprintln!("{:#?}", hc);
867
868        let expected = vec![
869            (
870                5,
871                18,
872                Link::Text2Dest(Cow::from("text1"), Cow::from("destination1"), Cow::from("")),
873            ),
874            (
875                31,
876                12,
877                Link::Text2Dest(
878                    Cow::from("text_label2"),
879                    Cow::from("destination2"),
880                    Cow::from(""),
881                ),
882            ),
883            (
884                52,
885                7,
886                Link::Text2Dest(Cow::from("text3"), Cow::from("destination3"), Cow::from("")),
887            ),
888            (
889                68,
890                12,
891                Link::Text2Label(Cow::from("text_label4"), Cow::from("text_label4")),
892            ),
893            (
894                89,
895                7,
896                Link::Text2Dest(Cow::from("text5"), Cow::from("destination5"), Cow::from("")),
897            ),
898        ];
899        assert_eq!(hc.text2dest_label, expected);
900    }
901
902    #[test]
903    fn test_resolve_text2label_references3() {
904        let i = r#"
905abc[my homepage]abc
906abc
907
908[my homepage]: https://getreu.net
909abc"#;
910
911        let mut hc = MarkupLinkCollection::from(i, false);
912        eprintln!("{:#?}", hc);
913        hc.resolve_label2label_references();
914        //eprintln!("{:#?}", hc);
915        hc.resolve_text2label_references();
916        //eprintln!("{:#?}", hc);
917
918        let expected = vec![(
919            4,
920            13,
921            Link::Text2Dest(
922                Cow::from("my homepage"),
923                Cow::from("https://getreu.net"),
924                Cow::from(""),
925            ),
926        )];
927        assert_eq!(hc.text2dest_label, expected);
928    }
929
930    #[test]
931    fn test_next() {
932        let i = r#"abc[text0](destination0)abc
933abc[text1][label1]abc
934abc [text2](destination2 "title2")
935  [label3]: destination3 "title3"
936  [label1]: destination1 "title1"
937   .. _label4: label3_
938abc[label3]abc[label5]abc
939label4_
940abc[text5-1![alt5](src5)text5-2](dest5 "title5")abc
941        "#;
942
943        let mut iter = MarkupLink::new(i, false);
944
945        let expected =
946            Link::Text2Dest(Cow::from("text0"), Cow::from("destination0"), Cow::from(""));
947        let item = iter.next().unwrap();
948        //eprintln!("item: {:#?}", item);
949        assert_eq!(item.1, expected);
950
951        let expected = Link::Text2Dest(
952            Cow::from("text1"),
953            Cow::from("destination1"),
954            Cow::from("title1"),
955        );
956        let item = iter.next().unwrap();
957        //eprintln!("item: {:#?}", item);
958        assert_eq!(item.1, expected);
959
960        let expected = Link::Text2Dest(
961            Cow::from("text2"),
962            Cow::from("destination2"),
963            Cow::from("title2"),
964        );
965        let item = iter.next().unwrap();
966        //eprintln!("item: {:#?}", item);
967        assert_eq!(item.1, expected);
968
969        let expected = Link::Text2Dest(
970            Cow::from("label3"),
971            Cow::from("destination3"),
972            Cow::from("title3"),
973        );
974        let item = iter.next().unwrap();
975        //eprintln!("item: {:#?}", item);
976        assert_eq!(item.1, expected);
977
978        let expected = Link::Text2Dest(
979            Cow::from("label4"),
980            Cow::from("destination3"),
981            Cow::from("title3"),
982        );
983        let item = iter.next().unwrap();
984        //eprintln!("item: {:#?}", item);
985        assert_eq!(item.1, expected);
986
987        let expected = Link::Image2Dest(
988            Cow::from("text5-1"),
989            Cow::from("alt5"),
990            Cow::from("src5"),
991            Cow::from("text5-2"),
992            Cow::from("dest5"),
993            Cow::from("title5"),
994        );
995        let item = iter.next().unwrap();
996        //eprintln!("item: {:#?}", item);
997        assert_eq!(item.1, expected);
998
999        let expected = None;
1000        let item = iter.next();
1001        //eprintln!("item: {:#?}", item);
1002        assert_eq!(item, expected);
1003
1004        let expected = None;
1005        let item = iter.next();
1006        //eprintln!("item: {:#?}", item);
1007        assert_eq!(item, expected);
1008    }
1009
1010    #[test]
1011    fn test_next1() {
1012        let i = r#"Some autolink: <tpnote:locallink.md>,
1013more autolinks: <tpnote:20>, <getreu@web.de>,
1014boring link text: [http://domain.com](http://getreu.net)
1015[Jens Getreu's blog](https://blog.getreu.net "My blog")
1016Some more text."#;
1017        let mut iter = MarkupLink::new(i, false);
1018
1019        let expected = Link::Text2Dest(
1020            Cow::from("tpnote:locallink.md"),
1021            Cow::from("tpnote:locallink.md"),
1022            Cow::from(""),
1023        );
1024        let item = iter.next().unwrap();
1025        //eprintln!("item: {:#?}", item);
1026        assert_eq!(item.1, expected);
1027
1028        let expected = Link::Text2Dest(
1029            Cow::from("tpnote:20"),
1030            Cow::from("tpnote:20"),
1031            Cow::from(""),
1032        );
1033        let item = iter.next().unwrap();
1034        //eprintln!("item: {:#?}", item);
1035        assert_eq!(item.1, expected);
1036
1037        let expected = Link::Text2Dest(
1038            Cow::from("getreu@web.de"),
1039            Cow::from("mailto:getreu@web.de"),
1040            Cow::from(""),
1041        );
1042        let item = iter.next().unwrap();
1043        //eprintln!("item: {:#?}", item);
1044        assert_eq!(item.1, expected);
1045
1046        let expected = Link::Text2Dest(
1047            Cow::from("http://domain.com"),
1048            Cow::from("http://getreu.net"),
1049            Cow::from(""),
1050        );
1051        let item = iter.next().unwrap();
1052        //eprintln!("item: {:#?}", item);
1053        assert_eq!(item.1, expected);
1054    }
1055
1056    #[test]
1057    fn test_next2() {
1058        let i = r#"[te\_xt](ur\_l)[te_xt](ur_l)"#;
1059        let mut iter = MarkupLink::new(i, false);
1060
1061        let expected = Link::Text2Dest(Cow::from("te_xt"), Cow::from("ur_l"), Cow::from(""));
1062        let item = iter.next().unwrap();
1063        //eprintln!("item: {:#?}", item);
1064        assert_eq!(item.1, expected);
1065
1066        let item = iter.next().unwrap();
1067        //eprintln!("item: {:#?}", item);
1068        assert_eq!(item.1, expected);
1069    }
1070}