Code Annotations

You can extend how the Pre component renders code by using annotation handlers.

For example, here we define two components: one that adds a red border to a block of code and another that adds a dark background.

my-code.tsx
import type { AnnotationHandler, RawCode } from "codehike/code"
import { Pre, highlight } from "codehike/code"
export async function MyCode({ codeblock: RawCode }) {
const highlighted = await highlight(codeblock, "github-dark")
return <Pre code={highlighted} handlers={[borderHandler, bgHandler]} />
}
const borderHandler: AnnotationHandler = {
name: "border",
Block: ({ annotation, children }) => (
<div style={{ border: "1px solid red" }}>{children}</div>
),
}
const bgHandler: AnnotationHandler = {
name: "bg",
Inline: ({ annotation, children }) => (
<span style={{ background: "#2d26" }}>{children}</span>
),
}

We can use the name of those handlers as comments in the code to use the components:

content.md
```js
// !border(1:2)
const lorem = ipsum == null ? 0 : 1
dolor = lorem - sit(dolor)
// !bg[5:16]
let amet = lorem ? consectetur(ipsum) : 3
```
const lorem = ipsum == null ? 0 : 1
dolor = lorem - sit(dolor)
let amet = lorem ? consectetur(ipsum) : 3

Annotation Comments

We use comments to annotate codeblocks. The comment syntax depends on the language. For example, in javascript we use // !name(1:5) but in python we use # !name(1:5). For JSON (that doesn't support comments), the recommendation is to instead use jsonc or json5, which support comments.

In the previous example we can see the two types of annotations:

  • Block annotations are applied to a block of lines. They use parenthesis () to define the range of lines. The numbers are relative to the line where the comment is placed.
  • Inline annotations are applied to a group of tokens inside a line. They use square brackets [] to define the range of columns included in the annotation.

Annotation Query

Any extra content in the annotation comment is passed to the annotation components as query.

For example, we can change the components from the previous example to use the query to define the color of the border and background:

my-code.tsx
const borderHandler: AnnotationHandler = {
name: "border",
Block: ({ annotation, children }) => {
const borderColor = annotation.query || "red"
return <div style={{ border: "1px solid", borderColor }}>{children}</div>
},
}
const bgHandler: AnnotationHandler = {
name: "bg",
Inline: ({ annotation, children }) => {
const background = annotation.query || "#2d26"
return <span style={{ background }}>{children}</span>
},
}
content.md
```js
// !border(1:2) purple
const lorem = ipsum == null ? 0 : 1
dolor = lorem - sit(dolor)
// !bg[5:16] darkblue
let amet = lorem ? consectetur(ipsum) : 3
```
const lorem = ipsum == null ? 0 : 1
dolor = lorem - sit(dolor)
let amet = lorem ? consectetur(ipsum) : 3

Customizing Line and Token components

Sometimes you want to customize the rendering of every line or token, not just the annotated ones. You can do that by defining the Line and Token components:

my-code.tsx
import { InnerLine } from "codehike/code"
const myHandler: AnnotationHandler = {
name: "uglyLineNumbers",
Line: (props) => {
const { lineNumber, totalLines, indentation } = props
return (
<div>
{lineNumber} |
<InnerLine merge={props} className="inline-block" />
</div>
)
},
}

What's InnerLine? Since the same line can be targeted by many annotation handlers, we need to make the components composable. So InnerLine will chain and merge all the props from the different handlers.

For example, if we have these two handlers:

my-code.tsx
const bgHandler: AnnotationHandler = {
Line: (props) => (
<InnerLine
merge={props}
className="bg-red-200"
data-line={props.lineNumber}
/>
),
}
const paddingHandler: AnnotationHandler = {
Line: (props) => <InnerLine merge={props} className="px-2" />,
}

The final rendering will be:

output.html
<pre>
<div className="bg-red-200 px-2" data-line="1">...</div>
<div className="bg-red-200 px-2" data-line="2">...</div>
<div className="bg-red-200 px-2" data-line="3">...</div>
</pre>

Similarly, you can customize the rendering of every token, but this is far less common:

my-code.tsx
import { InnerToken } from "codehike/code"
const myHandler: AnnotationHandler = {
Token: (props) => {
const { value, style, lineNumber } = props
return <InnerToken merge={props} style={{ display: "inline-block" }} />
},
}

Customizing the Pre component

You can also customize the rendering of the <pre> element itself:

my-code.tsx
import { InnerPre, getPreRef } from "codehike/code"
const myHandler: AnnotationHandler = {
Pre: (props) => (
<InnerPre merge={props} className="rounded border border-blue-100" />
),
// If you need the ref to the pre element, use PreWithRef:
PreWithRef: (props) => {
const ref = getPreRef(props)
doSomethingWithRef(ref)
return <InnerPre merge={props} />
},
}

AnnotatedLine and AnnotatedToken

Similar to Line and Token, you can define AnnotatedLine and AnnotatedToken to customize the rendering of individual lines and tokens that are part of an annotation.

Sometimes it's useful to combine the Line and AnnotatedLine components to avoid repeating the same code.

For example, here we add a data-mark attribute to the lines inside a mark annotation, then we have a Line component that adds a border, but we only add border color to the lines that have the data-mark attribute:

my-code.tsx
const mark: AnnotationHandler = {
name: "mark",
AnnotatedLine: ({ annotation, ...props }) => (
<InnerLine merge={props} data-mark={true} />
),
Line: (props) => (
<InnerLine
merge={props}
className="px-2 border-l-4 border-transparent data-[mark]:border-blue-400"
/>
),
}
content.md
```js
const lorem = ipsum == null ? 0 : 1
// !mark(1:2)
dolor = lorem - sit(dolor)
let amet = lorem ? consectetur(ipsum) : 3
```
const lorem = ipsum == null ? 0 : 1
dolor = lorem - sit(dolor)
let amet = lorem ? consectetur(ipsum) : 3

Transforming annotations

You can also transform annotations before they are passed to the components. This is useful to:

my-code.tsx
const myHandler: AnnotationHandler = {
transform: (annotation: InlineAnnotation) => {
return {
...annotation,
data: { lorem: "ipsum" },
}
},
...
}

Using regular expressions instead of ranges

Instead of using line and column ranges, you can use regular expressions to match the content of the annotation.

You can use it for block annotations, but it's more common to use it for inline annotations:

content.md
```js
// !border[/ipsum/] yellow
const lorem = ipsum == null ? ipsum : 1
// !border[/dolor/g] lime
dolor = lorem - sit(dolor)
let amet = dolor ? consectetur(ipsum) : 3
```
```js
// !border[/ipsum/gm] orange
const lorem = ipsum == null ? ipsum : 1
dolor = lorem - sit(dolor)
let amet = dolor ? consectetur(ipsum) : 3
```
const lorem = ipsum == null ? ipsum : 1
dolor = lorem - sit(dolor)
let amet = dolor ? consectetur(ipsum) : 3
const lorem = ipsum == null ? ipsum : 1
dolor = lorem - sit(dolor)
let amet = dolor ? consectetur(ipsum) : 3

The regular expressions also support flags The two most common are g for global and m for multiline.

  • m: use it if you want to keep searching beyond the first line
  • g: use it if you want to match more than one occurrence

You can also use capturing groups (see fold example):

content.md
```jsx
// !border[/className="(.*?)"/gm] pink
function Foo() {
return (
<div className="bg-red-200 opacity-50">
<span className="border">hey</span>
</div>
)
}
```
function Foo() {
return (
<div className="bg-red-200 opacity-50">
<span className="border">hey</span>
</div>
)
}