syntect/
util.rs

1//! Convenient helper functions for common use cases:
2//! * Printing to terminal
3//! * Iterating lines with `\n`s
4//! * Modifying ranges of highlighted output
5
6use crate::highlighting::{Color, Style, StyleModifier};
7#[cfg(feature = "parsing")]
8use crate::parsing::ScopeStackOp;
9use std::fmt::Write;
10use std::ops::Range;
11
12#[inline]
13fn blend_fg_color(fg: Color, bg: Color) -> Color {
14    if fg.a == 0xff {
15        return fg;
16    }
17    let ratio = fg.a as u32;
18    let r = (fg.r as u32 * ratio + bg.r as u32 * (255 - ratio)) / 255;
19    let g = (fg.g as u32 * ratio + bg.g as u32 * (255 - ratio)) / 255;
20    let b = (fg.b as u32 * ratio + bg.b as u32 * (255 - ratio)) / 255;
21    Color {
22        r: r as u8,
23        g: g as u8,
24        b: b as u8,
25        a: 255,
26    }
27}
28
29/// Formats the styled fragments using 24-bit color terminal escape codes.
30/// Meant for debugging and testing.
31///
32/// This function is currently fairly inefficient in its use of escape codes.
33///
34/// Note that this does not currently ever un-set the color so that the end of a line will also get
35/// highlighted with the background.  This means if you might want to use `println!("\x1b[0m");`
36/// after to clear the coloring.
37///
38/// If `bg` is true then the background is also set
39pub fn as_24_bit_terminal_escaped(v: &[(Style, &str)], bg: bool) -> String {
40    let mut s: String = String::new();
41    for &(ref style, text) in v.iter() {
42        if bg {
43            write!(
44                s,
45                "\x1b[48;2;{};{};{}m",
46                style.background.r, style.background.g, style.background.b
47            )
48            .unwrap();
49        }
50        let fg = blend_fg_color(style.foreground, style.background);
51        write!(s, "\x1b[38;2;{};{};{}m{}", fg.r, fg.g, fg.b, text).unwrap();
52    }
53    // s.push_str("\x1b[0m");
54    s
55}
56
57const LATEX_REPLACE: [(&str, &str); 3] = [("\\", "\\\\"), ("{", "\\{"), ("}", "\\}")];
58
59/// Formats the styled fragments using LaTeX textcolor directive.
60///
61/// Usage is similar to the `as_24_bit_terminal_escaped` function:
62///
63/// ```
64/// use syntect::easy::HighlightLines;
65/// use syntect::parsing::SyntaxSet;
66/// use syntect::highlighting::{ThemeSet,Style};
67/// use syntect::util::{as_latex_escaped,LinesWithEndings};
68///
69/// // Load these once at the start of your program
70/// let ps = SyntaxSet::load_defaults_newlines();
71/// let ts = ThemeSet::load_defaults();
72///
73/// let syntax = ps.find_syntax_by_extension("rs").unwrap();
74/// let s = "pub struct Wow { hi: u64 }\nfn blah() -> u64 {}\n";
75///
76/// let mut h = HighlightLines::new(syntax, &ts.themes["InspiredGitHub"]);
77/// for line in LinesWithEndings::from(s) { // LinesWithEndings enables use of newlines mode
78///     let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps).unwrap();
79///     let escaped = as_latex_escaped(&ranges[..]);
80///     println!("{}", escaped);
81/// }
82/// ```
83///
84/// Returned content is intended to be placed inside a fancyvrb
85/// Verbatim environment:
86///
87/// ```latex
88/// \usepackage{fancyvrb}
89/// \usepackage{xcolor}
90/// % ...
91/// % enable comma-separated arguments inside \textcolor
92/// \makeatletter
93/// \def\verbatim@nolig@list{\do\`\do\<\do\>\do\'\do\-}
94/// \makeatother
95/// % ...
96/// \begin{Verbatim}[commandchars=\\\{\}]
97/// % content goes here
98/// \end{Verbatim}
99/// ```
100///
101/// Background color is ignored.
102pub fn as_latex_escaped(v: &[(Style, &str)]) -> String {
103    let mut s: String = String::new();
104    let mut prev_style: Option<Style> = None;
105    let mut content: String;
106    fn textcolor(style: &Style, first: bool) -> String {
107        format!(
108            "{}\\textcolor[RGB]{{{},{},{}}}{{",
109            if first { "" } else { "}" },
110            style.foreground.r,
111            style.foreground.b,
112            style.foreground.g
113        )
114    }
115    for &(style, text) in v.iter() {
116        if let Some(ps) = prev_style {
117            match text {
118                " " => {
119                    s.push(' ');
120                    continue;
121                }
122                "\n" => continue,
123                _ => (),
124            }
125            if style != ps {
126                write!(s, "{}", textcolor(&style, false)).unwrap();
127            }
128        } else {
129            write!(s, "{}", textcolor(&style, true)).unwrap();
130        }
131        content = text.to_string();
132        for &(old, new) in LATEX_REPLACE.iter() {
133            content = content.replace(old, new);
134        }
135        write!(s, "{}", &content).unwrap();
136        prev_style = Some(style);
137    }
138    s.push('}');
139    s
140}
141
142/// Print out the various push and pop operations in a vector
143/// with visual alignment to the line. Obviously for debugging.
144#[cfg(feature = "parsing")]
145pub fn debug_print_ops(line: &str, ops: &[(usize, ScopeStackOp)]) {
146    for &(i, ref op) in ops.iter() {
147        println!("{}", line.trim_end());
148        print!("{: <1$}", "", i);
149        match *op {
150            ScopeStackOp::Push(s) => {
151                println!("^ +{}", s);
152            }
153            ScopeStackOp::Pop(count) => {
154                println!("^ pop {}", count);
155            }
156            ScopeStackOp::Clear(amount) => {
157                println!("^ clear {:?}", amount);
158            }
159            ScopeStackOp::Restore => println!("^ restore"),
160            ScopeStackOp::Noop => println!("noop"),
161        }
162    }
163}
164
165/// An iterator over the lines of a string, including the line endings.
166///
167/// This is similar to the standard library's `lines` method on `str`, except
168/// that the yielded lines include the trailing newline character(s).
169///
170/// You can use it if you're parsing/highlighting some text that you have as a
171/// string. With this, you can use the "newlines" variant of syntax definitions,
172/// which is recommended.
173///
174/// # Examples
175///
176/// ```
177/// use syntect::util::LinesWithEndings;
178///
179/// let mut lines = LinesWithEndings::from("foo\nbar\nbaz");
180///
181/// assert_eq!(Some("foo\n"), lines.next());
182/// assert_eq!(Some("bar\n"), lines.next());
183/// assert_eq!(Some("baz"), lines.next());
184///
185/// assert_eq!(None, lines.next());
186/// ```
187pub struct LinesWithEndings<'a> {
188    input: &'a str,
189}
190
191impl<'a> LinesWithEndings<'a> {
192    pub fn from(input: &'a str) -> LinesWithEndings<'a> {
193        LinesWithEndings { input }
194    }
195}
196
197impl<'a> Iterator for LinesWithEndings<'a> {
198    type Item = &'a str;
199
200    #[inline]
201    fn next(&mut self) -> Option<&'a str> {
202        if self.input.is_empty() {
203            return None;
204        }
205        let split = self
206            .input
207            .find('\n')
208            .map(|i| i + 1)
209            .unwrap_or_else(|| self.input.len());
210        let (line, rest) = self.input.split_at(split);
211        self.input = rest;
212        Some(line)
213    }
214}
215
216/// Split a highlighted line at a byte index in the line into a before and
217/// after component.
218///
219/// This is just a helper that does the somewhat tricky logic including splitting
220/// a span if the index lies on a boundary.
221///
222/// This can be used to extract a chunk of the line out for special treatment
223/// like wrapping it in an HTML tag for extra styling.
224///
225/// Generic for testing purposes and fancier use cases, but intended for use with
226/// the `Vec<(Style, &str)>` returned by `highlight` methods. Look at the source
227/// code for `modify_range` for an example usage.
228#[allow(clippy::type_complexity)]
229pub fn split_at<'a, A: Clone>(
230    v: &[(A, &'a str)],
231    split_i: usize,
232) -> (Vec<(A, &'a str)>, Vec<(A, &'a str)>) {
233    // This function works by gradually reducing the problem into smaller sub-problems from the front
234    let mut rest = v;
235    let mut rest_split_i = split_i;
236
237    // Consume all tokens before the split
238    let mut before = Vec::new();
239    for tok in rest {
240        // Use for instead of a while to avoid bounds checks
241        if tok.1.len() > rest_split_i {
242            break;
243        }
244        before.push(tok.clone());
245        rest_split_i -= tok.1.len();
246    }
247    rest = &rest[before.len()..];
248
249    let mut after = Vec::new();
250    // If necessary, split the token the split falls inside
251    if !rest.is_empty() && rest_split_i > 0 {
252        let mut rest_split_index = rest_split_i;
253        // Splitting in the middle of a multibyte character causes panic,
254        // so if index is in the middle of such a character,
255        // reduce the index by 1.
256        while !rest[0].1.is_char_boundary(rest_split_index) && rest_split_index > 0 {
257            rest_split_index -= 1;
258        }
259        let (sa, sb) = rest[0].1.split_at(rest_split_index);
260        before.push((rest[0].0.clone(), sa));
261        after.push((rest[0].0.clone(), sb));
262        rest = &rest[1..];
263    }
264
265    after.extend_from_slice(rest);
266
267    (before, after)
268}
269
270/// Modify part of a highlighted line using a style modifier, useful for highlighting sections of a line.
271///
272/// # Examples
273///
274/// ```
275/// use syntect::util::modify_range;
276/// use syntect::highlighting::{Style, StyleModifier, FontStyle};
277///
278/// let plain = Style::default();
279/// let boldmod = StyleModifier { foreground: None, background: None, font_style: Some(FontStyle::BOLD) };
280/// let bold = plain.apply(boldmod);
281///
282/// let l = &[(plain, "abc"), (plain, "def"), (plain, "ghi")];
283/// let l2 = modify_range(l, 1..6, boldmod);
284/// assert_eq!(l2, &[(plain, "a"), (bold, "bc"), (bold, "def"), (plain, "ghi")]);
285/// ```
286pub fn modify_range<'a>(
287    v: &[(Style, &'a str)],
288    r: Range<usize>,
289    modifier: StyleModifier,
290) -> Vec<(Style, &'a str)> {
291    let (mut result, in_and_after) = split_at(v, r.start);
292    let (inside, mut after) = split_at(&in_and_after, r.end - r.start);
293
294    result.extend(inside.iter().map(|(style, s)| (style.apply(modifier), *s)));
295    result.append(&mut after);
296    result
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use crate::highlighting::FontStyle;
303
304    #[test]
305    fn test_lines_with_endings() {
306        fn lines(s: &str) -> Vec<&str> {
307            LinesWithEndings::from(s).collect()
308        }
309
310        assert!(lines("").is_empty());
311        assert_eq!(lines("f"), vec!["f"]);
312        assert_eq!(lines("foo"), vec!["foo"]);
313        assert_eq!(lines("foo\n"), vec!["foo\n"]);
314        assert_eq!(lines("foo\nbar"), vec!["foo\n", "bar"]);
315        assert_eq!(lines("foo\nbar\n"), vec!["foo\n", "bar\n"]);
316        assert_eq!(lines("foo\r\nbar"), vec!["foo\r\n", "bar"]);
317        assert_eq!(lines("foo\r\nbar\r\n"), vec!["foo\r\n", "bar\r\n"]);
318        assert_eq!(lines("\nfoo"), vec!["\n", "foo"]);
319        assert_eq!(lines("\n\n\n"), vec!["\n", "\n", "\n"]);
320    }
321
322    #[test]
323    fn test_split_at() {
324        let l: &[(u8, &str)] = &[];
325        let (before, after) = split_at(l, 0); // empty
326        assert_eq!((&before[..], &after[..]), (&[][..], &[][..]));
327
328        let l = &[(0u8, "abc"), (1u8, "def"), (2u8, "ghi")];
329
330        let (before, after) = split_at(l, 0); // at start
331        assert_eq!(
332            (&before[..], &after[..]),
333            (&[][..], &[(0u8, "abc"), (1u8, "def"), (2u8, "ghi")][..])
334        );
335
336        let (before, after) = split_at(l, 4); // inside token
337        assert_eq!(
338            (&before[..], &after[..]),
339            (
340                &[(0u8, "abc"), (1u8, "d")][..],
341                &[(1u8, "ef"), (2u8, "ghi")][..]
342            )
343        );
344
345        let (before, after) = split_at(l, 3); // between tokens
346        assert_eq!(
347            (&before[..], &after[..]),
348            (&[(0u8, "abc")][..], &[(1u8, "def"), (2u8, "ghi")][..])
349        );
350
351        let (before, after) = split_at(l, 9); // just after last token
352        assert_eq!(
353            (&before[..], &after[..]),
354            (&[(0u8, "abc"), (1u8, "def"), (2u8, "ghi")][..], &[][..])
355        );
356
357        let (before, after) = split_at(l, 10); // out of bounds
358        assert_eq!(
359            (&before[..], &after[..]),
360            (&[(0u8, "abc"), (1u8, "def"), (2u8, "ghi")][..], &[][..])
361        );
362
363        let l = &[(0u8, "こんにちは"), (1u8, "世界"), (2u8, "!")];
364
365        let (before, after) = split_at(l, 3);
366
367        assert_eq!(
368            (&before[..], &after[..]),
369            (
370                &[(0u8, "こ")][..],
371                &[(0u8, "んにちは"), (1u8, "世界"), (2u8, "!")][..]
372            )
373        );
374
375        //Splitting inside a multibyte character could cause panic,
376        //so if index is inside such a character,
377        //index is decreased by 1.
378        let (before, after) = split_at(l, 4);
379
380        assert_eq!(
381            (&before[..], &after[..]),
382            (
383                &[(0u8, "こ")][..],
384                &[(0u8, "んにちは"), (1u8, "世界"), (2u8, "!")][..]
385            )
386        );
387    }
388
389    #[test]
390    fn test_as_24_bit_terminal_escaped() {
391        let style = Style {
392            foreground: Color::WHITE,
393            background: Color::BLACK,
394            font_style: FontStyle::default(),
395        };
396
397        // With background
398        let s = as_24_bit_terminal_escaped(&[(style, "hello")], true);
399        assert_eq!(s, "\x1b[48;2;0;0;0m\x1b[38;2;255;255;255mhello");
400
401        // Without background
402        let s = as_24_bit_terminal_escaped(&[(style, "hello")], false);
403        assert_eq!(s, "\x1b[38;2;255;255;255mhello");
404
405        // Blend alpha
406        let mut foreground = Color::WHITE;
407        foreground.a = 128;
408        let style = Style {
409            foreground,
410            background: Color::BLACK,
411            font_style: FontStyle::default(),
412        };
413        let s = as_24_bit_terminal_escaped(&[(style, "hello")], true);
414        assert_eq!(s, "\x1b[48;2;0;0;0m\x1b[38;2;128;128;128mhello");
415    }
416}