kramdown-syntax_tree_sitter 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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