This repository has been archived by the owner on Feb 7, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introduce twoslash transformer (#39)
- Loading branch information
Showing
22 changed files
with
1,123 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# shikiji-twoslash | ||
|
||
A [shikiji](https://github.com/antfu/shikiji) transformer for [TypeScript's twoslash](https://www.typescriptlang.org/dev/twoslash/). | ||
Provides a similar output as [`shiki-twoslash`](https://shikijs.github.io/twoslash/). | ||
|
||
## Install | ||
|
||
```bash | ||
npm i -D shikiji-twoslash | ||
``` | ||
|
||
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. | ||
|
||
```ts | ||
import { | ||
codeToHtml, | ||
} from 'shikiji' | ||
import { | ||
transformerTwoSlash, | ||
} from 'shikiji-twoslash' | ||
|
||
const html = await codeToHtml(code, { | ||
lang: 'ts', | ||
theme: 'vitesse-dark', | ||
transformers: [ | ||
transformerTwoSlash(), // <-- here | ||
// ... | ||
], | ||
}) | ||
``` | ||
|
||
Same as `shiki-twoslash`, the output is unstyled. You need to add some extra CSS to make them look good. | ||
|
||
## Integrations | ||
|
||
### VitePress | ||
|
||
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. | ||
|
||
```ts | ||
// .vitepress/config.ts | ||
import { defineUserConfig } from 'vitepress' | ||
import { transformerTwoSlash } from 'shikiji-twoslash' | ||
|
||
export default defineUserConfig({ | ||
markdown: { | ||
codeTransformers: [ | ||
transformerTwoSlash() // <-- here | ||
] | ||
}, | ||
}) | ||
``` | ||
|
||
## License | ||
|
||
MIT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { defineBuildConfig } from 'unbuild' | ||
|
||
export default defineBuildConfig({ | ||
entries: [ | ||
'src/index.ts', | ||
], | ||
declaration: true, | ||
rollup: { | ||
emitCJS: false, | ||
}, | ||
externals: [ | ||
'hast', | ||
], | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
{ | ||
"name": "shikiji-twoslash", | ||
"type": "module", | ||
"version": "0.8.1", | ||
"description": "Shikiji transformer for twoslash", | ||
"author": "Anthony Fu <[email protected]>", | ||
"license": "MIT", | ||
"homepage": "https://github.com/antfu/shikiji#readme", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/antfu/shikiji.git", | ||
"directory": "packages/shikiji-twoslash" | ||
}, | ||
"bugs": "https://github.com/antfu/shikiji/issues", | ||
"keywords": [ | ||
"shiki", | ||
"twoslash" | ||
], | ||
"sideEffects": false, | ||
"exports": { | ||
".": { | ||
"types": "./dist/index.d.mts", | ||
"default": "./dist/index.mjs" | ||
}, | ||
"./*": "./dist/*" | ||
}, | ||
"main": "./dist/index.mjs", | ||
"module": "./dist/index.mjs", | ||
"types": "./dist/index.d.mts", | ||
"typesVersions": { | ||
"*": { | ||
"*": [ | ||
"./dist/*", | ||
"./*" | ||
] | ||
} | ||
}, | ||
"files": [ | ||
"dist" | ||
], | ||
"scripts": { | ||
"build": "unbuild", | ||
"dev": "unbuild --stub", | ||
"prepublishOnly": "nr build", | ||
"test": "vitest" | ||
}, | ||
"dependencies": { | ||
"@typescript/twoslash": "^3.2.4", | ||
"shikiji": "workspace:*" | ||
}, | ||
"devDependencies": { | ||
"shiki": "^0.14.6", | ||
"shiki-twoslash": "^3.1.2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { twoslasher } from '@typescript/twoslash' | ||
import type { ShikijiTransformer, ShikijiTransformerContext } from 'shikiji' | ||
import { addClassToHast } from 'shikiji' | ||
import type { Element, ElementContent, Text } from 'hast' | ||
import { rendererClassic } from './renderer-classic' | ||
import type { TransformerTwoSlashOptions } from './types' | ||
|
||
export * from './types' | ||
export * from './renderer-classic' | ||
|
||
export function transformerTwoSlash(options: TransformerTwoSlashOptions = {}): ShikijiTransformer { | ||
const { | ||
langs = ['ts', 'tsx'], | ||
twoslashOptions = { | ||
customTags: ['annotate', 'log', 'warn', 'error'], | ||
}, | ||
langAlias = { | ||
typescript: 'ts', | ||
json5: 'json', | ||
yml: 'yaml', | ||
}, | ||
renderer = rendererClassic, | ||
} = options | ||
const filter = options.filter || (lang => langs.includes(lang)) | ||
return { | ||
preprocess(code, shikijiOptions) { | ||
let lang = shikijiOptions.lang | ||
if (lang in langAlias) | ||
lang = langAlias[shikijiOptions.lang] | ||
|
||
if (filter(lang, code, shikijiOptions)) { | ||
shikijiOptions.mergeWhitespaces = false | ||
const twoslash = twoslasher(code, lang, twoslashOptions) | ||
this.meta.twoslash = twoslash | ||
return twoslash.code | ||
} | ||
}, | ||
pre(pre) { | ||
if (this.meta.twoslash) | ||
addClassToHast(pre, 'twoslash lsp') | ||
}, | ||
code(codeEl) { | ||
const twoslash = this.meta.twoslash | ||
if (!twoslash) | ||
return | ||
|
||
const insertAfterLine = (line: number, nodes: ElementContent[]) => { | ||
let index: number | ||
if (line >= this.lines.length) { | ||
index = codeEl.children.length | ||
} | ||
else { | ||
const lineEl = this.lines[line] | ||
index = codeEl.children.indexOf(lineEl) | ||
if (index === -1) | ||
return false | ||
} | ||
|
||
// If there is a newline after this line, remove it because we have the error element take place. | ||
const nodeAfter = codeEl.children[index + 1] | ||
if (nodeAfter && nodeAfter.type === 'text' && nodeAfter.value === '\n') | ||
codeEl.children.splice(index + 1, 1) | ||
codeEl.children.splice(index + 1, 0, ...nodes) | ||
return true | ||
} | ||
|
||
for (const info of twoslash.staticQuickInfos) { | ||
const token = locateTextToken(this, info.line, info.character) | ||
if (!token || token.type !== 'text') | ||
continue | ||
|
||
const clone = { ...token } | ||
Object.assign(token, renderer.nodeStaticInfo(info, clone)) | ||
} | ||
|
||
for (const error of twoslash.errors) { | ||
if (error.line == null || error.character == null) | ||
return | ||
const token = locateTextToken(this, error.line, error.character) | ||
if (!token) | ||
continue | ||
|
||
const clone = { ...token } | ||
Object.assign(token, renderer.nodeError(error, clone)) | ||
|
||
insertAfterLine(error.line, renderer.lineError(error)) | ||
} | ||
|
||
for (const query of twoslash.queries) { | ||
insertAfterLine( | ||
query.line, | ||
query.kind === 'completions' | ||
? renderer.lineCompletions(query) | ||
: query.kind === 'query' | ||
? renderer.lineQuery(query, locateTextToken(this, query.line, query.offset)) | ||
: [], | ||
) | ||
} | ||
|
||
for (const tag of twoslash.tags) | ||
insertAfterLine(tag.line, renderer.lineCustomTag(tag)) | ||
}, | ||
} | ||
} | ||
|
||
function locateTextToken( | ||
context: ShikijiTransformerContext, | ||
line: number, | ||
character: number, | ||
) { | ||
const lineEl = context.lines[line] | ||
if (!lineEl) | ||
return | ||
const textNodes = lineEl.children.flatMap(i => i.type === 'element' ? i.children || [] : []) as (Text | Element)[] | ||
let index = 0 | ||
for (const token of textNodes) { | ||
if ('value' in token && typeof token.value === 'string') | ||
index += token.value.length | ||
|
||
if (index > character) | ||
return token | ||
} | ||
} |
Oops, something went wrong.