Skip to main content

lady_deirdre/format/
snippet.rs

1////////////////////////////////////////////////////////////////////////////////
2// This file is part of "Lady Deirdre", a compiler front-end foundation       //
3// technology.                                                                //
4//                                                                            //
5// This work is proprietary software with source-available code.              //
6//                                                                            //
7// To copy, use, distribute, or contribute to this work, you must agree to    //
8// the terms of the General License Agreement:                                //
9//                                                                            //
10// https://github.com/Eliah-Lakhin/lady-deirdre/blob/master/EULA.md           //
11//                                                                            //
12// The agreement grants a Basic Commercial License, allowing you to use       //
13// this work in non-commercial and limited commercial products with a total   //
14// gross revenue cap. To remove this commercial limit for one of your         //
15// products, you must acquire a Full Commercial License.                      //
16//                                                                            //
17// If you contribute to the source code, documentation, or related materials, //
18// you must grant me an exclusive license to these contributions.             //
19// Contributions are governed by the "Contributions" section of the General   //
20// License Agreement.                                                         //
21//                                                                            //
22// Copying the work in parts is strictly forbidden, except as permitted       //
23// under the General License Agreement.                                       //
24//                                                                            //
25// If you do not or cannot agree to the terms of this Agreement,              //
26// do not use this work.                                                      //
27//                                                                            //
28// This work is provided "as is", without any warranties, express or implied, //
29// except where such disclaimers are legally invalid.                         //
30//                                                                            //
31// Copyright (c) 2024 Ilya Lakhin (Илья Александрович Лахин).                 //
32// All rights reserved.                                                       //
33////////////////////////////////////////////////////////////////////////////////
34
35use std::{
36    borrow::Cow,
37    fmt::{Display, Formatter},
38    iter::repeat,
39    mem::{replace, take},
40};
41
42use crate::{
43    format::{terminal::Escaped, Style},
44    lexis::{
45        Column,
46        Length,
47        Line,
48        Position,
49        PositionSpan,
50        Site,
51        SiteSpan,
52        SourceCode,
53        ToSite,
54        ToSpan,
55        Token,
56        TokenBuffer,
57    },
58    report::ld_unreachable,
59};
60
61/// A configuration of the [Snippet] look and feel features.
62///
63/// This structure is non-exhaustive; new configuration options may be added
64/// in future minor versions of this crate.
65#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
66#[non_exhaustive]
67pub struct SnippetConfig {
68    /// Whether the line numbers shall be shown on the left of the code content.
69    pub show_numbers: bool,
70
71    /// Whether the boxed frame shall surround the code content from all sides.
72    pub draw_frame: bool,
73
74    /// If the code annotations are present in the snippet, whether
75    /// the non-annotated parts of the source code text shall be rendered
76    /// dimmed to focus the user on annotations.
77    pub dim_code: bool,
78
79    /// Whether the box drawing characters shall be rendered using ASCII
80    /// symbols only.
81    pub ascii_drawing: bool,
82
83    /// Whether the CSI [styles](Style) shall be applied.
84    ///
85    /// When set to false, the renderer does not apply built-in styles
86    /// to annotations and other parts of the output, and the syntax
87    /// highlighter will disabled too.
88    pub style: bool,
89
90    /// Whether the snippet caption (header) shall be rendered or disabled.
91    pub caption: bool,
92
93    /// Whether the snippet summary (footer) shall be rendered or disabled.
94    pub summary: bool,
95}
96
97impl Default for SnippetConfig {
98    #[inline(always)]
99    fn default() -> Self {
100        Self::verbose()
101    }
102}
103
104impl SnippetConfig {
105    /// Returns a snippet configuration with all visual features being enabled.
106    #[inline(always)]
107    pub const fn verbose() -> Self {
108        Self {
109            show_numbers: true,
110            draw_frame: true,
111            dim_code: true,
112            ascii_drawing: false,
113            style: true,
114            caption: true,
115            summary: true,
116        }
117    }
118
119    /// Returns a snippet configuration with all visual features being disabled.
120    ///
121    /// In this mode, the [Snippet] will output the source code text without
122    /// annotations as it is.
123    #[inline(always)]
124    pub const fn minimal() -> Self {
125        Self {
126            show_numbers: false,
127            draw_frame: false,
128            dim_code: false,
129            ascii_drawing: false,
130            style: false,
131            caption: false,
132            summary: false,
133        }
134    }
135
136    #[inline(always)]
137    fn cover(&self) -> usize {
138        2
139    }
140
141    #[inline(always)]
142    fn continuation(&self) -> usize {
143        3
144    }
145
146    #[inline(always)]
147    fn margin(&self) -> Length {
148        80
149    }
150
151    #[inline(always)]
152    fn code_style(&self, dim: bool) -> Style {
153        match self.style && self.dim_code && dim {
154            false => Style::default(),
155            true => Style::default().bright_black(),
156        }
157    }
158
159    #[inline(always)]
160    fn annotation_style(&self, priority: AnnotationPriority) -> Style {
161        if !self.style {
162            return Style::default();
163        }
164
165        match priority {
166            AnnotationPriority::Default => Style::default().invert(),
167            AnnotationPriority::Primary => Style::default().invert().red(),
168            AnnotationPriority::Secondary => Style::default().invert().blue(),
169            AnnotationPriority::Note => Style::default().invert().yellow(),
170        }
171    }
172
173    #[inline(always)]
174    fn control(&self) -> char {
175        match self.ascii_drawing {
176            true => ' ',
177            false => '💻',
178        }
179    }
180
181    #[inline(always)]
182    fn placeholder(&self) -> char {
183        match self.ascii_drawing {
184            true => ' ',
185            false => ' ',
186        }
187    }
188
189    #[inline(always)]
190    fn etc(&self) -> &'static PrintString<'static> {
191        static ASCII: PrintString<'static> = PrintString::borrowed("...");
192        static NON_ASCII: PrintString<'static> = PrintString::borrowed("…");
193
194        match self.ascii_drawing {
195            true => &ASCII,
196            false => &NON_ASCII,
197        }
198    }
199
200    #[inline(always)]
201    fn tab(&self) -> &'static PrintString<'static> {
202        static STRING: PrintString<'static> = PrintString::borrowed("    ");
203
204        &STRING
205    }
206
207    #[inline(always)]
208    fn box_vertical(&self) -> &'static PrintString<'static> {
209        static ASCII: PrintString<'static> = PrintString::borrowed("|");
210        static NON_ASCII: PrintString<'static> = PrintString::borrowed("│");
211
212        match self.ascii_drawing {
213            true => &ASCII,
214            false => &NON_ASCII,
215        }
216    }
217
218    #[inline(always)]
219    fn box_horizontal(&self) -> &'static PrintString<'static> {
220        static ASCII: PrintString<'static> = PrintString::borrowed("-");
221        static NON_ASCII: PrintString<'static> = PrintString::borrowed("─");
222
223        match self.ascii_drawing {
224            true => &ASCII,
225            false => &NON_ASCII,
226        }
227    }
228
229    #[inline(always)]
230    fn box_top_left(&self) -> &'static PrintString<'static> {
231        static ASCII: PrintString<'static> = PrintString::borrowed(" ");
232        static NON_ASCII: PrintString<'static> = PrintString::borrowed("╭");
233
234        match self.ascii_drawing {
235            true => &ASCII,
236            false => &NON_ASCII,
237        }
238    }
239
240    #[inline(always)]
241    fn box_top_right(&self) -> &'static PrintString<'static> {
242        static ASCII: PrintString<'static> = PrintString::borrowed("");
243        static NON_ASCII: PrintString<'static> = PrintString::borrowed("╮");
244
245        match self.ascii_drawing {
246            true => &ASCII,
247            false => &NON_ASCII,
248        }
249    }
250
251    #[inline(always)]
252    fn box_bottom_left(&self) -> &'static PrintString<'static> {
253        static ASCII: PrintString<'static> = PrintString::borrowed(" ");
254        static NON_ASCII: PrintString<'static> = PrintString::borrowed("╰");
255
256        match self.ascii_drawing {
257            true => &ASCII,
258            false => &NON_ASCII,
259        }
260    }
261
262    #[inline(always)]
263    fn box_bottom_right(&self) -> &'static PrintString<'static> {
264        static ASCII: PrintString<'static> = PrintString::borrowed("");
265        static NON_ASCII: PrintString<'static> = PrintString::borrowed("╯");
266
267        match self.ascii_drawing {
268            true => &ASCII,
269            false => &NON_ASCII,
270        }
271    }
272
273    #[inline(always)]
274    fn box_middle_delimiter(&self) -> &'static PrintString<'static> {
275        static ASCII: PrintString<'static> = PrintString::borrowed("|=");
276        static ASCII_ALONE: PrintString<'static> = PrintString::borrowed("|==");
277        static NON_ASCII: PrintString<'static> = PrintString::borrowed("╞═");
278        static NON_ASCII_ALONE: PrintString<'static> = PrintString::borrowed("╞══");
279
280        match (self.ascii_drawing, self.draw_frame) {
281            (true, true) => &ASCII,
282            (true, false) => &ASCII_ALONE,
283            (false, true) => &NON_ASCII,
284            (false, false) => &NON_ASCII_ALONE,
285        }
286    }
287
288    #[inline(always)]
289    fn box_middle_left(&self) -> &'static PrintString<'static> {
290        static ASCII: PrintString<'static> = PrintString::borrowed("|");
291        static NON_ASCII: PrintString<'static> = PrintString::borrowed("├");
292
293        match self.ascii_drawing {
294            true => &ASCII,
295            false => &NON_ASCII,
296        }
297    }
298
299    #[inline(always)]
300    fn box_middle_right(&self) -> &'static PrintString<'static> {
301        static ASCII: PrintString<'static> = PrintString::borrowed("|");
302        static NON_ASCII: PrintString<'static> = PrintString::borrowed("┤");
303
304        match self.ascii_drawing {
305            true => &ASCII,
306            false => &NON_ASCII,
307        }
308    }
309
310    #[inline(always)]
311    fn caption_start(&self) -> &'static PrintString<'static> {
312        static ASCII: PrintString<'static> = PrintString::borrowed("-[ ");
313        static NON_ASCII: PrintString<'static> = PrintString::borrowed("─╢ ");
314
315        match self.ascii_drawing {
316            true => &ASCII,
317            false => &NON_ASCII,
318        }
319    }
320
321    #[inline(always)]
322    fn caption_end(&self) -> &'static PrintString<'static> {
323        static ASCII: PrintString<'static> = PrintString::borrowed(" ]");
324        static NON_ASCII: PrintString<'static> = PrintString::borrowed(" ╟");
325        static NON_ASCII_ALONE: PrintString<'static> = PrintString::borrowed(" ║");
326
327        match self.ascii_drawing {
328            true => &ASCII,
329            false => match self.draw_frame {
330                true => &NON_ASCII,
331                false => &NON_ASCII_ALONE,
332            },
333        }
334    }
335
336    #[inline(always)]
337    fn arrow_up_right(&self) -> &'static PrintString<'static> {
338        static ASCII: PrintString<'static> = PrintString::borrowed("|- ");
339        static NON_ASCII: PrintString<'static> = PrintString::borrowed("╭╴ ");
340
341        match self.ascii_drawing {
342            true => &ASCII,
343            false => &NON_ASCII,
344        }
345    }
346
347    #[inline(always)]
348    fn arrow_down_right(&self) -> &'static PrintString<'static> {
349        static ASCII: PrintString<'static> = PrintString::borrowed("|- ");
350        static NON_ASCII: PrintString<'static> = PrintString::borrowed("╰╴ ");
351
352        match self.ascii_drawing {
353            true => &ASCII,
354            false => &NON_ASCII,
355        }
356    }
357
358    #[inline(always)]
359    fn arrow_down_middle(&self) -> &'static PrintString<'static> {
360        static ASCII: PrintString<'static> = PrintString::borrowed("|");
361        static NON_ASCII: PrintString<'static> = PrintString::borrowed("│");
362
363        match self.ascii_drawing {
364            true => &ASCII,
365            false => &NON_ASCII,
366        }
367    }
368}
369
370/// An extension trait of the [Formatter] object that provides a constructor
371/// of the [Snippet].
372pub trait SnippetFormatter<'f> {
373    /// Returns a [Snippet] builder.
374    ///
375    /// Via this builder, you can annotate the source code, configure
376    /// caption (header), and summary (footer) parts of the snippet, and
377    /// configure other snippet rendering features.
378    ///
379    /// Calling the [Snippet::finish] function prints the snippet to
380    /// the Formatter's output.
381    ///
382    /// The `code` parameter specifies a [SourceCode] that needs to be printed.
383    ///
384    /// By default, the snippet uses [minimal](SnippetConfig::minimal) rendering
385    /// configuration in the non-[alternate](Formatter::alternate) mode,
386    /// and the [verbose](SnippetConfig::verbose) configuration in
387    /// the alternate mode.
388    fn snippet<'a, C: SourceCode>(&'a mut self, code: &'a C) -> Snippet<'a, 'f, C>;
389}
390
391impl<'f> SnippetFormatter<'f> for Formatter<'f> {
392    #[inline(always)]
393    fn snippet<'a, C: SourceCode>(&'a mut self, code: &'a C) -> Snippet<'a, 'f, C> {
394        static VERBOSE: SnippetConfig = SnippetConfig::verbose();
395        static MINIMAL: SnippetConfig = SnippetConfig::minimal();
396
397        let config = match self.alternate() {
398            true => &VERBOSE,
399            false => &MINIMAL,
400        };
401
402        Snippet {
403            formatter: self,
404            code,
405            config,
406            caption: PrintString::empty(),
407            summary: PrintString::empty(),
408            highlighter: None,
409            annotations: Vec::with_capacity(4),
410        }
411    }
412}
413
414/// A builder of the source code snippet.
415///
416/// Through the methods of the builder, you can configure snippet's rendering
417/// features and annotate the source code fragments.
418///
419/// The snippets are intended to be used in the custom objects'
420/// [Debug](std::fmt::Debug) and [Display] implementations.
421///
422/// The object is created by calling a [snippet](SnippetFormatter::snippet)
423/// function on the [Formatter] instance (via the [SnippetFormatter] trait).
424///
425/// The [finish](Snippet::finish) method finishes the builder and renders
426/// the snippet into the Formatter's output.
427///
428/// Note that the exact representation of the snippet rendering is not specified
429/// and is a subject to changes and improvements in future minor versions
430/// of this crate.
431pub struct Snippet<'a, 'f, C: SourceCode> {
432    formatter: &'a mut Formatter<'f>,
433    code: &'a C,
434    config: &'a SnippetConfig,
435    caption: PrintString<'a>,
436    summary: PrintString<'a>,
437    highlighter: Option<Box<dyn Highlighter<C::Token> + 'a>>,
438    annotations: Vec<Annotation<'a>>,
439}
440
441impl<'a, 'f, C: SourceCode> Snippet<'a, 'f, C> {
442    /// Sets snippet's general look and feel configuration options.
443    #[inline(always)]
444    pub fn set_config(&mut self, config: &'a SnippetConfig) -> &mut Self {
445        self.config = config;
446
447        self
448    }
449
450    /// Sets snippet's header caption.
451    ///
452    /// **Panic**
453    ///
454    /// Panics if the caption contains more than one line (delimited by `\n`).
455    #[inline(always)]
456    pub fn set_caption(&mut self, caption: impl Into<Cow<'a, str>>) -> &mut Self {
457        let caption = caption.into();
458
459        if caption.contains('\n') {
460            panic!("Multiline captions not supported.");
461        }
462
463        self.caption = PrintString::from_cow(caption);
464
465        self
466    }
467
468    /// Sets snippet's footer summary text.
469    #[inline(always)]
470    pub fn set_summary(&mut self, summary: impl Into<Cow<'a, str>>) -> &mut Self {
471        self.summary = PrintString::from_cow(summary.into());
472
473        self
474    }
475
476    /// Sets the syntax highlighter that tells the renderer how to stylize
477    /// individual tokens of the source code.
478    ///
479    /// Note that since the [Highlighter] is a stateful object, the Snippet
480    /// renderer cannot use it more than once. Therefore, calling the
481    /// [finish](Self::finish) function a second time will not highlight
482    /// the source code.
483    #[inline(always)]
484    pub fn set_highlighter(&mut self, highlighter: impl Highlighter<C::Token> + 'a) -> &mut Self {
485        self.highlighter = Some(Box::new(highlighter));
486
487        self
488    }
489
490    /// Adds an annotation to the source code.
491    ///
492    /// Annotations are the [spans](ToSpan) of source code that you want
493    /// to highlight for the end user, with or without a message,
494    /// such as syntax errors.
495    ///
496    /// The `span` parameter specifies the annotation span.
497    ///
498    /// The `priority` parameter specifies the importance of the annotation.
499    ///
500    /// The `message` parameter specifies a message that will be shown near the
501    /// annotated span. This parameter can be omitted (set to an empty string),
502    /// but the message string must be one line (it should not contain
503    /// `\n` chars).
504    ///
505    /// When the snippet has annotations, the renderer will only show the source
506    /// code lines where the annotations are present, plus a few lines
507    /// surrounding the annotated spans.
508    ///
509    /// **Panic**
510    ///
511    /// Panics if the message has `\n` characters.
512    pub fn annotate(
513        &mut self,
514        span: impl ToSpan,
515        priority: AnnotationPriority,
516        message: impl Into<Cow<'a, str>>,
517    ) -> &mut Self {
518        let message = message.into();
519
520        if message.contains('\n') {
521            panic!("Multiline annotation messages not supported.");
522        }
523
524        let span = match span.to_site_span(self.code) {
525            Some(span) => span,
526
527            None => panic!("Invalid annotation span."),
528        };
529
530        self.annotations.push(Annotation {
531            span,
532            priority,
533            message: PrintString::from_cow(message),
534        });
535
536        self
537    }
538
539    /// Finishes the snippet builder and renders the snippet into
540    /// the Formatter's output.
541    ///
542    /// This function returns a format result with any format errors that may
543    /// occur during interactions with the Formatter. Normally, this function
544    /// returns an Ok result.
545    pub fn finish(&mut self) -> std::fmt::Result {
546        // PREPARE
547
548        let (cover, mut lines) = self.scan();
549
550        let mut code_length = 0;
551
552        for print_line in &mut lines {
553            for string in &print_line.before {
554                code_length = code_length.max(string.length);
555            }
556
557            code_length = code_length.max(print_line.code.length);
558
559            for string in &print_line.after {
560                code_length = code_length.max(string.length);
561            }
562        }
563
564        let caption = match self.config.caption {
565            false => StyleString::empty(),
566            true => StyleString::from_str(self.config, self.caption.as_str()),
567        };
568
569        let summary = match self.config.summary {
570            false => Vec::new(),
571
572            true => {
573                let mut summary = Vec::with_capacity(4);
574
575                if !self.summary.is_empty() {
576                    for summary_line in self.summary.as_str().lines() {
577                        summary.push(StyleString::from_str(self.config, summary_line));
578                    }
579                }
580
581                summary
582            }
583        };
584
585        if self.config.draw_frame && caption.length > 0 {
586            code_length = code_length.max(
587                caption.length
588                    + self.config.caption_start().length
589                    + self.config.caption_end().length,
590            );
591        }
592
593        if self.config.draw_frame && !self.summary.is_empty() {
594            for summary_line in &summary {
595                code_length = code_length.max(summary_line.length);
596            }
597        }
598
599        let numbers_length = (cover.end.line.checked_ilog10().unwrap_or(0) as usize + 1)
600            .max(self.config.etc().length);
601
602        let mut margin: usize = self.config.margin();
603
604        if self.config.draw_frame {
605            margin = margin
606                .checked_sub(2 + self.config.box_vertical().length * 2)
607                .unwrap_or_default();
608        }
609
610        if self.config.show_numbers {
611            margin = margin.checked_sub(numbers_length + 2).unwrap_or_default();
612        }
613
614        code_length = code_length.max(margin);
615
616        // RENDER
617
618        let dim = !self.annotations.is_empty();
619        let has_caption = caption.length > 0;
620        let has_summary = !summary.is_empty();
621        let mut is_first = true;
622
623        if self.config.draw_frame || has_caption || has_summary {
624            StyleString::start(is_first)
625                .with_header_blank(self.config, numbers_length)
626                .with_caption(self.config, code_length, caption)
627                .end(&mut is_first, self.formatter)?;
628        }
629
630        let mut back_distance: usize = 0;
631        let mut skip = false;
632        let mut distances = Vec::with_capacity(lines.len());
633
634        for line in lines.iter().rev() {
635            match line.annotated {
636                false => back_distance += 1,
637                true => back_distance = 0,
638            }
639
640            distances.push(back_distance);
641        }
642
643        back_distance = 0;
644
645        for (forward_distance, line) in distances.into_iter().rev().zip(lines) {
646            if line.annotated || !self.config.show_numbers || !dim {
647                back_distance = 0;
648                skip = false;
649
650                for string in line.before {
651                    StyleString::start(is_first)
652                        .with_header_blank(self.config, numbers_length)
653                        .with_code(
654                            self.config,
655                            dim,
656                            has_caption,
657                            has_summary,
658                            code_length,
659                            string,
660                        )
661                        .end(&mut is_first, self.formatter)?;
662                }
663
664                StyleString::start(is_first)
665                    .with_header_number(self.config, numbers_length, line.number)
666                    .with_code(
667                        self.config,
668                        dim,
669                        has_caption,
670                        has_summary,
671                        code_length,
672                        line.code,
673                    )
674                    .end(&mut is_first, self.formatter)?;
675
676                for string in line.after {
677                    StyleString::start(is_first)
678                        .with_header_blank(self.config, numbers_length)
679                        .with_code(
680                            self.config,
681                            dim,
682                            has_caption,
683                            has_summary,
684                            code_length,
685                            string,
686                        )
687                        .end(&mut is_first, self.formatter)?;
688                }
689
690                continue;
691            }
692
693            back_distance += 1;
694
695            let min_distance = forward_distance.min(back_distance);
696
697            if skip {
698                match min_distance <= self.config.cover() {
699                    true => skip = false,
700                    false => continue,
701                }
702            }
703
704            if min_distance > self.config.cover() {
705                if forward_distance >= self.config.continuation() {
706                    StyleString::start(is_first)
707                        .with_header_etc(self.config, numbers_length)
708                        .with_code_blank(self.config, dim, has_caption, has_summary, code_length)
709                        .end(&mut is_first, self.formatter)?;
710                    skip = true;
711                    continue;
712                }
713            }
714
715            StyleString::start(is_first)
716                .with_header_number(self.config, numbers_length, line.number)
717                .with_code(
718                    self.config,
719                    dim,
720                    has_caption,
721                    has_summary,
722                    code_length,
723                    line.code,
724                )
725                .end(&mut is_first, self.formatter)?;
726        }
727
728        if has_summary {
729            StyleString::start(is_first)
730                .with_header_blank(self.config, numbers_length)
731                .with_delimiter(self.config, code_length)
732                .end(&mut is_first, self.formatter)?;
733
734            for summary in summary {
735                StyleString::start(is_first)
736                    .with_header_blank(self.config, numbers_length)
737                    .with_summary(self.config, code_length, summary)
738                    .end(&mut is_first, self.formatter)?;
739            }
740        }
741
742        if self.config.draw_frame || has_caption || has_summary {
743            StyleString::start(is_first)
744                .with_header_blank(self.config, numbers_length)
745                .with_footer(self.config, code_length)
746                .end(&mut is_first, self.formatter)?;
747        }
748
749        Ok(())
750    }
751
752    fn scan(&mut self) -> (PositionSpan, Vec<ScanLine>) {
753        struct Scanner {
754            position_cover: PositionSpan,
755            site_cover: SiteSpan,
756            buffer: Vec<ScanLine>,
757            empty: bool,
758            line: Line,
759            pending: ScanLine,
760            stack: Vec<usize>,
761        }
762
763        impl Scanner {
764            fn new<C: SourceCode>(snippet: &Snippet<C>) -> Self {
765                let position_cover = snippet
766                    .annotations
767                    .iter()
768                    .map(|annotation| annotation.span.clone())
769                    .reduce(|a, b| a.start.min(b.start)..a.end.max(b.end))
770                    .map(|cover| {
771                        let mut cover = match cover.to_position_span(snippet.code) {
772                            Some(span) => span,
773
774                            // Safety: Site spans are always valid to resolve.
775                            None => unsafe { ld_unreachable!("Invalid site span.") },
776                        };
777
778                        cover.start.line = cover
779                            .start
780                            .line
781                            .checked_sub(snippet.config.cover())
782                            .unwrap_or(1)
783                            .max(1);
784
785                        cover.start.column = 1;
786
787                        cover.end.line = cover
788                            .end
789                            .line
790                            .checked_add(snippet.config.cover())
791                            .unwrap_or(usize::MAX);
792                        cover.end.column = Column::MAX;
793
794                        cover
795                    })
796                    .unwrap_or_else(|| {
797                        let end = match Site::MAX.to_position(snippet.code) {
798                            Some(mut position) => {
799                                position.column = usize::MAX;
800
801                                position
802                            }
803
804                            // Safety: Sites are always valid to resolve.
805                            None => unsafe { ld_unreachable!("Invalid end site.") },
806                        };
807
808                        Position::default()..end
809                    });
810
811                let buffer =
812                    Vec::with_capacity(position_cover.end.line - position_cover.start.line + 1);
813                let line = position_cover.start.line;
814                let pending = ScanLine::new(line);
815                let stack = Vec::with_capacity(snippet.annotations.len());
816                let site_cover = match position_cover.to_site_span(snippet.code) {
817                    Some(span) => span,
818                    // Safety: Position spans are always valid to resolve.
819                    None => unsafe { ld_unreachable!("Invalid position span.") },
820                };
821
822                Self {
823                    position_cover,
824                    site_cover,
825                    buffer,
826                    empty: true,
827                    line,
828                    pending,
829                    stack,
830                }
831            }
832
833            #[inline(always)]
834            fn submit(&mut self, config: &SnippetConfig) {
835                self.line += 1;
836
837                let mut pending = replace(&mut self.pending, ScanLine::new(self.line));
838
839                pending.expand(config);
840
841                pending
842                    .messages
843                    .sort_by_key(|message| message.priority.order());
844
845                self.buffer.push(pending);
846            }
847
848            #[inline(always)]
849            fn top(&self) -> Option<usize> {
850                self.stack.last().copied()
851            }
852        }
853
854        let mut scanner = Scanner::new(self);
855
856        let dim = !self.annotations.is_empty();
857
858        let code_style = self.config.code_style(dim);
859        let mut token_style = None;
860
861        'chunk_loop: for chunk in self.code.chunks(&scanner.site_cover) {
862            let mut site = chunk.site;
863
864            if self.config.style {
865                if let Some(highlighter) = &mut self.highlighter {
866                    token_style = highlighter.token_style(dim, chunk.token);
867                }
868            }
869
870            for ch in chunk.string.chars() {
871                if site < scanner.site_cover.start {
872                    site += 1;
873                    continue;
874                }
875
876                for (index, annotation) in self.annotations.iter().enumerate() {
877                    if annotation.span.end != site {
878                        continue;
879                    }
880
881                    scanner.stack.retain(|item| *item != index);
882                }
883
884                for (index, annotation) in self.annotations.iter().enumerate() {
885                    if annotation.span.start != site {
886                        continue;
887                    }
888
889                    if !annotation.message.is_empty() {
890                        scanner
891                            .pending
892                            .messages
893                            .push(annotation.message(self.config, scanner.pending.code.length));
894                    }
895
896                    match annotation.span.end == site {
897                        true => {
898                            scanner.pending.code.style =
899                                self.config.annotation_style(annotation.priority);
900                            scanner.pending.code.write_placeholder(self.config);
901                            scanner.pending.annotated = true;
902                        }
903
904                        false => {
905                            if ch == '\n' {
906                                scanner.pending.code.style =
907                                    self.config.annotation_style(annotation.priority);
908                                scanner.pending.code.write_placeholder(self.config);
909                            }
910
911                            scanner.stack.push(index);
912                        }
913                    }
914                }
915
916                scanner.pending.code.style = match scanner.top() {
917                    None => token_style.unwrap_or(code_style),
918
919                    Some(top) => {
920                        let priority = match self.annotations.get(top) {
921                            Some(annotation) => annotation.priority,
922
923                            // Safety: Annotation stack is well-formed.
924                            None => unsafe { ld_unreachable!("Missing annotation.") },
925                        };
926
927                        scanner.pending.annotated = true;
928
929                        self.config.annotation_style(priority)
930                    }
931                };
932
933                scanner.empty = false;
934
935                match ch {
936                    '\n' => scanner.submit(self.config),
937                    '\t' => scanner.pending.code.write_tab(self.config),
938                    _ => scanner.pending.code.write_code_char(self.config, ch),
939                }
940
941                site += 1;
942
943                if site >= scanner.site_cover.end {
944                    break 'chunk_loop;
945                }
946            }
947        }
948
949        for annotation in self.annotations.iter() {
950            if annotation.span.start != scanner.site_cover.end {
951                continue;
952            }
953
954            if !annotation.span.is_empty() {
955                continue;
956            }
957
958            if !annotation.message.is_empty() {
959                scanner
960                    .pending
961                    .messages
962                    .push(annotation.message(self.config, scanner.pending.code.length));
963            }
964
965            scanner.pending.annotated = true;
966            scanner.pending.code.style = self.config.annotation_style(annotation.priority);
967            scanner.pending.code.write_placeholder(self.config);
968
969            scanner.empty = false;
970        }
971
972        if !scanner.empty {
973            scanner.submit(self.config);
974        }
975
976        (scanner.position_cover, scanner.buffer)
977    }
978}
979
980/// A syntax highlighter for the [Snippet]'s source code.
981///
982/// The Snippet's renderer sequentially feeds tokens to the Highlighter, and
983/// the Highlighter decides how this token should be stylized.
984///
985/// The implementor could be a stateful object that makes decisions based on
986/// the prior token's context.
987pub trait Highlighter<T: Token> {
988    /// Returns a [style](Style) of the token.
989    ///
990    /// The `dim` flag specifies if the token style is assumed to be dimmed,
991    /// with lesser contrast than usual. In other words, if the end user
992    /// attention should not be focused on this token.
993    ///
994    /// The `token` parameter specifies a token that needs to be stylized.
995    ///
996    /// This function can take into account the previous tokens based on the
997    /// implementor's inner state, and the function can change the inner state
998    /// for the future token styles.
999    ///
1000    /// If the function returns None, the token style is left to the renderer's
1001    /// defaults.
1002    fn token_style(&mut self, dim: bool, token: T) -> Option<Style>;
1003}
1004
1005/// A degree of importance of the [Snippet]'s annotation.
1006#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
1007pub enum AnnotationPriority {
1008    /// An annotation without extra user attention priority.
1009    #[default]
1010    Default,
1011
1012    /// An annotation with the highest user attention priority.
1013    Primary,
1014
1015    /// An annotation with moderate user attention priority.
1016    Secondary,
1017
1018    /// An annotation with the lowest user attention priority.
1019    Note,
1020}
1021
1022impl AnnotationPriority {
1023    #[inline(always)]
1024    fn order(&self) -> usize {
1025        match self {
1026            Self::Primary => 1,
1027            Self::Secondary => 2,
1028            Self::Note => 3,
1029            Self::Default => 4,
1030        }
1031    }
1032}
1033
1034struct ScanLine {
1035    number: Line,
1036    before: Vec<StyleString>,
1037    code: StyleString,
1038    after: Vec<StyleString>,
1039    messages: Vec<Message>,
1040    annotated: bool,
1041}
1042
1043impl ScanLine {
1044    #[inline(always)]
1045    fn new(number: Line) -> Self {
1046        Self {
1047            number,
1048            before: Vec::new(),
1049            code: StyleString::new(),
1050            after: Vec::new(),
1051            messages: Vec::new(),
1052            annotated: false,
1053        }
1054    }
1055
1056    fn expand(&mut self, config: &SnippetConfig) {
1057        enum Segment {
1058            End(Message),
1059            Middle {
1060                offset: Column,
1061                priority: AnnotationPriority,
1062            },
1063        }
1064
1065        impl Segment {
1066            #[inline(always)]
1067            fn span(&self, config: &SnippetConfig) -> SiteSpan {
1068                match self {
1069                    Self::Middle { offset, .. } => Message::span_down_middle(config, *offset),
1070                    Self::End(message) => message.span_down_right(config),
1071                }
1072            }
1073        }
1074
1075        let mut pending = take(&mut self.messages)
1076            .into_iter()
1077            .map(|message| Some(message))
1078            .collect::<Vec<_>>();
1079
1080        let mut left = pending.len();
1081
1082        if left > 1 {
1083            for message in pending.iter_mut() {
1084                if let Some(message) = message {
1085                    if message.priority == AnnotationPriority::Primary {
1086                        continue;
1087                    }
1088                }
1089
1090                let message = match take(message) {
1091                    Some(message) => message,
1092
1093                    // Safety: All messages initialized in the beginning.
1094                    None => unsafe { ld_unreachable!("Unset first message.") },
1095                };
1096
1097                left -= 1;
1098
1099                let mut string = StyleString::new();
1100
1101                string.style = config.code_style(true);
1102                string.write_blanks(message.offset);
1103
1104                string.style = config.annotation_style(message.priority).no_emphasis();
1105                string.write_sanitized(config.arrow_up_right());
1106
1107                string.style = Style::new();
1108                string.append(message.string);
1109
1110                self.before.push(string);
1111
1112                break;
1113            }
1114        }
1115
1116        let mut segments = Vec::<Segment>::with_capacity(left);
1117
1118        while left > 0 {
1119            'outer: for pending in pending.iter_mut().rev() {
1120                let message = match pending {
1121                    Some(pending) => pending,
1122                    None => continue,
1123                };
1124
1125                let mut index = 0;
1126
1127                let span = message.span_down_right(config);
1128
1129                for probe in segments.iter() {
1130                    let probe_span = probe.span(config);
1131
1132                    if span.end > probe_span.start && span.start < probe_span.end {
1133                        continue 'outer;
1134                    }
1135
1136                    if span.start >= probe_span.end {
1137                        index += 1;
1138                        continue;
1139                    }
1140
1141                    break;
1142                }
1143
1144                let segment = match take(pending) {
1145                    Some(message) => Segment::End(message),
1146
1147                    // Safety: Discriminant checked above.
1148                    None => unsafe { ld_unreachable!("Missing pending item.") },
1149                };
1150
1151                left -= 1;
1152
1153                segments.insert(index, segment);
1154            }
1155
1156            'outer: for message in pending.iter().flatten() {
1157                let mut index = 0;
1158
1159                let span = Message::span_down_middle(config, message.offset);
1160
1161                for probe in segments.iter() {
1162                    let probe_span = probe.span(config);
1163
1164                    if span.end > probe_span.start && span.start < probe_span.end {
1165                        continue 'outer;
1166                    }
1167
1168                    if span.start >= probe_span.end {
1169                        index += 1;
1170                        continue;
1171                    }
1172
1173                    break;
1174                }
1175
1176                segments.insert(
1177                    index,
1178                    Segment::Middle {
1179                        offset: message.offset,
1180                        priority: message.priority,
1181                    },
1182                );
1183            }
1184
1185            let mut string = StyleString::new();
1186
1187            let mut cursor = 0;
1188            for segment in replace(&mut segments, Vec::with_capacity(left)) {
1189                match segment {
1190                    Segment::Middle { offset, priority } => {
1191                        string.style = config.code_style(true);
1192                        string.write_blanks(offset - cursor);
1193
1194                        let drawing = config.arrow_down_middle();
1195
1196                        string.style = config.annotation_style(priority).no_emphasis();
1197                        string.write_sanitized(&drawing);
1198
1199                        cursor = offset + drawing.length;
1200                    }
1201
1202                    Segment::End(message) => {
1203                        let span = message.span_down_right(config);
1204
1205                        string.style = config.code_style(true);
1206                        string.write_blanks(span.start - cursor);
1207
1208                        string.style = config.annotation_style(message.priority).no_emphasis();
1209                        string.write_sanitized(config.arrow_down_right());
1210
1211                        string.append(message.string);
1212
1213                        cursor = span.end;
1214                    }
1215                }
1216            }
1217
1218            self.after.push(string);
1219        }
1220    }
1221}
1222
1223struct Annotation<'a> {
1224    span: SiteSpan,
1225    priority: AnnotationPriority,
1226    message: PrintString<'a>,
1227}
1228
1229impl<'a> Annotation<'a> {
1230    #[inline(always)]
1231    fn message(&self, config: &SnippetConfig, offset: Column) -> Message {
1232        Message {
1233            offset,
1234            priority: self.priority,
1235            string: StyleString::from_str(config, self.message.as_str()),
1236        }
1237    }
1238}
1239
1240struct Message {
1241    offset: Column,
1242    priority: AnnotationPriority,
1243    string: StyleString,
1244}
1245
1246impl Message {
1247    #[inline(always)]
1248    #[allow(unused)]
1249    fn span_up_right(&self, config: &SnippetConfig) -> SiteSpan {
1250        let drawing = config.arrow_up_right();
1251
1252        self.offset..(self.offset + drawing.length + self.string.length)
1253    }
1254
1255    #[inline(always)]
1256    fn span_down_right(&self, config: &SnippetConfig) -> SiteSpan {
1257        let drawing = config.arrow_down_right();
1258
1259        self.offset..(self.offset + drawing.length + self.string.length)
1260    }
1261
1262    #[inline(always)]
1263    fn span_down_middle(config: &SnippetConfig, offset: Column) -> SiteSpan {
1264        let drawing = config.arrow_down_middle();
1265
1266        offset..(offset + drawing.length)
1267    }
1268}
1269
1270struct StyleString {
1271    text: String,
1272    length: Length,
1273    start_style: Style,
1274    end_style: Style,
1275    style: Style,
1276}
1277
1278impl Display for StyleString {
1279    #[inline(always)]
1280    fn fmt(&self, formatter: &mut Formatter) -> std::fmt::Result {
1281        formatter.write_str(&self.text)
1282    }
1283}
1284
1285impl StyleString {
1286    #[inline(always)]
1287    fn new() -> Self {
1288        Self {
1289            text: String::with_capacity(120),
1290            length: 0,
1291            start_style: Style::new(),
1292            end_style: Style::new(),
1293            style: Style::new(),
1294        }
1295    }
1296
1297    #[inline(always)]
1298    fn empty() -> Self {
1299        Self {
1300            text: String::new(),
1301            length: 0,
1302            start_style: Style::new(),
1303            end_style: Style::new(),
1304            style: Style::new(),
1305        }
1306    }
1307
1308    fn from_str(config: &SnippetConfig, source: impl AsRef<str>) -> Self {
1309        let source = source.as_ref();
1310
1311        let mut target = Self::new();
1312
1313        let buffer = TokenBuffer::from(source);
1314
1315        for chunk in buffer.chunks(..) {
1316            match chunk.token {
1317                Escaped::CSI => {
1318                    if !config.style {
1319                        continue;
1320                    }
1321                }
1322                _ => target.length += chunk.length,
1323            }
1324
1325            target.text.push_str(chunk.string);
1326        }
1327
1328        target
1329    }
1330
1331    #[inline]
1332    fn start(is_first: bool) -> Self {
1333        let mut string = Self::new();
1334
1335        if !is_first {
1336            string.text.push('\n');
1337        }
1338
1339        string
1340    }
1341
1342    fn with_header_blank(self, config: &SnippetConfig, alignment: Length) -> Self {
1343        self.with_header(config, alignment, "")
1344    }
1345
1346    fn with_header_etc(self, config: &SnippetConfig, alignment: Length) -> Self {
1347        self.with_header(config, alignment, config.etc().as_str())
1348    }
1349
1350    fn with_header_number(self, config: &SnippetConfig, alignment: Length, number: Line) -> Self {
1351        self.with_header(config, alignment, number.to_string().as_str())
1352    }
1353
1354    #[inline]
1355    fn with_header(mut self, config: &SnippetConfig, alignment: Length, text: &str) -> Self {
1356        if !config.show_numbers {
1357            return self;
1358        }
1359
1360        self.write_blanks(1);
1361        self.write_sanitized(&PrintString::owned(format!("{: >1$}", text, alignment)));
1362        self.write_blanks(1);
1363
1364        self
1365    }
1366
1367    fn with_caption(
1368        mut self,
1369        config: &SnippetConfig,
1370        mut alignment: Length,
1371        caption: Self,
1372    ) -> Self {
1373        self.write_sanitized(config.box_top_left());
1374        self.write_sanitized(config.box_horizontal());
1375
1376        let has_caption = caption.length > 0;
1377
1378        if has_caption {
1379            alignment -= config.caption_start().length;
1380            self.write_sanitized(config.caption_start());
1381
1382            alignment -= caption.length;
1383            self.append(caption);
1384
1385            alignment -= config.caption_end().length;
1386            self.write_sanitized(config.caption_end());
1387        }
1388
1389        match config.draw_frame {
1390            true => {
1391                self.repeat_sanitized(config.box_horizontal(), alignment + 1);
1392                self.write_sanitized(config.box_top_right());
1393            }
1394
1395            false => {
1396                if !has_caption {
1397                    self.write_sanitized(config.box_horizontal());
1398                }
1399            }
1400        }
1401
1402        self
1403    }
1404
1405    fn with_code(
1406        mut self,
1407        config: &SnippetConfig,
1408        dim: bool,
1409        has_caption: bool,
1410        has_summary: bool,
1411        mut alignment: Length,
1412        code: Self,
1413    ) -> Self {
1414        let code_style = config.code_style(dim);
1415
1416        if config.draw_frame || config.show_numbers || has_caption || has_summary {
1417            self.write_sanitized(config.box_vertical());
1418            self.style = code_style;
1419            self.write_blanks(1);
1420        }
1421
1422        alignment -= code.length;
1423        self.append(code);
1424
1425        if config.draw_frame {
1426            self.style = code_style;
1427            self.write_blanks(alignment + 1);
1428
1429            self.style = Style::new();
1430            self.write_sanitized(config.box_vertical());
1431        }
1432
1433        self
1434    }
1435
1436    fn with_code_blank(
1437        mut self,
1438        config: &SnippetConfig,
1439        dim: bool,
1440        has_caption: bool,
1441        has_summary: bool,
1442        alignment: Length,
1443    ) -> Self {
1444        if config.draw_frame || config.show_numbers || has_caption || has_summary {
1445            self.write_sanitized(config.box_vertical());
1446        }
1447
1448        if config.draw_frame {
1449            self.style = config.code_style(dim);
1450            self.write_blanks(alignment + 2);
1451
1452            self.style = Style::new();
1453            self.write_sanitized(config.box_vertical());
1454        }
1455
1456        if self.length == 0 {
1457            self.length = 1;
1458        }
1459
1460        self
1461    }
1462
1463    fn with_delimiter(mut self, config: &SnippetConfig, alignment: Length) -> Self {
1464        match config.draw_frame {
1465            true => {
1466                self.write_sanitized(config.box_middle_left());
1467                self.repeat_sanitized(config.box_horizontal(), alignment + 2);
1468                self.write_sanitized(config.box_middle_right());
1469            }
1470
1471            false => {
1472                self.write_sanitized(config.box_middle_delimiter());
1473            }
1474        }
1475
1476        self
1477    }
1478
1479    fn with_summary(
1480        mut self,
1481        config: &SnippetConfig,
1482        mut alignment: Length,
1483        summary: Self,
1484    ) -> Self {
1485        self.write_sanitized(config.box_vertical());
1486        self.write_blanks(1);
1487
1488        alignment -= summary.length;
1489        self.append(summary);
1490
1491        if config.draw_frame {
1492            self.write_blanks(alignment + 1);
1493            self.write_sanitized(config.box_vertical());
1494        }
1495
1496        self
1497    }
1498
1499    fn with_footer(mut self, config: &SnippetConfig, alignment: Length) -> Self {
1500        self.write_sanitized(config.box_bottom_left());
1501        self.write_sanitized(config.box_horizontal());
1502
1503        match config.draw_frame {
1504            true => {
1505                self.repeat_sanitized(config.box_horizontal(), alignment + 1);
1506                self.write_sanitized(config.box_bottom_right());
1507            }
1508
1509            false => {
1510                self.write_sanitized(config.box_horizontal());
1511            }
1512        }
1513
1514        self
1515    }
1516
1517    #[inline]
1518    fn end(mut self, is_first: &mut bool, formatter: &mut Formatter) -> std::fmt::Result {
1519        if self.length == 0 {
1520            return Ok(());
1521        }
1522
1523        *is_first = false;
1524
1525        self.style = Style::new();
1526        self.submit_style();
1527
1528        Display::fmt(&self, formatter)
1529    }
1530
1531    #[inline(always)]
1532    fn write_code_char(&mut self, config: &SnippetConfig, mut ch: char) {
1533        if ch.is_control() {
1534            ch = config.control();
1535        }
1536
1537        self.submit_style();
1538
1539        self.text.push(ch);
1540        self.length += 1;
1541    }
1542
1543    #[inline(always)]
1544    fn write_sanitized(&mut self, string: &PrintString) {
1545        self.submit_style();
1546
1547        self.text.push_str(string.as_str());
1548        self.length += string.length;
1549    }
1550
1551    #[inline(always)]
1552    fn repeat_sanitized(&mut self, string: &PrintString, mut count: usize) {
1553        if count == 0 {
1554            return;
1555        }
1556
1557        self.submit_style();
1558
1559        while count > 0 {
1560            self.text.push_str(string.as_str());
1561            self.length += string.length;
1562            count -= 1;
1563        }
1564    }
1565
1566    #[inline(always)]
1567    fn write_placeholder(&mut self, config: &SnippetConfig) {
1568        self.submit_style();
1569
1570        self.text.push(config.placeholder());
1571        self.length += 1;
1572    }
1573
1574    #[inline(always)]
1575    fn write_tab(&mut self, config: &SnippetConfig) {
1576        self.write_sanitized(config.tab());
1577    }
1578
1579    #[inline(always)]
1580    fn write_blanks(&mut self, count: Length) {
1581        if count == 0 {
1582            return;
1583        }
1584
1585        self.submit_style();
1586
1587        self.text.extend(repeat(' ').take(count));
1588        self.length += count;
1589    }
1590
1591    fn append(&mut self, other: StyleString) {
1592        self.style = other.start_style;
1593
1594        if !other.text.is_empty() {
1595            self.submit_style();
1596            self.text.push_str(other.text.as_str());
1597        }
1598
1599        self.length += other.length;
1600
1601        self.end_style = other.end_style;
1602        self.style = other.style;
1603    }
1604
1605    fn submit_style(&mut self) {
1606        if self.end_style == self.style {
1607            return;
1608        }
1609
1610        Style::change(&self.end_style, &self.style, &mut self.text);
1611
1612        self.end_style = self.style;
1613
1614        if self.length == 0 {
1615            self.start_style = self.end_style;
1616        }
1617    }
1618}
1619
1620struct PrintString<'a> {
1621    string: Cow<'a, str>,
1622    length: Length,
1623}
1624
1625impl<'a> PrintString<'a> {
1626    #[inline(always)]
1627    const fn empty() -> Self {
1628        Self {
1629            string: Cow::Borrowed(""),
1630            length: 0,
1631        }
1632    }
1633
1634    #[inline(always)]
1635    fn owned(string: String) -> Self {
1636        Self {
1637            length: string.chars().count(),
1638            string: Cow::from(string),
1639        }
1640    }
1641
1642    #[inline(always)]
1643    const fn borrowed(string: &'a str) -> Self {
1644        Self {
1645            length: Self::length_of(string.as_bytes()),
1646            string: Cow::Borrowed(string),
1647        }
1648    }
1649
1650    #[inline(always)]
1651    fn from_cow(string: Cow<'a, str>) -> Self {
1652        Self {
1653            length: string.chars().count(),
1654            string,
1655        }
1656    }
1657
1658    #[inline(always)]
1659    fn as_str(&self) -> &str {
1660        self.string.as_ref()
1661    }
1662
1663    #[inline(always)]
1664    fn is_empty(&self) -> bool {
1665        self.string.is_empty()
1666    }
1667
1668    #[inline(always)]
1669    const fn length_of(bytes: &[u8]) -> Length {
1670        const PAT_1: u8 = 0b10000000;
1671        const PAT_3: u8 = 0b11100000;
1672        const PAT_4: u8 = 0b11110000;
1673
1674        let mut index = 0;
1675        let mut length = 0;
1676
1677        while index < bytes.len() {
1678            length += 1;
1679
1680            let first = bytes[index];
1681
1682            if first & PAT_1 == 0 {
1683                index += 1;
1684                continue;
1685            }
1686
1687            let prefix = first & PAT_4;
1688
1689            match prefix {
1690                PAT_4 => index += 4,
1691                PAT_3 => index += 3,
1692                _ => index += 2,
1693            }
1694        }
1695
1696        length
1697    }
1698}
1699
1700#[cfg(test)]
1701mod tests {
1702    use crate::format::{snippet::StyleString, SnippetConfig, Style, TerminalString};
1703
1704    #[test]
1705    fn test_csi_detection() {
1706        let string = StyleString::from_str(&SnippetConfig::verbose(), "hello world");
1707        assert_eq!(string.length, 11);
1708
1709        let string = StyleString::from_str(
1710            &SnippetConfig::verbose(),
1711            &format!("hello{}world", " ".apply(Style::new())),
1712        );
1713        assert_eq!(string.length, 11);
1714        assert_eq!(string.text.len(), 11);
1715
1716        let string = StyleString::from_str(
1717            &SnippetConfig::verbose(),
1718            &format!("hello{}world", " ".apply(Style::new().bold())),
1719        );
1720        assert_eq!(string.length, 11);
1721        assert_ne!(string.text.len(), 11);
1722
1723        let string = StyleString::from_str(
1724            &SnippetConfig::minimal(),
1725            &format!("hello{}world", " ".apply(Style::new().bold())),
1726        );
1727        assert_eq!(string.length, 11);
1728        assert_eq!(string.text.len(), 11);
1729    }
1730}