From fc67e9db32c53396cde1bcf89fbcde103f0f16cf Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sun, 14 Jan 2024 13:09:07 +0100 Subject: [PATCH] feat(twoslash)!: move to `twoslash` package for better performance and composablity (#91) --- .gitattributes | 1 + docs/.vitepress/config.ts | 2 +- docs/packages/transformers.md | 4 + docs/packages/twoslash.md | 7 +- docs/packages/vitepress.md | 38 ++++++ packages/shikiji-twoslash/package.json | 4 +- packages/shikiji-twoslash/scripts/icons.ts | 4 +- packages/shikiji-twoslash/src/core.ts | 70 +++++----- packages/shikiji-twoslash/src/icons.ts | 4 +- packages/shikiji-twoslash/src/index.ts | 9 +- .../shikiji-twoslash/src/renderer-classic.ts | 14 +- .../shikiji-twoslash/src/renderer-rich.ts | 13 +- packages/shikiji-twoslash/src/types.ts | 22 ++-- .../test/out/classic/completions.html | 2 +- .../test/out/classic/console_log.html | 3 +- .../classic/cuts_out_unnecessary_code.html | 6 +- .../test/out/rich/custom-tags.html | 2 +- .../test/out/rich/no-icons.html | 3 +- .../shikiji-twoslash/test/out/rich/rich.html | 7 +- .../vitepress-plugin-twoslash/package.json | 1 + .../vitepress-plugin-twoslash/src/client.ts | 8 +- .../vitepress-plugin-twoslash/src/index.ts | 12 +- .../src/renderer-floating-vue.ts | 9 +- .../vitepress-plugin-twoslash/src/style.css | 6 + pnpm-lock.yaml | 122 ++++++------------ tsconfig.json | 1 + 26 files changed, 200 insertions(+), 174 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..6313b56c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index ac25b5035..143249597 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -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_]*(\<[^\>]*\>)?\./, '') }, diff --git a/docs/packages/transformers.md b/docs/packages/transformers.md index e50c783fc..0ffa5d25c 100644 --- a/docs/packages/transformers.md +++ b/docs/packages/transformers.md @@ -1,3 +1,7 @@ +--- +outline: deep +--- + # shikiji-transformers diff --git a/docs/packages/twoslash.md b/docs/packages/twoslash.md index 7ecb73e18..bf1c8bd48 100644 --- a/docs/packages/twoslash.md +++ b/docs/packages/twoslash.md @@ -2,7 +2,7 @@ -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 @@ -44,7 +44,7 @@ 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. @@ -52,7 +52,8 @@ You might need to reference `shiki-twoslash`'s CSS to make it look good. [Here]( [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' diff --git a/docs/packages/vitepress.md b/docs/packages/vitepress.md index 46d772cce..a4ce81f80 100644 --- a/docs/packages/vitepress.md +++ b/docs/packages/vitepress.md @@ -1,3 +1,7 @@ +--- +outline: deep +--- + # VitePress Integration [VitePress](https://vitepress.dev/) uses Shikiji under the hood, so you don't need explicit integration. @@ -10,6 +14,8 @@ To enable [TypeScript TwoSlash](/packages/twoslash) (type hover on code snippets +### Setup + ```bash npm i -D vitepress-plugin-twoslash ``` @@ -78,3 +84,35 @@ It will be rendered as: console.log('hello') // ^? ``` + +
+ +### 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 + + + +``` diff --git a/packages/shikiji-twoslash/package.json b/packages/shikiji-twoslash/package.json index e4f4b04ed..3201c88c0 100644 --- a/packages/shikiji-twoslash/package.json +++ b/packages/shikiji-twoslash/package.json @@ -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", diff --git a/packages/shikiji-twoslash/scripts/icons.ts b/packages/shikiji-twoslash/scripts/icons.ts index f3096529b..70076cbc1 100644 --- a/packages/shikiji-twoslash/scripts/icons.ts +++ b/packages/shikiji-twoslash/scripts/icons.ts @@ -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[0] +import type { CompletionItem } from '../src/icons' async function buildIcons(filepath: string, map: Record) { const result = Object.fromEntries( diff --git a/packages/shikiji-twoslash/src/core.ts b/packages/shikiji-twoslash/src/core.ts index c8dccd9a7..7538796e7 100644 --- a/packages/shikiji-twoslash/src/core.ts +++ b/packages/shikiji-twoslash/src/core.ts @@ -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'], @@ -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) { @@ -115,8 +120,6 @@ export function createTransformerFactory(defaultTwoslasher: typeof twoslasher) { const skipTokens = new Set() for (const error of twoslash.errors) { - if (error.line == null || error.character == null) - return const token = locateTextToken(error.line, error.character) if (!token) continue @@ -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 diff --git a/packages/shikiji-twoslash/src/icons.ts b/packages/shikiji-twoslash/src/icons.ts index a7684029b..8ebad22f3 100644 --- a/packages/shikiji-twoslash/src/icons.ts +++ b/packages/shikiji-twoslash/src/icons.ts @@ -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[0] +export type CompletionItem = NonNullable[number] export const defaultCompletionIcons: Record = completionIcons as any export const defaultCustomTagIcons: Record = tagIcons as any diff --git a/packages/shikiji-twoslash/src/index.ts b/packages/shikiji-twoslash/src/index.ts index aa9f9bc4a..86f9f01f4 100644 --- a/packages/shikiji-twoslash/src/index.ts +++ b/packages/shikiji-twoslash/src/index.ts @@ -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(), +) diff --git a/packages/shikiji-twoslash/src/renderer-classic.ts b/packages/shikiji-twoslash/src/renderer-classic.ts index de84113ae..17fcbd8c2 100644 --- a/packages/shikiji-twoslash/src/renderer-classic.ts +++ b/packages/shikiji-twoslash/src/renderer-classic.ts @@ -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 { @@ -41,7 +41,7 @@ export function rendererClassic(): TwoSlashRenderers { children: [ { type: 'text', - value: error.renderedMessage, + value: error.text, }, ], }, @@ -69,7 +69,7 @@ export function rendererClassic(): TwoSlashRenderers { children: [ { type: 'text', - value: error.renderedMessage, + value: error.text, }, ], }, @@ -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', @@ -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 [ { @@ -179,7 +179,7 @@ export function rendererClassic(): TwoSlashRenderers { children: [ { type: 'text', - value: tag.annotation || '', + value: tag.text || '', }, ], }, diff --git a/packages/shikiji-twoslash/src/renderer-rich.ts b/packages/shikiji-twoslash/src/renderer-rich.ts index 6c38df25e..311020e8d 100644 --- a/packages/shikiji-twoslash/src/renderer-rich.ts +++ b/packages/shikiji-twoslash/src/renderer-rich.ts @@ -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' @@ -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, @@ -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, { @@ -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', @@ -282,7 +285,7 @@ export function rendererRich(options: RendererRichOptions = {}): TwoSlashRendere children: [ { type: 'text', - value: error.renderedMessage, + value: error.text, }, ], }, @@ -312,7 +315,7 @@ export function rendererRich(options: RendererRichOptions = {}): TwoSlashRendere : [], { type: 'text', - value: tag.annotation || '', + value: tag.text || '', }, ], }, diff --git a/packages/shikiji-twoslash/src/types.ts b/packages/shikiji-twoslash/src/types.ts index d1ebca663..1ad131215 100644 --- a/packages/shikiji-twoslash/src/types.ts +++ b/packages/shikiji-twoslash/src/types.ts @@ -1,4 +1,4 @@ -import type { TwoSlashOptions, TwoSlashReturn, twoslasher } from '@typescript/twoslash' +import type { NodeCompletion, NodeError, NodeHover, NodeQuery, NodeTag, TwoSlashOptions, TwoSlashReturn, twoslasher } from 'twoslash' import type { CodeToHastOptions, ShikijiTransformerContext } from 'shikiji-core' import type { Element, ElementContent, Text } from 'hast' @@ -39,7 +39,7 @@ export interface TransformerTwoSlashOptions { /** * Custom renderers to decide how each info should be rendered */ - renderer?: TwoSlashRenderers + renderer?: TwoSlashRenderer /** * Strictly throw when there is an error * @default true @@ -47,14 +47,14 @@ export interface TransformerTwoSlashOptions { throws?: boolean } -export interface TwoSlashRenderers { - lineError?(this: ShikijiTransformerContext, error: TwoSlashReturn['errors'][0]): ElementContent[] - lineCustomTag?(this: ShikijiTransformerContext, tag: TwoSlashReturn['tags'][0]): ElementContent[] - lineQuery?(this: ShikijiTransformerContext, query: TwoSlashReturn['queries'][0], targetNode?: Element | Text): ElementContent[] - lineCompletions?(this: ShikijiTransformerContext, query: TwoSlashReturn['queries'][0]): ElementContent[] +export interface TwoSlashRenderer { + lineError?(this: ShikijiTransformerContext, error: NodeError): ElementContent[] + lineCustomTag?(this: ShikijiTransformerContext, tag: NodeTag): ElementContent[] + lineQuery?(this: ShikijiTransformerContext, query: NodeQuery, targetNode?: Element | Text): ElementContent[] + lineCompletions?(this: ShikijiTransformerContext, query: NodeCompletion): ElementContent[] - nodeError?(this: ShikijiTransformerContext, error: TwoSlashReturn['errors'][0], node: Element | Text): Partial - nodeStaticInfo(this: ShikijiTransformerContext, info: TwoSlashReturn['staticQuickInfos'][0], node: Element | Text): Partial - nodeQuery?(this: ShikijiTransformerContext, query: TwoSlashReturn['queries'][0], node: Element | Text): Partial - nodeCompletions?(this: ShikijiTransformerContext, query: TwoSlashReturn['queries'][0], node: Element | Text): Partial + nodeError?(this: ShikijiTransformerContext, error: NodeError, node: Element | Text): Partial + nodeStaticInfo(this: ShikijiTransformerContext, info: NodeHover, node: Element | Text): Partial + nodeQuery?(this: ShikijiTransformerContext, query: NodeQuery, node: Element | Text): Partial + nodeCompletions?(this: ShikijiTransformerContext, query: NodeCompletion, node: Element | Text): Partial } diff --git a/packages/shikiji-twoslash/test/out/classic/completions.html b/packages/shikiji-twoslash/test/out/classic/completions.html index 7eadad1bd..743c282f5 100644 --- a/packages/shikiji-twoslash/test/out/classic/completions.html +++ b/packages/shikiji-twoslash/test/out/classic/completions.html @@ -4,4 +4,4 @@ html, body { margin: 0; } .shiki { padding: 2em; } -
const a = Number.isNaN(123)
\ No newline at end of file +
const a = Number.isNaN(123)
\ No newline at end of file diff --git a/packages/shikiji-twoslash/test/out/classic/console_log.html b/packages/shikiji-twoslash/test/out/classic/console_log.html index b48616980..eef051bd5 100644 --- a/packages/shikiji-twoslash/test/out/classic/console_log.html +++ b/packages/shikiji-twoslash/test/out/classic/console_log.html @@ -5,4 +5,5 @@ .shiki { padding: 2em; }
// Hello
-console.error("This is an error")
This is an error
\ No newline at end of file +console.error("This is an error") +
This is an error
\ No newline at end of file diff --git a/packages/shikiji-twoslash/test/out/classic/cuts_out_unnecessary_code.html b/packages/shikiji-twoslash/test/out/classic/cuts_out_unnecessary_code.html index 20a60c1ba..419fc8833 100644 --- a/packages/shikiji-twoslash/test/out/classic/cuts_out_unnecessary_code.html +++ b/packages/shikiji-twoslash/test/out/classic/cuts_out_unnecessary_code.html @@ -8,6 +8,6 @@ throw "unimplemented" } -let a = createLabel("typescript") -
let a: NameLabel
let b = createLabel(2.8) -
function createLabel<2.8>(idOrName: 2.8): IdLabel
let c = createLabel(Math.random() ? "hello" : 42)
\ No newline at end of file +let a = createLabel("typescript")
let a: NameLabel
+let b = createLabel(2.8)
function createLabel<2.8>(idOrName: 2.8): IdLabel
+let c = createLabel(Math.random() ? "hello" : 42)
\ No newline at end of file diff --git a/packages/shikiji-twoslash/test/out/rich/custom-tags.html b/packages/shikiji-twoslash/test/out/rich/custom-tags.html index fc6aff0f0..ecc24874d 100644 --- a/packages/shikiji-twoslash/test/out/rich/custom-tags.html +++ b/packages/shikiji-twoslash/test/out/rich/custom-tags.html @@ -29,7 +29,7 @@ const const shiki: HighlighterCoreshiki = await function getHighlighterCore(options?: HighlighterCoreOptions | undefined): Promise<HighlighterCore>
Create a Shikiji core highlighter instance, with no languages or themes bundled. Wasm and each language and theme must be loaded manually.
getHighlighterCore
({})
-const const a: 1a = 1
Custom log message
const const b: 1b = 1
Custom error message
const const c: 1c = 1
Custom warning message
Custom annotation message
+const const a: 1a = 1
Custom log message
const const b: 1b = 1
Custom error message
const const c: 1c = 1
Custom warning message
Custom annotation message