This package is an open source version of GitHub’s closed-source PrettyLights
project (more on that later).
It supports 600+ grammars and its extremely high quality.
It uses TextMate grammars which are also used in popular editors (SublimeText,
Atom, VS Code, &c).
They’re heavy but high quality.
When should I use this?
starry-night is a high quality highlighter
(when your readers or authors are programmers, you want this!)
that can support tons of grammars
(from new things like MDX to much more!)
which approaches how GitHub renders code.
It has a WASM dependency, and rather big grammars, which means that
starry-night might be too heavy particularly in browsers, in which case
lowlight or refractor might be more suitable.
This project is similar to the excellent shiki, and it uses the same
underlying dependencies, but starry-night is meant to match GitHub in that it
produces classes and works with the CSS it ships, making it easier to add dark
mode and other themes with CSS compared to inline styles.
Finally, this package produces objects (an AST), which makes it useful when you
want to perform syntax highlighting in a place where serialized HTML wouldn’t
work or wouldn’t work well.
For example, when you want to show code in a CLI by rendering to ANSI sequences,
when you’re using virtual DOM frameworks (such as React or Preact) so that
diffing can be performant, or when you’re working with hast or
rehype.
Bundled, minified, and gzipped, starry-night and the WASM binary are 185 kB.
There are two lists of grammars you can use: common (±35
languages, good for your own site) adds 250 kB and all (~600
languages, useful if you are making a site like GitHub) is 1.6 MB.
You can also manually choose which grammars to include (or add to common): a
language is typically between 3 and 5 kB.
To illustrate, Astro costs 2.1 kB and TSX costs 25.4 kB.
What is PrettyLights?
PrettyLights is the syntax highlighter that GitHub uses to turn this:
…which is what starry-night does too (some small differences in markup, but
essentially the same)!
PrettyLights is responsible for taking the flag markdown, looking it up in
languages.yml from github-linguist to figure out that that
means markdown, taking a corresponding grammar (in this case
wooorm/markdown-tm-language),
doing some GPL magic in C,
and turning it into spans with classes.
GitHub is using PrettyLights since December 2014, when it
replaced Pygments.
They wanted to open source it, but were unable due to licensing issues.
Recently (Feb 2019?), GitHub has slowly started to move towards
TreeLights, which is based on TreeSitter, and also closed source.
If TreeLights includes a language (currently: C, C#, CSS, CodeQL, EJS, Elixir,
ERB, Gleam, Go, HTML, Java, JS, Nix, PHP, Python, RegEx, Ruby, Rust, TLA, TS),
that’ll be used, for everything else PrettyLights is used.
starry-night does what PrettyLights does, not what TreeLights does.
I’m hopeful that that will be open sourced in the future and we can mimic both.
Install
This package is ESM only.
In Node.js (version 16+), install with npm:
This package exports the identifiers all,
common, and createStarryNight from
the main module.
There is no default export.
It also includes grammars directly in its export map.
Do not use the lang/ folder or the .js extension.
For CSS files, do use style/ but don’t use .css:
Create a StarryNight that can highlight things with the given grammars.
This is async to allow async loading and registering, which is currently
only used for WASM.
Promise that resolves to an instance which highlights with the bound
grammars (Promise<StarryNight>).
starryNight.flagToScope(flag)
Get the grammar scope (such as text.md) associated with a grammar name
(such as markdown) or grammar extension (such as .mdwn).
This function uses the first word (when splitting on spaces and tabs) that is
used after the opening of a fenced code block:
```js
console.log(1)
```
To match GitHub, this also accepts entire paths:
```path/to/example.js
console.log(1)
```
👉 Note: languages can use the same extensions.
For example, .h is reused by many languages.
In those cases, you will get one scope back, but it might not be the
most popular language associated with an extension.
Parameters
flag (string)
— grammar name (such as 'markdown'), grammar extension (such as
'.mdwn'), or entire file path ending in extension
Returns
Grammar scope, such as 'text.md' (string or undefined).
Function to get a URL to the oniguruma WASM (TypeScript type).
👉 Note: this must currently result in a version 2 URL of
onig.wasm from vscode-oniguruma.
⚠️ Danger: when you use this functionality, your project might break at
any time (when reinstalling dependencies), except when you make sure that
the WASM binary you load manually is what our internally used
vscode-oniguruma dependency expects.
To solve this, you could for example use an npm script called
dependencies (which runs everytime
node_modules is changed) which copies
vscode-oniguruma/release/onig.wasm to the place you want to host it.
Returns
URL object to a WASM binary (Promise<URL> or URL).
You don’t have to do preprocess things on a server.
Particularly, when you are not using Node.js or so.
Or, when you have a lot of often changing content (likely markdown), such as
on a page of comments.
In those cases, you can run starry-night in the browser.
Here is an example.
It also uses hast-util-to-dom, which is a light way to
turn the AST into DOM nodes.
Say we have this example.js on our browser (no bundling needed!):
import {
common,
createStarryNight
} from'https://esm.sh/@wooorm/starry-night@3?bundle'import {toDom} from'https://esm.sh/hast-util-to-dom@4?bundle'const starryNight = awaitcreateStarryNight(common)
const prefix = 'language-'const nodes = Array.from(document.body.querySelectorAll('code'))
for (const node of nodes) {
const className = Array.from(node.classList).find(function (d) {
return d.startsWith(prefix)
})
if (!className) continueconst scope = starryNight.flagToScope(className.slice(prefix.length))
if (!scope) continueconst tree = starryNight.highlight(node.textContent, scope)
node.replaceChildren(toDom(tree, {fragment: true}))
}
…and then, if we would have an index.html for our document:
GitHub itself does not add line numbers to the code they highlight.
You can do that, by transforming the AST.
Here’s an example of a utility that wraps each line into a span with a class and
a data attribute with its line number.
That way, you can style the lines as you please.
Or you can generate different elements for each line, of course.
Say we have our utility as hast-util-starry-night-gutter.js:
/**
* @typedef {import('hast').Element} Element
* @typedef {import('hast').ElementContent} ElementContent
* @typedef {import('hast').Root} Root
* @typedef {import('hast').RootContent} RootContent
*//**
* @param {Root} tree
* Tree.
* @returns {undefined}
* Nothing.
*/exportfunctionstarryNightGutter(tree) {
/** @type {Array<RootContent>} */const replacement = []
const search = /\r?\n|\r/glet index = -1let start = 0let startTextRemainder = ''let lineNumber = 0while (++index < tree.children.length) {
const child = tree.children[index]
if (child.type === 'text') {
let textStart = 0let match = search.exec(child.value)
while (match) {
// Nodes in this line.const line = /** @type {Array<ElementContent>} */ (
tree.children.slice(start, index)
)
// Prepend text from a partial matched earlier text.if (startTextRemainder) {
line.unshift({type: 'text', value: startTextRemainder})
startTextRemainder = ''
}
// Append text from this text.if (match.index > textStart) {
line.push({
type: 'text',
value: child.value.slice(textStart, match.index)
})
}
// Add a line, and the eol.
lineNumber += 1
replacement.push(createLine(line, lineNumber), {
type: 'text',
value: match[0]
})
start = index + 1
textStart = match.index + match[0].length
match = search.exec(child.value)
}
// If we matched, make sure to not drop the text after the last line ending.if (start === index + 1) {
startTextRemainder = child.value.slice(textStart)
}
}
}
const line = /** @type {Array<ElementContent>} */ (tree.children.slice(start))
// Prepend text from a partial matched earlier text.if (startTextRemainder) {
line.unshift({type: 'text', value: startTextRemainder})
startTextRemainder = ''
}
if (line.length > 0) {
lineNumber += 1
replacement.push(createLine(line, lineNumber))
}
// Replace children with new array.
tree.children = replacement
}
/**
* @param {Array<ElementContent>} children
* @param {number} line
* @returns {Element}
*/functioncreateLine(children, line) {
return {
type: 'element',
tagName: 'span',
properties: {className: 'line', dataLineNumber: line},
children
}
}
Example: integrate with unified, remark, and rehype
This example shows how to combine starry-night with unified:
using remark to parse the markdown and transforming it to HTML with
rehype.
If we have a markdown file example.md:
The generated hast starts with a root node, that represents the
fragment.
It contains up to three levels of <span>elements, each with a single class.
All these levels can contain text nodes with the actual code.
Interestingly, TextMate grammars work per line, so all line endings are in the
root directly, meaning that creating a gutter to display line numbers can be
generated rather naïvely by only looking through the root node.
CSS
starry-night does not inject CSS for the syntax highlighted code (because
well, starry-night doesn’t have to be turned into HTML and might not run in a
browser!).
If you are in a browser, you can use the packaged themes, or get creative with
CSS!
💅
All themes accept CSS variables (custom properties).
With the theme core.css, you have to define your own properties.
All other themes define the colors on :root.
Themes either have a dark or light suffix, or none, in which case they
automatically switch colors based on a @media (prefers-color-scheme: dark).
All themes are tiny (under 1 kB).
The shipped themes are as follows:
This project is compatible with maintained versions of Node.js.
When we cut a new major release, we drop support for unmaintained versions of
Node.
This means we try to keep the current release line, wooorm@starry-night@^3,
compatible with Node.js 16.
You can pass your own TextMate grammars, provided that they work with
vscode-textmate, and that they have the added fields
extensions, names, and scopeName (see types for the definitions and the
grammars in lang/ for examples).
The grammars included in this package are covered by their repositories’
respective licenses, which are permissive (apache-2.0, mit, etc), and made
available in notice.
We found that @wooorm/starry-night demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.It has 0 open source maintainers collaborating on the project.
Package last updated on 10 Jun 2024
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.
A Stanford study reveals 9.5% of engineers contribute almost nothing, costing tech $90B annually, with remote work fueling the rise of "ghost engineers."