kramdown-syntax_tree_sitter 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02e58047b1cac1778ad13d25f88a64a42f0714faeba928322851a78afeb4ce66
4
- data.tar.gz: 87cc78c75fb83a9a56cca8bb663f72b1d3abbd03f3920e37bad6ac629c699d69
3
+ metadata.gz: 4ba9a8e090021116214ebf5938b8de67856e7aa0054aeb49de5a67041b0683a5
4
+ data.tar.gz: b9d7a9110a29f9abd175ea82c3f54eccb15ca334b69181299f291f43c8c6c35b
5
5
  SHA512:
6
- metadata.gz: 3b609ade16479c2e09504248970b1f95eb496b982cc641e766881dbfc74bedd580a45bc5f0c24f9b859535a3967a74e51126e6dfdf8df7448a5284da259e3105
7
- data.tar.gz: ae2cdab47a180b7435fc9c01bff81aefb377a353af341dc98f3f5bd8685105f6b976d35c25a900fcc7caa8744e3c34ef3fdbb63fb09dc27d52ee63f67644466e
6
+ metadata.gz: ff14f9dff79b8681771b4a0b078e8a1a820489cb2e338f91f5f2ca30d758495a19763f9449585429872c00943c38dca10e88a9465eb7a97cd1c64c298935d528
7
+ data.tar.gz: fc629b06c06b8df3614084dd40ccf8d38747ddc5ab9e096cb85f5976feb0a8f0729615e303a88f256b0bdd9be0c96309ec2cd461bedf472bf7e2f7aa5d4605a5
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Kramdown Tree-sitter Highlighter
2
2
 
