Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example for ClassedHTMLGenerator #274

Merged
merged 2 commits into from Dec 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
119 changes: 119 additions & 0 deletions examples/synhtml-css-classes.rs
@@ -0,0 +1,119 @@
//! Generates highlighted HTML with CSS classes for a Rust and a C++ source.
//! Run with ```cargo run --example synhtml-css-classes```
//!
//! will generate 4 files as usage example
//! * synhtml-css-classes.html
//! * synhtml-css-classes.css
//! * theme-dark.css
//! * theme-light.css
//!
//! You can open the html with a web browser and change between light and dark
//! mode.
use syntect::highlighting::ThemeSet;
use syntect::html::css_for_theme;
use syntect::html::ClassedHTMLGenerator;
use syntect::parsing::SyntaxSet;

use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;

fn main() -> Result<(), std::io::Error> {
// ---------------------------------------------------------------------------------------------
// generate html
let ss = SyntaxSet::load_defaults_newlines();

let html_file = File::create(Path::new("synhtml-css-classes.html"))?;
let mut html = BufWriter::new(&html_file);

// write html header
writeln!(html, "<!DOCTYPE html>")?;
writeln!(html, "<html>")?;
writeln!(html, " <head>")?;
writeln!(html, " <title>synhtml-css-classes.rs</title>")?;
writeln!(html, " <link rel=\"stylesheet\" href=\"synhtml-css-classes.css\">")?;
writeln!(html, " </head>")?;
writeln!(html, " <body>")?;

// Rust
let code_rs = "// Rust source
fn main() {
println!(\"Hello World!\");
}";

let sr_rs = ss.find_syntax_by_extension("rs").unwrap();
let mut rs_html_generator = ClassedHTMLGenerator::new(&sr_rs, &ss);
for line in code_rs.lines() {
rs_html_generator.parse_html_for_line(&line);
}
let html_rs = rs_html_generator.finalize();

writeln!(html, "<pre class=\"code\">")?;
writeln!(html, "{}", html_rs)?;
writeln!(html, "</pre>")?;

// C++
let code_cpp = "/* C++ source */
#include <iostream>
int main() {
std::cout << \"Hello World!\" << std::endl;
}";

let sr_cpp = ss.find_syntax_by_extension("cpp").unwrap();
let mut cpp_html_generator = ClassedHTMLGenerator::new(&sr_cpp, &ss);
for line in code_cpp.lines() {
cpp_html_generator.parse_html_for_line(&line);
}
let html_cpp = cpp_html_generator.finalize();

writeln!(html, "<pre class=\"code\">")?;
writeln!(html, "{}", html_cpp)?;
writeln!(html, "</pre>")?;

// write html end
writeln!(html, " </body>")?;
writeln!(html, "</html>")?;

// ---------------------------------------------------------------------------------------------
// generate css
let css = "@import url(\"theme-light.css\") (prefers-color-scheme: light);
@import url(\"theme-dark.css\") (prefers-color-scheme: dark);

@media (prefers-color-scheme: dark) {
body {
background-color: gray;
}
}
@media (prefers-color-scheme: light) {
body {
background-color: lightgray;
}
}";

let css_file = File::create(Path::new("synhtml-css-classes.css"))?;
let mut css_writer = BufWriter::new(&css_file);

writeln!(css_writer, "{}", css)?;

// ---------------------------------------------------------------------------------------------
// generate css files for themes
let ts = ThemeSet::load_defaults();

// create dark color scheme css
let dark_theme = &ts.themes["Solarized (dark)"];
let css_dark_file = File::create(Path::new("theme-dark.css"))?;
let mut css_dark_writer = BufWriter::new(&css_dark_file);

let css_dark = css_for_theme(dark_theme);
writeln!(css_dark_writer, "{}", css_dark)?;

// create light color scheme css
let light_theme = &ts.themes["Solarized (light)"];
let css_light_file = File::create(Path::new("theme-light.css"))?;
let mut css_light_writer = BufWriter::new(&css_light_file);

let css_light = css_for_theme(light_theme);
writeln!(css_light_writer, "{}", css_light)?;

Ok(())
}
21 changes: 13 additions & 8 deletions src/highlighting/selector.rs
Expand Up @@ -43,6 +43,11 @@ impl ScopeSelector {
}
Some(self.path.as_slice()[0])
}

/// extract all selectors for generating css
pub fn extract_scopes(&self) -> Vec<Scope> {
self.path.scopes.clone()
}
}

