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    /// Obtain the number of characters in `pre()`.
515    ///
516    /// Since all pre characters are ASCII, this is equivalent to the number of
517    /// bytes in `pre()`.
518    pub fn pre(&self) -> usize {
519        self.pre
520    }
521
522    /// Obtain the number of characters in `post()`.
523    ///
524    /// Since all pre characters are ASCII, this is equivalent to the number of
525    /// bytes in `post()`.
526    pub fn post(&self) -> usize {
527        self.post
528    }
529
530    /// The span that is covered by this literal.
531    ///
532    /// Covers only the content, no marker or helper characters.
533    pub fn span(&self) -> Span {
534        self.span
535    }
536
537    /// Access the characters via an iterator.
538    pub fn chars(&self) -> impl Iterator<Item = char> + '_ {
539        self.as_str().chars()
540    }
541
542    /// The string variant type, see [`CommentVariant`](self::CommentVariant)
543    /// for details.
544    pub fn variant(&self) -> CommentVariant {
545        self.variant.clone()
546    }
547
548    /// Display helper, mostly used for debug investigations
549    #[allow(unused)]
550    pub(crate) fn display(&self, highlight: Range) -> TrimmedLiteralDisplay {
551        TrimmedLiteralDisplay::from((self, highlight))
552    }
553}
554
555impl fmt::Debug for TrimmedLiteral {
556    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
557        use console::Style;
558
559        let pick = Style::new().on_black().underlined().dim().cyan();
560        let cutoff = Style::new().on_black().bold().dim().yellow();
561
562        write!(
563            formatter,
564            "{}{}{}",
565            cutoff.apply_to(&self.prefix()),
566            pick.apply_to(&self.as_str()),
567            cutoff.apply_to(&self.suffix()),
568        )
569    }
570}
571
572/// A display style wrapper for a trimmed literal.
573///
574/// Allows better display of coverage results without code duplication.
575///
576/// Consists of literal reference and a relative range to the start of the
577/// literal.
578#[derive(Debug, Clone)]
579pub struct TrimmedLiteralDisplay<'a>(pub &'a TrimmedLiteral, pub Range);
580
581impl<'a, R> From<(R, Range)> for TrimmedLiteralDisplay<'a>
582where
583    R: Into<&'a TrimmedLiteral>,
584{
585    fn from(tuple: (R, Range)) -> Self {
586        let tuple0 = tuple.0.into();
587        Self(tuple0, tuple.1)
588    }
589}
590
591impl<'a> From<TrimmedLiteralDisplay<'a>> for (&'a TrimmedLiteral, Range) {
592    fn from(val: TrimmedLiteralDisplay<'a>) -> Self {
593        (val.0, val.1)
594    }
595}
596
597impl<'a> fmt::Display for TrimmedLiteralDisplay<'a> {
598    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
599        use console::Style;
600
601        // part that is hidden by the trimmed literal, but still present in the actual literal
602        let cutoff = Style::new().on_black().bold().underlined().yellow();
603        // the contextual characters not covered by range `self.1`
604        let context = Style::new().on_black().bold().cyan();
605        // highlight the mistake
606        let highlight = Style::new().on_black().bold().underlined().red().italic();
607        // a special style for any errors, to visualize out of bounds access
608        let oob = Style::new().blink().bold().on_yellow().red();
609
610        // simplify
611        let literal = self.0;
612        let start = self.1.start;
613        let end = self.1.end;
614
615        assert!(start <= end);
616
617        // content without quote characters
618        let data = literal.as_str();
619
620        // colour the preceding quote character
621        // and the context preceding the highlight
622        let (pre, ctx1) = if start > literal.pre() {
623            (
624                // ok since that is ascii, so it's single bytes
625                cutoff.apply_to(&data[..literal.pre()]).to_string(),
626                {
627                    let s = sub_chars(data, literal.pre()..start);
628                    context.apply_to(s.as_str()).to_string()
629                },
630            )
631        } else if start <= literal.len_in_chars() {
632            let s = sub_chars(data, 0..start);
633            (cutoff.apply_to(s.as_str()).to_string(), String::new())
634        } else {
635            (String::new(), "!!!".to_owned())
636        };
637        // highlight the given range
638        let highlight = if end >= literal.len_in_chars() {
639            let s = sub_chars(data, start..literal.len_in_chars());
640            oob.apply_to(s.as_str()).to_string()
641        } else {
642            let s = sub_chars(data, start..end);
643            highlight.apply_to(s.as_str()).to_string()
644        };
645        // color trailing context if any as well as the closing quote character
646        let post_idx = literal.pre() + literal.len_in_chars();
647        let (ctx2, post) = if post_idx > end {
648            let s_ctx = sub_chars(data, end..post_idx);
649            let s_cutoff = sub_chars(data, post_idx..literal.len_in_chars());
650            (
651                context.apply_to(s_ctx.as_str()).to_string(),
652                cutoff.apply_to(s_cutoff.as_str()).to_string(),
653            )
654        } else if end < literal.len_in_chars() {
655            let s = sub_chars(
656                data,
657                end..(literal.len_in_chars() + literal.pre() + literal.post()),
658            );
659            (String::new(), cutoff.apply_to(s.as_str()).to_string())
660        } else {
661            (String::new(), oob.apply_to("!!!").to_string())
662        };
663
664        write!(formatter, "{pre}{ctx1}{highlight}{ctx2}{post}")
665    }
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671    use crate::testcase::annotated_literals_raw;
672    use assert_matches::assert_matches;
673
674    #[test]
675    fn variant_detect() {
676        let content = r###"#[doc=r"foo"]"###.to_owned();
677        let rendered = r##"r"foo""##.to_owned();
678        assert_matches!(
679        detect_comment_variant(content.as_str(), &rendered, Span{
680            start: LineColumn {
681                line: 1,
682                column: 6,
683            },
684            end: LineColumn {
685                line: 1,
686                column: 12 + 1,
687            },
688        }), Ok((CommentVariant::MacroDocEqStr(prefix, n_pounds), _, _, _)) => {
689            assert_eq!(n_pounds, 1);
690            assert_eq!(prefix, "#[doc=");
691        });
692    }
693
694    macro_rules! block_comment_test {
695        ($name:ident, $content:literal) => {
696            #[test]
697            fn $name() {
698                const CONTENT: &str = $content;
699                let mut literals = annotated_literals_raw(CONTENT);
700                let literal = literals.next().unwrap();
701                assert!(literals.next().is_none());
702
703                let tl = TrimmedLiteral::load_from(CONTENT, Span::from(literal.span())).unwrap();
704                assert!(CONTENT.starts_with(tl.prefix()));
705                assert!(CONTENT.ends_with(tl.suffix()));
706                assert_eq!(
707                    CONTENT
708                        .chars()
709                        .skip(tl.pre())
710                        .take(tl.len_in_chars())
711                        .collect::<String>(),
712                    tl.as_str().to_owned()
713                )
714            }
715        };
716    }
717
718    block_comment_test!(trimmed_oneline_doc, "/** dooc */");
719    block_comment_test!(trimmed_oneline_mod, "/*! dooc */");
720
721    block_comment_test!(
722        trimmed_multi_doc,
723        "/**
724mood
725*/"
726    );
727    block_comment_test!(
728        trimmed_multi_mod,
729        "/*!
730mood
731*/"
732    );
733}