pulldown_cmark_mdcat/render/
highlighting.rs

1// Copyright 2018-2020 Sebastian Wiesner <sebastian@swsnr.de>
2
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7//! Tools for syntax highlighting.
8
9use anstyle::{AnsiColor, Effects};
10use std::{
11    io::{Result, Write},
12    sync::OnceLock,
13};
14use syntect::highlighting::{FontStyle, Highlighter, Style, Theme};
15
16static SOLARIZED_DARK_DUMP: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/theme.dump"));
17static THEME: OnceLock<Theme> = OnceLock::new();
18static HIGHLIGHTER: OnceLock<Highlighter> = OnceLock::new();
19
20fn theme() -> &'static Theme {
21    THEME.get_or_init(|| syntect::dumps::from_binary(SOLARIZED_DARK_DUMP))
22}
23
24pub fn highlighter() -> &'static Highlighter<'static> {
25    HIGHLIGHTER.get_or_init(|| Highlighter::new(theme()))
26}
27
28/// Write regions as ANSI 8-bit coloured text.
29///
30/// We use this function to simplify syntax highlighting to 8-bit ANSI values
31/// which every theme provides.  Contrary to 24 bit colours this gives us a good
32/// guarantee that highlighting works with any terminal colour theme, whether
33/// light or dark, and saves us all the hassle of mismatching colours.
34///
35/// We assume Solarized colours here: Solarized cleanly maps to 8-bit ANSI
36/// colours so we can safely map its RGB colour values back to ANSI colours.  We
37/// do so for all accent colours, but leave "base*" colours alone: Base colours
38/// change depending on light or dark Solarized; to address both light and dark
39/// backgrounds we must map all base colours to the default terminal colours.
40///
41/// Furthermore we completely ignore any background colour settings, to avoid
42/// conflicts with the terminal colour themes.
43pub fn write_as_ansi<'a, W: Write, I: Iterator<Item = (Style, &'a str)>>(
44    writer: &mut W,
45    regions: I,
46) -> Result<()> {
47    for (style, text) in regions {
48        let rgb = {
49            let fg = style.foreground;
50            (fg.r, fg.g, fg.b)
51        };
52        let color = match rgb {
53            // base03, base02, base01, base00, base0, base1, base2, and base3
54            (0x00, 0x2b, 0x36)
55            | (0x07, 0x36, 0x42)
56            | (0x58, 0x6e, 0x75)
57            | (0x65, 0x7b, 0x83)
58            | (0x83, 0x94, 0x96)
59            | (0x93, 0xa1, 0xa1)
60            | (0xee, 0xe8, 0xd5)
61            | (0xfd, 0xf6, 0xe3) => None,
62            (0xb5, 0x89, 0x00) => Some(AnsiColor::Yellow.into()),
63            (0xcb, 0x4b, 0x16) => Some(AnsiColor::BrightRed.into()),
64            (0xdc, 0x32, 0x2f) => Some(AnsiColor::Red.into()),
65            (0xd3, 0x36, 0x82) => Some(AnsiColor::Magenta.into()),
66            (0x6c, 0x71, 0xc4) => Some(AnsiColor::BrightMagenta.into()),
67            (0x26, 0x8b, 0xd2) => Some(AnsiColor::Blue.into()),
68            (0x2a, 0xa1, 0x98) => Some(AnsiColor::Cyan.into()),
69            (0x85, 0x99, 0x00) => Some(AnsiColor::Green.into()),
70            (r, g, b) => panic!("Unexpected RGB colour: #{r:2>0x}{g:2>0x}{b:2>0x}"),
71        };
72        let font = style.font_style;
73        let effects = Effects::new()
74            .set(Effects::BOLD, font.contains(FontStyle::BOLD))
75            .set(Effects::ITALIC, font.contains(FontStyle::ITALIC))
76            .set(Effects::UNDERLINE, font.contains(FontStyle::UNDERLINE));
77        let style = anstyle::Style::new().fg_color(color).effects(effects);
78        write!(writer, "{}{}{}", style.render(), text, style.render_reset())?;
79    }
80    Ok(())
81}