Skip to content

Commit cbd6a30

Browse files
authored
Merge pull request #1259 from vbfox/css_syntax_highlighting
feat: Add a 'css' theme for syntax highlighting
2 parents 8417ce7 + 04a6d60 commit cbd6a30

File tree

26 files changed

+461
-22
lines changed

26 files changed

+461
-22
lines changed

crates/engarde/src/syntax.rs

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ use std::path::Path;
22
use syntect::easy::HighlightLines;
33
use syntect::highlighting::ThemeSet;
44
use syntect::html::{
5-
IncludeBackground, append_highlighted_html_for_styled_line, start_highlighted_html_snippet,
5+
ClassStyle, ClassedHTMLGenerator, IncludeBackground, append_highlighted_html_for_styled_line,
6+
css_for_theme_with_class_style, start_highlighted_html_snippet,
67
};
78
use syntect::parsing::{SyntaxReference, SyntaxSet};
89
use syntect::util::LinesWithEndings;
@@ -16,6 +17,8 @@ pub struct Syntax {
1617
default_theme: Option<String>,
1718
}
1819

20+
const CSS_THEME: &str = "css";
21+
1922
impl Syntax {
2023
pub fn new() -> Self {
2124
Self {
@@ -31,12 +34,30 @@ impl Syntax {
3134
self.syntax_set = builder.build();
3235
}
3336

37+
pub fn css_theme_name() -> &'static str {
38+
CSS_THEME
39+
}
40+
3441
pub fn has_theme(&self, name: &str) -> bool {
35-
self.theme_set.themes.contains_key(name)
42+
name == CSS_THEME || self.theme_set.themes.contains_key(name)
3643
}
3744

3845
pub fn themes(&self) -> impl Iterator<Item = String> + '_ {
39-
self.theme_set.themes.keys().cloned()
46+
let mut themes: Vec<_> = self.theme_set.themes.keys().cloned().collect();
47+
themes.push(CSS_THEME.to_string());
48+
themes.sort_by_key(|a| a.to_ascii_lowercase());
49+
50+
themes.into_iter()
51+
}
52+
53+
fn css_class_style() -> ClassStyle {
54+
ClassStyle::SpacedPrefixed { prefix: "c-" }
55+
}
56+
57+
/// Get the content of a css file to apply the specified color theme when using the 'css' theme
58+
pub fn css_for_theme(&self, name: &str) -> String {
59+
let theme = &self.theme_set.themes[name];
60+
css_for_theme_with_class_style(theme, Self::css_class_style()).unwrap()
4061
}
4162

4263
pub fn syntaxes(&self) -> impl Iterator<Item = String> + '_ {
@@ -66,32 +87,63 @@ impl Syntax {
6687
self.default_theme = Some(theme.into());
6788
}
6889

90+
fn format_inline_theme(&self, code: &str, theme: &str, syntax: &SyntaxReference) -> String {
91+
let theme = &self.theme_set.themes[theme];
92+
93+
// Essentially the same as `syntect::html::highlighted_html_for_string`,
94+
// but adding <code> tags between the <pre> tags
95+
// See: https://docs.rs/syntect/5.0.0/src/syntect/html.rs.html#269
96+
let mut highlighter = HighlightLines::new(syntax, theme);
97+
let (mut output, bg) = start_highlighted_html_snippet(theme);
98+
output.push_str("<code>");
99+
100+
for line in LinesWithEndings::from(code) {
101+
let regions = highlighter.highlight_line(line, &self.syntax_set).unwrap();
102+
append_highlighted_html_for_styled_line(
103+
&regions[..],
104+
IncludeBackground::IfDifferent(bg),
105+
&mut output,
106+
)
107+
.unwrap();
108+
}
109+
110+
output.push_str("</code></pre>\n");
111+
output
112+
}
113+
114+
fn format_css_theme(&self, code: &str, lang: Option<&str>, syntax: &SyntaxReference) -> String {
115+
let mut html_generator = ClassedHTMLGenerator::new_with_class_style(
116+
syntax,
117+
&self.syntax_set,
118+
Self::css_class_style(),
119+
);
120+
121+
for line in LinesWithEndings::from(code) {
122+
html_generator
123+
.parse_html_for_line_which_includes_newline(line)
124+
.unwrap();
125+
}
126+
127+
let language_class = lang.map(|l| format!("language-{l} ")).unwrap_or_default();
128+
let mut output = format!("<pre class=\"{language_class}highlighter-syntect\">");
129+
output.push_str("<code class=\"highlight\">");
130+
output.push_str(&html_generator.finalize());
131+
output.push_str("</code></pre>");
132+
133+
output
134+
}
135+
69136
pub fn format(&self, code: &str, lang: Option<&str>, theme: Option<&str>) -> String {
70137
if let Some(theme) = theme.or_else(|| self.default_theme()) {
71-
let theme = &self.theme_set.themes[theme];
72-
73138
let syntax = lang
74139
.and_then(|l| self.syntax_set.find_syntax_by_token(l))
75140
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
76141

77-
// Essentially the same as `syntect::html::highlighted_html_for_string`,
78-
// but adding <code> tags between the <pre> tags
79-
// See: https://docs.rs/syntect/5.0.0/src/syntect/html.rs.html#269
80-
let mut highlighter = HighlightLines::new(syntax, theme);
81-
let (mut output, bg) = start_highlighted_html_snippet(theme);
82-
output.push_str("<code>");
83-
84-
for line in LinesWithEndings::from(code) {
85-
let regions = highlighter.highlight_line(line, &self.syntax_set).unwrap();
86-
append_highlighted_html_for_styled_line(
87-
&regions[..],
88-
IncludeBackground::IfDifferent(bg),
89-
&mut output,
90-
)
91-
.unwrap();
142+
if theme == CSS_THEME {
143+
self.format_css_theme(code, lang, syntax)
144+
} else {
145+
self.format_inline_theme(code, theme, syntax)
92146
}
93-
output.push_str("</code></pre>\n");
94-
output
95147
} else {
96148
crate::Raw::new().format(code, lang, theme)
97149
}

src/bin/cobalt/debug.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use crate::args;
22
use crate::error::Result;
3+
#[cfg(feature = "syntax-highlight")]
4+
use std::path::PathBuf;
35

46
/// Print site debug information
57
#[derive(Clone, Debug, PartialEq, Eq, clap::Subcommand)]
@@ -34,6 +36,19 @@ pub(crate) enum HighlightCommands {
3436
#[command(flatten, next_help_heading = "Config")]
3537
config: args::ConfigArgs,
3638
},
39+
40+
/// Save the css file for a theme
41+
#[cfg(feature = "syntax-highlight")]
42+
SaveThemeCss {
43+
#[command(flatten, next_help_heading = "Config")]
44+
config: args::ConfigArgs,
45+
46+
/// Name of the theme to generate a css file for
47+
name: String,
48+
49+
/// Path of the css file
50+
path: PathBuf,
51+
},
3752
}
3853

3954
impl DebugCommands {
@@ -58,6 +73,23 @@ impl DebugCommands {
5873
println!("{name}");
5974
}
6075
}
76+
#[cfg(feature = "syntax-highlight")]
77+
Self::Highlight(HighlightCommands::SaveThemeCss { config, name, path }) => {
78+
let config = config.load_config()?;
79+
let config = cobalt::cobalt_model::Config::from_config(config)?;
80+
if name == engarde::Syntax::css_theme_name() || !config.syntax.has_theme(name) {
81+
return Err(anyhow::anyhow!("Unknown theme: {name}"));
82+
}
83+
84+
let css = config.syntax.css_for_theme(name);
85+
86+
std::fs::write(path, css)?;
87+
88+
println!(
89+
"CSS theme '{name}' successfully saved to: {}",
90+
path.display()
91+
);
92+
}
6193
Self::Files { collection, config } => {
6294
let config = config.load_config()?;
6395
let config = cobalt::cobalt_model::Config::from_config(config)?;

tests/cli_tests.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ fn cli_tests() {
99
t.skip("tests/cmd/log_level.md");
1010
t.skip("tests/cmd/vimwiki_not_templated.md");
1111
t.skip("tests/cmd/custom_syntax_highlighting.md");
12+
t.skip("tests/cmd/syntax_highlighting.md");
13+
t.skip("tests/cmd/syntax_highlighting_disabled.md");
14+
t.skip("tests/cmd/syntax_highlighting_css.md");
1215
}
1316
#[cfg(not(feature = "serve"))]
1417
{
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>test</title>
5+
</head>
6+
<body>
7+
<h1>{{ page.permalink }}</h1>
8+
9+
{{ page.content }}
10+
</body>
11+
</html>
12+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>My blog - {{ page.title }}</title>
5+
</head>
6+
<body>
7+
{{ page.content }}
8+
</body>
9+
</html>
10+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
layout: default.liquid
3+
---
4+
This is my Index page!
5+
6+
{% for post in collections.posts.pages %}
7+
<a href="{{post.permalink}}">{{ post.title }}</a>
8+
{% endfor %}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
title: Some rust code
3+
published_date: 2022-08-23 22:01:41 +0000
4+
layout: default.liquid
5+
---
6+
# Some rust code
7+
8+
```rust
9+
// This is a comment, and is ignored by the compiler.
10+
// You can test this code by clicking the "Run" button over there ->
11+
// or if you prefer to use your keyboard, you can use the "Ctrl + Enter"
12+
// shortcut.
13+
14+
// This code is editable, feel free to hack it!
15+
// You can always return to the original code by clicking the "Reset" button ->
16+
17+
// This is the main function.
18+
fn main() {
19+
// Statements here are executed when the compiled binary is called.
20+
21+
// Print text to the console.
22+
println!("Hello World!");
23+
}
24+
```
25+

tests/cmd/syntax_highlighting.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
```console
2+
$ cobalt -v build --destination _dest
3+
WARN: No _cobalt.yml file found in current directory, using default config.
4+
Building from `.` into `[CWD]/_dest`
5+
DEBUG: glob converted to regex: Glob { glob: "**/.*", re: "(?-u)^(?:/?|.*/)//.[^/]*$", opts: GlobOptions { case_insensitive: false, literal_separator: true, backslash_escape: true, empty_alternates: false }, tokens: Tokens([RecursivePrefix, Literal('.'), ZeroOrMore]) }
6+
DEBUG: glob converted to regex: Glob { glob: "**/_*", re: "(?-u)^(?:/?|.*/)_[^/]*$", opts: GlobOptions { case_insensitive: false, literal_separator: true, backslash_escape: true, empty_alternates: false }, tokens: Tokens([RecursivePrefix, Literal('_'), ZeroOrMore]) }
7+
DEBUG: built glob set; 5 literals, 0 basenames, 0 extensions, 0 prefixes, 0 suffixes, 0 required extensions, 2 regexes
8+
DEBUG: Loading data from `./_data`
9+
DEBUG: glob converted to regex: Glob { glob: "**/.*", re: "(?-u)^(?:/?|.*/)//.[^/]*$", opts: GlobOptions { case_insensitive: false, literal_separator: true, backslash_escape: true, empty_alternates: false }, tokens: Tokens([RecursivePrefix, Literal('.'), ZeroOrMore]) }
10+
DEBUG: glob converted to regex: Glob { glob: "**/_*", re: "(?-u)^(?:/?|.*/)_[^/]*$", opts: GlobOptions { case_insensitive: false, literal_separator: true, backslash_escape: true, empty_alternates: false }, tokens: Tokens([RecursivePrefix, Literal('_'), ZeroOrMore]) }
11+
DEBUG: built glob set; 0 literals, 0 basenames, 0 extensions, 0 prefixes, 0 suffixes, 0 required extensions, 2 regexes
12+
DEBUG: Loading snippets from `./_includes`
13+
Build successful
14+
15+
```
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>test</title>
5+
</head>
6+
<body>
7+
<h1>index.html</h1>
8+
9+
This is my Index page!
10+
11+
12+
<a href="posts/2022-08-23-my-first-post.html">Some rust code</a>
13+
14+
15+
</body>
16+
</html>
17+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>test</title>
5+
</head>
6+
<body>
7+
<h1>posts/2022-08-23-my-first-post.html</h1>
8+
9+
<h1>Some rust code</h1>
10+
<pre style="background-color:#2b303b;">
11+
<code><span style="color:#65737e;">// This is a comment, and is ignored by the compiler.
12+
</span><span style="color:#65737e;">// You can test this code by clicking the &quot;Run&quot; button over there -&gt;
13+
</span><span style="color:#65737e;">// or if you prefer to use your keyboard, you can use the &quot;Ctrl + Enter&quot;
14+
</span><span style="color:#65737e;">// shortcut.
15+
</span><span style="color:#c0c5ce;">
16+
</span><span style="color:#65737e;">// This code is editable, feel free to hack it!
17+
</span><span style="color:#65737e;">// You can always return to the original code by clicking the &quot;Reset&quot; button -&gt;
18+
</span><span style="color:#c0c5ce;">
19+
</span><span style="color:#65737e;">// This is the main function.
20+
</span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">main</span><span style="color:#c0c5ce;">() {
21+
</span><span style="color:#c0c5ce;"> </span><span style="color:#65737e;">// Statements here are executed when the compiled binary is called.
22+
</span><span style="color:#c0c5ce;">
23+
</span><span style="color:#c0c5ce;"> </span><span style="color:#65737e;">// Print text to the console.
24+
</span><span style="color:#c0c5ce;"> println!(&quot;</span><span style="color:#a3be8c;">Hello World!</span><span style="color:#c0c5ce;">&quot;);
25+
</span><span style="color:#c0c5ce;">}
26+
</span></code></pre>
27+
28+
</body>
29+
</html>
30+

0 commit comments

Comments
 (0)