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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
//! Rendering highlighted code as HTML+CSS
use std::fmt::Write;
use parsing::{ScopeStackOp, BasicScopeStackOp, Scope, ScopeStack, SyntaxDefinition, SyntaxSet, SCOPE_REPO};
use easy::{HighlightLines, HighlightFile};
use highlighting::{self, Style, Theme, Color};
use escape::Escape;
use std::io::{self, BufRead};
use std::path::Path;

/// Only one style for now, I may add more class styles later.
/// Just here so I don't have to change the API
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ClassStyle {
    /// The classes are the atoms of the scope separated by spaces
    /// (e.g `source.php` becomes `source php`).
    /// This isn't that fast since it has to use the scope repository
    /// to look up scope names.
    Spaced,
}

fn scope_to_classes(s: &mut String, scope: Scope, style: ClassStyle) {
    assert!(style == ClassStyle::Spaced); // TODO more styles
    let repo = SCOPE_REPO.lock().unwrap();
    for i in 0..(scope.len()) {
        let atom = scope.atom_at(i as usize);
        let atom_s = repo.atom_str(atom);
        if i != 0 {
            s.push_str(" ")
        }
        s.push_str(atom_s);
    }
}

/// Convenience method that combines `start_coloured_html_snippet`, `styles_to_coloured_html`
/// and `HighlightLines` from `syntect::easy` to create a full highlighted HTML snippet for
/// a string (which can contain many lines).
///
/// Note that the `syntax` passed in must be from a `SyntaxSet` compiled for no newline characters.
/// This is easy to get with `SyntaxSet::load_defaults_nonewlines()`. If you think this is the wrong
/// choice of `SyntaxSet` to accept, I'm not sure of it either, email me.
pub fn highlighted_snippet_for_string(s: &str, syntax: &SyntaxDefinition, theme: &Theme) -> String {
    let mut output = String::new();
    let mut highlighter = HighlightLines::new(syntax, theme);
    let c = theme.settings.background.unwrap_or(highlighting::WHITE);
    write!(output,
           "<pre style=\"background-color:#{:02x}{:02x}{:02x};\">\n",
           c.r,
           c.g,
           c.b)
        .unwrap();
    for line in s.lines() {
        let regions = highlighter.highlight(line);
        let html = styles_to_coloured_html(&regions[..], IncludeBackground::IfDifferent(c));
        output.push_str(&html);
        output.push('\n');
    }
    output.push_str("</pre>\n");
    output
}

/// Convenience method that combines `start_coloured_html_snippet`, `styles_to_coloured_html`
/// and `HighlightFile` from `syntect::easy` to create a full highlighted HTML snippet for
/// a file.
///
/// Note that the `syntax` passed in must be from a `SyntaxSet` compiled for no newline characters.
/// This is easy to get with `SyntaxSet::load_defaults_nonewlines()`. If you think this is the wrong
/// choice of `SyntaxSet` to accept, I'm not sure of it either, email me.
pub fn highlighted_snippet_for_file<P: AsRef<Path>>(path: P,
                                                    ss: &SyntaxSet,
                                                    theme: &Theme)
                                                    -> io::Result<String> {
    // TODO reduce code duplication with highlighted_snippet_for_string
    let mut output = String::new();
    let mut highlighter = HighlightFile::new(path, ss, theme)?;
    let c = theme.settings.background.unwrap_or(highlighting::WHITE);
    write!(output,
           "<pre style=\"background-color:#{:02x}{:02x}{:02x};\">\n",
           c.r,
           c.g,
           c.b)
        .unwrap();
    for maybe_line in highlighter.reader.lines() {
        let line = maybe_line?;
        let regions = highlighter.highlight_lines.highlight(&line);
        let html = styles_to_coloured_html(&regions[..], IncludeBackground::IfDifferent(c));
        output.push_str(&html);
        output.push('\n');
    }
    output.push_str("</pre>\n");
    Ok(output)
}

