├── .npmrc ├── .prettierrc ├── static └── favicon.png ├── src ├── lib │ ├── snippets │ │ ├── script.js │ │ ├── style.css │ │ ├── index.html │ │ └── Counter.svelte │ ├── components │ │ ├── Codeblock.svelte │ │ └── Header.svelte │ └── server │ │ └── codes.ts ├── routes │ ├── +layout.server.ts │ ├── +page.svelte │ ├── app.css │ └── +layout.svelte ├── app.d.ts └── app.html ├── vite.config.ts ├── .gitignore ├── svelte.config.js ├── tsconfig.json ├── package.json ├── LICENSE └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "tabWidth": 4 4 | } 5 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScriptRaccoon/codeblocks/HEAD/static/favicon.png -------------------------------------------------------------------------------- /src/lib/snippets/script.js: -------------------------------------------------------------------------------- 1 | // script.js 2 | 3 | for (let i = 0; i < 10; i++) { 4 | console.log(i); 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/snippets/style.css: -------------------------------------------------------------------------------- 1 | /* style.css */ 2 | 3 | .section { 4 | padding-block: 1rem; 5 | color: #222; 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/snippets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Hi there

6 | 7 | 8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /src/lib/snippets/Counter.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 |

{count}

11 | -------------------------------------------------------------------------------- /src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | 3 | import { compute_codes } from "$lib/server/codes"; 4 | 5 | export const load = async () => { 6 | const codes = await compute_codes(); 7 | return { codes }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/routes/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | font-family: Arial, Helvetica, sans-serif; 9 | color: #222; 10 | line-height: 1.5; 11 | color: #222; 12 | } 13 | 14 | a { 15 | color: inherit; 16 | } 17 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-static"; 2 | import { vitePreprocess } from "@sveltejs/kit/vite"; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | preprocess: vitePreprocess(), 7 | kit: { 8 | adapter: adapter(), 9 | }, 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | Codeblock Components with Shiki in SvelteKit 8 | 9 | 10 |
11 | 12 |
13 | 14 |
15 | 16 | 23 | -------------------------------------------------------------------------------- /src/lib/components/Codeblock.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | {#if code} 8 |
{@html code}
9 | {:else} 10 |
Invalid code snippet: {snippet}
11 | {/if} 12 | 13 | 22 | -------------------------------------------------------------------------------- /src/lib/components/Header.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Codeblock Components with Shiki in SvelteKit

3 |

4 | Check out the source code on 5 | GitHub 6 |

7 |
8 | 9 | 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | }, 13 | "exclude": ["src/lib/snippets/*"] 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // 16 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 17 | // from the referenced tsconfig.json - TypeScript does not merge them in 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeblock", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 11 | }, 12 | "devDependencies": { 13 | "@sveltejs/adapter-auto": "^2.0.0", 14 | "@sveltejs/adapter-static": "^2.0.2", 15 | "@sveltejs/kit": "^1.5.0", 16 | "@types/node": "^20.1.4", 17 | "svelte": "^3.54.0", 18 | "svelte-check": "^3.0.1", 19 | "tslib": "^2.4.1", 20 | "typescript": "^5.0.0", 21 | "vite": "^4.3.0" 22 | }, 23 | "type": "module", 24 | "dependencies": { 25 | "shiki": "^0.14.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/server/codes.ts: -------------------------------------------------------------------------------- 1 | import { getHighlighter } from "shiki"; 2 | 3 | export async function compute_codes() { 4 | const highlighter = await getHighlighter({ 5 | theme: "dark-plus", 6 | langs: ["html", "js", "css", "svelte"], 7 | }); 8 | 9 | const snippets = import.meta.glob("$lib/snippets/*", { 10 | as: "raw", 11 | eager: true, 12 | }); 13 | 14 | const codes = Object.fromEntries(Object.entries(snippets).map(transform)); 15 | 16 | function transform([path, file_content]: [string, string]) { 17 | const file_name = path.split("/").at(-1)!; 18 | const lang = file_name.split(".").at(-1); 19 | const code = highlighter.codeToHtml(file_content, { lang }); 20 | return [file_name, code]; 21 | } 22 | 23 | return codes; 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 ScriptRaccoon 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Codeblock Components with Shiki in SvelteKit 2 | 3 | This repository shows how to implement reusable Codeblock components inside of a [SvelteKit project](https://kit.svelte.dev). Syntax highlighting is implemented via [Shiki](https://github.com/shikijs/shiki). 4 | 5 | This is an alternative to the more common approach with markdown (see for example [SvelteKit Shiki Syntax Highlighting: Markdown Code Blocks](https://rodneylab.com/sveltekit-shiki-syntax-highlighting/) by Rodney Johnson). 6 | 7 | Demo: https://codeblocks-shiki.netlify.app/ 8 | 9 | ## Snippets 10 | 11 | The snippets are located in the folder [`$lib/snippets`](https://github.com/ScriptRaccoon/codeblocks/tree/main/src/lib/snippets). This folder can be changed. For example: 12 | 13 | ```css 14 | /* $lib/snippets/style.css */ 15 | 16 | .section { 17 | padding-block: 1rem; 18 | color: #222; 19 | } 20 | ``` 21 | 22 | Saving each snippet, even when it is just one line, in a separate file may sound overkill, but this is necessary for the method presented here, and it also has the advantage that your editor does the code formatting for you. (Shiki only highlights the code.) 23 | 24 | ## Shiki code 25 | 26 | We install Shiki with `npm i shiki`. 27 | 28 | The file [`codes.ts`](https://github.com/ScriptRaccoon/codeblocks/tree/main/src/lib/server/codes.ts) exports a function which uses Shiki to compute an object with all HTML codes of the snippets. The keys are the file names. Here you can also adjust the supported languages and themes as well as the snippet path folder (this has to be a string literal, hence we cannot make it into a variable). 29 | 30 | ```typescript 31 | // $lib/server/codes.ts 32 | 33 | import { getHighlighter } from "shiki"; 34 | 35 | export async function compute_codes() { 36 | const highlighter = await getHighlighter({ 37 | theme: "dark-plus", 38 | langs: ["html", "js", "css", "svelte"], 39 | }); 40 | 41 | const snippets = import.meta.glob("$lib/snippets/*", { 42 | as: "raw", 43 | eager: true, 44 | }); 45 | 46 | const codes = Object.fromEntries(Object.entries(snippets).map(transform)); 47 | 48 | function transform([path, file_content]: [string, string]) { 49 | const file_name = path.split("/").at(-1)!; 50 | const lang = file_name.split(".").at(-1); 51 | const code = highlighter.codeToHtml(file_content, { lang }); 52 | return [file_name, code]; 53 | } 54 | 55 | return codes; 56 | } 57 | ``` 58 | 59 | To explain this a little bit, notice that Vite's [`import.meta.glob`](https://vitejs.dev/guide/features.html) returns an object whose keys are the file paths and whose values are the file contents. In the `transform` function, we let Shiki operate on the file content and replace the file path by the file name. Shiki needs the language, which we can extract from the file extension. 60 | 61 | ## Page Data 62 | 63 | The layout server load uses this function to make the codes available as page data. 64 | 65 | ```typescript 66 | // +layout.server.ts 67 | 68 | export const prerender = true; 69 | 70 | import { compute_codes } from "$lib/server/codes"; 71 | 72 | export const load = async () => { 73 | const codes = await compute_codes(); 74 | return { codes }; 75 | }; 76 | ``` 77 | 78 | Notice that pages with code blocks need to be [prerendered](https://kit.svelte.dev/docs/glossary#prerendering), and Shiki needs to run on the server only. Otherwise there will be an error. This is why we set `prerender = true` here. 79 | 80 | When the code blocks are located only on a single page, you can also use a page server load instead. 81 | 82 | ## Codeblock component 83 | 84 | These codes are then used in the [`Codeblock.svelte`](https://github.com/ScriptRaccoon/codeblocks/blob/main/src/lib/components/Codeblock.svelte) component. It exports a prop `snippet` and computes the rendered code via `codes[snippet]`. 85 | 86 | ```svelte 87 | 88 | 89 | 94 | 95 | {#if code} 96 |
{@html code}
97 | {/if} 98 | ``` 99 | 100 | This produces rather crude-looking code blocks, though. You can improve the styling here as follows. 101 | 102 | ```css 103 | div :global(pre) { 104 | font-size: 1rem; 105 | padding: 1.25rem; 106 | border-radius: 0.5rem; 107 | margin-block: 1rem; 108 | overflow: auto; 109 | } 110 | ``` 111 | 112 | The last property is important, since it adds scrollbars when the code is too wide. We attach a padding to the `pre` element generated by Shiki in order to keep the background color determined by the theme. 113 | 114 | When an invalid snippet is passed to the component, nothing is rendered. In order to catch bugs during development, you can expand the if-block as follows: 115 | 116 | ```svelte 117 | {:else} 118 |
Invalid code snippet: {snippet}
119 | {/if} 120 | ``` 121 | 122 | ## Using the component 123 | 124 | The `Codeblock.svelte` component accepts the file name of one of these snippets as a prop. 125 | 126 | ```svelte 127 | 128 | 129 | 130 | 131 | 132 | 133 | ``` 134 | 135 | ## Acknowledgements 136 | 137 | Thanks to `karimfromjordan` and `Patrick` on the Svelte Discord server for their help with the implementation. 138 | --------------------------------------------------------------------------------