diff --git a/Readme.md b/Readme.md index fea6373a..16e2d724 100644 --- a/Readme.md +++ b/Readme.md @@ -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). @@ -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 `
` tags or 24-bit colour ANSI terminal escape sequences.
 
 ## Screenshots
 
diff --git a/src/escape.rs b/src/escape.rs
new file mode 100644
index 00000000..f04e1cc7
--- /dev/null
+++ b/src/escape.rs
@@ -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  or the MIT license
+// , 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 {
+                        '>' => ">",
+                        '<' => "<",
+                        '&' => "&",
+                        '\'' => "'",
+                        '"' => """,
+                        _ => unreachable!()
+                    };
+                    try!(fmt.write_str(s));
+                    last = i + 1;
+                }
+                _ => {}
+            }
+        }
+
+        if last < s.len() {
+            try!(fmt.write_str(&pile_o_bits[last..]));
+        }
+        Ok(())
+    }
+}
diff --git a/src/highlighting/highlighter.rs b/src/highlighting/highlighter.rs
index ff7460ff..15db97fc 100644
--- a/src/highlighting/highlighter.rs
+++ b/src/highlighting/highlighter.rs
@@ -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(),
         }
     }
diff --git a/src/html.rs b/src/html.rs
index ab7c040a..34db2601 100644
--- a/src/html.rs
+++ b/src/html.rs
@@ -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
@@ -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, "
\n", c.r, c.g, c.b).unwrap();
+    for line in s.lines() {
+        let regions = highlighter.highlight(line);
+        let html = styles_to_coloured_html(®ions[..], IncludeBackground::IfDifferent(c));
+        output.push_str(&html);
+        output.push('\n');
+    }
+    output.push_str("
\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>(path: P, ss: &SyntaxSet, theme: &Theme) -> io::Result { + // 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, "
\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(®ions[..], IncludeBackground::IfDifferent(c));
+        output.push_str(&html);
+        output.push('\n');
+    }
+    output.push_str("
\n"); + Ok(output) +} + /// Output HTML for a line of code with `` 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 `
`
+/// 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 {
@@ -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 `` 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(®ions[..], IncludeBackground::No);
+/// assert_eq!(html, "5");
+/// ```
+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," true,
+            IncludeBackground::No => false,
+            IncludeBackground::IfDifferent(c) => (style.background != c),
+        };
+        if include_bg {
             write!(s,
                    "background-color:#{:02x}{:02x}{:02x};",
                    style.background.r,
@@ -86,12 +176,26 @@ 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 `
\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 `
`. This is meant to be used with `IncludeBackground::IfDifferent`.
+///
+/// You're responsible for creating the string `
` 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!("
\n", c.r, c.g, c.b)
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -99,7 +203,7 @@ mod tests {
     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**";
@@ -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(®ions[..], true);
+        let html2 = styles_to_coloured_html(®ions[..], 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);
+    }
 }
diff --git a/src/lib.rs b/src/lib.rs
index 3394f58f..e7963ee9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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;
diff --git a/testdata/test3.html b/testdata/test3.html
new file mode 100644
index 00000000..91a41141
--- /dev/null
+++ b/testdata/test3.html
@@ -0,0 +1,32 @@
+
+<script type="text/javascript">
+  var lol = "JS nesting";
+  class WithES6 extends THREE.Mesh {
+    static highQuality() { // such classes
+      return this.toString();
+    }
+  }
+  <%
+    # The outer syntax is HTML (Rails) detected from the .erb extension
+    puts "Ruby #{'nesting' * 2}"
+    here = <<-WOWCOOL
+      high quality parsing even supports custom heredoc endings
+      #{
+      nested = 5 * <<-ZOMG
+        nested heredocs! (no highlighting: 5 * 6, yes highlighting: #{5 * 6})
+      ZOMG
+      }
+    WOWCOOL
+    sql = <<-SQL
+      select * from heredocs where there_are_special_heredoc_names = true
+    SQL
+  %>
+</script>
+<style type="text/css">
+  /* the HTML syntax also supports CSS of course */
+  .stuff #wow {
+    border: 5px #ffffff;
+    background: url("wow");
+  }
+</style>
+