├── .npmrc ├── .prettierignore ├── lib ├── types.js ├── types.d.ts └── index.js ├── index.js ├── index.d.ts ├── .gitignore ├── .editorconfig ├── test ├── fixtures │ ├── empty │ │ ├── input.html │ │ └── output.html │ └── basic │ │ ├── input.html │ │ └── output.html └── index.js ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── tsconfig.json ├── license ├── package.json └── readme.md /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.html 3 | *.md 4 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `types.d.ts`. 2 | export {} 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `index.d.ts`. 2 | export {default} from './lib/index.js' 3 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export {default} from './lib/index.js' 2 | export type {Options} from './lib/types.js' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.d.ts 3 | *.log 4 | *.map 5 | *.tsbuildinfo 6 | coverage/ 7 | node_modules/ 8 | yarn.lock 9 | !/lib/types.d.ts 10 | !/index.d.ts 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /test/fixtures/empty/input.html: -------------------------------------------------------------------------------- 1 |
old pond
 2 | frog leaps in
 3 | water’s sound
4 | 5 | alert(1) 6 | 7 |
alert(1)
 8 | 
9 | 10 |
alert(1)
11 | 
12 | 13 |
alert(1)
14 | 
15 | -------------------------------------------------------------------------------- /test/fixtures/empty/output.html: -------------------------------------------------------------------------------- 1 |
old pond
 2 | frog leaps in
 3 | water’s sound
4 | 5 | alert(1) 6 | 7 |
alert(1)
 8 | 
9 | 10 |
alert(1)
11 | 
12 | 13 |
alert(1)
14 | 
15 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | runs-on: ubuntu-latest 4 | steps: 5 | - uses: unifiedjs/beep-boop-beta@main 6 | with: 7 | repo-token: ${{secrets.GITHUB_TOKEN}} 8 | name: bb 9 | on: 10 | issues: 11 | types: [closed, edited, labeled, opened, reopened, unlabeled] 12 | pull_request_target: 13 | types: [closed, edited, labeled, opened, reopened, unlabeled] 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | name: ${{matrix.node}} 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v4 7 | - uses: actions/setup-node@v4 8 | with: 9 | node-version: ${{matrix.node}} 10 | - run: npm install 11 | - run: npm test 12 | - uses: codecov/codecov-action@v4 13 | strategy: 14 | matrix: 15 | node: 16 | - lts/hydrogen 17 | - node 18 | name: main 19 | on: 20 | - pull_request 21 | - push 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "declarationMap": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | "noUncheckedIndexedAccess": true, 12 | "strict": true, 13 | "target": "es2022" 14 | }, 15 | "exclude": ["coverage/", "node_modules/"], 16 | "include": ["**/*.js", "lib/types.d.ts", "index.d.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /lib/types.d.ts: -------------------------------------------------------------------------------- 1 | import type {Grammar, Options as StarryNightOptions} from '@wooorm/starry-night' 2 | 3 | /** 4 | * Distance tuple. 5 | */ 6 | export type DistanceTuple = [name: string, distance: number] 7 | 8 | /** 9 | * Configuration for `rehype-starry-night`. 10 | */ 11 | export interface Options extends StarryNightOptions { 12 | /** 13 | * Do not warn for missing scopes (default: `false`). 14 | */ 15 | allowMissingScopes?: boolean | null | undefined 16 | /** 17 | * Grammars to support (default: `common`). 18 | */ 19 | grammars?: ReadonlyArray | null | undefined 20 | /** 21 | * List of language names to not highlight (default: `[]`). 22 | */ 23 | plainText?: ReadonlyArray | null | undefined 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/basic/input.html: -------------------------------------------------------------------------------- 1 |
console.log(1)
 2 | 
3 |
console.log(1)
 4 | 
5 |
console.log(1)
 6 | 
7 |
em { color: red }
 8 | 
9 |
# hi
10 | 
11 |
# hi
12 | 
13 |
# hi
14 | 
15 |
x
16 | 
17 |

18 | 
19 |
let fragment = <jsx />
20 | 
21 |
let fragment = <jsx />
22 | 
23 |
let fragment = <jsx />
24 | 
25 |
let fragment = <jsx />
26 | 
27 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) Julien Barbay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/fixtures/basic/output.html: -------------------------------------------------------------------------------- 1 |
console.log(1)
 2 | 