/// Output HTML for a line of code with `<span>` elements
/// specifying classes for each token. The span elements are nested
/// like the scope stack and the scopes are mapped to classes based
/// on the `ClassStyle` (see it's docs).
///
/// For this to work correctly you must concatenate all the lines in a `<pre>`
/// tag since some span tags opened on a line may not be closed on that line
/// and later lines may close tags from previous lines.
pub fn tokens_to_classed_html(line: &str,
                              ops: &[(usize, ScopeStackOp)],
                              style: ClassStyle)
                              -> String {
    let mut s = String::with_capacity(line.len() + ops.len() * 8); // a guess
    let mut cur_index = 0;
    let mut stack = ScopeStack::new();
    for &(i, ref op) in ops {
        if i > cur_index {
            write!(s, "{}", Escape(&line[cur_index..i])).unwrap();
            cur_index = i
        }
        stack.apply_with_hook(op, |basic_op, _| {
            match basic_op {
                BasicScopeStackOp::Push(scope) => {
                    s.push_str("<span class=\"");
                    scope_to_classes(&mut s, scope, style);
                    s.push_str("\">");
                }
                BasicScopeStackOp::Pop => {
                    s.push_str("</span>");
                }
            }
        });
    }
    s
}

/// Determines how background colour attributes are generated
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum IncludeBackground {
    /// Don't include `background-color`, for performance or so that you can use your own background.
    No,
    /// Set background colour attributes on every node
    Yes,
    /// Only set the `background-color` if it is different than the default (presumably set on a parent element)
    IfDifferent(Color),
}

fn write_css_color(s: &mut String, c: Color) {
    if c.a != 0xFF {
        write!(s,"#{:02x}{:02x}{:02x}{:02x}",c.r,c.g,c.b,c.a).unwrap();
    } else {
        write!(s,"#{:02x}{:02x}{:02x}",c.r,c.g,c.b).unwrap();
    }
}

/// Output HTML for a line of code with `<span>` elements using inline
/// `style` attributes to set the correct font attributes.
/// The `bg` attribute determines if the spans will have the `background-color`
/// attribute set. See the `IncludeBackground` enum's docs.
///
/// The lines returned don't include a newline at the end.
/// # Examples
///
/// ```
/// use syntect::easy::HighlightLines;
/// use syntect::parsing::SyntaxSet;
/// use syntect::highlighting::{ThemeSet, Style};
/// use syntect::html::{styles_to_coloured_html, IncludeBackground};
///
/// // Load these once at the start of your program
/// let ps = SyntaxSet::load_defaults_nonewlines();
/// let ts = ThemeSet::load_defaults();
///
/// let syntax = ps.find_syntax_by_name("Ruby").unwrap();
/// let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]);
/// let regions = h.highlight("5");
/// let html = styles_to_coloured_html(&regions[..], IncludeBackground::No);
/// assert_eq!(html, "<span style=\"color:#d08770;\">5</span>");
/// ```
pub fn styles_to_coloured_html(v: &[(Style, &str)], bg: IncludeBackground) -> String {
    let mut s: String = String::new();
    for &(ref style, text) in v.iter() {
        write!(s, "<span style=\"").unwrap();
        let include_bg = match bg {
            IncludeBackground::Yes => true,
            IncludeBackground::No => false,
            IncludeBackground::IfDifferent(c) => (style.background != c),
        };
        if include_bg {
            write!(s, "background-color:").unwrap();
            write_css_color(&mut s, style.background);
            write!(s, ";").unwrap();
        }
        if style.font_style.contains(highlighting::FONT_STYLE_UNDERLINE) {
            write!(s, "text-decoration:underline;").unwrap();
        }
        if style.font_style.contains(highlighting::FONT_STYLE_BOLD) {
            write!(s, "font-weight:bold;").unwrap();
        }
        if style.font_style.contains(highlighting::FONT_STYLE_ITALIC) {
            write!(s, "font-style:italic;").unwrap();
        }
        write!(s, "color:").unwrap();
        write_css_color(&mut s, style.foreground);
        write!(s, ";\">{}</span>", Escape(text)).unwrap();
    }
    s
}