impl FromStr for ScopeSelector {
Expand Down Expand Up @@ -113,29 +118,29 @@ mod tests {
let first_sel = &sels.selectors[0];
assert_eq!(format!("{:?}", first_sel),
"ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<source.php>, <meta.preprocessor>] }, excludes: [ScopeStack { clear_stack: [], scopes: [<string.quoted>] }] }");

let sels = ScopeSelectors::from_str("source.php meta.preprocessor -string.quoted|\
source string")
.unwrap();
assert_eq!(sels.selectors.len(), 2);
let first_sel = &sels.selectors[0];
assert_eq!(format!("{:?}", first_sel),
"ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<source.php>, <meta.preprocessor>] }, excludes: [ScopeStack { clear_stack: [], scopes: [<string.quoted>] }] }");

let sels = ScopeSelectors::from_str("text.xml meta.tag.preprocessor.xml punctuation.separator.key-value.xml")
.unwrap();
assert_eq!(sels.selectors.len(), 1);
let first_sel = &sels.selectors[0];
assert_eq!(format!("{:?}", first_sel),
"ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<text.xml>, <meta.tag.preprocessor.xml>, <punctuation.separator.key-value.xml>] }, excludes: [] }");

let sels = ScopeSelectors::from_str("text.xml meta.tag.preprocessor.xml punctuation.separator.key-value.xml - text.html - string")
.unwrap();
assert_eq!(sels.selectors.len(), 1);
let first_sel = &sels.selectors[0];
assert_eq!(format!("{:?}", first_sel),
"ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<text.xml>, <meta.tag.preprocessor.xml>, <punctuation.separator.key-value.xml>] }, excludes: [ScopeStack { clear_stack: [], scopes: [<text.html>] }, ScopeStack { clear_stack: [], scopes: [<string>] }] }");

let sels = ScopeSelectors::from_str("text.xml meta.tag.preprocessor.xml punctuation.separator.key-value.xml - text.html - string, source - comment")
.unwrap();
assert_eq!(sels.selectors.len(), 2);
Expand All @@ -145,7 +150,7 @@ mod tests {
let second_sel = &sels.selectors[1];
assert_eq!(format!("{:?}", second_sel),
"ScopeSelector { path: ScopeStack { clear_stack: [], scopes: [<source>] }, excludes: [ScopeStack { clear_stack: [], scopes: [<comment>] }] }");

let sels = ScopeSelectors::from_str(" -a.b|j.g")
.unwrap();
assert_eq!(sels.selectors.len(), 2);
Expand Down Expand Up @@ -185,7 +190,7 @@ mod tests {
.does_match(ScopeStack::from_str("a.b c.d e.f").unwrap().as_slice()),
Some(MatchPower(0o201u64 as f64)));
}

#[test]
fn empty_stack_matching_works() {
use crate::parsing::{ScopeStack, MatchPower};
Expand Down Expand Up @@ -226,7 +231,7 @@ mod tests {
.unwrap()
.does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
Some(MatchPower(0o1u64 as f64)));

assert_eq!(ScopeSelector::from_str(" -a.b")
.unwrap()
.does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
Expand Down Expand Up @@ -260,7 +265,7 @@ mod tests {
.does_match(ScopeStack::from_str("a.b c.d j e.f").unwrap().as_slice()),
Some(MatchPower(0o1u64 as f64)));
}

#[test]
fn multiple_excludes_matching_works() {
use crate::parsing::{ScopeStack, MatchPower};
Expand Down
59 changes: 57 additions & 2 deletions src/html.rs
Expand Up @@ -65,6 +65,8 @@ impl<'a> ClassedHTMLGenerator<'a> {
ClassStyle::Spaced);
self.open_spans += delta;
self.html.push_str(formatted_line.as_str());
// retain newline
self.html.push_str("\n");
}

/// Close all open `<span>` tags and return the finished HTML string
Expand All @@ -76,6 +78,59 @@ impl<'a> ClassedHTMLGenerator<'a> {
}
}

/// Create a complete CSS for a given theme. Can be used inline, or written to
/// a CSS file.
pub fn css_for_theme(theme: &Theme) -> String {
let mut css = String::new();

css.push_str("/*\n");
let name = theme.name.clone().unwrap_or("unknown theme".to_string());
css.push_str(&format!(" * theme \"{}\" generated by syntect\n", name));
css.push_str(" */\n\n");

css.push_str(".code {\n");
if let Some(fgc) = theme.settings.foreground {
css.push_str(&format!(" color: #{:02x}{:02x}{:02x};\n", fgc.r, fgc.g, fgc.b));
}
if let Some(bgc) = theme.settings.background {
css.push_str(&format!(" background-color: #{:02x}{:02x}{:02x};\n", bgc.r, bgc.g, bgc.b));
}
css.push_str("}\n\n");

for i in &theme.scopes {

for scope_selector in &i.scope.selectors {
let scopes = scope_selector.extract_scopes();
for k in &scopes {
css.push_str(&format!(".{} {{\n", k));

if let Some(fg) = i.style.foreground {
css.push_str(&format!(" color: #{:02x}{:02x}{:02x};\n", fg.r, fg.g, fg.b));
}

if let Some(bg) = i.style.background {
css.push_str(&format!(" background-color: #{:02x}{:02x}{:02x};\n", bg.r, bg.g, bg.b));
}

if let Some(fs) = i.style.font_style {
if fs.contains(FontStyle::UNDERLINE) {
css.push_str(&format!("font-style: underline;\n"));
}
if fs.contains(FontStyle::BOLD) {
css.push_str(&format!("font-weight: bold;\n"));
}
if fs.contains(FontStyle::ITALIC) {
css.push_str(&format!("font-style: italic;\n"));
}
}
css.push_str("}\n");
}
}
}

css
}

/// 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)]
Expand Down Expand Up @@ -394,6 +449,6 @@ mod tests {
html_generator.parse_html_for_line(&line);
}
let html = html_generator.finalize();
assert_eq!(html, r#"<span class="source r">x <span class="keyword operator arithmetic r">+</span> y</span>"#);
assert_eq!(html, "<span class=\"source r\">x <span class=\"keyword operator arithmetic r\">+</span> y\n</span>");
}
}
}
4 changes: 2 additions & 2 deletions src/parsing/scope.rs
Expand Up @@ -74,7 +74,7 @@ pub struct ScopeRepository {
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct ScopeStack {
clear_stack: Vec<Vec<Scope>>,
scopes: Vec<Scope>,
pub scopes: Vec<Scope>,
}

#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
Expand Down Expand Up @@ -390,7 +390,7 @@ impl ScopeStack {
ScopeStackOp::Clear(amount) => {
let cleared = match amount {
ClearAmount::TopN(n) => {
// don't try to clear more scopes than are on the stack
// don't try to clear more scopes than are on the stack
let to_leave = self.scopes.len() - min(n, self.scopes.len());
self.scopes.split_off(to_leave)
}
Expand Down