8000 feat: introduce twoslash transformer (#39) · antfu/shikiji@0674284 · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
This repository was archived by the owner on Feb 7, 2024. It is now read-only.

Commit 0674284

Browse files
authored
feat: introduce twoslash transformer (#39)
1 parent 382d77d commit 0674284

22 files changed

+1123
-46
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55

66
An ESM-focused rewrite of [shiki](https://github.com/shikijs/shiki), a beautiful syntax highlighter based on TextMate grammars. And [a little bit more](#additional-features).
77

8-
## Changes
8+
## Features
99

1010
- All grammars/themes/wasm served as pure-ESM, no more [CDN](https://github.com/shikijs/shiki#specify-a-custom-root-directory), no more [assets](https://github.com/shikijs/shiki#specify-how-to-load-webassembly).
1111
- Portable. Does not rely on Node.js APIs or the filesystem, works in any modern JavaScript runtime.
1212
- Drop CJS and IIFE build, focus on ESM (or you can use bundlers).
1313
- [Bundles languages/themes composedly](#fine-grained-bundle).
1414
- [Light/Dark themes support](#lightdark-dual-themes).
1515
- [`hast` support](#codetohast).
16+
- [AST-based transformers addons](#hast-transformers).
17+
- [TwoSlash transformer](./packages/shikiji-twoslash).
1618
- [List of breaking changes from shiki](#breaking-changes-from-shiki).
1719
- Please don't hate me Pine 😜 ([What's Next?](#whats-next))
1820

@@ -455,6 +457,8 @@ const code = await codeToHtml('foo\bar', {
455457
})
456458
```
457459

460+
We also provide some common transformers for you to use, check [`shikiji-transforms`](./packages/shikiji-transformers) for more details.
461+
458462
### Custom Language Aliases
459463

460464
You can register custom language aliases with `langAlias` option. For example:

packages/shikiji-twoslash/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# shikiji-twoslash
2+
3+
A [shikiji](https://github.com/antfu/shikiji) transformer for [TypeScript's twoslash](https://www.typescriptlang.org/dev/twoslash/).
4+
Provides a similar output as [`shiki-twoslash`](https://shikijs.github.io/twoslash/).
5+
6+
## Install
7+
8+
```bash
9+
npm i -D shikiji-twoslash
10+
```
11+
12+
Unlike `shiki-twoslash` that wraps around `shiki`, this package is **a transformer addon** to Shikiji. This means that for every integration that supports shikiji transformers, you can use this package.
13+
14+
```ts
15+
import {
16+
codeToHtml,
17+
} from 'shikiji'
18+
import {
19+
transformerTwoSlash,
20+
} from 'shikiji-twoslash'
21+
22+
const html = await codeToHtml(code, {
23+
lang: 'ts',
24+
theme: 'vitesse-dark',
25+
transformers: [
26+
transformerTwoSlash(), // <-- here
27+
// ...
28+
],
29+
})
30+
```
31+
32+
Same as `shiki-twoslash`, the output is unstyled. You need to add some extra CSS to make them look good.
33+
34+
## Integrations
35+
36+
### VitePress
37+
38+
VitePress uses Shikiji for syntax highlighting since [`1.0.0-rc.30`](https://github.com/vuejs/vitepress/blob/main/CHANGELOG.md#100-rc30-2023-11-23). To use this transformer, you can add it to the `markdown.codeTransformers` option in your VitePress config file.
39+
40+
```ts
41+
// .vitepress/config.ts
42+
import { defineUserConfig } from 'vitepress'
43+
import { transformerTwoSlash } from 'shikiji-twoslash'
44+
45+
export default defineUserConfig({
46+
markdown: {
47+
codeTransformers: [
48+
transformerTwoSlash() // <-- here
49+
]
50+
},
51+
})
52+
```
53+
54+
## License
55+
56+
MIT
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { defineBuildConfig } from 'unbuild'
2+
3+
export default defineBuildConfig({
4+
entries: [
5+
'src/index.ts',
6+
],
7+
declaration: true,
8+
rollup: {
9+
emitCJS: false,
10+
},
11+
externals: [
12+
'hast',
13+
],
14+
})
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "shikiji-twoslash",
3+
"type": "module",
4+
"version": "0.8.1",
5+
"description": "Shikiji transformer for twoslash",
6+
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
7+
"license": "MIT",
8+
"homepage": "https://github.com/antfu/shikiji#readme",
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/antfu/shikiji.git",
12+
"directory": "packages/shikiji-twoslash"
13+
},
14+
"bugs": "https://github.com/antfu/shikiji/issues",
15+
"keywords": [
16+
"shiki",
17+
"twoslash"
18+
],
19+
"sideEffects": false,
20+
"exports": {
21+
".": {
22+
"types": "./dist/index.d.mts",
23+
"default": "./dist/index.mjs"
24+
},
25+
"./*": "./dist/*"
26+
},
27+
"main": "./dist/index.mjs",
28+
"module": "./dist/index.mjs",
29+
"types": "./dist/index.d.mts",
30+
"typesVersions": {
31+
"*": {
32+
"*": [
33+
"./dist/*",
34+
"./*"
35+
]
36+
}
37+
},
38+
"files": [
39+
"dist"
40+
],
41+
"scripts": {
42+
"build": "unbuild",
43+
"dev": "unbuild --stub",
44+
"prepublishOnly": "nr build",
45+
"test": "vitest"
46+
},
47+
"dependencies": {
48+
"@typescript/twoslash": "^3.2.4",
49+
"shikiji": "workspace:*"
50+
},
51+
"devDependencies": {
52+
"shiki": "^0.14.6",
53+
"shiki-twoslash": "^3.1.2"
54+
}
55+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { twoslasher } from '@typescript/twoslash'
2+
import type { ShikijiTransformer, ShikijiTransformerContext } from 'shikiji'
3+
import { addClassToHast } from 'shikiji'
4+
import type { Element, ElementContent, Text } from 'hast'
5+
import { rendererClassic } from './renderer-classic'
6+
import type { TransformerTwoSlashOptions } from './types'
7+
8+
export * from './types'
9+
export * from './renderer-classic'
10+
11+
export function transformerTwoSlash(options: TransformerTwoSlashOptions = {}): ShikijiTransformer {
12+
const {
13+
langs = ['ts', 'tsx'],
14+
twoslashOptions = {
15+
customTags: ['annotate', 'log', 'warn', 'error'],
16+
},
17+
langAlias = {
18+
typescript: 'ts',
19+
json5: 'json',
20+
yml: 'yaml',
21+
},
22+
renderer = rendererClassic,
23+
} = options
24+
const filter = options.filter || (lang => langs.includes(lang))
25+
return {
26+
preprocess(code, shikijiOptions) {
27+
let lang = shikijiOptions.lang
28+
if (lang in langAlias)
29+
lang = langAlias[shikijiOptions.lang]
30+
31+
if (filter(lang, code, shikijiOptions)) {
32+
shikijiOptions.mergeWhitespaces = false
33+
const twoslash = twoslasher(code, lang, twoslashOptions)
34+
this.meta.twoslash = twoslash
35+
return twoslash.code
36+
}
37+
},
38+
pre(pre) {
39+
if (this.meta.twoslash)
40+
addClassToHast(pre, 'twoslash lsp')
41+
},
42+
code(codeEl) {
43+
const twoslash = this.meta.twoslash
44+
if (!twoslash)
45+
return
46+
47+
const insertAfterLine = (line: number, nodes: ElementContent[]) => {
48+
let index: number
49+
if (line >= this.lines.length) {
50+
index = codeEl.children.length
51+
}
52+
else {
53+
const lineEl = this.lines[line]
54+
index = codeEl.children.indexOf(lineEl)
55+
if (index === -1)
56+
return false
57+
}
58+
59+
// If there is a newline after this line, remove it because we have the error element take place.
60+
const nodeAfter = codeEl.children[index + 1]
61+
if (nodeAfter && nodeAfter.type === 'text' && nodeAfter.value === '\n')
62+
codeEl.children.splice(index + 1, 1)
63+
codeEl.children.splice(index + 1, 0, ...nodes)
64+
return true
65+
}
66+
67+
for (const info of twoslash.staticQuickInfos) {
68+
const token = locateTextToken(this, info.line, info.character)
69+
if (!token || token.type !== 'text')
70+
continue
71+
72+
const clone = { ...token }
73+
Object.assign(token, renderer.nodeStaticInfo(info, clone))
74+
}
75+
76+
for (const error of twoslash.errors) {
77+
if (error.line == null || error.character == null)
78+
return
79+
const token = locateTextToken(this, error.line, error.character)
80+
if (!token)
81+
continue
82+
83+
const clone = { ...token }
84+
Object.assign(token, renderer.nodeError(error, clone))
85+
86+
insertAfterLine(error.line, renderer.lineError(error))
87+
}
88+
89+
for (const query of twoslash.queries) {
90+
insertAfterLine(
91+
query.line,
92+
query.kind === 'completions'
93+
? renderer.lineCompletions(query)
94+
: query.kind === 'query'
95+
? renderer.lineQuery(query, locateTextToken(this, query.line, query.offset))
96+
: [],
97+
)
98+
}
99+
100+
for (const tag of twoslash.tags)
101+
insertAfterLine(tag.line, renderer.lineCustomTag(tag))
102+
},
103+
}
104+
}
105+
106+
function locateTextToken(
107+
context: ShikijiTransformerContext,
108+
line: number,
109+
character: number,
110+
) {
111+
const lineEl = context.lines[line]
112+
if (!lineEl)
113+
return
114+
const textNodes = lineEl.children.flatMap(i => i.type === 'element' ? i.children || [] : []) as (Text | Element)[]
115+
let index = 0
116+
for (const token of textNodes) {
117+
if ('value' in token && typeof token.value === 'string')
118+
index += token.value.length
119+
120+
if (index > character)
121+
return token
122+
}
123+
}

0 commit comments

Comments
 (0)
0