Skip to content
This repository has been archived by the owner on Feb 12, 2018. It is now read-only.

Commit

Permalink
Much more HTML output functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
trishume committed Jun 22, 2016
1 parent 1817de4 commit eebf24a
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 12 deletions.
3 changes: 2 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ It is currently mostly complete and can parse, interpret and highlight based on
`syntect` is [available on crates.io](https://crates.io/crates/syntect). You can install it by adding this line to your `Cargo.toml`:

```toml
syntect = "0.3.3"
syntect = "0.4.0"
```

After that take a look at the [documentation](http://thume.ca/rustdoc/syntect/syntect/) and the [examples](https://github.com/trishume/syntect/tree/master/examples).
Expand All @@ -32,6 +32,7 @@ After that take a look at the [documentation](http://thume.ca/rustdoc/syntect/sy
- [x] High quality highlighting, supporting things like heredocs and complex syntaxes (like Rust's).
- [x] Include a compressed dump of all the default syntax definitions in the library binary so users don't have to manage a folder of syntaxes.
- [x] Well documented, I've tried to add a useful documentation comment to everything that isn't utterly self explanatory.
- [x] Built-in output to coloured HTML `<pre>` tags or 24-bit colour ANSI terminal escape sequences.

## Screenshots

Expand Down
53 changes: 53 additions & 0 deletions src/escape.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2013 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! HTML Escaping
//!
//! This module contains one unit-struct which can be used to HTML-escape a
//! string of text (for use in a format string).
use std::fmt;

/// Wrapper struct which will emit the HTML-escaped version of the contained
/// string when passed to a format string.
pub struct Escape<'a>(pub &'a str);

impl<'a> fmt::Display for Escape<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
// Because the internet is always right, turns out there's not that many
// characters to escape: http://stackoverflow.com/questions/7381974
let Escape(s) = *self;
let pile_o_bits = s;
let mut last = 0;
for (i, ch) in s.bytes().enumerate() {
match ch as char {
'<' | '>' | '&' | '\'' | '"' => {
try!(fmt.write_str(&pile_o_bits[last.. i]));
let s = match ch as char {
'>' => "&gt;",
'<' => "&lt;",
'&' => "&amp;",
'\'' => "&#39;",
'"' => "&quot;",
_ => unreachable!()
};
try!(fmt.write_str(s));
last = i + 1;
}
_ => {}
}
}

if last < s.len() {
try!(fmt.write_str(&pile_o_bits[last..]));
}
Ok(())
}
}
4 changes: 2 additions & 2 deletions src/highlighting/highlighter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ impl<'a> Highlighter<'a> {
/// Basically what plain text gets highlighted as.
pub fn get_default(&self) -> Style {
Style {
foreground: self.theme.settings.foreground.unwrap_or(WHITE),
background: self.theme.settings.background.unwrap_or(BLACK),
foreground: self.theme.settings.foreground.unwrap_or(BLACK),
background: self.theme.settings.background.unwrap_or(WHITE),
font_style: FontStyle::empty(),
}
}
Expand Down
135 changes: 126 additions & 9 deletions src/html.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
//! Rendering highlighted code as HTML+CSS
use std::fmt::Write;
use parsing::{ScopeStackOp, Scope, SCOPE_REPO};
use highlighting::{Style, self};
use parsing::{ScopeStackOp, Scope, SyntaxDefinition, SyntaxSet, SCOPE_REPO};
use easy::{HighlightLines, HighlightFile};
use highlighting::{Style, Theme, Color, self};
use escape::Escape;
use std::io::{BufRead, self};
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
Expand All @@ -27,16 +31,66 @@ fn scope_to_classes(s: &mut String, scope: Scope, style: ClassStyle) {
}
}

/// 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 = try!(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 = try!(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;
for &(i, ref op) in ops {
if i > cur_index {
s.push_str(&line[cur_index..i]);
write!(s, "{}", Escape(&line[cur_index..i])).unwrap();
cur_index = i
}
match op {
Expand All @@ -56,15 +110,51 @@ pub fn tokens_to_classed_html(line: &str, ops: &[(usize, ScopeStackOp)], style:
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),
}

/// 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. This adds a lot more text but allows different backgrounds.
pub fn styles_to_coloured_html(v: &[(Style, &str)], bg: bool) -> String {
/// 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();
if bg {
let include_bg = match bg {
IncludeBackground::Yes => true,
IncludeBackground::No => false,
IncludeBackground::IfDifferent(c) => (style.background != c),
};
if include_bg {
write!(s,
"background-color:#{:02x}{:02x}{:02x};",
style.background.r,
Expand All @@ -86,20 +176,34 @@ pub fn styles_to_coloured_html(v: &[(Style, &str)], bg: bool) -> String {
style.foreground.r,
style.foreground.g,
style.foreground.b,
text)
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_from_folder("testdata/Packages").unwrap();
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**";
Expand All @@ -117,7 +221,20 @@ mod tests {
let iter = HighlightIterator::new(&mut highlight_state, &ops[..], line, &highlighter);
let regions: Vec<(Style, &str)> = iter.collect();

let html2 = styles_to_coloured_html(&regions[..], true);
let html2 = styles_to_coloured_html(&regions[..], IncludeBackground::Yes);
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"]);
println!("{}", html);
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);
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub mod util;
pub mod dumps;
pub mod easy;
pub mod html;
mod escape;

use std::io::Error as IoError;
use parsing::ParseSyntaxError;
Expand Down
32 changes: 32 additions & 0 deletions testdata/test3.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<pre style="background-color:#2b303b;">
<span style="color:#c0c5ce;">&lt;</span><span style="color:#bf616a;">script</span><span style="color:#c0c5ce;"> </span><span style="color:#d08770;">type</span><span style="color:#c0c5ce;">=</span><span style="color:#c0c5ce;">&quot;</span><span style="color:#a3be8c;">text/javascript</span><span style="color:#c0c5ce;">&quot;</span><span style="color:#c0c5ce;">&gt;</span>
<span style="color:#c0c5ce;"> </span><span style="color:#b48ead;">var</span><span style="color:#c0c5ce;"> </span><span style="color:#bf616a;">lol</span><span style="color:#c0c5ce;"> </span><span style="color:#c0c5ce;">=</span><span style="color:#c0c5ce;"> </span><span style="color:#c0c5ce;">&quot;</span><span style="color:#a3be8c;">JS nesting</span><span style="color:#c0c5ce;">&quot;</span><span style="color:#c0c5ce;">;</span>
<span style="color:#c0c5ce;"> </span><span style="color:#b48ead;">class</span><span style="color:#eff1f5;"> </span><span style="color:#ebcb8b;">WithES6</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">extends</span><span style="color:#eff1f5;"> </span><span style="color:#a3be8c;">THREE</span><span style="color:#eff1f5;">.</span><span style="color:#a3be8c;">Mesh</span><span style="color:#eff1f5;"> </span><span style="color:#eff1f5;">{</span>
<span style="color:#eff1f5;"> </span><span style="color:#b48ead;">static</span><span style="color:#eff1f5;"> </span><span style="color:#8fa1b3;">highQuality</span><span style="color:#c0c5ce;">(</span><span style="color:#c0c5ce;">)</span><span style="color:#eff1f5;"> </span><span style="color:#eff1f5;">{</span><span style="color:#eff1f5;"> </span><span style="color:#65737e;">//</span><span style="color:#65737e;"> such classes</span>
<span style="color:#eff1f5;"> </span><span style="color:#b48ead;">return</span><span style="color:#eff1f5;"> </span><span style="color:#bf616a;">this</span><span style="color:#eff1f5;">.</span><span style="color:#bf616a;">toString</span><span style="color:#eff1f5;">(</span><span style="color:#eff1f5;">)</span><span style="color:#eff1f5;">;</span>
<span style="color:#eff1f5;"> </span><span style="color:#eff1f5;">}</span>
<span style="color:#eff1f5;"> </span><span style="color:#eff1f5;">}</span>
<span style="color:#c0c5ce;"> </span><span style="color:#ab7967;">&lt;%</span>
<span style="color:#65737e;"> </span><span style="color:#65737e;">#</span><span style="color:#65737e;"> The outer syntax is HTML (Rails) detected from the .erb extension</span>
<span style="color:#c0c5ce;"> puts </span><span style="color:#c0c5ce;">&quot;</span><span style="color:#a3be8c;">Ruby </span><span style="color:#ab7967;">#{</span><span style="color:#c0c5ce;">&#39;</span><span style="color:#a3be8c;">nesting</span><span style="color:#c0c5ce;">&#39;</span><span style="color:#a3be8c;"> </span><span style="color:#c0c5ce;">*</span><span style="color:#a3be8c;"> </span><span style="color:#d08770;">2</span><span style="color:#ab7967;">}</span><span style="color:#c0c5ce;">&quot;</span>
<span style="color:#c0c5ce;"> here </span><span style="color:#c0c5ce;">=</span><span style="color:#c0c5ce;"> </span><span style="color:#c0c5ce;">&lt;&lt;-WOWCOOL</span>
<span style="color:#a3be8c;"> high quality parsing even supports custom heredoc endings</span>
<span style="color:#a3be8c;"> </span><span style="color:#ab7967;">#{</span>
<span style="color:#a3be8c;"> nested </span><span style="color:#c0c5ce;">=</span><span style="color:#a3be8c;"> </span><span style="color:#d08770;">5</span><span style="color:#a3be8c;"> </span><span style="color:#c0c5ce;">*</span><span style="color:#a3be8c;"> </span><span style="color:#c0c5ce;">&lt;&lt;-ZOMG</span>
<span style="color:#a3be8c;"> nested heredocs! (no highlighting: 5 * 6, yes highlighting: </span><span style="color:#ab7967;">#{</span><span style="color:#d08770;">5</span><span style="color:#a3be8c;"> </span><span style="color:#c0c5ce;">*</span><span style="color:#a3be8c;"> </span><span style="color:#d08770;">6</span><span style="color:#ab7967;">}</span><span style="color:#a3be8c;">)</span>
<span style="color:#c0c5ce;"> ZOMG</span>
<span style="color:#a3be8c;"> </span><span style="color:#ab7967;">}</span>
<span style="color:#c0c5ce;"> WOWCOOL</span>
<span style="color:#c0c5ce;"> sql </span><span style="color:#c0c5ce;">=</span><span style="color:#c0c5ce;"> </span><span style="color:#c0c5ce;">&lt;&lt;-SQL</span>
<span style="color:#a3be8c;"> </span><span style="color:#b48ead;">select</span><span style="color:#a3be8c;"> </span><span style="color:#c0c5ce;">*</span><span style="color:#a3be8c;"> </span><span style="color:#b48ead;">from</span><span style="color:#a3be8c;"> heredocs </span><span style="color:#b48ead;">where</span><span style="color:#a3be8c;"> there_are_special_heredoc_names </span><span style="color:#c0c5ce;">=</span><span style="color:#a3be8c;"> </span><span style="color:#d08770;">true</span>
<span style="color:#c0c5ce;"> SQL</span>
<span style="color:#c0c5ce;"> </span><span style="color:#ab7967;">%&gt;</span>
<span style="color:#c0c5ce;">&lt;/</span><span style="color:#bf616a;">script</span><span style="color:#c0c5ce;">&gt;</span>
<span style="color:#c0c5ce;">&lt;</span><span style="color:#bf616a;">style</span><span style="color:#c0c5ce;"> </span><span style="color:#d08770;">type</span><span style="color:#c0c5ce;">=</span><span style="color:#c0c5ce;">&quot;</span><span style="color:#a3be8c;">text/css</span><span style="color:#c0c5ce;">&quot;</span><span style="color:#c0c5ce;">&gt;</span>
<span style="color:#c0c5ce;"> </span><span style="color:#65737e;">/*</span><span style="color:#65737e;"> the HTML syntax also supports CSS of course </span><span style="color:#65737e;">*/</span>
<span style="color:#b48ead;"> .stuff #wow </span><span style="color:#c0c5ce;">{</span>
<span style="color:#c0c5ce;"> </span><span style="color:#c0c5ce;">border</span><span style="color:#c0c5ce;">:</span><span style="color:#c0c5ce;"> 5px </span><span style="color:#96b5b4;">#</span><span style="color:#96b5b4;">ffffff</span><span style="color:#c0c5ce;">;</span>
<span style="color:#c0c5ce;"> </span><span style="color:#c0c5ce;">background</span><span style="color:#c0c5ce;">:</span><span style="color:#c0c5ce;"> </span><span style="color:#96b5b4;">url</span><span style="color:#c0c5ce;">(</span><span style="color:#c0c5ce;">&quot;</span><span style="color:#a3be8c;">wow</span><span style="color:#c0c5ce;">&quot;</span><span style="color:#c0c5ce;">)</span><span style="color:#c0c5ce;">;</span>
<span style="color:#c0c5ce;"> </span><span style="color:#c0c5ce;">}</span>
<span style="color:#c0c5ce;">&lt;/</span><span style="color:#bf616a;">style</span><span style="color:#c0c5ce;">&gt;</span>
</pre>

0 comments on commit eebf24a

Please sign in to comment.