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.
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:
```js// !border(1:2)const lorem = ipsum == null ? 0 : 1dolor = lorem - sit(dolor)// !bg[5:16]let amet = lorem ? consectetur(ipsum) : 3```
const lorem = ipsum == null ? 0 : 1dolor = 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:
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>},}
```js// !border(1:2) purpleconst lorem = ipsum == null ? 0 : 1dolor = lorem - sit(dolor)// !bg[5:16] darkbluelet amet = lorem ? consectetur(ipsum) : 3```
const lorem = ipsum == null ? 0 : 1dolor = 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:
import { InnerLine } from "codehike/code"const myHandler: AnnotationHandler = {name: "uglyLineNumbers",Line: (props) => {const { lineNumber, totalLines, indentation } = propsreturn (<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:
const bgHandler: AnnotationHandler = {Line: (props) => (<InnerLinemerge={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:
<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:
import { InnerToken } from "codehike/code"const myHandler: AnnotationHandler = {Token: (props) => {const { value, style, lineNumber } = propsreturn <InnerToken merge={props} style={{ display: "inline-block" }} />},}
Customizing the Pre component
You can also customize the rendering of the <pre>
element itself:
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:
const mark: AnnotationHandler = {name: "mark",AnnotatedLine: ({ annotation, ...props }) => (<InnerLine merge={props} data-mark={true} />),Line: (props) => (<InnerLinemerge={props}className="px-2 border-l-4 border-transparent data-[mark]:border-blue-400"/>),}
```jsconst lorem = ipsum == null ? 0 : 1// !mark(1:2)dolor = lorem - sit(dolor)let amet = lorem ? consectetur(ipsum) : 3```
const lorem = ipsum == null ? 0 : 1dolor = 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:
- split annotations (see collapse example)
- transform between inline and block annotations (see callout example)
- add extra information to the annotation (see callout example)
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:
```js// !border[/ipsum/] yellowconst lorem = ipsum == null ? ipsum : 1// !border[/dolor/g] limedolor = lorem - sit(dolor)let amet = dolor ? consectetur(ipsum) : 3``````js// !border[/ipsum/gm] orangeconst lorem = ipsum == null ? ipsum : 1dolor = lorem - sit(dolor)let amet = dolor ? consectetur(ipsum) : 3```
const lorem = ipsum == null ? ipsum : 1dolor = lorem - sit(dolor)let amet = dolor ? consectetur(ipsum) : 3
const lorem = ipsum == null ? ipsum : 1dolor = 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 lineg
: use it if you want to match more than one occurrence
You can also use capturing groups (see fold example):
```jsx// !border[/className="(.*?)"/gm] pinkfunction 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>)}