Shaku - code annotation made easy

Shaku makes it super easy to annotate code with special directives in comments.

A very basic example

const Hello = "World!"
// ^
// [Hello World!]
const Hello = "World!"
// ^
// [Hello World!]

Above code is already self-explanatory, but with Shaku it is rendered into sth even better.

const Hello = "World!"

Hello World!

const Hello = "World!"

Hello World!

Now code and annotation are visually separated, super cool to explain code, right?

Usage

Choose the right tool for your use case.

  1. shaku-code-annotate-core - Shaku parser that parses the syntax in the comments. Tokenizer not included.
  2. shaku-code-annotate-shiki - Shaku code annotation based on the tokenizer from shiki. You can use this package directly in node.js or browser.
  3. shaku-code-annotate-sugar-high - Enable Shaku on the lightweight syntax highlighter - Sugar High.
  4. remark-shaku-code-annotate - plugin for remark to easily integrate Shaku in Markdown/MDX(Astro .etc)
  5. marked-shaku-code-annotate - a plugin for another markdown engine: marked

Also some demos you can refer to:

  1. Shaku + MDX + Astro
  2. Shaku + MDX + Next.js

Or you can just inspect the source code of this website - CodeBlock.tsx, CodePreviewRemark.tsx or CodePreviewMarked.tsx

Supported Languages

Shaku on Shiki supports most languages that are supported by Shiki. You can find the +150 languages from Shaku Snippet.

Styling

Shaku renders code into a <pre /> with class.shaku, and shaku elements have class names prefixed with .shaku, you can use the CSS from this website and adapt to your needs.

The class names for each Shaku element will be explained in Syntax section.

Dark mode support

You can render multiple themes by setting themes in the Shaku plugins, the theme lang is put on the <pre> tag. Thus we can control the visibility by CSS

const marked = new Marked();
marked.use(
markedShakuCodeAnnotate({
themes: ["github-light", "github-dark"],
langs: ["javascript", "css", "jsx", "html", "typescript", "tsx"],
})
);
const marked = new Marked();
marked.use(
markedShakuCodeAnnotate({
themes: ["github-light", "github-dark"],
langs: ["javascript", "css", "jsx", "html", "typescript", "tsx"],
})
);
pre.shaku.github-dark {
display: none;
}
@media (prefers-color-scheme: dark) {
pre.shaku.github-dark {
display: block;
}
pre.shaku.github-light {
display: none;
}
}
pre.shaku.github-dark {
display: none;
}
@media (prefers-color-scheme: dark) {
pre.shaku.github-dark {
display: block;
}
pre.shaku.github-light {
display: none;
}
}

Syntax

You can also try out the syntax on Shaku Playground or Shaku Snippet.

Callout

const blog = "https://jser.dev"

JSer.dev is the homepage for JSer.

Check it out! jser.dev

// This is a normal comment
const blog = "https://jser.dev"

JSer.dev is the homepage for JSer.

Check it out! jser.dev

// This is a normal comment

Place ^ for the arrow, and [] for the text, you can also enable basic markdown support.

const blog = "https://jser.dev"
// ^
// [JSer.dev is the *homepage* for JSer.]
// [Check it out! [jser.dev](https://jser.dev)]
// This is a normal comment
const blog = "https://jser.dev"
// ^
// [JSer.dev is the *homepage* for JSer.]
// [Check it out! [jser.dev](https://jser.dev)]
// This is a normal comment

.shaku-callout and .shaku-callout-arrow are used to style callout.