3 |
console.log(1)
 4 | 
5 |
console.log(1)
 6 | 
7 |
em { color: red }
 8 | 
9 |
# hi
10 | 
11 |
# hi
12 | 
13 |
# hi
14 | 
15 |
x
16 | 
17 |

18 | 
19 |
let fragment = <jsx />
20 | 
21 |
let fragment = <jsx />
22 | 
23 |
let fragment = <jsx />
24 | 
25 |
let fragment = <jsx />
26 | 
27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rehype-starry-night", 3 | "version": "2.2.0", 4 | "description": "rehype plugin to highlight code with `starry-night`", 5 | "license": "MIT", 6 | "keywords": [ 7 | "hast", 8 | "highlight", 9 | "html", 10 | "night", 11 | "plugin", 12 | "rehype", 13 | "rehype-plugin", 14 | "starry-night", 15 | "starry", 16 | "syntax", 17 | "unified" 18 | ], 19 | "repository": "rehypejs/rehype-starry-night", 20 | "bugs": "https://github.com/rehypejs/rehype-starry-night/issues", 21 | "funding": { 22 | "type": "opencollective", 23 | "url": "https://opencollective.com/unified" 24 | }, 25 | "author": "Julien Barbay ", 26 | "contributors": [ 27 | "Julien Barbay ", 28 | "Titus Wormer (https://wooorm.com)" 29 | ], 30 | "sideEffects": false, 31 | "type": "module", 32 | "exports": "./index.js", 33 | "files": [ 34 | "lib/", 35 | "index.d.ts", 36 | "index.js" 37 | ], 38 | "dependencies": { 39 | "@types/hast": "^3.0.0", 40 | "@wooorm/starry-night": "^3.0.0", 41 | "hast-util-to-string": "^3.0.0", 42 | "levenshtein-edit-distance": "^3.0.0", 43 | "unist-util-visit-parents": "^6.0.0", 44 | "vfile": "^6.0.0" 45 | }, 46 | "devDependencies": { 47 | "@types/node": "^22.0.0", 48 | "c8": "^10.0.0", 49 | "prettier": "^3.0.0", 50 | "rehype-parse": "^9.0.0", 51 | "rehype-stringify": "^10.0.0", 52 | "remark-api": "^1.0.0", 53 | "remark-cli": "^12.0.0", 54 | "remark-preset-wooorm": "^10.0.0", 55 | "to-vfile": "^8.0.0", 56 | "type-coverage": "^2.0.0", 57 | "typescript": "^5.0.0", 58 | "unified": "^11.0.0", 59 | "xo": "^0.59.0" 60 | }, 61 | "scripts": { 62 | "build": "tsc --build --clean && tsc --build && type-coverage", 63 | "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", 64 | "prepack": "npm run build && npm run format", 65 | "test": "npm run build && npm run format && npm run test-coverage", 66 | "test-api": "node --conditions development test/index.js", 67 | "test-coverage": "c8 --100 --check-coverage --reporter lcov npm run test-api" 68 | }, 69 | "prettier": { 70 | "bracketSpacing": false, 71 | "singleQuote": true, 72 | "semi": false, 73 | "tabWidth": 2, 74 | "trailingComma": "none", 75 | "useTabs": false 76 | }, 77 | "remarkConfig": { 78 | "plugins": [ 79 | "remark-preset-wooorm", 80 | "remark-api" 81 | ] 82 | }, 83 | "typeCoverage": { 84 | "atLeast": 100, 85 | "detail": true, 86 | "ignoreCatch": true, 87 | "strict": true 88 | }, 89 | "xo": { 90 | "overrides": [ 91 | { 92 | "files": [ 93 | "**/*.d.ts" 94 | ], 95 | "rules": { 96 | "@typescript-eslint/array-type": [ 97 | "error", 98 | { 99 | "default": "generic" 100 | } 101 | ], 102 | "@typescript-eslint/ban-types": [ 103 | "error", 104 | { 105 | "extendDefaults": true 106 | } 107 | ], 108 | "@typescript-eslint/consistent-type-definitions": [ 109 | "error", 110 | "interface" 111 | ] 112 | } 113 | }, 114 | { 115 | "files": [ 116 | "test/**/*.js" 117 | ], 118 | "rules": { 119 | "no-await-in-loop": "off" 120 | } 121 | } 122 | ], 123 | "prettier": true 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {ElementContent, Root} from 'hast' 3 | * @import {VFile} from 'vfile' 4 | * @import {DistanceTuple, Options} from './types.js' 5 | */ 6 | 7 | import {createStarryNight, common} from '@wooorm/starry-night' 8 | import {toString} from 'hast-util-to-string' 9 | import {levenshteinEditDistance} from 'levenshtein-edit-distance' 10 | import {SKIP, visitParents} from 'unist-util-visit-parents' 11 | 12 | const listFormatDisjunction = new Intl.ListFormat('en', {type: 'disjunction'}) 13 | const listFormatUnit = new Intl.ListFormat('en', {type: 'unit'}) 14 | 15 | /** @type {Readonly} */ 16 | const emptyOptions = {} 17 | /** @type {ReadonlyArray} */ 18 | const emptyPlainText = [] 19 | 20 | const prefix = 'language-' 21 | 22 | const relativeThreshold = 0.33 23 | const max = 4 24 | 25 | /** 26 | * Plugin to highlight code with `starry-night`. 27 | * 28 | * @param {Readonly | null | undefined} [options] 29 | * Configuration (optional). 30 | * @returns 31 | * Transform. 32 | */ 33 | export default function rehypeStarryNight(options) { 34 | const settings = options || emptyOptions 35 | const grammars = settings.grammars || common 36 | const plainText = settings.plainText || emptyPlainText 37 | const starryNightPromise = createStarryNight(grammars, settings) 38 | const names = grammars.flatMap(function (d) { 39 | return d.names 40 | }) 41 | let checked = false 42 | 43 | /** 44 | * Transform. 45 | * 46 | * @param {Root} tree 47 | * Tree. 48 | * @param {VFile} file 49 | * File. 50 | * @returns {Promise} 51 | * Nothing. 52 | */ 53 | // To do: `unified` should support `undefined` instead of `Root`. 54 | return async function (tree, file) { 55 | const starryNight = await starryNightPromise 56 | 57 | if (!settings.allowMissingScopes && !checked) { 58 | const missingScopes = starryNight.missingScopes() 59 | 60 | if (missingScopes.length > 0) { 61 | file.message( 62 | 'Unexpected missing scope' + 63 | (missingScopes.length === 1 ? '' : 's') + 64 | ' likely needed for highlighting to work: ' + 65 | listFormatUnit.format( 66 | missingScopes.map(function (d) { 67 | return '`' + d + '`' 68 | }) 69 | ), 70 | { 71 | ancestors: [tree], 72 | place: tree.position, 73 | ruleId: 'missing-scopes', 74 | source: 'rehype-starry-night' 75 | } 76 | ) 77 | } 78 | 79 | checked = true 80 | } 81 | 82 | visitParents(tree, 'element', function (node, parents) { 83 | if (node.tagName !== 'code') return 84 | 85 | const classes = node.properties.className 86 | 87 | if (Array.isArray(classes)) { 88 | // Cast as we check if it’s a string in `find`. 89 | const languageClass = /** @type {string | undefined} */ ( 90 | classes.find(function (d) { 91 | return typeof d === 'string' && d.startsWith(prefix) 92 | }) 93 | ) 94 | 95 | if (languageClass) { 96 | const language = languageClass.slice(prefix.length) 97 | const scope = starryNight.flagToScope(language) 98 | 99 | if (plainText.includes(language)) { 100 | // Empty. 101 | } else if (scope) { 102 | const fragment = starryNight.highlight(toString(node), scope) 103 | node.children = /** @type {Array} */ ( 104 | fragment.children 105 | ) 106 | } else { 107 | let reason = 108 | 'Unexpected unknown language `' + 109 | language + 110 | '` defined with `language-` class, expected a known name' 111 | 112 | const similar = propose(language, names) 113 | 114 | if (similar.length > 0) { 115 | reason += 116 | '; did you mean ' + 117 | listFormatDisjunction.format( 118 | similar.map(function (d) { 119 | return '`' + d + '`' 120 | }) 121 | ) 122 | } 123 | 124 | file.message(reason, { 125 | ancestors: [...parents, node], 126 | place: node.position, 127 | ruleId: 'missing-language', 128 | source: 'rehype-starry-night' 129 | }) 130 | } 131 | } 132 | } 133 | 134 | return SKIP 135 | }) 136 | 137 | return tree 138 | } 139 | } 140 | 141 | /** 142 | * @param {string} value 143 | * @param {ReadonlyArray} ideas 144 | * @returns {Array} 145 | */ 146 | function propose(value, ideas) { 147 | return ideas 148 | .map(function (d) { 149 | return score(value, d) 150 | }) 151 | .sort(sort) 152 | .filter(function (d) { 153 | return filter(d) 154 | }) 155 | .map(function (d) { 156 | return pick(d) 157 | }) 158 | .slice(0, max) 159 | } 160 | 161 | /** 162 | * @param {string} value 163 | * @param {string} d 164 | * @returns {DistanceTuple} 165 | */ 166 | function score(value, d) { 167 | return [d, levenshteinEditDistance(value, d) / value.length] 168 | } 169 | 170 | /** 171 | * @param {DistanceTuple} a 172 | * @param {DistanceTuple} b 173 | * @returns {number} 174 | */ 175 | function sort(a, b) { 176 | return a[1] - b[1] 177 | } 178 | 179 | /** 180 | * @param {DistanceTuple} d 181 | * @returns {boolean} 182 | */ 183 | function filter(d) { 184 | return d[1] < relativeThreshold 185 | } 186 | 187 | /** 188 | * @param {DistanceTuple} d 189 | * @returns {string} 190 | */ 191 | function pick(d) { 192 | return d[0] 193 | } 194 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import fs from 'node:fs/promises' 3 | import process from 'node:process' 4 | import test from 'node:test' 5 | import {common} from '@wooorm/starry-night' 6 | import textXmlSvg from '@wooorm/starry-night/text.xml.svg' 7 | import sourceObjc from '@wooorm/starry-night/source.objc' 8 | import sourceTsx from '@wooorm/starry-night/source.tsx' 9 | import rehypeParse from 'rehype-parse' 10 | import rehypeStarryNight from 'rehype-starry-night' 11 | import rehypeStringify from 'rehype-stringify' 12 | import {read, write} from 'to-vfile' 13 | import {unified} from 'unified' 14 | import {VFile} from 'vfile' 15 | 16 | test('rehypeStarryNight', async function (t) { 17 | await t.test('should expose the public api', async function () { 18 | assert.deepEqual(Object.keys(await import('rehype-starry-night')).sort(), [ 19 | 'default' 20 | ]) 21 | }) 22 | 23 | await t.test('should work', async function () { 24 | const file = await unified() 25 | .use(rehypeParse, {fragment: true}) 26 | .use(rehypeStarryNight) 27 | .use(rehypeStringify) 28 | .process( 29 | ` 30 |
const hi = 'Hello'
 31 | alert(hi)
 32 | 
