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

Classy highlighting #262

Merged
merged 5 commits into from
Dec 2, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
"rust-analyzer.checkOnSave.command": "clippy",
"[ruby]": {
"editor.defaultFormatter": "Shopify.ruby-lsp"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
67 changes: 48 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,26 @@ providing further niceties.

#### Syntax Highlighter Plugin

The library comes with [a set of pre-existing themes](https://docs.rs/syntect/5.0.0/syntect/highlighting/struct.ThemeSet.html#implementations) for highlighting code:

- `"base16-ocean.dark"`
- `"base16-eighties.dark"`
- `"base16-mocha.dark"`
- `"base16-ocean.light"`
- `"InspiredGitHub"`
- `"Solarized (dark)"`
- `"Solarized (light)"`

````ruby
code = <<~CODE
```ruby
def hello
puts "hello"
puts "hello"
end
```
CODE

# pass in a theme name from a pre-existing set
puts Commonmarker.to_html(code, plugins: { syntax_highlighter: { theme: "InspiredGitHub" } })

# <pre style="background-color:#ffffff;" lang="ruby"><code>
Expand All @@ -123,32 +135,49 @@ puts Commonmarker.to_html(code, plugins: { syntax_highlighter: { theme: "Inspire
# </code></pre>
````

To disable this plugin, pass `nil`:
By default, the plugin uses the `"base16-ocean.dark"` theme to syntax highlight code.

To disable this plugin, set the value to `nil`:

````ruby
code = <<~CODE
```ruby
def hello
puts "hello"
end
```
CODE

```ruby
Commonmarker.to_html(code, plugins: { syntax_highlighter: nil })
# or
Commonmarker.to_html(code, plugins: { syntax_highlighter: { theme: nil } })
```

You can also provide a `path` to a directory containing `.tmtheme` files to load:
# <pre lang="ruby"><code>def hello
# puts &quot;hello&quot;
# end
# </code></pre>
````

To output CSS classes instead of `style` attributes, set the `theme` key to `""`:

```ruby
````ruby
code = <<~CODE
```ruby
def hello
puts "hello"
end
CODE

Commonmarker.to_html(code, plugins: { syntax_highlighter: { theme: "Monokai", path: "./themes" } })
```
Commonmarker.to_html(code, plugins: { syntax_highlighter: { theme: "" } })

##### Available themes
# <pre class="syntax-highlighting"><code><span class="source ruby"><span class="meta function ruby"><span class="keyword control def ruby">def</span></span><span class="meta function ruby"> # <span class="entity name function ruby">hello</span></span>
# <span class="support function builtin ruby">puts</span> <span class="string quoted double ruby"><span class="punctuation definition string begin ruby">&quot;</span>hello<span class="punctuation definition string end ruby">&quot;</span></span>
# <span class="keyword control ruby">end</span>\n</span></code></pre>
````

Here's [a list of themes available by default](https://docs.rs/syntect/5.0.0/syntect/highlighting/struct.ThemeSet.html#implementations):
To use a custom theme, you can provide a `path` to a directory containing `.tmtheme` files to load:

- `"base16-ocean.dark"`
- `"base16-eighties.dark"`
- `"base16-mocha.dark"`
- `"base16-ocean.light"`
- `"InspiredGitHub"`
- `"Solarized (dark)"`
- `"Solarized (light)"`
```ruby
Commonmarker.to_html(code, plugins: { syntax_highlighter: { theme: "Monokai", path: "./themes" } })
```

## Output formats

Expand Down
142 changes: 72 additions & 70 deletions ext/commonmarker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ use options::iterate_options_hash;

mod plugins;
use plugins::{
syntax_highlighting::{
fetch_syntax_highlighter_path, fetch_syntax_highlighter_theme,
SYNTAX_HIGHLIGHTER_PLUGIN_DEFAULT_THEME,
},
syntax_highlighting::{fetch_syntax_highlighter_path, fetch_syntax_highlighter_theme},
SYNTAX_HIGHLIGHTER_PLUGIN,
};

Expand Down Expand Up @@ -57,78 +54,83 @@ fn commonmark_to_html(args: &[Value]) -> Result<String, magnus::Error> {

let theme = match rb_plugins.get(Symbol::new(SYNTAX_HIGHLIGHTER_PLUGIN)) {
Some(syntax_highlighter_options) => {
fetch_syntax_highlighter_theme(syntax_highlighter_options)?
}
None => SYNTAX_HIGHLIGHTER_PLUGIN_DEFAULT_THEME.to_string(), // no `syntax_highlighter:` defined
};

let path = match rb_plugins.get(Symbol::new(SYNTAX_HIGHLIGHTER_PLUGIN)) {
Some(syntax_highlighter_options) => {
fetch_syntax_highlighter_path(syntax_highlighter_options)?
match fetch_syntax_highlighter_theme(syntax_highlighter_options) {
Ok(theme) => theme,
Err(e) => {
return Err(e);
}
}
}
None => PathBuf::from("".to_string()), // no `syntax_highlighter:` defined
None => None, // no `syntax_highlighter:` defined
};

if !path.eq(&PathBuf::from("".to_string())) && !path.exists() {
return Err(Error::new(
exception::arg_error(),
"path does not exist".to_string(),
));
}

if theme.is_empty() && path.exists() {
return Err(Error::new(
exception::arg_error(),
"`path` also needs `theme` passed into the `syntax_highlighter`",
));
}
if path.exists() && !path.is_dir() {
return Err(Error::new(
exception::arg_error(),
"`path` needs to be a directory",
));
}

if path.exists() {
let builder = SyntectAdapterBuilder::new();
let mut ts = ThemeSet::load_defaults();

match ts.add_from_folder(&path) {
Ok(_) => {}
Err(e) => {
return Err(Error::new(
exception::arg_error(),
format!("failed to load theme set from path: {e}"),
));
match theme {
None => syntax_highlighter = None,
Some(theme) => {
if theme.is_empty() {
// no theme? uss css classes
adapter = SyntectAdapter::new(None);
syntax_highlighter = Some(&adapter);
} else {
let path = match rb_plugins.get(Symbol::new(SYNTAX_HIGHLIGHTER_PLUGIN)) {
Some(syntax_highlighter_options) => {
fetch_syntax_highlighter_path(syntax_highlighter_options)?
}
None => PathBuf::from("".to_string()), // no `syntax_highlighter:` defined
};

if path.exists() {
if !path.is_dir() {
return Err(Error::new(
exception::arg_error(),
"`path` needs to be a directory",
));
}

let builder = SyntectAdapterBuilder::new();
let mut ts = ThemeSet::load_defaults();

match ts.add_from_folder(&path) {
Ok(_) => {}
Err(e) => {
return Err(Error::new(
exception::arg_error(),
format!("failed to load theme set from path: {e}"),
));
}
}

// check if the theme exists in the dir
match ts.themes.get(&theme) {
Some(theme) => theme,
None => {
return Err(Error::new(
exception::arg_error(),
format!("theme `{}` does not exist", theme),
));
}
};

adapter = builder.theme_set(ts).theme(&theme).build();

syntax_highlighter = Some(&adapter);
} else {
// no path? default theme lookup
ThemeSet::load_defaults()
.themes
.get(&theme)
.ok_or_else(|| {
Error::new(
exception::arg_error(),
format!("theme `{}` does not exist", theme),
)
})?;
adapter = SyntectAdapter::new(Some(&theme));
syntax_highlighter = Some(&adapter);
}
}
}

ts.themes.get(&theme).ok_or_else(|| {
Error::new(
exception::arg_error(),
format!("theme `{}` does not exist", theme),
)
})?;

adapter = builder.theme_set(ts).theme(&theme).build();

syntax_highlighter = Some(&adapter);
} else if theme.is_empty() || theme == "none" {
syntax_highlighter = None;
} else {
ThemeSet::load_defaults()
.themes
.get(&theme)
.ok_or_else(|| {
Error::new(
exception::arg_error(),
format!("theme `{}` does not exist", theme),
)
})?;
adapter = SyntectAdapter::new(Some(&theme));
syntax_highlighter = Some(&adapter);
}

comrak_plugins.render.codefence_syntax_highlighter = syntax_highlighter;

Ok(markdown_to_html_with_plugins(
Expand Down
33 changes: 24 additions & 9 deletions ext/commonmarker/src/plugins/syntax_highlighting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,43 @@ use crate::EMPTY_STR;

pub const SYNTAX_HIGHLIGHTER_PLUGIN_THEME_KEY: &str = "theme";
pub const SYNTAX_HIGHLIGHTER_PLUGIN_PATH_KEY: &str = "path";
pub const SYNTAX_HIGHLIGHTER_PLUGIN_DEFAULT_THEME: &str = "base16-ocean.dark";

pub fn fetch_syntax_highlighter_theme(value: Value) -> Result<String, magnus::Error> {
pub fn fetch_syntax_highlighter_theme(value: Value) -> Result<Option<String>, magnus::Error> {
if value.is_nil() {
// `syntax_highlighter: nil`
return Ok(EMPTY_STR.to_string());
return Ok(None);
}

let syntax_highlighter_plugin: RHash = match TryConvert::try_convert(value) {
Ok(plugin) => plugin, // `syntax_highlighter: { theme: "<something>" }`
Err(e) => {
// not a hash!
return Err(e);
}
};

if syntax_highlighter_plugin.is_nil() || syntax_highlighter_plugin.is_empty() {
return Err(magnus::Error::new(
magnus::exception::type_error(),
"theme cannot be blank hash",
));
}

let syntax_highlighter_plugin: RHash = TryConvert::try_convert(value)?;
let theme_key = Symbol::new(SYNTAX_HIGHLIGHTER_PLUGIN_THEME_KEY);

match syntax_highlighter_plugin.get(theme_key) {
Some(theme) => {
if theme.is_nil() {
// `syntax_highlighter: { theme: nil }`
return Ok(EMPTY_STR.to_string());
return Err(magnus::Error::new(
magnus::exception::type_error(),
"theme cannot be nil",
));
}
Ok(theme.try_convert::<String>()?)
Ok(TryConvert::try_convert(theme)?)
}
None => {
// `syntax_highlighter: { }`
Ok(EMPTY_STR.to_string())
// `syntax_highlighter: { theme: nil }`
Ok(None)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/commonmarker/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Commonmarker
VERSION = "1.0.0.pre11"
VERSION = "1.0.0.pre12"
end
Loading