jekyll-shiki 0.1.1 → 0.1.2

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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +394 -12
  3. data/lib/jekyll-shiki.rb +16 -1
  4. data/lib/version.rb +1 -1
  5. metadata +5 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8497177fdb6ecfd49f90d3f911318f3222a237c692599b9c47c7e17427d880a
4
- data.tar.gz: 57db7e764d5f6f40493babb1cd5493beec518f0a847b07840e7dda80fb914700
3
+ metadata.gz: 2d817cbf9b2c57a70533d9e027c90b409b330af9baaa1e13ba9665c3ece231e4
4
+ data.tar.gz: b047f687b0632dabbf5fbe0959790c8b97c3acd258a0a86cfc4a340d225fc79d
5
5
  SHA512:
6
- metadata.gz: a082dcba05993fc78aef2eeaf911a3f023dfaa61dea548fb2146721f70bb1dbb04a69b33969e661ede02bdbcf67279d390fce7db7d7d744484cbee484c94f080
7
- data.tar.gz: 6f042cb0f7b057e2354c49cbb2c7b85a9cb325fda8022f593d586ae6e92ae7989628899653b8a672b53da89c24e7a75cfdeb8b8326f87c58c1fa90a4fa3812cb
6
+ metadata.gz: e6bbab53e0d7b252926e406ba2ea6a9cd12d7a0c29da93a737ed872f87ef1ba0969f7a060068bdf4ef8695a6ab1790055c468e91f73613b22dc096e78dc13f8e
7
+ data.tar.gz: e676ed0561ca127973a2bc15beefce6282ba1394c40b91dea1b5ac9218e8bf8b7b9d610027eaadb4ebbbc1a483ec4e5d8e08eebb2ae8021dc3c7e1c3ca58ee5e
data/README.md CHANGED
@@ -7,27 +7,409 @@
7
7
 
8
8
  ## Overview
9
9
 