.shaku-callout {
background-color: var(--color-shaku-callout-light, #0685ce);
color: #fff;
padding: 0.5em 1ch;
position: relative;
margin: 0.5em 0 0 -0.2ch;
display: inline-block;
border-radius: 2px;
}
.shaku-callout-arrow {
width: 1ch;
height: 1ch;
display: inline-block;
background-color: var(--color-shaku-callout-light, #0685ce);
position: absolute;
top: -0.5ch;
transform: rotate(45deg);
margin-left: 0.2ch;
}
.shaku-callout {
background-color: var(--color-shaku-callout-light, #0685ce);
color: #fff;
padding: 0.5em 1ch;
position: relative;
margin: 0.5em 0 0 -0.2ch;
display: inline-block;
border-radius: 2px;
}
.shaku-callout-arrow {
width: 1ch;
height: 1ch;
display: inline-block;
background-color: var(--color-shaku-callout-light, #0685ce);
position: absolute;
top: -0.5ch;
transform: rotate(45deg);
margin-left: 0.2ch;
}

Underlines

// This is normal comments from source code.
const blog = "https://jser.dev"
----------------

JSer.dev is the homepage for JSer.

Check it out! jser.dev

const blog = "jser.dev"
--------
const blog = "jser.dev"
~~~~~~~~
const blog = "jser.dev"
........
// This is normal comments from source code.
const blog = "https://jser.dev"
----------------

JSer.dev is the homepage for JSer.

Check it out! jser.dev

const blog = "jser.dev"
--------
const blog = "jser.dev"
~~~~~~~~
const blog = "jser.dev"
........

Simply use -----, ...., ~~~~~ for underlines.

// This is normal comments from source code.
const blog = "https://jser.dev"
// ----------------
// [JSer.dev is the **homepage** for JSer.]
// [Check it out! [jser.dev](https://jser.dev)]
const blog = "jser.dev"
// --------
const blog = "jser.dev"
// ~~~~~~~~
const blog = "jser.dev"
// ........
// This is normal comments from source code.
const blog = "https://jser.dev"
// ----------------
// [JSer.dev is the **homepage** for JSer.]
// [Check it out! [jser.dev](https://jser.dev)]
const blog = "jser.dev"
// --------
const blog = "jser.dev"
// ~~~~~~~~
const blog = "jser.dev"
// ........

.shaku-underline is the base style, .shaku-underline-wavy, .shaku-underline-solidand .shaku-underline-dotted are for the variations.

.shaku-underline {
padding: 0 1ch;
position: relative;
display: block;
border-radius: 3px;
color: var(--color-shaku-underline-light, red);
margin: 0;
width: min-content;
}
.shaku-underline-line {
line-height: 0;
top: 0.5em;
position: absolute;
text-decoration-line: overline;
text-decoration-color: var(--color-shaku-underline-light, red);
color: transparent;
pointer-events: none;
user-select: none;
text-decoration-thickness: 2px;
}
.shaku-underline-wavy > .shaku-underline-line {
text-decoration-style: wavy;
top: 0.7em;
}
.shaku-underline-solid > .shaku-underline-line {
text-decoration-style: solid;
}
.shaku-underline-dotted > .shaku-underline-line {
text-decoration-style: dotted;
}
.shaku-underline {
padding: 0 1ch;
position: relative;
display: block;
border-radius: 3px;
color: var(--color-shaku-underline-light, red);
margin: 0;
width: min-content;
}
.shaku-underline-line {
line-height: 0;
top: 0.5em;
position: absolute;
text-decoration-line: overline;
text-decoration-color: var(--color-shaku-underline-light, red);
color: transparent;
pointer-events: none;
user-select: none;
text-decoration-thickness: 2px;
}
.shaku-underline-wavy > .shaku-underline-line {
text-decoration-style: wavy;
top: 0.7em;
}
.shaku-underline-solid > .shaku-underline-line {
text-decoration-style: solid;
}
.shaku-underline-dotted > .shaku-underline-line {
text-decoration-style: dotted;
}

Highlight Lines

function useSomeEffect({blog}) {
useEffect(() => {
// do some stuff
return () => {
location.href = 'https://jser.dev'
}
}, [blog])
}
function useSomeEffect({blog}) {
useEffect(() => {
// do some stuff
return () => {
location.href = 'https://jser.dev'
}
}, [blog])
}

Use @highlight to highlight next line, append start /end(or v / ^) to mark multiple lines.

// @highlight
function useSomeEffect({blog}) {
useEffect(() => {
// do some stuff
// @highlight start
return () => {
location.href = 'https://jser.dev'
}
// @highlight end
}, [blog])
}
// @highlight
function useSomeEffect({blog}) {
useEffect(() => {
// do some stuff
// @highlight start
return () => {
location.href = 'https://jser.dev'
}
// @highlight end
}, [blog])
}

.shaku .line.highlight could be used to set highlight style.

pre.shaku .line.highlight {
background-color: var(
--color-shaku-highlight-light,
color-mix(in srgb, rgb(5, 118, 149) 15%, #fff)
);
display: block;
}
pre.shaku .line.highlight {
background-color: var(
--color-shaku-highlight-light,
color-mix(in srgb, rgb(5, 118, 149) 15%, #fff)
);
display: block;
}

Highlight Words(inline)

function useSomeEffect({blog}) {
useEffect(() => {
return () => {
location.href = 'https://jser.dev'
}
}, [blog])
}
function useSomeEffect({blog}) {
useEffect(() => {
return () => {
location.href = 'https://jser.dev'
}
}, [blog])
}

( and ) are used to mark the selection of next line. optional id inside could be used to map to different color.

// ( )
function useSomeEffect({blog}) {
//( r )
useEffect(() => {
return () => {
//( g ) ( b )
location.href = 'https://jser.dev'
}
}, [blog])
}
// ( )
function useSomeEffect({blog}) {
//( r )
useEffect(() => {
return () => {
//( g ) ( b )
location.href = 'https://jser.dev'
}
}, [blog])
}

.shaku-inline-highlight is used to style the inline blocks, target specific blocks with the id you set - [data-id=*].

.shaku-inline-highlight {
background-color: #fa05f230;
border-bottom: 2px solid rgb(235, 4, 158);
margin: 0 1px;
border-radius: 3px;
padding: 0 3px;
}
.shaku-inline-highlight[data-id="r"] {
background-color: #fa05f230;
border-bottom: 2px solid rgb(235, 4, 158);
}
.shaku-inline-highlight[data-id="g"] {
background-color: #05faa930;
border-bottom: 2px solid green;
}
.shaku-inline-highlight[data-id="b"] {
background-color: #05a4fa30;
border-bottom: 2px solid rgb(9, 113, 239);
}
.shaku-inline-highlight {
background-color: #fa05f230;
border-bottom: 2px solid rgb(235, 4, 158);
margin: 0 1px;
border-radius: 3px;
padding: 0 3px;
}
.shaku-inline-highlight[data-id="r"] {
background-color: #fa05f230;
border-bottom: 2px solid rgb(235, 4, 158);
}
.shaku-inline-highlight[data-id="g"] {
background-color: #05faa930;
border-bottom: 2px solid green;
}
.shaku-inline-highlight[data-id="b"] {
background-color: #05a4fa30;
border-bottom: 2px solid rgb(9, 113, 239);
}

Diff lines

function useSomeEffect({blog}) {
useEffect(() => {
return () => {
location.href = 'https://jser.dev'
}
console.log('remove this')
}, [blog])
}
function useSomeEffect({blog}) {
useEffect(() => {
return () => {
location.href = 'https://jser.dev'
}
console.log('remove this')
}, [blog])
}

Use @diff + and @diff - to mark next line as diff, append start /end(or v / ^) to diff multiple lines.

function useSomeEffect({blog}) {
useEffect(() => {
// @diff + start
return () => {
location.href = 'https://jser.dev'
}
// @diff + end
// @diff -
console.log('remove this')
}, [blog])
}
function useSomeEffect({blog}) {
useEffect(() => {
// @diff + start
return () => {
location.href = 'https://jser.dev'
}
// @diff + end
// @diff -
console.log('remove this')
}, [blog])
}

.diff, .diff-insert and .diff-delete are used to style the diff lines.

pre.shaku .line.diff::before {
position: absolute;
margin-left: -1ch;
}
pre.shaku .line.diff-insert {
background-color: rgba(46, 160, 67, 0.2);
}
pre.shaku .line.diff-insert::before {
content: "+";
}
pre.shaku .line.diff-delete {
background-color: rgba(248, 81, 73, 0.2);
}
pre.shaku .line.diff-delete::before {
content: "-";
}
pre.shaku .line.diff::before {
position: absolute;
margin-left: -1ch;
}
pre.shaku .line.diff-insert {
background-color: rgba(46, 160, 67, 0.2);
}
pre.shaku .line.diff-insert::before {
content: "+";
}
pre.shaku .line.diff-delete {
background-color: rgba(248, 81, 73, 0.2);
}
pre.shaku .line.diff-delete::before {
content: "-";
}

Dim lines

function useSomeEffect({blog}) {
useEffect(() => {
// do some stuff
return () => {
location.href = 'https://jser.dev'
}
}, [blog])
}
function useSomeEffect({blog}) {
useEffect(() => {
// do some stuff
return () => {
location.href = 'https://jser.dev'
}
}, [blog])
}

Similar to highlighting, use @dim to dim next line, append start /end(or v / ^) to dim multiple lines.

function useSomeEffect({blog}) {
// @dim
useEffect(() => {
// do some stuff
// @dim start
return () => {
location.href = 'https://jser.dev'
}
// @dim end
}, [blog])
}
function useSomeEffect({blog}) {
// @dim
useEffect(() => {
// do some stuff
// @dim start
return () => {
location.href = 'https://jser.dev'
}
// @dim end
}, [blog])
}

Use .dim to style dimmed lines.

.shaku .line.dim {
filter: blur(2px) brightness(0.5);
}
.shaku .line.dim {
filter: blur(2px) brightness(0.5);
}

Focus lines

function useSomeEffect({blog}) {
useEffect(() => {
// do some stuff
return () => {
location.href = 'https://jser.dev'
}
}, [blog])
}
function useSomeEffect({blog}) {
useEffect(() => {
// do some stuff
return () => {
location.href = 'https://jser.dev'
}
}, [blog])
}

Focus means to highlight some by dimming the other lines. Use @focus to focus next line, append start /end(or v / ^) to focus multiple lines.

function useSomeEffect({blog}) {
// @focus
useEffect(() => {
// do some stuff
return () => {
// @focus start
location.href = 'https://jser.dev'
// @focus end
}
}, [blog])
}
function useSomeEffect({blog}) {
// @focus
useEffect(() => {
// do some stuff
return () => {
// @focus start
location.href = 'https://jser.dev'
// @focus end
}
}, [blog])
}

Since it is actually @dim, there is no special class for it.

Fold lines

function useSomeEffect({blog}) {
useEffect(() => {
{...}
// do some stuff
return () => {
location.href = 'https://jser.dev'
}
}, [blog])
}
function useSomeEffect({blog}) {
useEffect(() => {
{...}
// do some stuff
return () => {
location.href = 'https://jser.dev'
}
}, [blog])
}

@fold is to collapse lines, append start / end (or v / ^) to mark the lines.

function useSomeEffect({blog}) {
useEffect(() => {
// @fold start
// do some stuff
return () => {
location.href = 'https://jser.dev'
}
// @fold end
}, [blog])
}
function useSomeEffect({blog}) {
useEffect(() => {
// @fold start
// do some stuff
return () => {
location.href = 'https://jser.dev'
}
// @fold end
}, [blog])
}

@fold renders <details><summary><mark></mark></summary></details>, class .shaku-expandcould be used to style.

.shaku-expand summary mark {
color: var(--color-text-sub);
cursor: pointer;
border-radius: 3px;
}
.shaku-expand summary::-webkit-details-marker,
.shaku-expand summary::marker {
display: none;
content: "";
}
.shaku-expand[open] summary {
display: none;
}
.shaku-expand summary mark {
color: var(--color-text-sub);
cursor: pointer;
border-radius: 3px;
}
.shaku-expand summary::-webkit-details-marker,
.shaku-expand summary::marker {
display: none;
content: "";
}
.shaku-expand[open] summary {
display: none;
}

Cut lines

In case you want to remove some lines from the rendered output but keep them in the source code, you can use @cut to cut one line, or append start / end (or v / ^) to cut multiple lines.

// @cut v
import Button from './Button'
import { useEffect } from 'react'
// @cut ^
function component() {
return <Button
class="button"
disabled
/>
}
// @cut v
import Button from './Button'
import { useEffect } from 'react'
// @cut ^
function component() {
return <Button
class="button"
disabled
/>
}
Above annotation will be rendered as below.
function component() {
return <Button
class="button"
disabled
/>
}
function component() {
return <Button
class="button"
disabled
/>
}

Position Shift

function component() {

This is very beginning

return <Button
class="button"
--------------

Hello World!

disabled
/>
}
function component() {

This is very beginning

return <Button
class="button"
--------------

Hello World!

disabled
/>
}

Sometimes it is hard to position it right(with formatter .etc), you can use position shift < to move shaku elements toward left

function component() {
//^<<
//[This is very beginning ] <<
return <Button
class="button"
//--------------<<
//[Hello World!]<<<
disabled
/>
}
function component() {
//^<<
//[This is very beginning ] <<
return <Button
class="button"
//--------------<<
//[Hello World!]<<<
disabled
/>
}

Escape

For cases where rendering raw comments are desirable, we can put !at the end of shaku lines to escape.

const Hello = "World!"
// ^
// [Hello World!]

Above two lines are not rendered into UI elements!

Since they have ! at the end and ! is removed when rendered

const Hello = "World!"
// ^
// [Hello World!]

Above two lines are not rendered into UI elements!

Since they have ! at the end and ! is removed when rendered

Custom class names for lines

Sometimes you might want to add custom class names for a line, this could be easily done by @class directive.

// @class custom-class1 custom-class2
const Hello = "World!"

Open the dev console and inspect this line,

you'll see this line is rendered with the custom class names!

// @class custom-class1 custom-class2
const Hello = "World!"

Open the dev console and inspect this line,

you'll see this line is rendered with the custom class names!

Custom data attributes

You can also use @data to add custom data attributes to a line, which could be useful if you are building something on top of Shaku.

// @data hello=world jser="dev"
const Hello = "World!"

Open the dev console and inspect this line,

you'll see this line is rendered with the custom data attributes!

// @data hello=world jser="dev"
const Hello = "World!"

Open the dev console and inspect this line,

you'll see this line is rendered with the custom data attributes!

Dev Tools

We got some tools to understand how Shaku works, such as Shiki Token Inspector and Sugar High Token Inspector

Showcases

Check out some examples built with Shaku.


Got a bug or have better ideas? Raise an issue on shaku repo.