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