Skip to content

Commit d031f9b

Browse files
authored
feat: add @shikijs/stream and @shikijs/magic-move packages (#1283)
1 parent c809af9 commit d031f9b

100 files changed

Lines changed: 7427 additions & 5 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/.vitepress/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ const INTEGRATIONS = [
5252
{ text: 'Astro', link: '/packages/astro' },
5353
{ text: 'Common Transformers', link: '/packages/transformers' },
5454
{ text: 'Colorized Brackets', link: '/packages/colorized-brackets' },
55+
{ text: 'Magic Move', link: '/packages/magic-move' },
56+
{ text: 'Stream', link: '/packages/stream' },
5557
{ text: 'Codegen', link: '/packages/codegen' },
5658
{ text: 'CLI', link: '/packages/cli' },
5759
] as const satisfies (DefaultTheme.NavItemWithLink | DefaultTheme.SidebarItem)[]

docs/packages/magic-move.md

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# @shikijs/magic-move
6+
7+
<Badges name="@shikijs/magic-move" />
8+
9+
Smoothly animated code blocks with Shiki. Useful for code-step animations in slide decks (e.g. [Slidev](https://sli.dev/guide/syntax#shiki-magic-move)) and tutorials.
10+
11+
`@shikijs/magic-move` is a low-level library: at its core is a framework-agnostic [diff/animation machine](https://github.com/shikijs/shiki/blob/main/packages/magic-move/src/core.ts) and [renderer](https://github.com/shikijs/shiki/blob/main/packages/magic-move/src/renderer.ts), with thin wrappers for Vue, React, Solid, Svelte, and a Web Component.
12+
13+
Each framework wrapper provides three components:
14+
15+
- **`ShikiMagicMove`** — the main component; wraps a code block and animates whenever `code` changes
16+
- **`ShikiMagicMovePrecompiled`** — animations for compiled tokens, without the Shiki dependency at runtime
17+
- **`ShikiMagicMoveRenderer`** — the low-level renderer component
18+
19+
`ShikiMagicMove` needs a Shiki highlighter instance plus the bundled stylesheet (`@shikijs/magic-move/style.css`). Whenever `code` changes, the component animates the diff.
20+
21+
## Install
22+
23+
::: code-group
24+
25+
```sh [npm]
26+
npm i -D @shikijs/magic-move shiki
27+
```
28+
29+
```sh [yarn]
30+
yarn add -D @shikijs/magic-move shiki
31+
```
32+
33+
```sh [pnpm]
34+
pnpm add -D @shikijs/magic-move shiki
35+
```
36+
37+
```sh [bun]
38+
bun add -D @shikijs/magic-move shiki
39+
```
40+
41+
```sh [deno]
42+
deno add npm:@shikijs/magic-move npm:shiki
43+
```
44+
45+
:::
46+
47+
## Usage
48+
49+
### Vue
50+
51+
Import `@shikijs/magic-move/vue` and pass the highlighter instance to the `ShikiMagicMove` component.
52+
53+
```vue
54+
<script setup>
55+
import { ShikiMagicMove } from '@shikijs/magic-move/vue'
56+
import { createHighlighter } from 'shiki'
57+
import { ref } from 'vue'
58+
59+
import '@shikijs/magic-move/style.css'
60+
61+
const highlighter = await createHighlighter({
62+
themes: ['nord'],
63+
langs: ['javascript', 'typescript'],
64+
})
65+
66+
const code = ref(`const hello = 'world'`)
67+
68+
function animate() {
69+
code.value = `let hi = 'hello'`
70+
}
71+
</script>
72+
73+
<template>
74+
<ShikiMagicMove
75+
lang="ts"
76+
theme="nord"
77+
:highlighter="highlighter"
78+
:code="code"
79+
:options="{ duration: 800, stagger: 0.3, lineNumbers: true }"
80+
/>
81+
<button @click="animate">
82+
Animate
83+
</button>
84+
</template>
85+
```
86+
87+
### React
88+
89+
Import `@shikijs/magic-move/react` and pass the highlighter instance to the `ShikiMagicMove` component.
90+
91+
```tsx
92+
import type { HighlighterCore } from 'shiki'
93+
import { ShikiMagicMove } from '@shikijs/magic-move/react'
94+
import { useEffect, useState } from 'react'
95+
import { createHighlighter } from 'shiki'
96+
97+
import '@shikijs/magic-move/style.css'
98+
99+
function App() {
100+
const [code, setCode] = useState(`const hello = 'world'`)
101+
const [highlighter, setHighlighter] = useState<HighlighterCore>()
102+
103+
useEffect(() => {
104+
async function initializeHighlighter() {
105+
const h = await createHighlighter({
106+
themes: ['nord'],
107+
langs: ['javascript', 'typescript'],
108+
})
109+
setHighlighter(h)
110+
}
111+
initializeHighlighter()
112+
}, [])
113+
114+
function animate() {
115+
setCode(`let hi = 'hello'`)
116+
}
117+
118+
return (
119+
<div>
120+
{highlighter && (
121+
<>
122+
<ShikiMagicMove
123+
lang="ts"
124+
theme="nord"
125+
highlighter={highlighter}
126+
code={code}
127+
options={{ duration: 800, stagger: 0.3, lineNumbers: true }}
128+
/>
129+
<button onClick={animate}>Animate</button>
130+
</>
131+
)}
132+
</div>
133+
)
134+
}
135+
```
136+
137+
### Solid
138+
139+
Import `@shikijs/magic-move/solid` and pass the highlighter instance to the `ShikiMagicMove` component.
140+
141+
```tsx
142+
import { ShikiMagicMove } from '@shikijs/magic-move/solid'
143+
import { bundledLanguages, bundledThemes, createHighlighter } from 'shiki'
144+
import { createResource, createSignal, Show } from 'solid-js'
145+
146+
import '@shikijs/magic-move/style.css'
147+
148+
function App() {
149+
const [code, setCode] = createSignal(`const hello = 'world'`)
150+
151+
const [highlighter] = createResource(async () => {
152+
return await createHighlighter({
153+
themes: Object.keys(bundledThemes),
154+
langs: Object.keys(bundledLanguages),
155+
})
156+
})
157+
158+
function animate() {
159+
setCode(`let hi = 'hello'`)
160+
}
161+
162+
return (
163+
<div>
164+
<Show when={highlighter()}>
165+
{h => (
166+
<>
167+
<ShikiMagicMove
168+
lang="ts"
169+
theme="nord"
170+
highlighter={h()}
171+
code={code()}
172+
options={{ duration: 800, stagger: 0.3, lineNumbers: true }}
173+
/>
174+
<button onClick={animate}>Animate</button>
175+
</>
176+
)}
177+
</Show>
178+
</div>
179+
)
180+
}
181+
```
182+
183+
### Svelte
184+
185+
Import `@shikijs/magic-move/svelte` and pass the highlighter instance to the `ShikiMagicMove` component.
186+
187+
```svelte
188+
<script lang="ts">
189+
import { ShikiMagicMove } from '@shikijs/magic-move/svelte'
190+
import { createHighlighter } from 'shiki'
191+
192+
import '@shikijs/magic-move/style.css'
193+
194+
const highlighter = createHighlighter({
195+
themes: ['nord'],
196+
langs: ['javascript', 'typescript'],
197+
})
198+
199+
let code = $state(`const hello = 'world'`)
200+
201+
function animate() {
202+
code = `let hi = 'hello'`
203+
}
204+
</script>
205+
206+
{#await highlighter then highlighter}
207+
<ShikiMagicMove
208+
lang="ts"
209+
theme="nord"
210+
{highlighter}
211+
{code}
212+
options={{ duration: 800, stagger: 0.3, lineNumbers: true }}
213+
/>
214+
<button onclick={animate}>Animate</button>
215+
{/await}
216+
```
217+
218+
## `ShikiMagicMovePrecompiled`
219+
220+
`ShikiMagicMovePrecompiled` is a lighter variant of `ShikiMagicMove` that doesn't need Shiki at runtime — useful when you want to ship pre-tokenized step data (e.g. produced at build time) and animate between steps in the browser.
221+
222+
```vue
223+
<script setup>
224+
import { ShikiMagicMovePrecompiled } from '@shikijs/magic-move/vue'
225+
import { ref } from 'vue'
226+
227+
const step = ref(1)
228+
const compiledSteps = [/* Compiled token steps */]
229+
</script>
230+
231+
<template>
232+
<ShikiMagicMovePrecompiled
233+
:steps="compiledSteps"
234+
:step="step"
235+
/>
236+
<button @click="step++">
237+
Next
238+
</button>
239+
</template>
240+
```
241+
242+
To produce the compiled tokens, run this somewhere with access to Shiki (build script, server, etc.) and serialize the result:
243+
244+
```ts
245+
import { codeToKeyedTokens, createMagicMoveMachine } from '@shikijs/magic-move/core'
246+
import { createHighlighter } from 'shiki'
247+
248+
const shiki = await createHighlighter({
249+
themes: ['nord'],
250+
langs: ['javascript', 'typescript'],
251+
})
252+
253+
const codeSteps = [
254+
`const hello = 'world'`,
255+
`let hi = 'hello'`,
256+
]
257+
258+
const machine = createMagicMoveMachine(
259+
code => codeToKeyedTokens(shiki, code, {
260+
lang: 'ts',
261+
theme: 'nord',
262+
}),
263+
{
264+
// options
265+
},
266+
)
267+
268+
const compiledSteps = codeSteps.map(code => machine.commit(code).current)
269+
270+
// Pass `compiledSteps` to the precompiled component.
271+
// Since these are plain serialisable objects, you can stringify them at build time
272+
// and rehydrate them in the browser without needing Shiki.
273+
```
274+
275+
## How it works
276+
277+
For a deep dive into the algorithm behind the animations, see Anthony's article [**The Magic In Shiki Magic Move**](https://antfu.me/posts/shiki-magic-move).

0 commit comments

Comments
 (0)