Skip to content
This repository has been archived by the owner on Feb 7, 2024. It is now read-only.

Commit

Permalink
feat: introduce twoslash transformer (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu authored Dec 11, 2023
1 parent 382d77d commit 0674284
Show file tree
Hide file tree
Showing 22 changed files with 1,123 additions and 46 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

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).

## Changes
## Features

- 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).
- Portable. Does not rely on Node.js APIs or the filesystem, works in any modern JavaScript runtime.
- Drop CJS and IIFE build, focus on ESM (or you can use bundlers).
- [Bundles languages/themes composedly](#fine-grained-bundle).
- [Light/Dark themes support](#lightdark-dual-themes).
- [`hast` support](#codetohast).
- [AST-based transformers addons](#hast-transformers).
- [TwoSlash transformer](./packages/shikiji-twoslash).
- [List of breaking changes from shiki](#breaking-changes-from-shiki).
- Please don't hate me Pine 😜 ([What's Next?](#whats-next))

Expand Down Expand Up @@ -455,6 +457,8 @@ const code = await codeToHtml('foo\bar', {
})
```

We also provide some common transformers for you to use, check [`shikiji-transforms`](./packages/shikiji-transformers) for more details.

### Custom Language Aliases

You can register custom language aliases with `langAlias` option. For example:
Expand Down
56 changes: 56 additions & 0 deletions packages/shikiji-twoslash/README.md
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
14 changes: 14 additions & 0 deletions packages/shikiji-twoslash/build.config.ts
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',
],
})
55 changes: 55 additions & 0 deletions packages/shikiji-twoslash/package.json
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"
}
}
123 changes: 123 additions & 0 deletions packages/shikiji-twoslash/src/index.ts
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
}
}
Loading

0 comments on commit 0674284

Please sign in to comment.