10
- Jekyll plugin for [Shiki JS](https://shiki.style/)
10
+ Jekyll plugin for [Shiki JS](https://shiki.style/), that bridges the Ruby-based Jekyll environment with the Node.js Shiki ecosystem.
11
11
 
12
- > [!WARNING]
13
- > The first build can take some time, depending on the number of code blocks, before the cache is generated. It also requires the path to the `shiki` code used on the Node.js side.
14
- > This plugin was created for use with the `mmdocs` theme. Performance has not been thoroughly tested, and the documentation is not yet complete.
12
+ ## Installation and Usage
15
13
 
16
- ## Installation
14
+ ### Installation
17
15
 
18
- Install the gem and add to the application's Gemfile by executing:
16
+ The jekyll-shiki gem requires Ruby version `>= 3.3.0` and `Jekyll ~> 4.4`.
19
17
 
20
- ```bash
21
- bundle add jekyll-shiki
18
+ - **Add the Gem**
19
+
20
+ Add the gem to your project's `Gemfile`:
21
+
22
+ ```ruby
23
+ group :jekyll_plugins do
24
+ # other jekyll plugins
25
+ gem "jekyll-shiki"
26
+ end
27
+ ```
28
+
29
+ Alternatively, execute the following command:
30
+
31
+ ```sh
32
+ bundle add jekyll-shiki
33
+ ```
34
+
35
+ - **Install Dependencies**
36
+
37
+ Run bundler to install the gem and its requirements (including nokogiri for HTML parsing)
38
+
39
+ ```sh
40
+ bundle install
41
+ ```
42
+
43
+ ### Configuration
44
+
45
+ The plugin requires a specific configuration block in your Jekyll `_config.yml` file to locate the Node.js Shiki implementation.
46
+ You must define the shiki key. The most critical sub-key is `shiki.file_path`, which points to the JavaScript file responsible for performing the actual highlighting via the Node.js bridge.
47
+
48
+ `_config.yml`
49
+
50
+ ```yml
51
+ plugins:
52
+ # other jekyll plugins
53
+ - jekyll-shiki
54
+ # this is required to run shiki code
55
+ shiki:
56
+ # recommended to use .mjs extension to avoid conflict with `type` in `package.json`.
57
+ # Don't use .ts extension
58
+ file_path: shiki/index.mjs
59
+ ```
60
+
61
+ ### First Build and Latency
62
+
63
+ Users should be aware that the first build will experience significant latency.
64
+
65
+ Why the delay occurs:
66
+
67
+ 1. **Process Spawning**: The plugin must initialize the Node.js environment for code blocks.
68
+ 2. **Cache Generation**: On the first run, every code block must be processed and transformed. Subsequent builds utilize the `Jekyll::Cache` system to skip already-processed blocks.
69
+ 3. **Dependency Loading**: Shiki JS loads themes and grammars into memory during its first execution.
70
+
71
+ ### Creating shiki highlighter
72
+
73
+ You can find detail at [Shiki Installation & Usage](https://shiki.style/guide/install).
74
+
75
+ #### Install Shiki
76
+
77
+ ```sh
78
+ npm i shiki
22
79
  ```
23
80
 
24
- If bundler is not being used to manage dependencies, install the gem by executing:
81
+ #### Example `shiki.file_path` Js file and structure
82
+
83
+ ```js
84
+ import { createHighlighter } from "shiki";
85
+
86
+ // create shiki highlighter
87
+ async function shikiHL(code, lang) {
88
+ // Load shiki bundledLanguages as you want
89
+ /** @type {import("shiki").BundledLanguage} */
90
+ const defaultLangs = [
91
+ "js",
92
+ "ts",
93
+ "sh",
94
+ "json",
95
+ "html",
96
+ "css",
97
+ "ruby",
98
+ "md",
99
+ "yaml",
100
+ "yml",
101
+ "bash",
102
+ ];
103
+ const highlighter = await createHighlighter({
104
+ langs: [...defaultLangs, "text"],
105
+ // load shiki bundled themes (light and dark mode)
106
+ themes: ["dark-plus", "light-plus"],
107
+ });
108
+ lang = defaultLangs.includes(lang) ? lang : "text";
109
+ return highlighter.codeToHtml(code, {
110
+ lang: lang,
111
+ // Defined both light and dark mode for correct load from ruby side
112
+ themes: {
113
+ light: "light-plus",
114
+ dark: "dark-plus",
115
+ },
116
+ });
117
+ }
118
+ // The following structure must required to correct load from ruby side
119
+ async function readStdin() {
120
+ const chunks = [];
121
+
122
+ for await (const chunk of process.stdin) {
123
+ chunks.push(chunk);
124
+ }
125
+
126
+ return chunks.join("");
127
+ }
128
+ if (import.meta.url === new URL(process.argv[1], "file:").href) {
129
+ try {
130
+ const args = await readStdin();
131
+ const { code: c, lang: t } = JSON.parse(args);
132
+ // Replace `shikiHL` to your actual shiki highlighter function name.
133
+ const highlighted = await shikiHL(c, t);
134
+ process.stdout.write(highlighted);
135
+ } catch (error) {
136
+ process.stderr.write(
137
+ `${error instanceof Error ? error.message : String(error)}`,
138
+ );
139
+ process.exit(1);
140
+ }
141
+ }
142
+ ```
143
+
144
+ ### Wrapper HTML Structure
145
+
146
+ The plugin does not just return the highlighted code from Shiki; it wraps it in a UI-friendly container. The `create_wrapper`method generates a `div` with specific classes and data attributes for styling and functionality.
147
+
148
+ | Element | Class/Attribute | Purpose |
149
+ | ----------- | ------------------------ | ------------------------------------------------------ |
150
+ | Container | `div.shiki_code` | Main wrapper for the code block. |
151
+ | Hook | `data-shiki-highlighter` | Attribute for JS or CSS targeting. |
152
+ | Header | `div.code_head` | Contains the language label and copy button. |
153
+ | Label | `span` | Displays the raw language string (e.g., ruby). |
154
+ | Copy Button | `button[data-copy-btn]` | An empty button intended for clipboard JS integration. |
155
+ | Output | `shiki_highlight` | The actual HTML returned by the Node.js Shiki process. |
156
+
157
+ ### Generated HTML Template
158
+
159
+ The structure is defined as a heredoc in Ruby:
25
160
 
26
- ```bash
27
- gem install jekyll-shiki
161
+ ```html
162
+ <div class="shiki_code" data-shiki-highlighter>
163
+ <div class="code_head">
164
+ <span>#{lan}</span>
165
+ <button type="button" aria-label="Highlight-#{lang}" data-copy-btn></button>
166
+ </div>
167
+ #{highlighted_code}
168
+ </div>
28
169
  ```
29
170
 
30
- ## Usage
171
+ #### Styles for generated HTML template
172
+
173
+ Create `scss` file `_codeBlock.scss` with the following code.
174
+
175
+ You can edit color or whatever but don't edit `shiki` and `shiki span` in dark mode , that are rely to shiki generated code.
176
+
177
+ ```scss
178
+ // cspell:disable
179
+ $iconCopy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E");
180
+ $iconCopied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E");
181
+
182
+ @mixin iconMask($icon, $color) {
183
+ background-color: $color;
184
+ -webkit-mask-image: $icon;
185
+ mask-image: $icon;
186
+ -webkit-mask-position: 50%;
187
+ mask-position: 50%;
188
+ -webkit-mask-repeat: no-repeat;
189
+ mask-repeat: no-repeat;
190
+ -webkit-mask-size: 20px;
191
+ mask-size: 20px;
192
+ }
193
+
194
+ :root {
195
+ --hr-bg: #ffffff;
196
+ --hr-bg-2: #f3f3f3;
197
+ --hr-lang: #1f1f1f;
198
+ --hr-lang-muted: #616161;
199
+ --hr-border: #e5e5e5;
200
+ --hr-success: #18794e;
201
+ }
202
+
203
+ .dark,
204
+ *[data-theme="dark"] {
205
+ --hr-bg: #1e1e1e;
206
+ --hr-bg-2: #252526;
207
+ --hr-lang: #d4d4d4;
208
+ --hr-lang-muted: #cccccc;
209
+ --hr-border: #3c3c3c;
210
+ --hr-success: #3dd68c;
211
+ // don't edit
212
+ .shiki,
213
+ .shiki span {
214
+ color: var(--shiki-dark) !important;
215
+ background-color: var(--shiki-dark-bg) !important;
216
+ /* Optional, if you also want font styles */
217
+ font-style: var(--shiki-dark-font-style) !important;
218
+ font-weight: var(--shiki-dark-font-weight) !important;
219
+ text-decoration: var(--shiki-dark-text-decoration) !important;
220
+ }
221
+ }
222
+
223
+ div.shiki_code {
224
+ position: relative;
225
+ margin: auto;
226
+ width: 100%;
227
+ background-color: var(--hr-bg);
228
+ overflow: hidden;
229
+ transition: background-color 0.5s;
230
+ margin-top: 7px;
231
+ margin-bottom: 7px;
232
+ border-radius: 8px;
233
+ border: 1px solid var(--hr-border);
234
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--hr-border) 10%, transparent);
235
+
236
+ @media (max-width: 640px) {
237
+ border-radius: 8px;
238
+ margin: 16px 0;
239
+ }
240
+ }
241
+
242
+ div.shiki_code > div.code_head {
243
+ display: flex;
244
+ flex-direction: row;
245
+ padding: 0.42rem 1rem;
246
+ align-items: center;
247
+ justify-content: space-between;
248
+ background-color: var(--hr-bg-2);
249
+ border-bottom: 1px solid var(--hr-border);
250
+ }
251
+ div.shiki_code > div.code_head > span {
252
+ font-size: 0.84rem;
253
+ font-weight: 400;
254
+ letter-spacing: 0.01em;
255
+ -webkit-user-select: none;
256
+ user-select: none;
257
+ color: var(--hr-lang-muted);
258
+ transition:
259
+ color 0.4s,
260
+ opacity 0.4s;
261
+ }
262
+ div.shiki_code > div.code_head > button {
263
+ position: relative;
264
+ border: none;
265
+ border-radius: 4px;
266
+ width: 20px;
267
+ height: 20px;
268
+ background-color: transparent;
269
+ cursor: pointer;
270
+ transition:
271
+ border-color 0.25s,
272
+ background-color 0.25s,
273
+ opacity 0.25s;
274
+
275
+ &:hover {
276
+ background-color: color-mix(in srgb, var(--hr-lang-muted) 10%, transparent);
277
+ }
278
+
279
+ &::before {
280
+ content: "";
281
+ position: absolute;
282
+ inset: 0;
283
+ @include iconMask($iconCopy, var(--hr-lang));
284
+ transition: background-color 0.25s;
285
+ }
286
+ }
287
+ div.shiki_code > div.code_head > button.copied,
288
+ div.shiki_code > div.code_head > button:hover.copied {
289
+ &::before {
290
+ @include iconMask($iconCopied, var(--hr-success));
291
+ }
292
+ }
293
+
294
+ div.shiki_code > pre.shiki {
295
+ position: relative;
296
+ z-index: 1;
297
+ margin: 0;
298
+ padding: 12px 0;
299
+ background: transparent;
300
+ overflow-x: auto;
301
+ scrollbar-gutter: stable;
302
+ direction: ltr;
303
+ text-align: left;
304
+ white-space: pre;
305
+ word-spacing: normal;
306
+ word-break: normal;
307
+ word-wrap: normal;
308
+ -moz-tab-size: 4;
309
+ -o-tab-size: 4;
310
+ tab-size: 4;
311
+ -webkit-hyphens: none;
312
+ -moz-hyphens: none;
313
+ -ms-hyphens: none;
314
+ hyphens: none;
315
+ }
316
+
317
+ .shiki_code > pre.shiki > code {
318
+ display: block;
319
+ padding: 0 24px;
320
+ width: fit-content;
321
+ min-width: 100%;
322
+ line-height: 1.65;
323
+ font-size: 0.95rem;
324
+ font-family: var(--font-mono);
325
+ color: var(--hr-lang);
326
+ transition: color 0.5s;
327
+ direction: ltr;
328
+ text-align: left;
329
+ white-space: pre;
330
+ word-spacing: normal;
331
+ word-break: normal;
332
+ word-wrap: normal;
333
+ -moz-tab-size: 4;
334
+ -o-tab-size: 4;
335
+ tab-size: 4;
336
+ -webkit-hyphens: none;
337
+ -moz-hyphens: none;
338
+ -ms-hyphens: none;
339
+ hyphens: none;
340
+ }
341
+ ```
342
+
343
+ Use in your `sass` entry (like `main.scss`) , make sure your actual `sass` entry and `_codeBlock.scss` are same directory.
344
+
345
+ ```scss
346
+ ---
347
+ ---
348
+
349
+ @use "codeBlock";
350
+ ```
351
+
352
+ #### Js for data-copy-btn
353
+
354
+ You can use following Js code for `data-copy-btn`.
355
+
356
+ ```js
357
+ function codeBlockCopy() {
358
+ const codeBlocks = document.querySelectorAll("[data-shiki-highlighter]");
359
+ if (!codeBlocks.length) return;
360
+
361
+ function fallbackCopy(text) {
362
+ const textarea = $.document.createElement("textarea");
363
+ textarea.value = text;
364
+ textarea.setAttribute("readonly", "");
365
+ textarea.style.position = "fixed";
366
+ textarea.style.opacity = "0";
367
+ document.body.appendChild(textarea);
368
+ textarea.select();
369
+
370
+ let success = false;
371
+ try {
372
+ success = document.execCommand("copy");
373
+ } catch (e) {
374
+ success = false;
375
+ }
376
+
377
+ $.document.body.removeChild(textarea);
378
+ return success;
379
+ }
380
+
381
+ async function copyText(text) {
382
+ if (navigator.clipboard && navigator.clipboard.writeText) {
383
+ try {
384
+ await navigator.clipboard.writeText(text);
385
+ return true;
386
+ } catch (e) {
387
+ return fallbackCopy(text);
388
+ }
389
+ }
390
+ return fallbackCopy(text);
391
+ }
392
+
393
+ codeBlocks.forEach((block) => {
394
+ const copyBtn = block.querySelector("[data-copy-btn]");
395
+ const code = block.querySelector("pre code");
396
+ if (!copyBtn || !code) return;
397
+
398
+ copyBtn.addEventListener("click", async () => {
399
+ const text = code.textContent;
400
+ const success = await copyText(text);
401
+ if (success) {
402
+ copyBtn.classList.add("copied");
403
+
404
+ setTimeout(() => {
405
+ copyBtn.classList.remove("copied");
406
+ }, 1000);
407
+ }
408
+ });
409
+ });
410
+ }
411
+ codeBlockCopy();
412
+ ```
31
413
 
32
414
  ## Contributing
33
415
 
data/lib/jekyll-shiki.rb CHANGED
@@ -11,8 +11,11 @@ require "digest"
11
11
  require_relative "version"
12
12
 
13
13
  module Jekyll
14
- # module Jekyll::ShikiCodeBlock
14
+ # Shiki is a small Jekyll plugin that replaces standard markdown code blocks
15
+ # with syntax-highlighted HTML generated by a Shiki bundle executed via Node.
15
16
  module Shiki
17
+ # Resolve the configured Shiki bundle path from the Jekyll site config.
18
+ # Raises when the required `shiki.file_path` value is missing.
16
19
  def self.resolve_shiki_bundle_path(site)
17
20
  shiki_config = site.config["shiki"]
18
21
  raise "Shiki highlight failed: Shiki config not found in jekyll config" unless shiki_config
@@ -23,6 +26,8 @@ module Jekyll
23
26
  File.join(site.source, bundle_path)
24
27
  end
25
28
 
29
+ # Highlight the given code using the Shiki bundle, caching results per code/lang.
30
+ # The method uses Open3 to execute the Node script and returns highlighted HTML.
26
31
  def self.shiki_highlight(code, lang, site)
27
32
  script_path = resolve_shiki_bundle_path(site)
28
33
 
@@ -36,6 +41,8 @@ module Jekyll
36
41
  end
37
42
  end
38
43
 
44
+ # Build the final markup wrapper around Shiki highlighted HTML.
45
+ # This includes the language label and a copy button container.
39
46
  def self.create_wrapper(lan, code, site)
40
47
  lang = lan.capitalize
41
48
  highlighted_code = shiki_highlight(code, lan, site)
@@ -50,6 +57,8 @@ module Jekyll
50
57
  HTML
51
58
  end
52
59
 
60
+ # Replace a matching <pre><code class="language-..."></code></pre> node
61
+ # with the Shiki wrapper generated from the raw code text.
53
62
  def self.replace_elements(node, site)
54
63
  code_el = node.at_css('> code[class^="language-"]')
55
64
  return unless code_el
@@ -63,10 +72,14 @@ module Jekyll
63
72
  node.replace(fragment)
64
73
  end
65
74
 
75
+ # Detect whether the provided HTML is a full document or just a fragment.
76
+ # This controls whether Nokogiri should parse it as an HTML document or fragment.
66
77
  def self.full_document?(html_content)
67
78
  html_content.match?(/\A\s*(<!doctype\s+html|<html\b)/i)
68
79
  end
69
80
 
81
+ # Transform the supplied HTML content by highlighting all code blocks.
82
+ # It preserves non-code markup and rewrites only language-specific code blocks.
70
83
  def self.transform_html(html_content, site)
71
84
  doc = if full_document?(html_content)
72
85
  Nokogiri::HTML.parse(html_content)
@@ -82,12 +95,14 @@ module Jekyll
82
95
  end
83
96
  end
84
97
 
98
+ # Register Jekyll hook for rendered pages so HTML outputs are rewritten after page rendering.
85
99
  Jekyll::Hooks.register :pages, :post_render do |page|
86
100
  next unless page.output_ext == ".html"
87
101
 
88
102
  page.output = Jekyll::Shiki.transform_html(page.output, page.site)
89
103
  end
90
104
 
105
+ # Register Jekyll hook for rendered documents so HTML outputs are rewritten after document rendering.
91
106
  Jekyll::Hooks.register :documents, :post_render do |document|
92
107
  next unless document.output_ext == ".html"
93
108
 
data/lib/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module Shiki
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-shiki
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - phothinmg
@@ -47,12 +47,12 @@ files:
47
47
  - README.md
48
48
  - lib/jekyll-shiki.rb
49
49
  - lib/version.rb
50
- homepage: https://rubygems.org/gems/jekyll-shiki
50
+ homepage: https://phothinmg.github.io/jekyll-shiki/
51
51
  licenses:
52
52
  - MIT
53
53
  metadata:
54
- source_code_uri: https://github.com/mmdocs/jekyll-shiki
55
- changelog_uri: https://github.com/mmdocs/jekyll-shiki/blob/main/CHANGELOG.md
54
+ source_code_uri: https://github.com/phothinmg/jekyll-shiki
55
+ changelog_uri: https://github.com/phothinmg/jekyll-shiki/blob/main/CHANGELOG.md
56
56
  rubygems_mfa_required: 'true'
57
57
  rdoc_options: []
58
58
  require_paths:
@@ -70,5 +70,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
70
  requirements: []
71
71
  rubygems_version: 4.0.15
72
72
  specification_version: 4
73
- summary: Jekyll plugin for Shiki Js, for mmdocs theme
73
+ summary: Jekyll plugin for Shiki Js
74
74
  test_files: []