├── .eslintignore ├── .eslintrc ├── tsup.config.ts ├── tsconfig.json ├── src ├── worker.ts └── index.ts ├── LICENSE ├── .gitignore ├── package.json ├── README.md └── pnpm-lock.yaml /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | typings 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu" 3 | } 4 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | shims: true, 5 | }) 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": ["esnext"], 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "resolveJsonModule": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/worker.ts: -------------------------------------------------------------------------------- 1 | import type { Highlighter, HtmlRendererOptions, ILanguageRegistration, IThemeRegistration } from 'shiki' 2 | import { getHighlighter } from 'shiki' 3 | import { runAsWorker } from 'synckit' 4 | 5 | let h: Highlighter 6 | 7 | function handler(command: 'getHighlighter', options: { 8 | themes: IThemeRegistration[] 9 | langs: ILanguageRegistration[] 10 | }): void 11 | function handler(command: 'codeToHtml', options: { 12 | code: string 13 | lang: string 14 | theme: string | undefined 15 | lineOptions: HtmlRendererOptions['lineOptions'] 16 | }): Promise 17 | async function handler(command: 'getHighlighter' | 'codeToHtml', options: any) { 18 | if (command === 'getHighlighter') { 19 | h = await getHighlighter(options) 20 | } 21 | else if (command === 'codeToHtml') { 22 | const { code, lang, theme, lineOptions } = options 23 | const loadedLanguages = h.getLoadedLanguages() 24 | if (loadedLanguages.includes(lang)) 25 | return h.codeToHtml(code, { lang, theme, lineOptions }) 26 | else 27 | return h.codeToHtml(code, { lang: 'text', theme, lineOptions }) 28 | } 29 | } 30 | 31 | runAsWorker(handler) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anthony Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE 81 | .idea 82 | 83 | .DS_Store 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-it-shiki", 3 | "version": "0.9.0", 4 | "packageManager": "pnpm@8.4.0", 5 | "description": "Markdown It plugin for Shiki", 6 | "author": "Anthony Fu ", 7 | "license": "MIT", 8 | "homepage": "https://github.com/antfu/markdown-it-shiki#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/antfu/markdown-it-shiki.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/antfu/markdown-it-shiki/issues" 15 | }, 16 | "keywords": [ 17 | "markdown-it", 18 | "markdown-it-plugin", 19 | "highlight", 20 | "shiki" 21 | ], 22 | "sideEffects": false, 23 | "exports": { 24 | ".": { 25 | "types": "./dist/index.d.ts", 26 | "require": "./dist/index.js", 27 | "import": "./dist/index.mjs" 28 | } 29 | }, 30 | "main": "./dist/index.js", 31 | "module": "./dist/index.mjs", 32 | "types": "./dist/index.d.ts", 33 | "files": [ 34 | "dist" 35 | ], 36 | "scripts": { 37 | "prepublishOnly": "npm run build", 38 | "watch": "npm run build -- --watch", 39 | "dev": "esno src/index.ts", 40 | "lint": "eslint .", 41 | "build": "tsup src/index.ts src/worker.ts --format cjs,esm --dts --external ./worker", 42 | "publish:ci": "npm publish --access public", 43 | "release": "npx bumpp --commit --push --tag && npm run publish:ci" 44 | }, 45 | "dependencies": { 46 | "shiki": "^0.14.2", 47 | "synckit": "^0.8.5" 48 | }, 49 | "devDependencies": { 50 | "@antfu/eslint-config": "^0.38.5", 51 | "@types/markdown-it": "^12.2.3", 52 | "@types/node": "^18.16.3", 53 | "eslint": "^8.39.0", 54 | "esno": "^0.16.3", 55 | "markdown-it": "^13.0.1", 56 | "tsup": "^6.7.0", 57 | "typescript": "^5.0.4" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | This plugin is no longer maintained. We recommend using [Shikiji](https://github.com/antfu/shikiji) with [markdown-it-shikiji](https://github.com/antfu/shikiji/tree/main/packages/markdown-it-shikiji). 4 | 5 | ---- 6 | 7 | # markdown-it-shiki 8 | 9 | [Markdown It](https://markdown-it.github.io/) plugin for [Shiki](https://github.com/shikijs/shiki) 10 | 11 | ## Install 12 | 13 | ```bash 14 | npm i -D markdown-it-shiki 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```ts 20 | import MarkdownIt from 'markdown-it' 21 | import Shiki from 'markdown-it-shiki' 22 | 23 | const md = MarkdownIt() 24 | 25 | md.use(Shiki, { 26 | theme: 'nord' 27 | }) 28 | ``` 29 | 30 | ### Dark mode 31 | 32 | ```js 33 | md.use(Shiki, { 34 | theme: { 35 | dark: 'min-dark', 36 | light: 'min-light' 37 | } 38 | }) 39 | ``` 40 | 41 | Add then these to your CSS 42 | 43 | 44 | ```css 45 | /* Query based dark mode */ 46 | 47 | @media (prefers-color-scheme: dark) { 48 | .shiki-light { 49 | display: none; 50 | } 51 | } 52 | 53 | @media (prefers-color-scheme: light), (prefers-color-scheme: no-preference) { 54 | .shiki-dark { 55 | display: none; 56 | } 57 | } 58 | 59 | ``` 60 | 61 | ```css 62 | /* Class based dark mode */ 63 | 64 | html.dark .shiki-light { 65 | display: none; 66 | } 67 | 68 | html:not(.dark) .shiki-dark { 69 | display: none; 70 | } 71 | ``` 72 | 73 | ### Highlight lines 74 | 75 | ```js 76 | md.use(Shiki, { 77 | highlightLines: true 78 | }) 79 | ``` 80 | 81 | Add these to your CSS 82 | 83 | ```css 84 | code[v-pre] { 85 | display: flex; 86 | flex-direction: column; 87 | } 88 | 89 | .shiki .highlighted { 90 | background: #7f7f7f20; 91 | display: block; 92 | margin: 0 -1rem; 93 | padding: 0 1rem; 94 | } 95 | ``` 96 | 97 | Then you can highlight lines in code block. 98 | 99 | ~~~ 100 | ```js {1-2} 101 | const md = new MarkdownIt() 102 | md.use(Shiki) 103 | 104 | const res = md.render(/** ... */) 105 | console.log(res) 106 | ``` 107 | ~~~ 108 | 109 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | import type { Highlighter, HtmlRendererOptions, ILanguageRegistration, IShikiTheme, IThemeRegistration } from 'shiki' 3 | import type MarkdownIt from 'markdown-it' 4 | import { createSyncFn } from 'synckit' 5 | 6 | export interface DarkModeThemes { 7 | dark: IThemeRegistration 8 | light: IThemeRegistration 9 | } 10 | 11 | export interface Options { 12 | theme?: IThemeRegistration | DarkModeThemes 13 | langs?: ILanguageRegistration[] 14 | timeout?: number 15 | highlighter?: Highlighter 16 | highlightLines?: boolean 17 | } 18 | 19 | function getThemeName(theme: IThemeRegistration) { 20 | if (typeof theme === 'string') 21 | return theme 22 | return (theme as IShikiTheme).name 23 | } 24 | 25 | const RE = /{([\d,-]+)}/ 26 | 27 | export function resolveOptions(options: Options) { 28 | const themes: IThemeRegistration[] = [] 29 | let darkModeThemes: DarkModeThemes | undefined 30 | 31 | if (!options.theme) { 32 | themes.push('nord') 33 | } 34 | else if (typeof options.theme === 'string') { 35 | themes.push(options.theme) 36 | } 37 | else { 38 | if ('dark' in options.theme || 'light' in options.theme) { 39 | darkModeThemes = options.theme as DarkModeThemes 40 | themes.push(darkModeThemes.dark) 41 | themes.push(darkModeThemes.light) 42 | } 43 | else { 44 | themes.push(options.theme) 45 | } 46 | } 47 | 48 | return { 49 | ...options, 50 | themes, 51 | darkModeThemes: darkModeThemes 52 | ? { 53 | dark: getThemeName(darkModeThemes.dark), 54 | light: getThemeName(darkModeThemes.light), 55 | } 56 | : undefined, 57 | } 58 | } 59 | 60 | function attrsToLines(attrs: string): HtmlRendererOptions['lineOptions'] { 61 | const result: number[] = [] 62 | if (!attrs.trim()) 63 | return [] 64 | 65 | attrs 66 | .split(',') 67 | .map(v => v.split('-').map(v => parseInt(v, 10))) 68 | .forEach(([start, end]) => { 69 | if (start && end) { 70 | result.push( 71 | ...Array.from({ length: end - start + 1 }, (_, i) => start + i), 72 | ) 73 | } 74 | else { 75 | result.push(start) 76 | } 77 | }) 78 | return result.map(v => ({ 79 | line: v, 80 | classes: ['highlighted'], 81 | })) 82 | } 83 | 84 | const MarkdownItShiki: MarkdownIt.PluginWithOptions = (markdownit, options = {}) => { 85 | const _highlighter = options.highlighter 86 | 87 | const { 88 | langs, 89 | themes, 90 | darkModeThemes, 91 | highlightLines, 92 | } = resolveOptions(options) 93 | 94 | let syncRun: any 95 | if (!_highlighter) { 96 | const require = createRequire(import.meta.url) 97 | syncRun = createSyncFn(require.resolve('./worker')) 98 | syncRun('getHighlighter', { langs, themes }) 99 | } 100 | 101 | const highlightCode = (code: string, lang: string, theme?: string, lineOptions?: HtmlRendererOptions['lineOptions']): string => { 102 | if (_highlighter) 103 | return _highlighter.codeToHtml(code, { lang: lang || 'text', theme, lineOptions }) 104 | 105 | return syncRun('codeToHtml', { 106 | code, 107 | theme, 108 | lang: lang || 'text', 109 | lineOptions, 110 | }) 111 | } 112 | 113 | markdownit.options.highlight = (code, lang = 'text', attrs) => { 114 | let lineOptions 115 | if (highlightLines) { 116 | const match = RE.exec(attrs) 117 | if (match) 118 | lineOptions = attrsToLines(match[1]) 119 | } 120 | if (darkModeThemes) { 121 | const dark = highlightCode(code, lang, darkModeThemes.dark, lineOptions) 122 | .replace('