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