1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
use std::borrow::Cow;
use std::marker::PhantomData;
use std::path::PathBuf;

use serde::{Deserialize, Serialize};
use syntect::easy::HighlightLines;
use syntect::highlighting::{Theme, ThemeSet};
use syntect::html::{styled_line_to_highlighted_html, IncludeBackground};
use syntect::parsing::SyntaxSet;

use crate::event::{AnnotatedEvent, CodeBlockEvent, Event, RawHtmlEvent};

const DEFAULT_THEME: &str = "InspiredGitHub";

/// Implements syntax highlighting via [`syntect`].
///
/// When applied this wraps the stream in a [`SyntectIter`].
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct Syntect {
    /// The name of the theme to use.  If both this and `theme_path` is not
    /// set then a default theme is loaded.  If `theme_path` is set then the
    /// theme in the `theme_path` folder is used.
    pub theme: Option<String>,
    /// When `theme` is not set, then the path to the `.tmTheme` file to load
    /// otherwise the folder to a collection of theme files.
    pub theme_path: Option<PathBuf>,
}

impl Default for Syntect {
    fn default() -> Syntect {
        Syntect {
            theme: None,
            theme_path: None,
        }
    }
}

implement_processor!(Syntect, SyntectIter);

/// The iterator implementing [`Syntect`].
pub struct SyntectIter<'data, 'options, I: Iterator<Item = AnnotatedEvent<'data>>> {
    source: I,
    syntax_set: SyntaxSet,
    theme: Theme,
    options: PhantomData<&'options Syntect>,
}

impl<'data, 'options, I: Iterator<Item = AnnotatedEvent<'data>>> SyntectIter<'data, 'options, I> {
    pub fn new<O: Into<Cow<'options, Syntect>>>(iterator: I, options: O) -> Self {
        let options = options.into();
        let theme = match (&options.theme, &options.theme_path) {
            (Some(theme), None) => {
                let mut theme_set = ThemeSet::load_defaults();
                match theme_set.themes.remove(theme) {
                    Some(theme) => theme,
                    None => theme_set.themes.remove(DEFAULT_THEME).unwrap(),
                }
            }
            (Some(theme), Some(path)) => {
                let mut theme_set =
                    ThemeSet::load_from_folder(path).expect("failed to initialized theme folder");
                match theme_set.themes.remove(theme) {
                    Some(theme) => theme,
                    None => theme_set.themes.remove(DEFAULT_THEME).unwrap(),
                }
            }
            (None, Some(ref path)) => {
                ThemeSet::get_theme(path).expect("failed to load theme by path")
            }
            (None, None) => {
                let mut theme_set = ThemeSet::load_defaults();
                theme_set.themes.remove(DEFAULT_THEME).unwrap()
            }
        };
        Self {
            source: iterator,
            syntax_set: SyntaxSet::load_defaults_nonewlines(),
            theme,
            options: PhantomData,
        }
    }
}

impl<'data, 'options, I: Iterator<Item = AnnotatedEvent<'data>>> Iterator
    for SyntectIter<'data, 'options, I>
{
    type Item = AnnotatedEvent<'data>;

    fn next(&mut self) -> Option<Self::Item> {
        let annotated_event = self.source.next()?;
        if let Event::CodeBlock(CodeBlockEvent {
            language: Some(ref language),
            ref code,
            ..
        }) = annotated_event.event
        {
            let language = language.as_str();
            let syntax = self
                .syntax_set
                .find_syntax_by_token(language)
                .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
            let mut h = HighlightLines::new(syntax, &self.theme);
            let regions = h.highlight(code.as_str(), &self.syntax_set);
            return Some(AnnotatedEvent::new(
                RawHtmlEvent {
                    html: format!(
                        "<pre><code>{}</code></pre>",
                        styled_line_to_highlighted_html(&regions[..], IncludeBackground::No)
                    )
                    .into(),
                },
                annotated_event.location,
            ));
        }
        Some(annotated_event)
    }
}