parse_hyperlinks/parser/
asciidoc.rs

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