3
+ [![Build status](https://img.shields.io/github/workflow/status/andrewtbiehl/kramdown-syntax_tree_sitter/Quality%20Control?style=flat-square&logo=github)](https://github.com/andrewtbiehl/kramdown-syntax_tree_sitter/actions/workflows/quality-control.yml)
4
+ [![RubyGems page](https://img.shields.io/gem/v/kramdown-syntax_tree_sitter?style=flat-square&color=blue&logo=rubygems&logoColor=white)](https://rubygems.org/gems/kramdown-syntax_tree_sitter)
5
+
3
6
  ***Syntax highlight code with [Tree-sitter](https://tree-sitter.github.io/tree-sitter)
4
7
  via [Kramdown](https://kramdown.gettalong.org).***
5
8
 
@@ -29,14 +32,13 @@ For projects using [Bundler](https://bundler.io) for dependency management, run
29
32
  following command to both install the gem and add it to the Gemfile:
30
33
 
31
34
  ```shell
32
- bundle add kramdown-syntax_tree_sitter --github andrewtbiehl/kramdown-syntax_tree_sitter
35
+ bundle add kramdown-syntax_tree_sitter
33
36
  ```
34
37
 
35
- Otherwise, download this project's repository and then run the following command from
36
- within it to build and install the gem:
38
+ Otherwise, install the gem via the following command:
37
39
 
38
40
  ```shell
39
- gem build && gem install kramdown-syntax_tree_sitter
41
+ gem install kramdown-syntax_tree_sitter
40
42
  ```
41
43
 
42
44
  ## Usage
@@ -176,14 +178,58 @@ block will only be correctly highlighted if the language identifier provided for
176
178
  code block is its language's corresponding Tree-sitter scope string. This is illustrated
177
179
  by the code block used in the [Quickstart](#quickstart) example.
178
180
 
181
+ ### CSS styling
182
+
183
+ Code highlights can be rendered either via inline CSS styles or via CSS classes, which
184
+ are applied to each token in the parsed code.
185
+
186
+ To use CSS classes for highlighting, set the `css_classes` Kramdown syntax-highlighting
187
+ option to `true`. Otherwise, the plugin will apply highlights via inline CSS styles.
188
+
189
+ The inline CSS styles are derived from Tree-sitter's built-in default highlighting
190
+ theme, repeated here for convenience:
191
+
192
+ | Token name | CSS style |
193
+ | :-- | :-- |
194
+ | attribute | `color: #af0000; font-style: italic;` |
195
+ | comment | `color: #8a8a8a; font-style: italic;` |
196
+ | constant | `color: #875f00;` |
197
+ | function | `color: o#005fd7;` |
198
+ | function.builtin | `color: #005fd7; font-weight: bold;` |
199
+ | keyword | `color: #5f00d7;` |
200
+ | operator | `color: #4e4e4e; font-weight: bold;` |
201
+ | property | `color: #af0000;` |
202
+ | string | `color: #008700;` |
203
+ | string.special | `color: #008787;` |
204
+ | tag | `color: #000087;` |
205
+ | type | `color: #005f5f;` |
206
+ | type.builtin | `color: #005f5f; font-weight: bold;` |
207
+ | variable.builtin | `font-weight: bold;` |
208
+ | variable.parameter | `text-decoration: underline;` |
209
+ | constant.builtin, number | `color: #875f00; font-weight: bold;` |
210
+ | constructor, module | `color: #af8700;` |
211
+ | punctuation.bracket, punctuation.delimiter | `color: #4e4e4e;` |
212
+
213
+ Any and all token types not represented in this default theme are consequently not
214
+ highlighted when using the inline CSS styles option.
215
+
216
+ The CSS class names are derived directly from Tree-sitter token names by replacing all
217
+ full stops ('.') with dashes ('-') and adding the prefix 'ts-'. For example, the CSS
218
+ class for 'function.builtin' tokens is 'ts-function-builtin'. The use of CSS classes
219
+ allows for customization of highlighting styles, including the ability to highlight more
220
+ token types than with the default inline CSS method. Of course, this also requires that
221
+ an externally created CSS stylesheet defining the style for each token type is provided
222
+ whenever the Kramdown-generated HTML is rendered.
223
+
179
224
  ### Configuration
180
225
 
181
226
  This Kramdown plugin currently supports the following options when provided as sub-keys
182
227
  of the Kramdown option `syntax_highlighter_opts`:
183
228
 
184
- | Key | Description | Default value |
185
- | :-- | :-- | :-- |
186
- | `tree_sitter_parsers_dir` | The path to the Tree-sitter language parsers directory. | `~/tree_sitter_parsers` |
229
+ | Key | Description | Type | Default value |
230
+ | :-- | :-- | :-- | :-- |
231
+ | `tree_sitter_parsers_dir` | The path to the Tree-sitter language parsers directory. | String | `~/tree_sitter_parsers` |
232
+ | `css_classes` | Whether to use CSS classes for highlights. | Boolean | `false` |
187
233
 
188
234
  ## Contributing
189
235
 
@@ -1,21 +1,28 @@
1
1
  #[macro_use]
2
2
  extern crate rutie;
3
3
 
4
- use rutie::{AnyException, Class, Exception, Object, RString, VM};
4
+ use rutie::{AnyException, Boolean, Class, Exception, Object, RString, VM};
5
5
 
6
6
  mod tree_sitter_adapter;
7
7
 
8
8
  class!(TreeSitterAdapter);
9
9
 
10
+ #[rustfmt::skip]
10
11
  methods!(
11
12
  TreeSitterAdapter,
12
13
  _rtself,
13
- fn pub_highlight(raw_code: RString, raw_parsers_dir: RString, raw_scope: RString) -> RString {
14
+ fn pub_highlight(
15
+ raw_code: RString,
16
+ raw_parsers_dir: RString,
17
+ raw_scope: RString,
18
+ css_classes: Boolean
19
+ ) -> RString {
14
20
  VM::unwrap_or_raise_ex(
15
21
  tree_sitter_adapter::highlight(
16
22
  &raw_code.unwrap().to_string(),
17
23
  &raw_parsers_dir.unwrap().to_string(),
18
24
  &raw_scope.unwrap().to_string(),
25
+ css_classes.unwrap().to_bool(),
19
26
  )
20
27
  .as_ref()
21
28
  .map(String::as_str)
@@ -1,7 +1,9 @@
1
- use anyhow::{Context, Error, Result};
1
+ use anyhow::{Context, Result};
2
+ use std::collections::HashMap;
2
3
  use std::convert;
3
4
  use std::path::PathBuf;
4
5
  use tree_sitter::Language;
6
+ use tree_sitter_cli::highlight::Style;
5
7
  use tree_sitter_cli::highlight::Theme;
6
8
  use tree_sitter_highlight::{Error as TSError, HighlightEvent};
7
9
  use tree_sitter_highlight::{Highlight, HighlightConfiguration, Highlighter, HtmlRenderer};
@@ -9,126 +11,93 @@ use tree_sitter_loader::{Config, LanguageConfiguration, Loader};
9
11
 
10
12
  const LOADER_ERROR_MSG: &str = "Error loading Tree-sitter parsers from directory";
11
13
  const NO_LANGUAGE_ERROR_MSG: &str = "Error retrieving language configuration for scope";
14
+ const NO_HIGHLIGHT_ERROR_MSG: &str = "Error retrieving highlight configuration for scope";
12
15
 
13
- pub fn highlight(code: &str, parsers_dir: &str, scope: &str) -> Result<String, String> {
14
- let parsers_dir = PathBuf::from(parsers_dir);
15
- let theme = Theme::default();
16
- let mut loader = Loader::new_from_dir(parsers_dir).map_err(Error::to_formatted_string)?;
17
- loader.configure_highlights(&theme.highlight_names);
18
- loader
19
- .language_configuration_from_scope(scope)
20
- .and_then(LanguageConfigurationAdapter::highlight_config)
21
- .and_then(|config| {
22
- let default_css_style = String::new();
23
- let css_attribute_callback = get_css_styles(&theme, &default_css_style);
24
- HighlighterAdapter::new(&loader, config)
25
- .highlight(code)
26
- .and_then(|highlights| render_html(highlights, &css_attribute_callback))
27
- })
28
- .map_err(Error::to_formatted_string)
29
- }
30
-
31
- trait LoaderExt {
32
- fn new_from_dir(parser_directory: PathBuf) -> Result<Loader>;
33
-
34
- fn language_configuration_from_scope<'a>(
35
- &'a self,
36
- scope: &'a str,
37
- ) -> Result<LanguageConfigurationAdapter<'a>>;
16
+ trait ResultExt<T, E> {
17
+ fn flatten_(self) -> Result<T, E>;
38
18
  }
39
19
 
40
- impl LoaderExt for Loader {
41
- fn new_from_dir(parser_directory: PathBuf) -> Result<Loader> {
42
- Loader::new()
43
- .and_then(|mut loader| {
44
- let config = {
45
- let parser_directory = parser_directory.clone();
46
- let parser_directories = vec![parser_directory];
47
- Config { parser_directories }
48
- };
49
- loader.find_all_languages(&config)?;
50
- Ok(loader)
51
- })
52
- .with_context(|| {
53
- let parser_directory_string = parser_directory.display();
54
- format!("{LOADER_ERROR_MSG} '{parser_directory_string}'")
55
- })
56
- }
57
-
58
- fn language_configuration_from_scope<'a>(
59
- &'a self,
60
- scope: &'a str,
61
- ) -> Result<LanguageConfigurationAdapter<'a>> {
62
- self.language_configuration_for_scope(scope)
63
- .transpose()
64
- .context("Language not found")
65
- .flatten_()
66
- .map(|(language, config)| LanguageConfigurationAdapter { language, config })
67
- .with_context(|| format!("{NO_LANGUAGE_ERROR_MSG} '{scope}'"))
20
+ impl<T, E> ResultExt<T, E> for Result<Result<T, E>, E> {
21
+ fn flatten_(self) -> Result<T, E> {
22
+ self.and_then(convert::identity)
68
23
  }
69
24
  }
70
25
 
71
- struct LanguageConfigurationAdapter<'a> {
72
- language: Language,
73
- config: &'a LanguageConfiguration<'a>,
26
+ fn loader(parser_directory: PathBuf) -> Result<Loader> {
27
+ Loader::new()
28
+ .and_then(|mut loader| {
29
+ let config = {
30
+ let parser_directory = parser_directory.clone();
31
+ let parser_directories = vec![parser_directory];
32
+ Config { parser_directories }
33
+ };
34
+ loader.find_all_languages(&config)?;
35
+ Ok(loader)
36
+ })
37
+ .with_context(|| {
38
+ let parser_directory_str = parser_directory.display();
39
+ format!("{LOADER_ERROR_MSG} '{parser_directory_str}'")
40
+ })
74
41
  }
75
42
 
76
- impl<'a> LanguageConfigurationAdapter<'a> {
77
- fn highlight_config(self) -> Result<&'a HighlightConfiguration> {
78
- self.config
79
- .highlight_config(self.language)
80
- .transpose()
81
- .context("Another issue")
82
- .flatten_()
83
- }
43
+ fn language_and_configuration<'a>(
44
+ loader: &'a Loader,
45
+ scope: &'a str,
46
+ ) -> Result<(Language, &'a LanguageConfiguration<'a>)> {
47
+ loader
48
+ .language_configuration_for_scope(scope)
49
+ .transpose()
50
+ .context("Language not found")
51
+ .flatten_()
52
+ .with_context(|| format!("{NO_LANGUAGE_ERROR_MSG} '{scope}'"))
84
53
  }
85
54
 
86
- struct HighlightsAdapter<'a, T: Iterator<Item = Result<HighlightEvent, TSError>>> {
87
- code: &'a str,
88
- highlights: T,
55
+ fn highlight_configuration<'a>(
56
+ language: Language,
57
+ config: &'a LanguageConfiguration<'a>,
58
+ scope: &'a str,
59
+ ) -> Result<&'a HighlightConfiguration> {
60
+ config
61
+ .highlight_config(language)
62
+ .transpose()
63
+ .with_context(|| format!("{NO_HIGHLIGHT_ERROR_MSG} '{scope}'"))
64
+ .flatten_()
89
65
  }
90
66
 
91
- struct HighlighterAdapter<'a> {
92
- loader: &'a Loader,
93
- config: &'a HighlightConfiguration,
94
- highlighter: Highlighter,
67
+ fn highlights(
68
+ code: &str,
69
+ config: &HighlightConfiguration,
70
+ loader: &Loader,
71
+ ) -> Result<impl Iterator<Item = Result<HighlightEvent, TSError>>> {
72
+ Highlighter::new()
73
+ .highlight(config, code.as_bytes(), None, |s| {
74
+ loader.highlight_config_for_injection_string(s)
75
+ })
76
+ .map(Iterator::collect)
77
+ .map(Vec::into_iter)
78
+ .map_err(Into::into)
95
79
  }
96
80
 
97
- impl<'a> HighlighterAdapter<'a> {
98
- fn new(loader: &'a Loader, config: &'a HighlightConfiguration) -> Self {
99
- Self {
100
- loader,
101
- config,
102
- highlighter: Highlighter::new(),
103
- }
104
- }
105
-
106
- fn highlight(
107
- &'a mut self,
108
- code: &'a str,
109
- ) -> Result<HighlightsAdapter<'a, impl Iterator<Item = Result<HighlightEvent, TSError>> + 'a>>
110
- {
111
- let Self {
112
- loader,
113
- config,
114
- highlighter,
115
- } = self;
116
- highlighter
117
- .highlight(config, code.as_bytes(), None, |s| {
118
- loader.highlight_config_for_injection_string(s)
119
- })
120
- .map(|highlights| HighlightsAdapter { code, highlights })
121
- .map_err(Into::into)
81
+ fn create_html_attribute_callback<'a>(
82
+ html_attributes: &'a [String],
83
+ ) -> impl Fn(Highlight) -> &'a [u8] {
84
+ |highlight| {
85
+ html_attributes
86
+ .get(highlight.0)
87
+ .map(String::as_str)
88
+ .unwrap_or_default()
89
+ .as_bytes()
122
90
  }
123
91
  }
124
92
 
125
- fn render_html<'a, F: Fn(Highlight) -> &'a [u8]>(
126
- highlights: HighlightsAdapter<'a, impl Iterator<Item = Result<HighlightEvent, TSError>> + 'a>,
127
- css_attribute_callback: &F,
93
+ fn render_html(
94
+ code: &str,
95
+ highlights: impl Iterator<Item = Result<HighlightEvent, TSError>>,
96
+ html_attributes: &[String],
128
97
  ) -> Result<String> {
129
- let HighlightsAdapter { code, highlights } = highlights;
98
+ let html_attribute_callback = create_html_attribute_callback(html_attributes);
130
99
  let mut renderer = HtmlRenderer::new();
131
- renderer.render(highlights, code.as_bytes(), css_attribute_callback)?;
100
+ renderer.render(highlights, code.as_bytes(), &html_attribute_callback)?;
132
101
  // Remove erroneously appended newline
133
102
  if renderer.html.ends_with(&[b'\n']) && !code.ends_with('\n') {
134
103
  renderer.html.pop();
@@ -136,36 +105,65 @@ fn render_html<'a, F: Fn(Highlight) -> &'a [u8]>(
136
105
  Ok(renderer.lines().collect())
137
106
  }
138
107
 
139
- fn get_css_styles<'a>(
140
- theme: &'a Theme,
141
- default_css_style: &'a String,
142
- ) -> impl Fn(Highlight) -> &'a [u8] {
143
- |highlight| {
144
- theme
145
- .styles
146
- .get(highlight.0)
147
- .and_then(|style| style.css.as_ref())
148
- .unwrap_or(default_css_style)
149
- .as_bytes()
150
- }
108
+ fn highlight_names(scope: &str, loader: &Loader) -> Result<Vec<String>> {
109
+ let (language, config) = language_and_configuration(loader, scope)?;
110
+ let highlight_config = highlight_configuration(language, config, scope)?;
111
+ Ok(highlight_config.names().iter().map(String::from).collect())
151
112
  }
152
113
 
153
- trait ResultExt<T, E> {
154
- fn flatten_(self) -> Result<T, E>;
114
+ fn highlight_name_styles() -> HashMap<String, Style> {
115
+ let theme = Theme::default();
116
+ theme
117
+ .highlight_names
118
+ .into_iter()
119
+ .zip(theme.styles.into_iter())
120
+ .collect()
155
121
  }
156
122
 
157
- impl<T, E> ResultExt<T, E> for Result<Result<T, E>, E> {
158
- fn flatten_(self) -> Result<T, E> {
159
- self.and_then(convert::identity)
160
- }
123
+ fn inline_css_attributes(highlight_names: &[String]) -> Vec<String> {
124
+ let highlight_name_styles = highlight_name_styles();
125
+ highlight_names
126
+ .iter()
127
+ .map(|n| highlight_name_styles.get(n))
128
+ .map(|o| o.and_then(|style| style.css.as_ref()))
129
+ .map(|o| o.map(String::from))
130
+ .map(Option::unwrap_or_default)
131
+ .collect()
161
132
  }
162
133
 
163
- trait ErrorExt {
164
- fn to_formatted_string(self) -> String;
134
+ fn css_class_attributes(highlight_names: &[String]) -> Vec<String> {
135
+ highlight_names
136
+ .iter()
137
+ .map(|s| s.replace('.', "-"))
138
+ .map(|s| format!("class='ts-{s}'"))
139
+ .collect()
165
140
  }
166
141
 
167
- impl ErrorExt for Error {
168
- fn to_formatted_string(self) -> String {
169
- format!("{self:#}")
170
- }
142
+ fn highlight_adapter(
143
+ code: &str,
144
+ parsers_dir: &str,
145
+ scope: &str,
146
+ css_classes: bool,
147
+ ) -> Result<String> {
148
+ let parsers_dir = PathBuf::from(parsers_dir);
149
+ let mut loader = loader(parsers_dir)?;
150
+ let highlight_names = highlight_names(scope, &loader)?;
151
+ loader.configure_highlights(&highlight_names);
152
+ let (language, config) = language_and_configuration(&loader, scope)?;
153
+ let highlight_config = highlight_configuration(language, config, scope)?;
154
+ let highlights = highlights(code, highlight_config, &loader)?;
155
+ let html_attributes = match css_classes {
156
+ true => css_class_attributes,
157
+ false => inline_css_attributes,
158
+ }(&highlight_names);
159
+ render_html(code, highlights, &html_attributes)
160
+ }
161
+
162
+ pub fn highlight(
163
+ code: &str,
164
+ parsers_dir: &str,
165
+ scope: &str,
166
+ css_classes: bool,
167
+ ) -> Result<String, String> {
168
+ highlight_adapter(code, parsers_dir, scope, css_classes).map_err(|e| format!("{e:#}"))
171
169
  }
@@ -16,14 +16,26 @@ module Kramdown
16
16
  def self.call(converter, raw_text, language, type, _)
17
17
  return nil unless language
18
18
 
19
- parsers_dir = get_option(converter, :tree_sitter_parsers_dir)
20
- .then { _1 || DEFAULT_PARSERS_DIR }
21
- .then { File.expand_path _1 }
22
- rendered_text = TreeSitterAdapter.highlight raw_text, parsers_dir, language
19
+ rendered_text = TreeSitterAdapter.highlight(
20
+ raw_text,
21
+ get_parsers_dir(converter),
22
+ language,
23
+ get_use_css_classes(converter)
24
+ )
23
25
  # Code blocks are additionally wrapped in HTML code tags
24
26
  type == :block ? "<pre><code>#{rendered_text}</code></pre>" : rendered_text
25
27
  end
26
28
 
29
+ def self.get_parsers_dir(converter)
30
+ File.expand_path(
31
+ get_option(converter, :tree_sitter_parsers_dir) || DEFAULT_PARSERS_DIR
32
+ )
33
+ end
34
+
35
+ def self.get_use_css_classes(converter)
36
+ get_option(converter, :css_classes) || false
37
+ end
38
+
27
39
  def self.get_option(converter, name)
28
40
  converter.options[:syntax_highlighter_opts][name]
29
41
  end
@@ -5,7 +5,7 @@ module Kramdown
5
5
  module SyntaxHighlighter
6
6
  module TreeSitter
7
7
  # Version of kramdown-syntax_tree_sitter gem
8
- VERSION = '0.1.0'
8
+ VERSION = '0.2.0'
9
9
  end
10
10
  end
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kramdown-syntax_tree_sitter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew T. Biehl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-11 00:00:00.000000000 Z
11
+ date: 2022-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: kramdown