Skip to content

Syntax highlighting with CSS classes #913

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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 39 additions & 1 deletion components/config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,24 @@ impl Default for Taxonomy {
}
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct ThemeCss {
/// Theme used for generating CSS
pub theme: String,
/// Filename for CSS
pub filename: String,
}

impl Default for ThemeCss {
fn default() -> ThemeCss {
ThemeCss {
theme: String::new(),
filename: String::new(),
}
}
}

type TranslateTerm = HashMap<String, String>;

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -154,6 +172,8 @@ pub struct Config {
/// Which themes to use for code highlighting. See Readme for supported themes
/// Defaults to "base16-ocean-dark"
pub highlight_theme: String,
/// Generate CSS files for Themes out of syntect
pub highlighting_themes_css: Vec<ThemeCss>,

/// Whether to generate a feed. Defaults to false.
pub generate_feed: bool,
Expand Down Expand Up @@ -214,7 +234,9 @@ impl Config {
bail!("A base URL is required in config.toml with key `base_url`");
}

if !THEME_SET.themes.contains_key(&config.highlight_theme) {
// If n ot generating css files, check if highlight_theme is available
if !(&config.highlight_theme == "css")
&& !THEME_SET.themes.contains_key(&config.highlight_theme) {
bail!("Highlight theme {} not available", config.highlight_theme)
}

Expand Down Expand Up @@ -386,6 +408,7 @@ impl Default for Config {
theme: None,
highlight_code: false,
highlight_theme: "base16-ocean-dark".to_string(),
highlighting_themes_css: Vec::new(),
default_language: "en".to_string(),
languages: Vec::new(),
generate_feed: false,
Expand Down Expand Up @@ -609,6 +632,21 @@ ignored_content = ["*.{graphml,iso}", "*.py?"]
assert!(!g.is_match("foo.py"));
}

#[test]
fn can_parse_theme_css() {
let config_str = r#"
title = "My site"
base_url = "example.com"
highlighting_themes_css = [
{ theme = "theme-0", filename = "theme-0.css" },
{ theme = "theme-1", filename = "theme-1.css" },
]
"#;
let config = Config::parse(config_str).unwrap();
let css_themes = config.highlighting_themes_css;
assert_eq!(css_themes.len(), 2);
}

#[test]
fn link_checker_skip_anchor_prefixes() {
let config_str = r#"
Expand Down
36 changes: 35 additions & 1 deletion components/rendering/src/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use syntect::easy::HighlightLines;
use syntect::html::{
start_highlighted_html_snippet, styled_line_to_highlighted_html, IncludeBackground,
};
use syntect::html::ClassedHTMLGenerator;

use crate::context::RenderContext;
use crate::table_of_contents::{make_table_of_contents, Heading};
Expand Down Expand Up @@ -174,6 +175,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render

let mut background = IncludeBackground::Yes;
let mut highlighter: Option<(HighlightLines, bool)> = None;
let mut css_highlighter: Option<ClassedHTMLGenerator> = None;

let mut inserted_anchors: Vec<String> = vec![];
let mut headings: Vec<Heading> = vec![];
Expand All @@ -192,6 +194,19 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
match event {
Event::Text(text) => {
// if we are in the middle of a code block
if &context.config.highlight_theme == "css" {
if let Some(ref mut css_highlighter) = css_highlighter {
for line in text.lines() {
css_highlighter.parse_html_for_line(&line);
}
// swap out highlighter because of borrowing
let sr_rs = &SYNTAX_SET.find_syntax_by_extension("rs").unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this only highlighting rust..?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the panic is because of that? It's trying to parse a different language than it is

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point i only need a unused syntax highlighter for swapping it out before calling finalize (We discussed that before).

The highlighter which is used to do the actual highlighting is created in line 239 with the correct syntax set from the codeblock:

 match &SYNTAX_SET.find_syntax_by_extension(info) {

let mut highlighter = ClassedHTMLGenerator::new(&sr_rs, &SYNTAX_SET);
std::mem::swap(&mut highlighter, css_highlighter);
let html = highlighter.finalize();
return Event::Html(html.into());
}
}
if let Some((ref mut highlighter, in_extra)) = highlighter {
let highlighted = if in_extra {
if let Some(ref extra) = context.config.extra_syntax_set {
Expand All @@ -204,7 +219,6 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
} else {
highlighter.highlight(&text, &SYNTAX_SET)
};
//let highlighted = &highlighter.highlight(&text, ss);
let html = styled_line_to_highlighted_html(&highlighted, background);
return Event::Html(html.into());
}
Expand All @@ -216,6 +230,22 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
if !context.config.highlight_code {
return Event::Html("<pre><code>".into());
}
if &context.config.highlight_theme == "css" {
match kind {
CodeBlockKind::Indented => (),
CodeBlockKind::Fenced(info) => {
match &SYNTAX_SET.find_syntax_by_extension(info) {
Some(syn_set) => css_highlighter = Some(ClassedHTMLGenerator::new(&syn_set, &SYNTAX_SET)),
None => {
// create fallback highlighter
let syn_set = &SYNTAX_SET.find_syntax_by_extension("txt").unwrap();
css_highlighter = Some(ClassedHTMLGenerator::new(&syn_set, &SYNTAX_SET));
}
};
}
};
return Event::Html("<pre class=\"code\"><code class=\"code\">".into());
}

let theme = &THEME_SET.themes[&context.config.highlight_theme];
match kind {
Expand All @@ -239,6 +269,10 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
if !context.config.highlight_code {
return Event::Html("</code></pre>\n".into());
}
if &context.config.highlight_theme == "css" {
css_highlighter = None;
return Event::Html("</code></pre>\n".into());
}
// reset highlight and close the code block
highlighter = None;
Event::Html("</code></pre>".into())
Expand Down
1 change: 1 addition & 0 deletions components/site/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ rayon = "1"
serde = "1"
serde_derive = "1"
sass-rs = "0.2"
syntect = "4.1"

errors = { path = "../errors" }
config = { path = "../config" }
Expand Down
22 changes: 22 additions & 0 deletions components/site/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ use utils::fs::{copy_directory, create_directory, create_file, ensure_directory_
use utils::net::get_available_port;
use utils::templates::{render_template, rewrite_theme_paths};

use config::highlighting::THEME_SET;
use syntect::html::css_for_theme;
use std::fs::File;
use std::io::{BufWriter, Write};

#[derive(Debug)]
pub struct Site {
/// The base path of the zola site
Expand Down Expand Up @@ -748,6 +753,10 @@ impl Site {
self.compile_sass(&self.base_path)?;
}

if self.config.highlighting_themes_css.len() > 0 {
self.generate_highlighting_themes()?;
}

if self.config.build_search_index {
self.build_search_index()?;
}
Expand Down Expand Up @@ -855,6 +864,19 @@ impl Site {
Ok(())
}

pub fn generate_highlighting_themes(&self) -> Result<()> {
ensure_directory_exists(&self.output_path)?;
for css_theme in &self.config.highlighting_themes_css {
let theme = &THEME_SET.themes[&css_theme.theme];
let css_file = File::create(Path::new(&self.output_path.join(&css_theme.filename)))?;
let mut css_writer = BufWriter::new(&css_file);

let css = css_for_theme(theme);
writeln!(css_writer, "{}", css)?;
}
Ok(())
}

fn compile_sass_glob(
&self,
sass_path: &Path,
Expand Down
72 changes: 72 additions & 0 deletions docs/content/documentation/content/css-syntax-highlighting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
+++
title = "CSS Syntax Highlighting"
weight = 80
+++

If you use a highlighting scheme like

```toml
highlight_theme = "base16-ocean-dark"
```

for a code block like

````md
```rs
let highlight = true;
```
````

you get the colors directly encoded in the html file

```html
<pre style="background-color:#2b303b;">
<code>
<span style="color:#b48ead;">let</span>
<span style="color:#c0c5ce;"> highlight = </span>
<span style="color:#d08770;">true</span>
<span style="color:#c0c5ce;">;
</span>
</code>
</pre>
```

this is nice, because if everything is inside one file, you get fast
page loadings. But if you would like to have the user choose a theme from a
list, or different color schemes for dark/light color schemes, ou need a
different solution.

If you use the special color scheme

```toml
highlight_theme = "css"
```

you get CSS class definitions

```html
<pre class="code">
<code class="code">
<span class="source rust">
<span class="storage type rust">let</span> highlight
<span class="keyword operator assignment rust">=</span>
<span class="constant language rust">true</span>
<span class="punctuation terminator rust">;</span>
</span>
</code>
</pre>
```

now you can generate and use CSS either manually or with Zola

```toml
highlighting_themes_css = [
{ theme = "base16-ocean-dark", filename = "syntax-theme-dark.css" },
{ theme = "base16-ocean-light", filename = "syntax-theme-light.css" },
]
```

```css
@import url("syntax-theme-dark.css") (prefers-color-scheme: dark);
@import url("syntax-theme-light.css") (prefers-color-scheme: light);
```
13 changes: 12 additions & 1 deletion docs/content/documentation/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ If you are not familiar with TOML, have a look at [the TOML spec](https://github
Only the `base_url` variable is mandatory; everything else is optional. All configuration variables
used by Zola as well as their default values are listed below:


```toml
# The base URL of the site; the only required configuration variable.
base_url = "mywebsite.com"
Expand All @@ -32,7 +31,19 @@ highlight_code = false

# The theme to use for code highlighting.
# See below for list of allowed values.
# This will put the colors of the theme directly in your html.
highlight_theme = "base16-ocean-dark"
# For using CSS class definitions in the higlighting, you can use the
# special theme "css". This is especially useful for dark/light themes,
# or letting the user decide the higlighting scheme.

# If the site uses some of the predefined syntax highlighing schemes as
# CSS, you can let Zola generate the CSS.
highlighting_themes_css = [
{ theme = "base16-ocean-dark", filename = "syntax-theme-dark.css" },
{ theme = "base16-ocean-light", filename = "syntax-theme-light.css" },
]


# When set to "true", a feed is automatically generated.
generate_feed = false
Expand Down