parse_hyperlinks/parser/
asciidoc.rs

1//! This module implements parsers for Asciidoc hyperlinks.
2#![allow(dead_code)]
3#![allow(clippy::type_complexity)]
4
5use crate::parser::parse::LABEL_LEN_MAX;
6use crate::parser::percent_decode;
7use crate::parser::Link;
8use nom::branch::alt;
9use nom::bytes::complete::tag;
10use nom::bytes::complete::tag_no_case;
11use nom::character::complete::char;
12use nom::character::complete::space0;
13use nom::combinator::peek;
14use std::borrow::Cow;
15
16/// Wrapper around `adoc_text2dest()` that packs the result in
17/// `Link::Text2Dest`.
18pub fn adoc_text2dest_link(i: &str) -> nom::IResult<&str, Link> {
19    let (i, (te, de, ti)) = adoc_text2dest(i)?;
20    Ok((i, Link::Text2Dest(te, de, ti)))
21}
22
23/// Parses an Asciidoc _inline link_.
24///
25/// This parser expects to start at the first letter of `http://`,
26/// `https://`, `link:http://` or `link:https://` (preceded by optional
27/// whitespaces) to succeed.
28///
29/// When it starts at the letter `h` or `l`, the caller must guarantee, that:
30/// * the parser is at the beginning of the input _or_
31/// * the preceding byte is a newline `\n` _or_
32/// * the preceding bytes are whitespaces _or_
33/// * the preceding bytes are whitespaces or newline, followed by one of `[(<`
34///
35/// When ist starts at a whitespace no further guarantee is required.
36///
37/// `link_title` is always the empty `Cow::Borrowed("")`.
38/// ```
39/// use parse_hyperlinks::parser::Link;
40/// use parse_hyperlinks::parser::asciidoc::adoc_text2dest;
41/// use std::borrow::Cow;
42///
43/// assert_eq!(
44///   adoc_text2dest("https://destination[name]abc"),
45///   Ok(("abc", (Cow::from("name"), Cow::from("https://destination"), Cow::from(""))))
46/// );
47/// assert_eq!(
48///   adoc_text2dest("https://destination[]abc"),
49///   Ok(("abc", (Cow::from("https://destination"), Cow::from("https://destination"), Cow::from(""))))
50/// );
51/// assert_eq!(
52///   adoc_text2dest("https://destination abc"),
53///   Ok((" abc", (Cow::from("https://destination"), Cow::from("https://destination"), Cow::from(""))))
54/// );
55/// ```
56pub fn adoc_text2dest(i: &str) -> nom::IResult<&str, (Cow<str>, Cow<str>, Cow<str>)> {
57    let (i, (link_destination, link_text)) = nom::sequence::preceded(
58        space0,
59        nom::sequence::pair(
60            adoc_inline_link_destination,
61            nom::combinator::opt(adoc_link_text),
62        ),
63    )(i)?;
64
65    let link_text = if let Some(lt) = link_text {
66        if lt.is_empty() {
67            link_destination.clone()
68        } else {
69            lt
70        }
71    } else {
72        link_destination.clone()
73    };
74
75    Ok((i, (link_text, link_destination, Cow::Borrowed(""))))
76}
77
78/// Wrapper around `adoc_label2dest()` that packs the result in
79/// `Link::Label2Dest`.
80pub fn adoc_label2dest_link(i: &str) -> nom::IResult<&str, Link> {
81    let (i, (te, de, ti)) = adoc_label2dest(i)?;
82    Ok((i, Link::Label2Dest(te, de, ti)))
83}
84
85/// Parses an Asciidoc _link reference definition_.
86///
87/// This parser expects to start at the first letter of `:`,
88/// ` `, or `\t` to succeed.
89///
90/// The caller must guarantee, that:
91/// * the parser is at the beginning of the input _or_
92/// * the preceding byte is a newline `\n`.
93///
94/// `link_label` is always of type `Cow::Borrowed(&str)`.
95/// `link_title` is always the empty `Cow::Borrowed("")`.
96/// ```
97/// use parse_hyperlinks::parser::Link;
98/// use parse_hyperlinks::parser::asciidoc::adoc_label2dest;
99/// use std::borrow::Cow;
100///
101/// assert_eq!(
102///   adoc_label2dest(":label: https://destination\nabc"),
103///   Ok(("\nabc", (Cow::from("label"), Cow::from("https://destination"), Cow::from(""))))
104/// );
105/// ```
106pub fn adoc_label2dest(i: &str) -> nom::IResult<&str, (Cow<str>, Cow<str>, Cow<str>)> {
107    let (i, (link_label, link_destination)) = nom::sequence::preceded(
108        space0,
109        nom::sequence::pair(
110            adoc_parse_colon_reference,
111            nom::sequence::delimited(
112                nom::character::complete::space1,
113                adoc_link_reference_definition_destination,
114                nom::character::complete::space0,
115            ),
116        ),
117    )(i)?;
118
119    if !i.is_empty() {
120        let _ = peek::<&str, _, nom::error::Error<_>, _>(nom::character::complete::newline)(i)?;
121    };
122
123    Ok((
124        i,
125        (
126            Cow::Borrowed(link_label),
127            link_destination,
128            Cow::Borrowed(""),
129        ),
130    ))
131}
132
133/// Wrapper around `adoc_text2label()` that packs the result in
134/// `Link::Text2Label`.
135pub fn adoc_text2label_link(i: &str) -> nom::IResult<&str, Link> {
136    let (i, (te, la)) = adoc_text2label(i)?;
137    Ok((i, Link::Text2Label(te, la)))
138}
139
140/// Parse a Asciidoc _reference link_.
141///
142/// There are three kinds of reference links `Text2Label`: full, collapsed, and
143/// shortcut.
144/// 1. A full reference link `{label}[text]` consists of a link label immediately
145/// followed by a link text. The label matches a link reference definition
146/// elsewhere in the document.
147/// 2. A collapsed reference link `{label}[]` consists of a link label that matches
148///    a link reference definition elsewhere in the document, followed by the string
149///    `[]`. In this case, the function returns an empty _link text_ `""`,
150///    indicating, that the empty string must be replaced later by the link
151///    destination `link_dest` of the matching _link reference definition_
152///    (`Label2Dest`).
153/// 3. A shortcut reference link consists of a link label that matches a link
154///    reference definition elsewhere in the document and is not followed by `[]` or
155///    a link text `[link text]`. This is a shortcut of case 2. above.
156///
157/// This parser expects to start at the beginning of the link `[` to succeed.
158/// It should always run at last position after all other parsers.
159/// ```rust
160/// use parse_hyperlinks::parser::Link;
161/// use parse_hyperlinks::parser::asciidoc::adoc_text2label;
162/// use std::borrow::Cow;
163///
164/// assert_eq!(
165///   adoc_text2label("{link-label}[link text]abc"),
166///   Ok(("abc", (Cow::from("link text"), Cow::from("link-label"))))
167/// );
168/// assert_eq!(
169///   adoc_text2label("{link-label}[]abc"),
170///   Ok(("abc", (Cow::from(""), Cow::from("link-label"))))
171/// );
172/// assert_eq!(
173///   adoc_text2label("{link-label}abc"),
174///   Ok(("abc", (Cow::from(""), Cow::from("link-label"))))
175/// );
176/// ```
177pub fn adoc_text2label(i: &str) -> nom::IResult<&str, (Cow<str>, Cow<str>)> {
178    let (i, (link_label, link_text)) = alt((
179        nom::sequence::pair(adoc_parse_curly_bracket_reference, adoc_link_text),
180        nom::combinator::map(adoc_parse_curly_bracket_reference, |s| (s, Cow::from(""))),
181    ))(i)?;
182
183    // Check that there is no `[` or `{` following. Do not consume.
184    if !i.is_empty() {
185        let _ = nom::character::complete::none_of("[{")(i)?;
186    }
187
188    Ok((i, (link_text, link_label)))
189}
190
191/// Parses the link label. To succeed the first letter must be `[` and the
192/// last letter `]`. A sequence of whitespaces including newlines, will be
193/// replaced by one space. There must be not contain more than one newline
194/// per sequence. The string can contain the `\]` which is replaced by `]`.
195fn adoc_link_text(i: &str) -> nom::IResult<&str, Cow<str>> {
196    nom::sequence::delimited(char('['), remove_newline_take_till(']'), char(']'))(i)
197}
198
199/// Takes all characters until the character `<pat>`. The escaped character
200/// `\<pat>` is taken as normal character. Then parser replaces the escaped character
201/// `\<pat>` with `<pat>`. A sequence of whitespaces including one newline, is
202/// replaced by one space ` `. Each sequence must not contain more than one
203/// newline.
204fn remove_newline_take_till<'a>(
205    pat: char,
206) -> impl Fn(&'a str) -> nom::IResult<&'a str, Cow<'a, str>> {
207    move |i: &str| {
208        let mut res = Cow::Borrowed("");
209        let mut j = i;
210        while !j.is_empty() {
211            // `till()` always succeeds. There are two situations, when it does not
212            // advance the parser:
213            // 1. Input is the empty string `""`.
214            // 2. The first character satisfy the condition of `take_till()`.
215            //
216            // Case 1.: Can not happen because of the `while` just before.
217            // Case 2.: Even if the parser does not advance here, the code below
218            // starting with `if let Ok...` it will advance the parser at least
219            // one character.
220            let (k, s1) =
221                nom::bytes::complete::take_till(|c| c == pat || c == '\n' || c == '\\')(j)?;
222
223            // Store the result.
224            res = match res {
225                Cow::Borrowed("") => Cow::Borrowed(s1),
226                Cow::Borrowed(res_str) => {
227                    let mut strg = res_str.to_string();
228                    strg.push_str(s1);
229                    Cow::Owned(strg)
230                }
231                Cow::Owned(mut strg) => {
232                    strg.push_str(s1);
233                    Cow::Owned(strg)
234                }
235            };
236
237            // If there is a character left, inspect. Then either quit or advance at least one character.
238            // Therefor no endless is loop possible.
239            if let (_, Some(c)) =
240                nom::combinator::opt(nom::combinator::peek(nom::character::complete::anychar))(k)?
241            {
242                let m = match c {
243                    // We completed our mission and found `pat`.
244                    // This is the only Ok exit from the while loop.
245                    c if c == pat => return Ok((k, res)),
246                    // We stopped at an escaped character.
247                    c if c == '\\' => {
248                        // Consume the escape `\`.
249                        let (l, _) = char('\\')(k)?;
250                        // `pat` is the only valid escaped character (not even `\\` is special in
251                        // Asciidoc).
252                        // If `<pat>` is found, `c=='<pat>'`, otherwise `c=='\\'`
253                        let (l, c) = alt((char(pat), nom::combinator::success('\\')))(l)?;
254
255                        // and append the escaped character to `res`.
256                        let mut strg = res.to_string();
257                        strg.push(c);
258                        // Store the result.
259                        res = Cow::Owned(strg);
260                        // Advance `k`.
261                        l
262                    }
263                    // We stopped at a newline.
264                    c if c == '\n' => {
265                        // Now consume the `\n`.
266                        let (l, _) = char('\n')(k)?;
267                        let (l, _) = space0(l)?;
268                        // Return error if there is one more `\n`. BTW, `not()` never consumes.
269                        let _ = nom::combinator::not(char('\n'))(l)?;
270
271                        // and append one space ` ` character to `res`.
272                        let mut strg = res.to_string();
273                        strg.push(' ');
274                        // Store the result.
275                        res = Cow::Owned(strg);
276                        // Advance `k`.
277                        l
278                    }
279                    _ => unreachable!(),
280                };
281                j = m;
282            } else {
283                // We are here because `k == ""`. We quit the while loop.
284                j = k;
285            }
286        }
287
288        // If we are here, `j` is empty `""`.
289        Ok(("", res))
290    }
291}
292
293/// Parses an link reference definition destination.
294/// The parser takes URLs until `[`, whitespace or newline.
295/// The parser succeeds, if one of the variants:
296/// `adoc_parse_http_link_destination()` or
297/// `adoc_parse_escaped_link_destination()` succeeds and returns its result.
298fn adoc_link_reference_definition_destination(i: &str) -> nom::IResult<&str, Cow<str>> {
299    alt((
300        adoc_parse_http_link_destination,
301        adoc_parse_escaped_link_destination,
302    ))(i)
303}
304
305/// Parses an inline link destination.
306/// The parser succeeds, if one of the variants:
307/// `adoc_parse_http_link_destination()`, `adoc_parse_literal_link_destination()`
308/// or `adoc_parse_escaped_link_destination()` succeeds and returns its result.
309fn adoc_inline_link_destination(i: &str) -> nom::IResult<&str, Cow<str>> {
310    alt((
311        adoc_parse_http_link_destination,
312        adoc_parse_literal_link_destination,
313        adoc_parse_escaped_link_destination,
314    ))(i)
315}
316
317/// Parses a link destination in URL form starting with `http://` or `https://`
318/// and ending with `[`. The latter is peeked, but no consumed.
319fn adoc_parse_http_link_destination(i: &str) -> nom::IResult<&str, Cow<str>> {
320    let (j, s) = nom::sequence::preceded(
321        peek(alt((tag_no_case("http://"), (tag_no_case("https://"))))),
322        nom::bytes::complete::take_till1(|c| c == '[' || c == ' ' || c == '\t' || c == '\n'),
323    )(i)?;
324    Ok((j, Cow::Borrowed(s)))
325}
326
327/// Parses a link destination starting with `link:http://` or `link:https://` ending
328/// with `]`, whitespace or newline. The later is peeked, but not consumed. The URL can contain percent
329/// encoded characters, which are decoded.
330fn adoc_parse_escaped_link_destination(i: &str) -> nom::IResult<&str, Cow<str>> {
331    nom::combinator::map_parser(
332        nom::sequence::preceded(
333            nom::sequence::pair(
334                tag("link:"),
335                peek(alt((tag_no_case("http://"), (tag_no_case("https://"))))),
336            ),
337            nom::bytes::complete::take_till1(|c| {
338                c == '[' || c == ' ' || c == '\t' || c == '\r' || c == '\n'
339            }),
340        ),
341        percent_decode,
342    )(i)
343}
344
345/// Parses a link destination starting with `link:+++` ending with `++`. Everything in
346/// between is taken as it is without any transformation.
347fn adoc_parse_literal_link_destination(i: &str) -> nom::IResult<&str, Cow<str>> {
348    let (j, s) = nom::sequence::preceded(
349        tag("link:"),
350        nom::sequence::delimited(tag("++"), nom::bytes::complete::take_until("++"), tag("++")),
351    )(i)?;
352    Ok((j, Cow::Borrowed(s)))
353}
354
355/// Parses the _link text_ (`label`) of `Label2Text` link.
356///
357/// The parser expects to start at the opening `{` to succeed.
358/// The result is always a borrowed reference.
359fn adoc_parse_curly_bracket_reference(i: &str) -> nom::IResult<&str, Cow<str>> {
360    nom::combinator::map(
361        nom::combinator::verify(
362            nom::sequence::delimited(
363                char('{'),
364                nom::bytes::complete::take_till1(|c| {
365                    c == '}' || c == ' ' || c == '\t' || c == '\r'
366                }),
367                char('}'),
368            ),
369            |s: &str| s.len() <= LABEL_LEN_MAX,
370        ),
371        Cow::Borrowed,
372    )(i)
373}
374
375/// Parses the label of a link reference definition.
376///
377/// The parser expects to start at the first colon `:` or at some whitespace to
378/// succeed.
379/// The caller must guaranty, that the byte before was a newline. The parser
380/// consumes all whitespace before the first colon and after the second.
381fn adoc_parse_colon_reference(i: &str) -> nom::IResult<&str, &str> {
382    nom::combinator::verify(
383        nom::sequence::delimited(
384            char(':'),
385            nom::bytes::complete::take_till1(|c| c == ':' || c == ' ' || c == '\t' || c == '\r'),
386            char(':'),
387        ),
388        |s: &str| s.len() <= LABEL_LEN_MAX,
389    )(i)
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use nom::error::ErrorKind;
396    use std::matches;
397
398    #[test]
399    fn test_adoc_text2dest() {
400        assert_eq!(
401            adoc_text2dest("http://getreu.net[]"),
402            Ok((
403                "",
404                (
405                    Cow::from("http://getreu.net"),
406                    Cow::from("http://getreu.net"),
407                    Cow::from("")
408                )
409            ))
410        );
411
412        assert_eq!(
413            adoc_text2dest("http://getreu.net[]abc"),
414            Ok((
415                "abc",
416                (
417                    Cow::from("http://getreu.net"),
418                    Cow::from("http://getreu.net"),
419                    Cow::from("")
420                )
421            ))
422        );
423
424        assert_eq!(
425            adoc_text2dest("  \t  http://getreu.net[My blog]abc"),
426            Ok((
427                "abc",
428                (
429                    Cow::from("My blog"),
430                    Cow::from("http://getreu.net"),
431                    Cow::from("")
432                )
433            ))
434        );
435
436        assert_eq!(
437            adoc_text2dest(r#"http://getreu.net[My blog[1\]]abc"#),
438            Ok((
439                "abc",
440                (
441                    Cow::from("My blog[1]"),
442                    Cow::from("http://getreu.net"),
443                    Cow::from("")
444                )
445            ))
446        );
447
448        assert_eq!(
449            adoc_text2dest("http://getreu.net[My\n    blog]abc"),
450            Ok((
451                "abc",
452                (
453                    Cow::from("My blog"),
454                    Cow::from("http://getreu.net"),
455                    Cow::from("")
456                )
457            ))
458        );
459
460        assert_eq!(
461            adoc_text2dest("link:http://getreu.net[My blog]abc"),
462            Ok((
463                "abc",
464                (
465                    Cow::from("My blog"),
466                    Cow::from("http://getreu.net"),
467                    Cow::from("")
468                )
469            ))
470        );
471
472        assert_eq!(
473            adoc_text2dest("link:https://getreu.net/?q=%5Ba%20b%5D[My blog]abc"),
474            Ok((
475                "abc",
476                (
477                    Cow::from("My blog"),
478                    Cow::from("https://getreu.net/?q=[a b]"),
479                    Cow::from("")
480                )
481            ))
482        );
483
484        assert_eq!(
485            adoc_text2dest("link:++https://getreu.net/?q=[a b]++[My blog]abc"),
486            Ok((
487                "abc",
488                (
489                    Cow::from("My blog"),
490                    Cow::from("https://getreu.net/?q=[a b]"),
491                    Cow::from("")
492                )
493            ))
494        );
495    }
496
497    #[test]
498    fn test_adoc_label2dest() {
499        assert_eq!(
500            adoc_label2dest(":label: http://getreu.net\n"),
501            Ok((
502                "\n",
503                (
504                    Cow::from("label"),
505                    Cow::from("http://getreu.net"),
506                    Cow::from("")
507                )
508            ))
509        );
510
511        assert_eq!(
512            adoc_label2dest("  :label: \thttp://getreu.net \t "),
513            Ok((
514                "",
515                (
516                    Cow::from("label"),
517                    Cow::from("http://getreu.net"),
518                    Cow::from("")
519                )
520            ))
521        );
522
523        assert_eq!(
524            adoc_label2dest("  :label: \thttp://getreu.net \t abc").unwrap_err(),
525            nom::Err::Error(nom::error::Error::new("abc", ErrorKind::Char))
526        );
527    }
528
529    #[test]
530    fn test_adoc_link_text() {
531        assert_eq!(adoc_link_text("[text]abc"), Ok(("abc", Cow::from("text"))));
532
533        assert_eq!(
534            adoc_link_text("[te\nxt]abc"),
535            Ok(("abc", Cow::from("te xt")))
536        );
537
538        assert_eq!(
539            adoc_link_text("[te\n\nxt]abc"),
540            Err(nom::Err::Error(nom::error::Error::new(
541                "\nxt]abc",
542                ErrorKind::Not
543            )))
544        );
545
546        assert_eq!(
547            adoc_link_text(r#"[text[i\]]abc"#),
548            Ok(("abc", Cow::from(r#"text[i]"#.to_string())))
549        );
550
551        assert_eq!(
552            adoc_link_text("[textabc"),
553            Err(nom::Err::Error(nom::error::Error::new("", ErrorKind::Char)))
554        );
555    }
556
557    #[test]
558    fn test_remove_newline_take_till() {
559        let res = remove_newline_take_till(']')("").unwrap();
560        assert_eq!(res, ("", Cow::from("")));
561        assert!(matches!(res.1, Cow::Borrowed { .. }));
562
563        let res = remove_newline_take_till(']')("text text]abc").unwrap();
564        assert_eq!(res, ("]abc", Cow::from("text text")));
565        assert!(matches!(res.1, Cow::Borrowed { .. }));
566
567        let res = remove_newline_take_till(']')("text text").unwrap();
568        assert_eq!(res, ("", Cow::from("text text")));
569        assert!(matches!(res.1, Cow::Borrowed { .. }));
570
571        let res = remove_newline_take_till(']')(r#"te\]xt]abc"#).unwrap();
572        assert_eq!(res, ("]abc", Cow::from("te]xt")));
573        assert!(matches!(res.1, Cow::Owned { .. }));
574
575        let res = remove_newline_take_till(']')(r#"text\]]abc"#).unwrap();
576        assert_eq!(res, ("]abc", Cow::from("text]")));
577        assert!(matches!(res.1, Cow::Owned { .. }));
578
579        let res = remove_newline_take_till(']')(r#"te\xt]abc"#).unwrap();
580        assert_eq!(res, ("]abc", Cow::from(r#"te\xt"#)));
581        assert!(matches!(res.1, Cow::Owned { .. }));
582
583        let res = remove_newline_take_till(']')("text\n   text]abc").unwrap();
584        assert_eq!(res, ("]abc", Cow::from("text text")));
585        assert!(matches!(res.1, Cow::Owned { .. }));
586
587        let res = remove_newline_take_till(']')("text\n   text]abc").unwrap();
588        assert_eq!(res, ("]abc", Cow::from("text text")));
589        assert!(matches!(res.1, Cow::Owned { .. }));
590
591        assert_eq!(
592            remove_newline_take_till(']')("text\n\ntext]abc").unwrap_err(),
593            nom::Err::Error(nom::error::Error::new("\ntext]abc", ErrorKind::Not))
594        );
595
596        assert_eq!(
597            remove_newline_take_till(']')("text\n  \n  text]abc").unwrap_err(),
598            nom::Err::Error(nom::error::Error::new("\n  text]abc", ErrorKind::Not))
599        );
600    }
601
602    #[test]
603    fn test_adoc_parse_http_link_destination() {
604        let res = adoc_parse_http_link_destination("http://destination/").unwrap();
605        assert_eq!(res, ("", Cow::from("http://destination/")));
606        assert!(matches!(res.1, Cow::Borrowed { .. }));
607
608        let res = adoc_parse_http_link_destination("http://destination/\nabc").unwrap();
609        assert_eq!(res, ("\nabc", Cow::from("http://destination/")));
610        assert!(matches!(res.1, Cow::Borrowed { .. }));
611
612        let res = adoc_parse_http_link_destination("http://destination/ abc").unwrap();
613        assert_eq!(res, (" abc", Cow::from("http://destination/")));
614        assert!(matches!(res.1, Cow::Borrowed { .. }));
615
616        let res = adoc_parse_http_link_destination("http://destination/[abc").unwrap();
617        assert_eq!(res, ("[abc", Cow::from("http://destination/")));
618        assert!(matches!(res.1, Cow::Borrowed { .. }));
619
620        let res = adoc_parse_http_link_destination("https://destination/[abc").unwrap();
621        assert_eq!(res, ("[abc", Cow::from("https://destination/")));
622        assert!(matches!(res.1, Cow::Borrowed { .. }));
623
624        assert_eq!(
625            adoc_parse_http_link_destination("http:/destination/[abc").unwrap_err(),
626            nom::Err::Error(nom::error::Error::new(
627                "http:/destination/[abc",
628                ErrorKind::Tag
629            ))
630        );
631    }
632
633    #[test]
634    fn test_adoc_parse_escaped_link_destination() {
635        let res = adoc_parse_escaped_link_destination("link:http://destination/").unwrap();
636        assert_eq!(res, ("", Cow::from("http://destination/")));
637        assert!(matches!(res.1, Cow::Borrowed { .. }));
638
639        let res = adoc_parse_escaped_link_destination("link:http://destination/[abc").unwrap();
640        assert_eq!(res, ("[abc", Cow::from("http://destination/")));
641        assert!(matches!(res.1, Cow::Borrowed { .. }));
642
643        let res = adoc_parse_escaped_link_destination("link:http://destination/ abc").unwrap();
644        assert_eq!(res, (" abc", Cow::from("http://destination/")));
645        assert!(matches!(res.1, Cow::Borrowed { .. }));
646
647        let res = adoc_parse_escaped_link_destination("link:http://destination/\nabc").unwrap();
648        assert_eq!(res, ("\nabc", Cow::from("http://destination/")));
649        assert!(matches!(res.1, Cow::Borrowed { .. }));
650
651        assert_eq!(
652            adoc_parse_escaped_link_destination("link:httpX:/destination/[abc").unwrap_err(),
653            nom::Err::Error(nom::error::Error::new(
654                "httpX:/destination/[abc",
655                ErrorKind::Tag
656            ))
657        );
658
659        let res = adoc_parse_escaped_link_destination("link:https://getreu.net/?q=%5Ba%20b%5D[abc")
660            .unwrap();
661        assert_eq!(res, ("[abc", Cow::from("https://getreu.net/?q=[a b]")));
662        assert!(matches!(res.1, Cow::Owned { .. }));
663
664        assert_eq!(
665            adoc_parse_escaped_link_destination("link:https://getreu.net/?q=%FF%FF[abc")
666                .unwrap_err(),
667            nom::Err::Error(nom::error::Error::new(
668                "https://getreu.net/?q=%FF%FF",
669                ErrorKind::EscapedTransform
670            ))
671        );
672    }
673
674    #[test]
675    fn test_adoc_parse_literal_link_destination() {
676        let res = adoc_parse_literal_link_destination("link:++https://getreu.net/?q=[a b]++[abc")
677            .unwrap();
678        assert_eq!(res, ("[abc", Cow::from("https://getreu.net/?q=[a b]")));
679
680        assert_eq!(
681            adoc_parse_literal_link_destination("link:++https://getreu.net/?q=[a b]+[abc")
682                .unwrap_err(),
683            nom::Err::Error(nom::error::Error::new(
684                "https://getreu.net/?q=[a b]+[abc",
685                ErrorKind::TakeUntil
686            ))
687        );
688    }
689
690    #[test]
691    fn test_adoc_text2label() {
692        let res = adoc_text2label("{label}[link text]abc").unwrap();
693        assert_eq!(res, ("abc", (Cow::from("link text"), Cow::from("label"))));
694
695        let res = adoc_text2label("{label}[]abc").unwrap();
696        assert_eq!(res, ("abc", (Cow::from(""), Cow::from("label"))));
697
698        let res = adoc_text2label("{label}abc").unwrap();
699        assert_eq!(res, ("abc", (Cow::from(""), Cow::from("label"))));
700
701        let res = adoc_text2label("{label}").unwrap();
702        assert_eq!(res, ("", (Cow::from(""), Cow::from("label"))));
703
704        let res = adoc_text2label("{label} [link text]abc").unwrap();
705        assert_eq!(
706            res,
707            (" [link text]abc", (Cow::from(""), Cow::from("label")))
708        );
709
710        assert_eq!(
711            adoc_text2label("{label}[abc").unwrap_err(),
712            nom::Err::Error(nom::error::Error::new("[abc", ErrorKind::NoneOf))
713        );
714    }
715
716    #[test]
717    fn test_adoc_parse_curly_bracket_reference() {
718        let res = adoc_parse_curly_bracket_reference("{label}").unwrap();
719        assert_eq!(res, ("", Cow::from("label")));
720
721        let res = adoc_parse_curly_bracket_reference("{label}[link text]").unwrap();
722        assert_eq!(res, ("[link text]", Cow::from("label")));
723
724        assert_eq!(
725            adoc_parse_curly_bracket_reference("").unwrap_err(),
726            nom::Err::Error(nom::error::Error::new("", ErrorKind::Char))
727        );
728
729        assert_eq!(
730            adoc_parse_curly_bracket_reference("{label }").unwrap_err(),
731            nom::Err::Error(nom::error::Error::new(" }", ErrorKind::Char))
732        );
733        assert_eq!(
734            adoc_parse_curly_bracket_reference("").unwrap_err(),
735            nom::Err::Error(nom::error::Error::new("", ErrorKind::Char))
736        );
737    }
738
739    #[test]
740    fn test_adoc_parse_colon_reference() {
741        let res = adoc_parse_colon_reference(":label:abc").unwrap();
742        assert_eq!(res, ("abc", "label"));
743
744        assert_eq!(
745            adoc_parse_colon_reference(":label abc").unwrap_err(),
746            nom::Err::Error(nom::error::Error::new(" abc", ErrorKind::Char))
747        );
748    }
749}