/// Returns a `<pre style="...">\n` tag with the correct background color for the given theme.
/// This is for if you want to roll your own HTML output, you probably just want to use
/// `highlighted_snippet_for_string`.
///
/// If you don't care about the background color you can just prefix the lines from
/// `styles_to_coloured_html` with a `<pre>`. This is meant to be used with `IncludeBackground::IfDifferent`.
///
/// You're responsible for creating the string `</pre>` to close this, I'm not gonna provide a
/// helper for that :-)
pub fn start_coloured_html_snippet(t: &Theme) -> String {
    let c = t.settings.background.unwrap_or(highlighting::WHITE);
    format!("<pre style=\"background-color:#{:02x}{:02x}{:02x}\">\n",
            c.r,
            c.g,
            c.b)
}

#[cfg(test)]
mod tests {
    use super::*;
    use parsing::{SyntaxSet, ParseState, ScopeStack};
    use highlighting::{ThemeSet, Style, Highlighter, HighlightIterator, HighlightState};
    #[test]
    fn tokens() {
        let ps = SyntaxSet::load_defaults_nonewlines();
        let syntax = ps.find_syntax_by_name("Markdown").unwrap();
        let mut state = ParseState::new(syntax);
        let line = "[w](t.co) *hi* **five**";
        let ops = state.parse_line(line);

        // use util::debug_print_ops;
        // debug_print_ops(line, &ops);

        let html = tokens_to_classed_html(line, &ops[..], ClassStyle::Spaced);
        println!("{}", html);
        assert_eq!(html, include_str!("../testdata/test2.html").trim_right());

        let ts = ThemeSet::load_defaults();
        let highlighter = Highlighter::new(&ts.themes["InspiredGitHub"]);
        let mut highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
        let iter = HighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
        let regions: Vec<(Style, &str)> = iter.collect();

        let html2 = styles_to_coloured_html(&regions[..], IncludeBackground::Yes);
        println!("{}", html2);
        assert_eq!(html2, include_str!("../testdata/test1.html").trim_right());
    }

    #[test]
    fn strings() {
        let ss = SyntaxSet::load_defaults_nonewlines();
        let ts = ThemeSet::load_defaults();
        let s = include_str!("../testdata/highlight_test.erb");
        let syntax = ss.find_syntax_by_extension("erb").unwrap();
        let html = highlighted_snippet_for_string(s, syntax, &ts.themes["base16-ocean.dark"]);
        assert_eq!(html, include_str!("../testdata/test3.html"));
        let html2 = highlighted_snippet_for_file("testdata/highlight_test.erb",
                                                 &ss,
                                                 &ts.themes["base16-ocean.dark"])
            .unwrap();
        assert_eq!(html2, html);

        // YAML is a tricky syntax and InspiredGitHub is a fancy theme, this is basically an integration test
        let html3 = highlighted_snippet_for_file("testdata/Packages/Rust/Cargo.sublime-syntax",
                                                 &ss,
                                                 &ts.themes["InspiredGitHub"])
            .unwrap();
        println!("{}", html3);
        assert_eq!(html3, include_str!("../testdata/test4.html"));
    }

    #[test]
    fn tricky_test_syntax() {
        // This syntax I wrote tests edge cases of prototypes
        // I verified the output HTML against what ST3 does with the same syntax and file
        let ss = SyntaxSet::load_from_folder("testdata").unwrap();
        let ts = ThemeSet::load_defaults();
        let html = highlighted_snippet_for_file("testdata/testing-syntax.testsyntax",
                                                &ss,
                                                &ts.themes["base16-ocean.dark"])
            .unwrap();
        println!("{}", html);
        assert_eq!(html, include_str!("../testdata/test5.html"));
    }
}