33 | ` 34 | ) 35 | 36 | assert.equal( 37 | String(file), 38 | ` 39 |
const hi = 'Hello'
 40 | alert(hi)
 41 | 
42 | ` 43 | ) 44 | assert.deepEqual(file.messages.map(String), []) 45 | }) 46 | 47 | await t.test('should warn for unregistered languages', async function () { 48 | const file = await unified() 49 | .use(rehypeParse, {fragment: true}) 50 | .use(rehypeStarryNight) 51 | .use(rehypeStringify) 52 | .process('
') 53 | 54 | assert.equal( 55 | String(file), 56 | '
' 57 | ) 58 | assert.deepEqual(file.messages.map(String), [ 59 | '1:6-1:47: Unexpected unknown language `hypescript` defined with `language-` class, expected a known name; did you mean `typescript` or `cakescript`' 60 | ]) 61 | }) 62 | 63 | await t.test('should ignore languages in `plainText`', async function () { 64 | const file = await unified() 65 | .use(rehypeParse, {fragment: true}) 66 | .use(rehypeStarryNight, {plainText: ['hypescript', 'javascript']}) 67 | .use(rehypeStringify) 68 | .process( 69 | '
"hi"
\n
' 70 | ) 71 | 72 | assert.equal( 73 | String(file), 74 | '
"hi"
\n
' 75 | ) 76 | assert.deepEqual(file.messages.map(String), []) 77 | }) 78 | 79 | await t.test('should warn for missing scopes (1)', async function () { 80 | const file = await unified() 81 | .use(rehypeParse, {fragment: true}) 82 | .use(rehypeStarryNight, {grammars: [textXmlSvg]}) 83 | .use(rehypeStringify) 84 | .process( 85 | '
<svg><rect/></svg>
' 86 | ) 87 | 88 | assert.equal( 89 | String(file), 90 | '
<svg><rect/></svg>
' 91 | ) 92 | assert.deepEqual(file.messages.map(String), [ 93 | '1:1-1:66: Unexpected missing scope likely needed for highlighting to work: `text.xml`' 94 | ]) 95 | }) 96 | 97 | await t.test('should warn for missing scopes (2)', async function () { 98 | const file = await unified() 99 | .use(rehypeParse, {fragment: true}) 100 | .use(rehypeStarryNight, {grammars: [sourceObjc]}) 101 | .use(rehypeStringify) 102 | .process( 103 | '
- (int)method:(int)i {\n  return [self square_root:i];\n}
' 104 | ) 105 | 106 | assert.equal( 107 | String(file), 108 | '
- (int)method:(int)i {\n  return [self square_root:i];\n}
' 109 | ) 110 | assert.deepEqual(file.messages.map(String), [ 111 | '1:1-3:8: Unexpected missing scopes likely needed for highlighting to work: `source.c`, `source.c.platform`, `source.objc.platform`' 112 | ]) 113 | }) 114 | 115 | await t.test( 116 | 'should not warn for missing scopes w/ `allowMissingScopes`', 117 | async function () { 118 | const file = await unified() 119 | .use(rehypeParse, {fragment: true}) 120 | .use(rehypeStarryNight, { 121 | allowMissingScopes: true, 122 | grammars: [textXmlSvg, sourceObjc] 123 | }) 124 | .use(rehypeStringify) 125 | .process('') 126 | 127 | assert.deepEqual(file.messages.map(String), []) 128 | } 129 | ) 130 | }) 131 | 132 | test('fixtures', async function (t) { 133 | const base = new URL('fixtures/', import.meta.url) 134 | const folders = await fs.readdir(base) 135 | 136 | for (const folder of folders) { 137 | if (folder.charAt(0) === '.') continue 138 | 139 | await t.test(folder, async function () { 140 | const folderUrl = new URL(folder + '/', base) 141 | const outputUrl = new URL('output.html', folderUrl) 142 | const input = await read(new URL('input.html', folderUrl)) 143 | const processor = await unified() 144 | .use(rehypeParse, {fragment: true}) 145 | .use(rehypeStarryNight, {grammars: [...common, sourceTsx]}) 146 | .use(rehypeStringify) 147 | 148 | await processor.process(input) 149 | 150 | /** @type {VFile} */ 151 | let output 152 | 153 | try { 154 | if ('UPDATE' in process.env) { 155 | throw new Error('Updating…') 156 | } 157 | 158 | output = await read(outputUrl) 159 | output.value = String(output) 160 | } catch { 161 | output = new VFile({ 162 | path: outputUrl, 163 | value: String(input) 164 | }) 165 | await write(output) 166 | } 167 | 168 | assert.equal(String(input), String(output)) 169 | 170 | // This has warnings, and that is expected. 171 | if (folder === 'empty') { 172 | return 173 | } 174 | 175 | assert.deepEqual(input.messages.map(String), []) 176 | }) 177 | } 178 | }) 179 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # rehype-starry-night 2 | 3 | [![Build][badge-build-image]][badge-build-url] 4 | [![Coverage][badge-coverage-image]][badge-coverage-url] 5 | [![Downloads][badge-downloads-image]][badge-downloads-url] 6 | [![Size][badge-size-image]][badge-size-url] 7 | [![Sponsors][badge-sponsors-image]][badge-collective-url] 8 | [![Backers][badge-backers-image]][badge-collective-url] 9 | [![Chat][badge-chat-image]][badge-chat-url] 10 | 11 | **[rehype][github-rehype]** plugin to apply syntax highlighting to code with 12 | [`starry-night`][github-starry-night]. 13 | 14 | ## Contents 15 | 16 | * [What is this?](#what-is-this) 17 | * [When should I use this?](#when-should-i-use-this) 18 | * [Install](#install) 19 | * [Use](#use) 20 | * [API](#api) 21 | * [`Options`](#options) 22 | * [`rehypeStarryNight(options) (default)`](#rehypestarrynightoptions-default) 23 | * [HTML](#html) 24 | * [CSS](#css) 25 | * [Compatibility](#compatibility) 26 | * [Security](#security) 27 | * [Related](#related) 28 | * [Contribute](#contribute) 29 | * [License](#license) 30 | 31 | ## What is this? 32 | 33 | This package is a [unified][github-unified] ([rehype][github-rehype]) plugin to 34 | perform syntax highlighting. 35 | It uses [`starry-night`][github-starry-night], 36 | which is a high quality highlighter that can support tons of grammars and 37 | approaches how GitHub renders code. 38 | 39 | ## When should I use this? 40 | 41 | This plugin is useful when you want to perform syntax highlighting in rehype. 42 | If you are not using rehype, 43 | you can instead use [`starry-night`][github-starry-night] directly. 44 | 45 | You can combine this package with [`rehype-twoslash`][github-rehype-twoslash]. 46 | That processes JavaScript and TypeScript code with [`twoslash`][twoslash] and 47 | also uses `starry-night` just for that code. 48 | 49 | `starry-night` has a WASM dependency, 50 | and rather big grammars, 51 | which means that this plugin might be too heavy particularly in browsers, 52 | in which case [`rehype-highlight`][github-rehype-highlight] might be more 53 | suitable. 54 | 55 | ## Install 56 | 57 | This package is [ESM only][github-gist-esm]. 58 | In Node.js (version 16+), install with [npm][npm-install]: 59 | 60 | ```sh 61 | npm install rehype-starry-night 62 | ``` 63 | 64 | In Deno with [`esm.sh`][esmsh]: 65 | 66 | ```js 67 | import rehypeStarryNight from 'https://esm.sh/rehype-starry-night@2' 68 | ``` 69 | 70 | In browsers with [`esm.sh`][esmsh]: 71 | 72 | ```html 73 | 76 | ``` 77 | 78 | ## Use 79 | 80 | Say we have the following file `example.md`: 81 | 82 | ````markdown 83 | # Neptune 84 | 85 | ```rs 86 | fn main() { 87 | println!("Hello, Neptune!"); 88 | } 89 | ``` 90 | ```` 91 | 92 | …and our module `example.js` contains: 93 | 94 | ```js 95 | import rehypeStarryNight from 'rehype-starry-night' 96 | import rehypeStringify from 'rehype-stringify' 97 | import remarkParse from 'remark-parse' 98 | import remarkRehype from 'remark-rehype' 99 | import {read} from 'to-vfile' 100 | import {unified} from 'unified' 101 | 102 | const file = await read('example.md') 103 | 104 | await unified() 105 | .use(remarkParse) 106 | .use(remarkRehype) 107 | .use(rehypeStarryNight) 108 | .use(rehypeStringify) 109 | .process(file) 110 | 111 | console.log(String(file)) 112 | ``` 113 | 114 | …then running `node example.js` yields: 115 | 116 | ```html 117 |

