Skip to content

Commit

Permalink
feat(twoslash)!: move to twoslash package for better performance an…
Browse files Browse the repository at this point in the history
…d composablity (#91)
  • Loading branch information
antfu authored Jan 14, 2024
1 parent 97c9636 commit fc67e9d
Show file tree
Hide file tree
Showing 26 changed files with 200 additions and 174 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
2 changes: 1 addition & 1 deletion docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default defineConfig({
processHoverInfo(info) {
return defaultHoverInfoProcessor(info)
// Remove shikiji_core namespace
.replace(/\bshikiji_core\./g, '')
.replace(/shikiji_core\./g, '')
// Remove member access
.replace(/^[a-zA-Z0-9_]*(\<[^\>]*\>)?\./, '')
},
Expand Down
4 changes: 4 additions & 0 deletions docs/packages/transformers.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
outline: deep
---

# shikiji-transformers

<Badges name="shikiji-transformers" />
Expand Down
7 changes: 4 additions & 3 deletions docs/packages/twoslash.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<Badges name="shikiji-twoslash" />

A Shikiji transformer for [TypeScript TwoSlash](https://www.typescriptlang.org/dev/twoslash/), provide inline type hover inside code blocks. Inspired by [`shiki-twoslash`](https://shikijs.github.io/twoslash/).
A Shikiji transformer for [`twoslash`](https://github.com/twoslashes/twoslash), provide inline type hover inside code blocks. Inspired by [`shiki-twoslash`](https://shikijs.github.io/twoslash/).

## Install

Expand Down Expand Up @@ -44,15 +44,16 @@ We provide two renderers built-in, and you can also create your own:

[Source code](https://github.com/antfu/shikiji/blob/main/packages/shikiji-twoslash/src/renderer-classic.ts)

This is the default renderer that aligns with the output of [`shiki-twoslash`](https://shikijs.github.io/twoslash/).
This renderer aligns with the output of [`shiki-twoslash`](https://shikijs.github.io/twoslash/).

You might need to reference `shiki-twoslash`'s CSS to make it look good. [Here](https://github.com/antfu/shikiji/blob/main/packages/shikiji-twoslash/style-classic.css) we also copied the CSS from `shiki-twoslash` but it might need some cleanup.

### `rendererRich`

[Source code](https://github.com/antfu/shikiji/blob/main/packages/shikiji-twoslash/src/renderer-rich.ts)

This renderer provides a more explicit class name that is always prefixed with `twoslash-` for better scoping. In addition, it runs syntax highlighting on the hover information.
This renderer provides a more explicit class name is prefixed with `twoslash-` for better scoping.
In addition, it runs syntax highlighting on the hover information.

```ts twoslash
import { rendererRich, transformerTwoSlash } from 'shikiji-twoslash'
Expand Down
38 changes: 38 additions & 0 deletions docs/packages/vitepress.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
outline: deep
---

# VitePress Integration

[VitePress](https://vitepress.dev/) uses Shikiji under the hood, so you don't need explicit integration.
Expand All @@ -10,6 +14,8 @@ To enable [TypeScript TwoSlash](/packages/twoslash) (type hover on code snippets

<Badges name="vitepress-plugin-twoslash" />

### Setup

```bash
npm i -D vitepress-plugin-twoslash
```
Expand Down Expand Up @@ -78,3 +84,35 @@ It will be rendered as:
console.log('hello')
// ^?
```

<br> <!-- leaving some space for the query above -->

### Vue Single File Component

In addition, this plugin also integrated [`twoslash-vue`](https://github.com/antfu/twoslash-vue) for you, so that you can also highlight Vue SFC blocks with `vue twoslash`:

```vue twoslash
<script setup>
import { onMounted, ref } from 'vue'
// reactive state
const count = ref(0)
// ^?
// functions that mutate state and trigger updates
function increment() {
count.value++
}
// lifecycle hooks
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<template>
<button @click="increment">
Count is: {{ count }}
</button>
</template>
```
4 changes: 2 additions & 2 deletions packages/shikiji-twoslash/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@
"test": "vitest"
},
"dependencies": {
"@typescript/twoslash": "^3.2.4",
"shikiji-core": "workspace:*"
"shikiji-core": "workspace:*",
"twoslash": "^0.0.6"
},
"devDependencies": {
"@iconify-json/carbon": "^1.1.27",
Expand Down
4 changes: 1 addition & 3 deletions packages/shikiji-twoslash/scripts/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import fs from 'node:fs/promises'
import { icons as codicon } from '@iconify-json/codicon'
import { icons as carbon } from '@iconify-json/carbon'
import { fromHtml } from 'hast-util-from-html'
import type { TwoSlashReturn } from '@typescript/twoslash'

type CompletionItem = NonNullable<TwoSlashReturn['queries'][0]['completions']>[0]
import type { CompletionItem } from '../src/icons'

async function buildIcons(filepath: string, map: Record<string, string>) {
const result = Object.fromEntries(
Expand Down
70 changes: 36 additions & 34 deletions packages/shikiji-twoslash/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,32 @@
* This file is the core of the shikiji-twoslash package,
* Decoupled from twoslash's implementation and allowing to introduce custom implementation or cache system.
*/
import type { twoslasher } from '@typescript/twoslash'
import type { TwoSlashExecuteOptions, TwoSlashReturn } from 'twoslash'
import type { ShikijiTransformer } from 'shikiji-core'
import type { Element, ElementContent, Text } from 'hast'
import type { ModuleKind, ScriptTarget } from 'typescript'

import { addClassToHast } from 'shikiji-core'
import { rendererClassic } from './renderer-classic'
import type { TransformerTwoSlashOptions } from './types'
import type { TransformerTwoSlashOptions, TwoSlashRenderer } from './types'

export * from './types'
export * from './renderer-classic'
export * from './renderer-rich'
export * from './renderer-classic'
export * from './icons'

export function defaultTwoSlashOptions() {
export function defaultTwoSlashOptions(): TwoSlashExecuteOptions {
return {
customTags: ['annotate', 'log', 'warn', 'error'],
defaultCompilerOptions: {
compilerOptions: {
module: 99 satisfies ModuleKind.ESNext,
target: 99 satisfies ScriptTarget.ESNext,
},
}
}

export function createTransformerFactory(defaultTwoslasher: typeof twoslasher) {
type TwoSlashFunction = (code: string, lang?: string, options?: TwoSlashExecuteOptions) => TwoSlashReturn

export function createTransformerFactory(defaultTwoslasher: TwoSlashFunction, defaultRenderer?: TwoSlashRenderer) {
return function transformerTwoSlash(options: TransformerTwoSlashOptions = {}): ShikijiTransformer {
const {
langs = ['ts', 'tsx'],
Expand All @@ -38,9 +39,13 @@ export function createTransformerFactory(defaultTwoslasher: typeof twoslasher) {
},
twoslasher = defaultTwoslasher,
explicitTrigger = false,
renderer = rendererClassic(),
renderer = defaultRenderer,
throws = true,
} = options

if (!renderer)
throw new Error('[shikiji-twoslash] Missing renderer')

const filter = options.filter || ((lang, _, options) => langs.includes(lang) && (!explicitTrigger || /\btwoslash\b/.test(options.meta?.__raw || '')))
return {
preprocess(code, shikijiOptions) {
Expand Down Expand Up @@ -115,8 +120,6 @@ export function createTransformerFactory(defaultTwoslasher: typeof twoslasher) {
const skipTokens = new Set<Element | Text>()

for (const error of twoslash.errors) {
if (error.line == null || error.character == null)
return
const token = locateTextToken(error.line, error.character)
if (!token)
continue
Expand All @@ -133,39 +136,38 @@ export function createTransformerFactory(defaultTwoslasher: typeof twoslasher) {
}

for (const query of twoslash.queries) {
if (query.kind === 'completions') {
const token = locateTextToken(query.line - 1, query.offset)
if (!token)
continue

skipTokens.add(token)
const token = locateTextToken(query.line, query.character)
if (!token)
continue

if (renderer.nodeCompletions) {
const clone = { ...token }
Object.assign(token, renderer.nodeCompletions.call(this, query, clone))
}
skipTokens.add(token)

if (renderer.lineCompletions)
insertAfterLine(query.line, renderer.lineCompletions.call(this, query))
if (renderer.nodeQuery) {
const clone = { ...token }
Object.assign(token, renderer.nodeQuery.call(this, query, clone))
}
else if (query.kind === 'query') {
const token = locateTextToken(query.line - 1, query.offset)
if (!token)
continue

skipTokens.add(token)
if (renderer.lineQuery)
insertAfterLine(query.line, renderer.lineQuery.call(this, query, token))
}

if (renderer.nodeQuery) {
const clone = { ...token }
Object.assign(token, renderer.nodeQuery.call(this, query, clone))
}
for (const completion of twoslash.completions) {
const token = locateTextToken(completion.line, completion.character)
if (!token)
continue

skipTokens.add(token)

if (renderer.lineQuery)
insertAfterLine(query.line, renderer.lineQuery.call(this, query, token))
if (renderer.nodeCompletions) {
const clone = { ...token }
Object.assign(token, renderer.nodeCompletions.call(this, completion, clone))
}

if (renderer.lineCompletions)
insertAfterLine(completion.line, renderer.lineCompletions.call(this, completion))
}

for (const info of twoslash.staticQuickInfos) {
for (const info of twoslash.hovers) {
const token = locateTextToken(info.line, info.character)
if (!token || token.type !== 'text')
continue
Expand Down
4 changes: 2 additions & 2 deletions packages/shikiji-twoslash/src/icons.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Element } from 'hast'
import type { TwoSlashReturn } from '@typescript/twoslash'
import type { NodeCompletion } from 'twoslash'
import completionIcons from './icons-completions.json'
import tagIcons from './icons-tags.json'

export type CompletionItem = NonNullable<TwoSlashReturn['queries'][0]['completions']>[0]
export type CompletionItem = NonNullable<NodeCompletion['completions']>[number]

export const defaultCompletionIcons: Record<CompletionItem['kind'], Element | undefined> = completionIcons as any
export const defaultCustomTagIcons: Record<string, Element | undefined> = tagIcons as any
9 changes: 6 additions & 3 deletions packages/shikiji-twoslash/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { twoslasher } from '@typescript/twoslash'
import { createTransformerFactory } from './core'
import { createTwoSlasher } from 'twoslash'
import { createTransformerFactory, rendererClassic } from './core'

export * from './core'

/**
* Factory function to create a Shikiji transformer for twoslash integrations.
*/
export const transformerTwoSlash = createTransformerFactory(twoslasher)
export const transformerTwoSlash = /* @__PURE__ */ createTransformerFactory(
/* @__PURE__ */ createTwoSlasher(),
/* @__PURE__ */ rendererClassic(),
)
14 changes: 7 additions & 7 deletions packages/shikiji-twoslash/src/renderer-classic.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { TwoSlashRenderers } from './types'
import type { TwoSlashRenderer } from './types'

/**
* The default renderer aligning with the original `shiki-twoslash` output.
*/
export function rendererClassic(): TwoSlashRenderers {
export function rendererClassic(): TwoSlashRenderer {
return {
nodeStaticInfo(info, node) {
return {
Expand Down Expand Up @@ -41,7 +41,7 @@ export function rendererClassic(): TwoSlashRenderers {
children: [
{
type: 'text',
value: error.renderedMessage,
value: error.text,
},
],
},
Expand Down Expand Up @@ -69,7 +69,7 @@ export function rendererClassic(): TwoSlashRenderers {
children: [
{
type: 'text',
value: error.renderedMessage,
value: error.text,
},
],
},
Expand All @@ -83,7 +83,7 @@ export function rendererClassic(): TwoSlashRenderers {
tagName: 'div',
properties: { class: 'meta-line' },
children: [
{ type: 'text', value: ' '.repeat(query.offset) },
{ type: 'text', value: ' '.repeat(query.character) },
{
type: 'element',
tagName: 'span',
Expand Down Expand Up @@ -134,7 +134,7 @@ export function rendererClassic(): TwoSlashRenderers {

lineQuery(query, targetNode) {
const targetText = targetNode?.type === 'text' ? targetNode.value : ''
const offset = Math.max(0, (query.offset || 0) + Math.floor(targetText.length / 2) - 1)
const offset = Math.max(0, (query.character || 0) + Math.floor(targetText.length / 2) - 1)

return [
{
Expand Down Expand Up @@ -179,7 +179,7 @@ export function rendererClassic(): TwoSlashRenderers {
children: [
{
type: 'text',
value: tag.annotation || '',
value: tag.text || '',
},
],
},
Expand Down
13 changes: 8 additions & 5 deletions packages/shikiji-twoslash/src/renderer-rich.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Element, ElementContent } from 'hast'
import type { ShikijiTransformerContextCommon } from 'shikiji-core'
import type { TwoSlashRenderers } from './types'
import type { TwoSlashRenderer } from './types'
import type { CompletionItem } from './icons'
import { defaultCompletionIcons, defaultCustomTagIcons } from './icons'

Expand Down Expand Up @@ -65,7 +65,7 @@ export interface RendererRichOptions {
* An alternative renderer that providers better prefixed class names,
* with syntax highlight for the info text.
*/
export function rendererRich(options: RendererRichOptions = {}): TwoSlashRenderers {
export function rendererRich(options: RendererRichOptions = {}): TwoSlashRenderer {
const {
completionIcons = defaultCompletionIcons,
customTagIcons = defaultCustomTagIcons,
Expand All @@ -85,7 +85,7 @@ export function rendererRich(options: RendererRichOptions = {}): TwoSlashRendere
return []

const text = processHoverInfo(info.text) ?? info.text
if (!text)
if (!text.trim())
return []

const themedContent = ((codeToHast(text, {
Expand Down Expand Up @@ -118,6 +118,9 @@ export function rendererRich(options: RendererRichOptions = {}): TwoSlashRendere
nodeStaticInfo(info, node) {
const themedContent = hightlightPopupContent(this.codeToHast, this.options, info)

if (!themedContent.length)
return node

return {
type: 'element',
tagName: 'span',
Expand Down Expand Up @@ -282,7 +285,7 @@ export function rendererRich(options: RendererRichOptions = {}): TwoSlashRendere
children: [
{
type: 'text',
value: error.renderedMessage,
value: error.text,
},
],
},
Expand Down Expand Up @@ -312,7 +315,7 @@ export function rendererRich(options: RendererRichOptions = {}): TwoSlashRendere
: [],
{
type: 'text',
value: tag.annotation || '',
value: tag.text || '',
},
],
},
Expand Down
Loading

0 comments on commit fc67e9d

Please sign in to comment.