├── .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 sound4 | 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 sound4 | 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?: ReadonlyArrayconsole.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 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 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 | 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