Neptune

118 |
fn main() {
119 |     println!("Hello, Neptune!");
120 | }
121 | 
122 | ``` 123 | 124 | ## API 125 | 126 | ### `Options` 127 | 128 | Configuration for `rehype-starry-night`. 129 | 130 | ###### Extends 131 | 132 | * `StarryNightOptions` 133 | 134 | ###### Fields 135 | 136 | * `allowMissingScopes?` (`boolean | null | undefined`) 137 | — do not warn for missing scopes (default: `false`) 138 | * `grammars?` (`ReadonlyArray | null | undefined`) 139 | — grammars to support (default: `common`) 140 | * `plainText?` (`ReadonlyArray | null | undefined`) 141 | — list of language names to not highlight (default: `[]`) 142 | 143 | ### `rehypeStarryNight(options) (default)` 144 | 145 | Plugin to highlight code with `starry-night`. 146 | 147 | ###### Parameters 148 | 149 | * `options?` (`Readonly | null | undefined`) 150 | — configuration (optional) 151 | 152 | ###### Returns 153 | 154 | Transform (`(tree: Root, file: VFile) => Promise`). 155 | 156 | ## HTML 157 | 158 | On the input side, 159 | this plugin looks for code blocks with a `language-*` class. 160 | 161 | On the output side, 162 | this plugin generates `span` elements with classes that can be enhanced with 163 | CSS. 164 | 165 | ## CSS 166 | 167 | See [“CSS” in `starry-night`][github-starry-night-css] for more info. 168 | 169 | ## Compatibility 170 | 171 | Projects maintained by the unified collective are compatible with maintained 172 | versions of Node.js. 173 | 174 | When we cut a new major release, we drop support for unmaintained versions of 175 | Node. 176 | This means we try to keep the current release line, `rehype-starry-night@2`, 177 | compatible with Node.js 16. 178 | 179 | ## Security 180 | 181 | This package is safe. 182 | 183 | ## Related 184 | 185 | * [`rehype-highlight`][github-rehype-highlight] 186 | — highlight code blocks with `lowlight` 187 | * [`rehype-twoslash`][github-rehype-twoslash] 188 | — process JavaScript/TypeScript code with `twoslash` and `starry-night` too 189 | 190 | ## Contribute 191 | 192 | See [`contributing.md`][health-contributing] in [`rehypejs/.github`][health] 193 | for ways to get started. 194 | See [`support.md`][health-support] for ways to get help. 195 | 196 | This project has a [code of conduct][health-coc]. 197 | By interacting with this repository, organization, or community you agree to 198 | abide by its terms. 199 | 200 | ## License 201 | 202 | [MIT][file-license] © [Julien Barbay][github-y-nk] 203 | 204 | 205 | 206 | [badge-backers-image]: https://opencollective.com/unified/backers/badge.svg 207 | 208 | [badge-build-image]: https://github.com/rehypejs/rehype-starry-night/actions/workflows/main.yml/badge.svg 209 | 210 | [badge-build-url]: https://github.com/rehypejs/rehype-starry-night/actions 211 | 212 | [badge-collective-url]: https://opencollective.com/unified 213 | 214 | [badge-coverage-image]: https://img.shields.io/codecov/c/github/rehypejs/rehype-starry-night.svg 215 | 216 | [badge-coverage-url]: https://codecov.io/github/rehypejs/rehype-starry-night 217 | 218 | [badge-downloads-image]: https://img.shields.io/npm/dm/rehype-starry-night.svg 219 | 220 | [badge-downloads-url]: https://www.npmjs.com/package/rehype-starry-night 221 | 222 | [badge-size-image]: https://img.shields.io/bundlejs/size/rehype-starry-night 223 | 224 | [badge-size-url]: https://bundlejs.com/?q=rehype-starry-night 225 | 226 | [badge-sponsors-image]: https://opencollective.com/unified/sponsors/badge.svg 227 | 228 | [badge-chat-image]: https://img.shields.io/badge/chat-discussions-success.svg 229 | 230 | [badge-chat-url]: https://github.com/rehypejs/rehype/discussions 231 | 232 | [esmsh]: https://esm.sh 233 | 234 | [file-license]: license 235 | 236 | [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 237 | 238 | [github-rehype]: https://github.com/rehypejs/rehype 239 | 240 | [github-rehype-highlight]: https://github.com/rehypejs/rehype-highlight 241 | 242 | [github-rehype-twoslash]: https://github.com/rehypejs/rehype-twoslash 243 | 244 | [github-starry-night]: https://github.com/wooorm/starry-night 245 | 246 | [github-starry-night-css]: https://github.com/wooorm/starry-night#css 247 | 248 | [github-unified]: https://github.com/unifiedjs/unified 249 | 250 | [github-y-nk]: https://github.com/y-nk 251 | 252 | [health-coc]: https://github.com/rehypejs/.github/blob/main/code-of-conduct.md 253 | 254 | [health-contributing]: https://github.com/rehypejs/.github/blob/main/contributing.md 255 | 256 | [health-support]: https://github.com/rehypejs/.github/blob/main/support.md 257 | 258 | [health]: https://github.com/rehypejs/.github 259 | 260 | [npm-install]: https://docs.npmjs.com/cli/install 261 | 262 | [twoslash]: https://twoslash.netlify.app 263 | --------------------------------------------------------------------------------