Skip to main content

doc_chunks/
literal.rs

1use crate::errors::*;
2use crate::util::{self, sub_chars};
3use crate::{Range, Span};
4
5use fancy_regex::Regex;
6use lazy_static::lazy_static;
7use proc_macro2::LineColumn;
8
9use std::fmt;
10
11/// Determine if a `CommentVariant` is a documentation comment or not.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum CommentVariantCategory {
14    /// Comment variant will end up in documentation.
15    Doc,
16    /// Comment variant is only visible in source code.
17    Dev,
18    /// It's a common mark file, and we actually don't know.
19    CommonMark,
20    /// Toml entries and such.
21    Unmergable,
22}
23
24/// Track what kind of comment the literal is
25#[derive(Debug, Clone, Hash, Eq, PartialEq)]
26#[non_exhaustive]
27pub enum CommentVariant {
28    /// `///`
29    TripleSlash,
30    /// `//!`
31    DoubleSlashEM,
32    /// `/*!`
33    SlashAsteriskEM,
34    /// `/**`
35    SlashAsteriskAsterisk,
36    /// `/*`
37    SlashAsterisk,
38    /// `#[doc=` with actual prefix like `#[doc=` and the total length of `r###`
39    /// etc. including `r` but without `"`
40    MacroDocEqStr(String, usize),
41    /// `#[doc= foo!(..)]`, content will be ignored, but allows clusters to not
42    /// continue.
43    MacroDocEqMacro,
44    /// Commonmark File
45    CommonMark,
46    /// Developer line comment
47    DoubleSlash,
48    /// Developer block comment
49    SlashStar,
50    /// Unknown Variant
51    Unknown,
52    /// Toml entry
53    TomlEntry,
54}
55
56impl Default for CommentVariant {
57    fn default() -> Self {
58        CommentVariant::Unknown
59    }
60}
61
62impl CommentVariant {
63    /// Obtain the comment variant category.
64    pub fn category(&self) -> CommentVariantCategory {
65        match self {
66            Self::TripleSlash => CommentVariantCategory::Doc,
67            Self::DoubleSlashEM => CommentVariantCategory::Doc,
68            Self::MacroDocEqStr(_, _) => CommentVariantCategory::Doc,
69            Self::MacroDocEqMacro => CommentVariantCategory::Doc,
70            Self::SlashAsteriskEM => CommentVariantCategory::Doc,
71            Self::SlashAsteriskAsterisk => CommentVariantCategory::Doc,
72            Self::CommonMark => CommentVariantCategory::CommonMark,
73            Self::TomlEntry => CommentVariantCategory::Unmergable,
74            _ => CommentVariantCategory::Dev,
75        }
76    }
77    /// Return the prefix string.
78    ///
79    /// Does not include whitespaces for `///` and `//!` variants!
80    pub fn prefix_string(&self) -> String {
81        match self {
82            CommentVariant::TripleSlash => "///".into(),
83            CommentVariant::DoubleSlashEM => "//!".into(),
84            CommentVariant::MacroDocEqMacro => "".into(),
85            CommentVariant::MacroDocEqStr(d, p) => {
86                let raw = match p {
87                    // TODO: make configureable if each line will start with #[doc ="
88                    // TODO: but not here!
89                    0 => "\"".to_owned(),
90                    x => format!("r{}\"", "#".repeat(x.saturating_sub(1))),
91                };
92                format!(r#"{d}{raw}"#)
93            }
94            CommentVariant::CommonMark => "".to_string(),
95            CommentVariant::DoubleSlash => "//".to_string(),
96            CommentVariant::SlashStar => "/*".to_string(),
97            CommentVariant::SlashAsterisk => "/*".to_string(),
98            CommentVariant::SlashAsteriskEM => "/*!".to_string(),
99            CommentVariant::SlashAsteriskAsterisk => "/**".to_string(),
100            CommentVariant::TomlEntry => "".to_owned(),
101            unhandled => {
102                unreachable!("String representation for comment variant {unhandled:?} exists. qed")
103            }
104        }
105    }
106    /// Return length (in bytes) of comment prefix for each variant.
107    ///
108    /// By definition matches the length of `prefix_string`.
109    pub fn prefix_len(&self) -> usize {
110        match self {
111            CommentVariant::TripleSlash | CommentVariant::DoubleSlashEM => 3,
112            CommentVariant::MacroDocEqMacro => 0,
113            CommentVariant::MacroDocEqStr(d, p) => d.len() + *p + 1,
114            CommentVariant::SlashAsterisk => 2,
115            CommentVariant::SlashAsteriskEM | CommentVariant::SlashAsteriskAsterisk => 3,
116            _ => self.prefix_string().len(),
117        }
118    }
119
120    /// Return suffix of different comment variants
121    pub fn suffix_len(&self) -> usize {
122        match self {
123            CommentVariant::MacroDocEqStr(_, 0) => 2,
124            CommentVariant::MacroDocEqStr(_, p) => p + 1,
125            CommentVariant::SlashAsteriskAsterisk
126            | CommentVariant::SlashAsteriskEM
127            | CommentVariant::SlashAsterisk => 2,
128            CommentVariant::MacroDocEqMacro => 0,
129            _ => 0,
130        }
131    }
132
133    /// Return string which will be appended to each line
134    pub fn suffix_string(&self) -> String {
135        match self {
136            CommentVariant::MacroDocEqStr(_, p) if *p == 0 || *p == 1 => r#""]"#.to_string(),
137            CommentVariant::MacroDocEqStr(_, p) => {
138                r#"""#.to_string() + &"#".repeat(p.saturating_sub(1)) + "]"
139            }
140            CommentVariant::SlashAsteriskAsterisk
141            | CommentVariant::SlashAsteriskEM
142            | CommentVariant::SlashAsterisk => "*/".to_string(),
143            _ => "".to_string(),
144        }
145    }
146}
147
148/// A literal with meta info where the first and list whitespace may be found.
149#[derive(Clone)]
150pub struct TrimmedLiteral {
151    /// Track what kind of comment the literal is
152    variant: CommentVariant,
153    /// The span of rendered content, minus pre and post already applied.
154    span: Span,
155    /// the complete rendered string including post and pre.
156    rendered: String,
157    /// Literal prefix length.
158    pre: usize,
159    /// Literal postfix length.
160    post: usize,
161    /// Length of rendered **minus** `pre` and `post` in UTF-8 characters.
162    len_in_chars: usize,
163    len_in_bytes: usize,
164}
165
166impl std::cmp::PartialEq for TrimmedLiteral {
167    fn eq(&self, other: &Self) -> bool {
168        if self.rendered != other.rendered {
169            return false;
170        }
171        if self.pre != other.pre {
172            return false;
173        }
174        if self.post != other.post {
175            return false;
176        }
177        if self.len() != other.len() {
178            return false;
179        }
180        if self.span != other.span {
181            return false;
182        }
183        if self.variant != other.variant {
184            return false;
185        }
186
187        true
188    }
189}
190
191impl std::cmp::Eq for TrimmedLiteral {}
192
193impl std::hash::Hash for TrimmedLiteral {
194    fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
195        self.variant.hash(hasher);
196        self.rendered.hash(hasher);
197        self.span.hash(hasher);
198        self.pre.hash(hasher);
199        self.post.hash(hasher);
200        self.len_in_bytes.hash(hasher);
201        self.len_in_chars.hash(hasher);
202    }
203}
204
205/// Adjust the provided span by a number of `pre` and `post` characters.
206fn trim_span(content: &str, span: &mut Span, pre: usize, post: usize) {
207    span.start.column += pre;
208    if span.end.column >= post {
209        span.end.column -= post;
210    } else {
211        // look for the last character in the previous line
212        let previous_line_length = content
213            .chars()
214            .rev()
215            // assumes \n, we want to skip the first one from the back
216            .skip(post + 1)
217            .take_while(|c| *c != '\n')
218            .count();
219        span.end = LineColumn {
220            line: span.end.line - 1,
221            column: previous_line_length,
222        };
223    }
224}
225
226/// Detect the comment variant based on the span based str content.
227///
228/// Became necessary, since the `proc_macro2::Span` does not distinguish between
229/// `#[doc=".."]` and `/// ..` comment variants, and for one, and the span can't
230/// cover both correctly.
231fn detect_comment_variant(
232    content: &str,
233    rendered: &String,
234    mut span: Span,
235) -> Result<(CommentVariant, Span, usize, usize)> {
236    let prefix_span = Span {
237        start: crate::LineColumn {
238            line: span.start.line,
239            column: 0,
240        },
241        end: crate::LineColumn {
242            line: span.start.line,
243            column: span.start.column.saturating_sub(1),
244        },
245    };
246    let prefix = util::load_span_from(content.as_bytes(), prefix_span)?
247        .trim_start()
248        .to_string();
249
250    let (variant, span, pre, post) = if rendered.starts_with("///") || rendered.starts_with("//!") {
251        let pre = 3; // `///`
252        let post = 0; // trailing `\n` is already accounted for above
253
254        span.start.column += pre;
255
256        // must always be a single line
257        assert_eq!(span.start.line, span.end.line);
258        // if the line includes quotes, the rustc converts them internally
259        // to `#[doc="content"]`, where - if `content` contains `"` will substitute
260        // them as `\"` which will inflate the number columns.
261        // Since we can not distinguish between orignally escaped, we simply
262        // use the content read from source.
263
264        let variant = if rendered.starts_with("///") {
265            CommentVariant::TripleSlash
266        } else {
267            CommentVariant::DoubleSlashEM
268        };
269
270        (variant, span, pre, post)
271    } else if rendered.starts_with("/*") && rendered.ends_with("*/") {
272        let variant = if rendered.starts_with("/*!") {
273            CommentVariant::SlashAsteriskEM
274        } else if rendered.starts_with("/**") {
275            CommentVariant::SlashAsteriskAsterisk
276        } else {
277            CommentVariant::SlashAsterisk
278        };
279
280        let pre = variant.prefix_len();
281        let post = variant.suffix_len();
282
283        #[cfg(debug_assertions)]
284        let orig = span;
285
286        trim_span(rendered, &mut span, pre, post);
287
288        #[cfg(debug_assertions)]
289        {
290            let raw = util::load_span_from(&mut content.as_bytes(), orig)?;
291            let adjusted = util::load_span_from(&mut content.as_bytes(), span)?;
292
293            // we know pre and post only consist of single byte characters
294            // so `.len()` is way faster here yet correct.
295            assert_eq!(adjusted.len() + pre + post, raw.len());
296        }
297
298        (variant, span, pre, post)
299    } else {
300        // pre and post are for the rendered content
301        // not necessarily for the span
302
303        //^r(#+?)"(?:.*\s*)+(?=(?:"\1))("\1)$
304        lazy_static! {
305            static ref BOUNDED_RAW_STR: Regex =
306                Regex::new(r##"^(r(#*)")(?:.*\s*)+?(?=(?:"\2))("\2)\s*\]?\s*$"##)
307                    .expect("BOUNEDED_RAW_STR regex compiles");
308            static ref BOUNDED_STR: Regex = Regex::new(r##"^"(?:.(?!"\\"))*?"*\s*\]?\s*"$"##)
309                .expect("BOUNEDED_STR regex compiles");
310        };
311
312        let (pre, post) =
313            if let Some(captures) = BOUNDED_RAW_STR.captures(rendered.as_str()).ok().flatten() {
314                log::trace!("raw str: >{}<", rendered.as_str());
315                let pre = if let Some(prefix) = captures.get(1) {
316                    log::trace!("raw str pre: >{}<", prefix.as_str());
317                    prefix.as_str().len()
318                } else {
319                    return Err(Error::Span(
320                        "Should have a raw str pre match with a capture group".to_string(),
321                    ));
322                };
323                let post = if let Some(suffix) = captures.get(captures.len() - 1) {
324                    log::trace!("raw str post: >{}<", suffix.as_str());
325                    suffix.as_str().len()
326                } else {
327                    return Err(Error::Span(
328                        "Should have a raw str post match with a capture group".to_string(),
329                    ));
330                };
331
332                // r####" must match "####
333                debug_assert_eq!(pre, post + 1);
334
335                (pre, post)
336            } else if let Some(_captures) = BOUNDED_STR.captures(rendered.as_str()).ok().flatten() {
337                // r####" must match "####
338                let pre = 1;
339                let post = 1;
340                debug_assert_eq!('"', rendered.as_bytes()[0_usize] as char);
341                debug_assert_eq!('"', rendered.as_bytes()[rendered.len() - 1_usize] as char);
342                (pre, post)
343            } else {
344                return Err(Error::Span(format!("Regex should match >{rendered}<")));
345            };
346
347        span.start.column += pre;
348        span.end.column = span.end.column.saturating_sub(post);
349
350        (
351            CommentVariant::MacroDocEqStr(prefix, pre.saturating_sub(1)),
352            span,
353            pre,
354            post,
355        )
356    };
357    Ok((variant, span, pre, post))
358}
359
360impl TrimmedLiteral {
361    /// Create an empty comment.
362    ///
363    /// Prime use case is for `#[doc = foo!()]` cases.
364    pub(crate) fn new_empty(
365        _content: impl AsRef<str>,
366        span: Span,
367        variant: CommentVariant,
368    ) -> Self {
369        Self {
370            // Track what kind of comment the literal is
371            variant,
372            span,
373            // .
374            rendered: String::new(),
375            pre: 0,
376            post: 0,
377            len_in_chars: 0,
378            len_in_bytes: 0,
379        }
380    }
381
382    pub(crate) fn load_from(content: &str, mut span: Span) -> Result<Self> {
383        // let rendered = literal.to_string();
384        // produces pretty unusable garabage, since it modifies the content of `///`
385        // comments which could contain " which will be escaped
386        // and therefor cause the `span()` to yield something that does
387        // not align with the rendered literal at all and there are too
388        // many pitfalls to sanitize all cases, so reading given span
389        // from the file again, and then determining its type is way safer.
390
391        // It's unclear why the trailing `]` character is part of the given span, it shout not be part
392        // of it, but the span we obtain from literal seems to be wrong, adding one trailing char.
393
394        // Either cut off `]` or `\n` - we don't need either.
395        span.end.column = span.end.column.saturating_sub(1);
396
397        // If the line ending has more than one character, we have to account
398        // for that. Otherwise cut of the last character of the ending such that
399        // we can't properly detect them anymore.
400        if crate::util::extract_delimiter(content)
401            .unwrap_or("\n")
402            .len()
403            > 1
404        {
405            log::trace!(target: "documentation", "Found two character line ending like CRLF");
406            span.end.column += 1;
407        }
408
409        let rendered = util::load_span_from(content.as_bytes(), span)?;
410
411        // TODO cache the offsets for faster processing and avoiding repeated O(n) ops
412        // let byteoffset2char = rendered.char_indices().enumerate().collect::<indexmap::IndexMap<_usize, (_usize, char)>>();
413        // let rendered_len = byteoffset2char.len();
414
415        let rendered_len = rendered.chars().count();
416
417        log::trace!("extracted from source: >{rendered}< @ {span:?}");
418        let (variant, span, pre, post) = detect_comment_variant(content, &rendered, span)?;
419
420        let len_in_chars = rendered_len.saturating_sub(post + pre);
421
422        if let Some(span_len) = span.one_line_len() {
423            if log::log_enabled!(log::Level::Trace) {
424                let extracted =
425                    sub_chars(rendered.as_str(), pre..rendered_len.saturating_sub(post));
426                log::trace!(target: "quirks", "{span:?} {pre}||{post} for \n extracted: >{extracted}<\n rendered:  >{rendered}<");
427                assert_eq!(len_in_chars, span_len);
428            }
429        }
430
431        let len_in_bytes = rendered.len().saturating_sub(post + pre);
432        let trimmed_literal = Self {
433            variant,
434            len_in_chars,
435            len_in_bytes,
436            rendered,
437            span,
438            pre,
439            post,
440        };
441        Ok(trimmed_literal)
442    }
443}
444
445impl TrimmedLiteral {
446    /// Creates a new (single line) literal from the variant, the content, the
447    /// size of the pre & post and the line/column on which it starts. Fails if
448    /// provided with multiline content (i.e. if the content contains a
449    /// line-break).
450    pub fn from(
451        variant: CommentVariant,
452        content: &str,
453        pre: usize,
454        post: usize,
455        line: usize,
456        column: usize,
457    ) -> std::result::Result<TrimmedLiteral, String> {
458        let content_chars_len = content.chars().count();
459        let mut span = Span {
460            start: LineColumn { line, column },
461            end: LineColumn {
462                line,
463                column: column + content_chars_len,
464            },
465        };
466
467        trim_span(content, &mut span, pre, post + 1);
468
469        Ok(TrimmedLiteral {
470            variant,
471            span,
472            rendered: content.to_string(),
473            pre,
474            post,
475            len_in_chars: content_chars_len - pre - post,
476            len_in_bytes: content.len() - pre - post,
477        })
478    }
479}
480
481impl TrimmedLiteral {
482    /// Represent the rendered content as `str`.
483    ///
484    /// Does not contain `pre` and `post` characters.
485    pub fn as_str(&self) -> &str {
486        &self.rendered.as_str()[self.pre..(self.pre + self.len_in_bytes)]
487    }
488
489    /// The prefix characters.
490    pub fn prefix(&self) -> &str {
491        &self.rendered.as_str()[..self.pre]
492    }
493
494    /// The suffix characters.
495    pub fn suffix(&self) -> &str {
496        &self.rendered.as_str()[(self.pre + self.len_in_bytes)..]
497    }
498
499    /// Full representation including `prefix` and `postfix` characters.
500    pub fn as_untrimmed_str(&self) -> &str {
501        self.rendered.as_str()
502    }
503
504    /// Length in characters, excluding `pre` and `post`.
505    pub fn len_in_chars(&self) -> usize {
506        self.len_in_chars
507    }
508
509    /// Length in bytes, excluding `pre` and `post`.
510    pub fn len(&self) -> usize {
511        self.len_in_bytes
512    }
513
514    pub fn is_empty(&self) -> bool {
515        self.len_in_bytes == 0
516    }
517
518    /// Obtain the number of characters in `pre()`.
519    ///
520    /// Since all pre characters are ASCII, this is equivalent to the number of
521    /// bytes in `pre()`.
522    pub fn pre(&self) -> usize {
523        self.pre
524    }
525
526    /// Obtain the number of characters in `post()`.
527    ///
528    /// Since all pre characters are ASCII, this is equivalent to the number of
529    /// bytes in `post()`.
530    pub fn post(&self) -> usize {
531        self.post
532    }
533
534    /// The span that is covered by this literal.
535    ///
536    /// Covers only the content, no marker or helper characters.
537    pub fn span(&self) -> Span {
538        self.span
539    }
540
541    /// Access the characters via an iterator.
542    pub fn chars(&self) -> impl Iterator<Item = char> + '_ {
543        self.as_str().chars()
544    }
545
546    /// The string variant type, see [`CommentVariant`](self::CommentVariant)
547    /// for details.
548    pub fn variant(&self) -> CommentVariant {
549        self.variant.clone()
550    }
551
552    /// Display helper, mostly used for debug investigations
553    #[allow(unused)]
554    pub(crate) fn display(&self, highlight: Range) -> TrimmedLiteralDisplay<'_> {
555        TrimmedLiteralDisplay::from((self, highlight))
556    }
557}
558
559impl fmt::Debug for TrimmedLiteral {
560    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
561        use console::Style;
562
563        let pick = Style::new().on_black().underlined().dim().cyan();
564        let cutoff = Style::new().on_black().bold().dim().yellow();
565
566        write!(
567            formatter,
568            "{}{}{}",
569            cutoff.apply_to(&self.prefix()),
570            pick.apply_to(&self.as_str()),
571            cutoff.apply_to(&self.suffix()),
572        )
573    }
574}
575
576/// A display style wrapper for a trimmed literal.
577///
578/// Allows better display of coverage results without code duplication.
579///
580/// Consists of literal reference and a relative range to the start of the
581/// literal.
582#[derive(Debug, Clone)]
583pub struct TrimmedLiteralDisplay<'a>(pub &'a TrimmedLiteral, pub Range);
584
585impl<'a, R> From<(R, Range)> for TrimmedLiteralDisplay<'a>
586where
587    R: Into<&'a TrimmedLiteral>,
588{
589    fn from(tuple: (R, Range)) -> Self {
590        let tuple0 = tuple.0.into();
591        Self(tuple0, tuple.1)
592    }
593}
594
595impl<'a> From<TrimmedLiteralDisplay<'a>> for (&'a TrimmedLiteral, Range) {
596    fn from(val: TrimmedLiteralDisplay<'a>) -> Self {
597        (val.0, val.1)
598    }
599}
600
601impl<'a> fmt::Display for TrimmedLiteralDisplay<'a> {
602    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
603        use console::Style;
604
605        // part that is hidden by the trimmed literal, but still present in the actual literal
606        let cutoff = Style::new().on_black().bold().underlined().yellow();
607        // the contextual characters not covered by range `self.1`
608        let context = Style::new().on_black().bold().cyan();
609        // highlight the mistake
610        let highlight = Style::new().on_black().bold().underlined().red().italic();
611        // a special style for any errors, to visualize out of bounds access
612        let oob = Style::new().blink().bold().on_yellow().red();
613
614        // simplify
615        let literal = self.0;
616        let start = self.1.start;
617        let end = self.1.end;
618
619        assert!(start <= end);
620
621        // content without quote characters
622        let data = literal.as_str();
623
624        // colour the preceding quote character
625        // and the context preceding the highlight
626        let (pre, ctx1) = if start > literal.pre() {
627            (
628                // ok since that is ascii, so it's single bytes
629                cutoff.apply_to(&data[..literal.pre()]).to_string(),
630                {
631                    let s = sub_chars(data, literal.pre()..start);
632                    context.apply_to(s.as_str()).to_string()
633                },
634            )
635        } else if start <= literal.len_in_chars() {
636            let s = sub_chars(data, 0..start);
637            (cutoff.apply_to(s.as_str()).to_string(), String::new())
638        } else {
639            (String::new(), "!!!".to_owned())
640        };
641        // highlight the given range
642        let highlight = if end >= literal.len_in_chars() {
643            let s = sub_chars(data, start..literal.len_in_chars());
644            oob.apply_to(s.as_str()).to_string()
645        } else {
646            let s = sub_chars(data, start..end);
647            highlight.apply_to(s.as_str()).to_string()
648        };
649        // color trailing context if any as well as the closing quote character
650        let post_idx = literal.pre() + literal.len_in_chars();
651        let (ctx2, post) = if post_idx > end {
652            let s_ctx = sub_chars(data, end..post_idx);
653            let s_cutoff = sub_chars(data, post_idx..literal.len_in_chars());
654            (
655                context.apply_to(s_ctx.as_str()).to_string(),
656                cutoff.apply_to(s_cutoff.as_str()).to_string(),
657            )
658        } else if end < literal.len_in_chars() {
659            let s = sub_chars(
660                data,
661                end..(literal.len_in_chars() + literal.pre() + literal.post()),
662            );
663            (String::new(), cutoff.apply_to(s.as_str()).to_string())
664        } else {
665            (String::new(), oob.apply_to("!!!").to_string())
666        };
667
668        write!(formatter, "{pre}{ctx1}{highlight}{ctx2}{post}")
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675    use crate::testcase::annotated_literals_raw;
676    use assert_matches::assert_matches;
677
678    #[test]
679    fn variant_detect() {
680        let content = r###"#[doc=r"foo"]"###.to_owned();
681        let rendered = r##"r"foo""##.to_owned();
682        assert_matches!(
683        detect_comment_variant(content.as_str(), &rendered, Span{
684            start: LineColumn {
685                line: 1,
686                column: 6,
687            },
688            end: LineColumn {
689                line: 1,
690                column: 12 + 1,
691            },
692        }), Ok((CommentVariant::MacroDocEqStr(prefix, n_pounds), _, _, _)) => {
693            assert_eq!(n_pounds, 1);
694            assert_eq!(prefix, "#[doc=");
695        });
696    }
697
698    macro_rules! block_comment_test {
699        ($name:ident, $content:literal) => {
700            #[test]
701            fn $name() {
702                const CONTENT: &str = $content;
703                let mut literals = annotated_literals_raw(CONTENT);
704                let literal = literals.next().unwrap();
705                assert!(literals.next().is_none());
706
707                let tl = TrimmedLiteral::load_from(CONTENT, Span::from(literal.span())).unwrap();
708                assert!(CONTENT.starts_with(tl.prefix()));
709                assert!(CONTENT.ends_with(tl.suffix()));
710                assert_eq!(
711                    CONTENT
712                        .chars()
713                        .skip(tl.pre())
714                        .take(tl.len_in_chars())
715                        .collect::<String>(),
716                    tl.as_str().to_owned()
717                )
718            }
719        };
720    }
721
722    block_comment_test!(trimmed_oneline_doc, "/** dooc */");
723    block_comment_test!(trimmed_oneline_mod, "/*! dooc */");
724
725    block_comment_test!(
726        trimmed_multi_doc,
727        "/**
728mood
729*/"
730    );
731    block_comment_test!(
732        trimmed_multi_mod,
733        "/*!
734mood
735*/"
736    );
737}