Repository: kivikakk/comrak (Rust)

Issue:
Syntect syntax highlighting: specify CSS class prefix
Hello,
In my setup, I have (and want) to use CSS to style the syntax highlighting. Right now, you can instruct the Syntect syntax-highlighter to emit CSS classes by calling `css()` on the `SyntectAdapterBuilder`. Unfortunately, [the class style is hardcoded to `ClassStyle::Spaced`](https://github.com/kivikakk/comrak/blob/ff33d99ceecf2806f71e3cce182392b089787a13/src/plugins/syntect.rs#L60). In my case, this leads to collisions of CSS classes which ultimately results in weird looking syntax highlighting. An easy and proper mitigation of this issue is to allow the user to specify a prefix for the CSS classes emitted by the `SyntectAdapter`.

Since Syntect has the necessary functionality, you only have to make it configurable in the `SyntectAdapter`/`SyntectAdapterBuilder`.
I created a small PR (#580) and am looking forward to your feedback!


Discussion:

Patch:
diff --git a/src/plugins/syntect.rs b/src/plugins/syntect.rs
index 477d7cd8..729766e7 100644
--- a/src/plugins/syntect.rs
+++ b/src/plugins/syntect.rs
@@ -19,6 +19,7 @@ pub struct SyntectAdapter {
     theme: Option<String>,
     syntax_set: SyntaxSet,
     theme_set: ThemeSet,
+    css_class_prefix: Option<&'static str>, // syntect::html::ClassStyle::SpacePrefixed requires prefix to be &'static str
 }
 
 impl SyntectAdapter {
@@ -29,6 +30,7 @@ impl SyntectAdapter {
             theme: theme.map(String::from),
             syntax_set: SyntaxSet::load_defaults_newlines(),
             theme_set: ThemeSet::load_defaults(),
+            css_class_prefix: None,
         }
     }
 
@@ -54,10 +56,14 @@ impl SyntectAdapter {
             }
             None => {
                 // fall back to HTML classes.
+                let class_style = match &self.css_class_prefix {
+                    None => ClassStyle::Spaced,
+                    Some(prefix) => ClassStyle::SpacedPrefixed { prefix },
+                };
                 let mut html_generator = ClassedHTMLGenerator::new_with_class_style(
                     syntax,
                     &self.syntax_set,
-                    ClassStyle::Spaced,
+                    class_style,
                 );
                 for line in LinesWithEndings::from(code) {
                     html_generator.parse_html_for_line_which_includes_newline(line)?;
@@ -188,6 +194,7 @@ pub struct SyntectAdapterBuilder {
     theme: Option<String>,
     syntax_set: Option<SyntaxSet>,
     theme_set: Option<ThemeSet>,
+    css_class_prefix: Option<&'static str>,
 }
 
 impl Default for SyntectAdapterBuilder {
@@ -196,6 +203,7 @@ impl Default for SyntectAdapterBuilder {
             theme: Some("InspiredGitHub".into()),
             syntax_set: None,
             theme_set: None,
+            css_class_prefix: None,
         }
     }
 }
@@ -218,6 +226,13 @@ impl SyntectAdapterBuilder {
         self
     }
 
+    /// Uses CSS classes with the specified prefix instead of a Syntect theme.
+    pub fn css_with_class_prefix(self, prefix: &'static str) -> Self {
+        let mut builder = self.css();
+        builder.css_class_prefix = Some(prefix);
+        builder
+    }
+
     /// Set the syntax set.
     pub fn syntax_set(mut self, s: SyntaxSet) -> Self {
         self.syntax_set.replace(s);
@@ -241,6 +256,7 @@ impl SyntectAdapterBuilder {
                 .syntax_set
                 .unwrap_or_else(SyntaxSet::load_defaults_newlines),
             theme_set: self.theme_set.unwrap_or_else(ThemeSet::load_defaults),
+            css_class_prefix: self.css_class_prefix,
         }
     }
 }


Test patch:
diff --git a/src/tests/plugins.rs b/src/tests/plugins.rs
index 0e171b5d..66352c94 100644
--- a/src/tests/plugins.rs
+++ b/src/tests/plugins.rs
@@ -124,3 +124,27 @@ fn syntect_plugin_with_css_classes() {
 
     html_plugins(input, expected, &plugins);
 }
+
+#[test]
+#[cfg(feature = "syntect")]
+fn syntect_plugin_with_prefixed_css_classes() {
+    let prefix = Box::leak(Box::new("prefix-"));
+    let adapter = crate::plugins::syntect::SyntectAdapterBuilder::new()
+        .css_with_class_prefix(prefix)
+        .build();
+
+    let input = concat!("```rust\n", "fn main<'a>();\n", "```\n");
+
+    let expected = concat!(
+        "<pre class=\"syntax-highlighting\"><code class=\"language-rust\">",
+        "<span class=\"prefix-source prefix-rust\"><span class=\"prefix-meta prefix-function prefix-rust\"><span class=\"prefix-meta prefix-function prefix-rust\"><span class=\"prefix-storage prefix-type prefix-function prefix-rust\">fn</span> </span><span class=\"prefix-entity prefix-name prefix-function prefix-rust\">main</span></span><span class=\"prefix-meta prefix-generic prefix-rust\"><span class=\"prefix-punctuation prefix-definition prefix-generic prefix-begin prefix-rust\">&lt;</span>",
+        "<span class=\"prefix-storage prefix-modifier prefix-lifetime prefix-rust\">&#39;a</span><span class=\"prefix-punctuation prefix-definition prefix-generic prefix-end prefix-rust\">&gt;</span></span><span class=\"prefix-meta prefix-function prefix-rust\"><span class=\"prefix-meta prefix-function prefix-parameters prefix-rust\"><span class=\"prefix-punctuation prefix-section prefix-parameters prefix-begin prefix-rust\">(</span></span><span class=\"prefix-meta prefix-function prefix-rust\">",
+        "<span class=\"prefix-meta prefix-function prefix-parameters prefix-rust\"><span class=\"prefix-punctuation prefix-section prefix-parameters prefix-end prefix-rust\">)</span></span></span></span><span class=\"prefix-punctuation prefix-terminator prefix-rust\">;</span>\n</span>",
+        "</code></pre>\n",
+    );
+
+    let mut plugins = Plugins::default();
+    plugins.render.codefence_syntax_highlighter = Some(&adapter);
+
+    html_plugins(input, expected, &plugins);
+}