├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.config.ts ├── eslint.config.js ├── package.json ├── patches └── shikiji-twoslash@0.9.18.patch ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src └── index.ts ├── test ├── index.test.ts └── out │ ├── example.raw.html │ └── example.vue.html └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [antfu] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v2 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: lts/* 25 | 26 | - name: Setup 27 | run: npm i -g @antfu/ni 28 | 29 | - name: Install 30 | run: nci 31 | 32 | - name: Lint 33 | run: nr lint 34 | 35 | typecheck: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v3 39 | 40 | - name: Install pnpm 41 | uses: pnpm/action-setup@v2 42 | 43 | - name: Set node 44 | uses: actions/setup-node@v3 45 | with: 46 | node-version: lts/* 47 | 48 | - name: Setup 49 | run: npm i -g @antfu/ni 50 | 51 | - name: Install 52 | run: nci 53 | 54 | - name: Typecheck 55 | run: nr typecheck 56 | 57 | test: 58 | runs-on: ${{ matrix.os }} 59 | 60 | strategy: 61 | matrix: 62 | node: [lts/*] 63 | os: [ubuntu-latest, windows-latest, macos-latest] 64 | fail-fast: false 65 | 66 | steps: 67 | - uses: actions/checkout@v3 68 | 69 | - name: Install pnpm 70 | uses: pnpm/action-setup@v2 71 | 72 | - name: Set node ${{ matrix.node }} 73 | uses: actions/setup-node@v3 74 | with: 75 | node-version: ${{ matrix.node }} 76 | 77 | - name: Setup 78 | run: npm i -g @antfu/ni 79 | 80 | - name: Install 81 | run: nci 82 | 83 | - name: Build 84 | run: nr build 85 | 86 | - name: Test 87 | run: nr test 88 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Install pnpm 20 | uses: pnpm/action-setup@v2 21 | 22 | - name: Set node 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: lts/* 26 | 27 | - run: npx changelogithub 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shell-emulator=true 3 | shamefully-hoist=true 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESlint flat config support 3 | "eslint.experimental.useFlatConfig": true, 4 | 5 | // Disable the default formatter, use eslint instead 6 | "prettier.enable": false, 7 | "editor.formatOnSave": false, 8 | 9 | // Auto fix 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll": "explicit", 12 | "source.organizeImports": "never" 13 | }, 14 | 15 | // Silent the stylistic rules in you IDE, but still auto fix them 16 | "eslint.rules.customizations": [ 17 | { "rule": "style/*", "severity": "off" }, 18 | { "rule": "*-indent", "severity": "off" }, 19 | { "rule": "*-spacing", "severity": "off" }, 20 | { "rule": "*-spaces", "severity": "off" }, 21 | { "rule": "*-order", "severity": "off" }, 22 | { "rule": "*-dangle", "severity": "off" }, 23 | { "rule": "*-newline", "severity": "off" }, 24 | { "rule": "*quotes", "severity": "off" }, 25 | { "rule": "*semi", "severity": "off" } 26 | ], 27 | 28 | // Enable eslint for all supported languages 29 | "eslint.validate": [ 30 | "javascript", 31 | "javascriptreact", 32 | "typescript", 33 | "typescriptreact", 34 | "vue", 35 | "html", 36 | "markdown", 37 | "json", 38 | "jsonc", 39 | "yaml" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please refer to https://github.com/antfu/contribute 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Anthony Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is moved to [twoslashes/twoslash](https://github.com/twoslashes/twoslash/tree/main/packages/twoslash-vue). 2 | -------------------------------------------------------------------------------- /build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: [ 5 | 'src/index', 6 | ], 7 | declaration: true, 8 | clean: true, 9 | rollup: { 10 | emitCJS: true, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config' 3 | 4 | export default antfu( 5 | { 6 | ignores: [ 7 | './test/out/**/*', 8 | ], 9 | }, 10 | ) 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twoslash-vue", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "packageManager": "pnpm@8.14.1", 6 | "description": "Extended TwoSlash that supports Vue SFC", 7 | "author": "Anthony Fu ", 8 | "license": "MIT", 9 | "funding": "https://github.com/sponsors/antfu", 10 | "homepage": "https://github.com/antfu/twoslash-vue#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/antfu/twoslash-vue.git" 14 | }, 15 | "bugs": "https://github.com/antfu/twoslash-vue/issues", 16 | "keywords": [], 17 | "sideEffects": false, 18 | "exports": { 19 | ".": { 20 | "types": "./dist/index.d.ts", 21 | "import": "./dist/index.mjs", 22 | "require": "./dist/index.cjs" 23 | } 24 | }, 25 | "main": "./dist/index.mjs", 26 | "module": "./dist/index.mjs", 27 | "types": "./dist/index.d.ts", 28 | "typesVersions": { 29 | "*": { 30 | "*": [ 31 | "./dist/*", 32 | "./dist/index.d.ts" 33 | ] 34 | } 35 | }, 36 | "files": [ 37 | "dist" 38 | ], 39 | "scripts": { 40 | "build": "unbuild", 41 | "dev": "unbuild --stub", 42 | "lint": "eslint .", 43 | "prepublishOnly": "nr build", 44 | "release": "bumpp && npm publish", 45 | "start": "esno src/index.ts", 46 | "test": "vitest", 47 | "typecheck": "tsc --noEmit", 48 | "prepare": "simple-git-hooks" 49 | }, 50 | "peerDependencies": { 51 | "typescript": "*" 52 | }, 53 | "dependencies": { 54 | "@vue/language-core": "^1.8.27", 55 | "twoslashes": "^0.0.5", 56 | "vue": "^3.4.11" 57 | }, 58 | "devDependencies": { 59 | "@antfu/eslint-config": "^2.6.2", 60 | "@antfu/ni": "^0.21.12", 61 | "@antfu/utils": "^0.7.7", 62 | "@types/node": "^20.11.0", 63 | "bumpp": "^9.2.1", 64 | "eslint": "^8.56.0", 65 | "esno": "^4.0.0", 66 | "lint-staged": "^15.2.0", 67 | "pnpm": "^8.14.1", 68 | "rimraf": "^5.0.5", 69 | "shikiji": "^0.9.18", 70 | "shikiji-twoslash": "^0.9.18", 71 | "simple-git-hooks": "^2.9.0", 72 | "typescript": "^5.3.3", 73 | "unbuild": "^2.0.0", 74 | "vite": "^5.0.11", 75 | "vitest": "^1.2.0" 76 | }, 77 | "pnpm": { 78 | "patchedDependencies": { 79 | "shikiji-twoslash@0.9.18": "patches/shikiji-twoslash@0.9.18.patch" 80 | } 81 | }, 82 | "simple-git-hooks": { 83 | "pre-commit": "pnpm lint-staged" 84 | }, 85 | "lint-staged": { 86 | "*": "eslint --fix" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /patches/shikiji-twoslash@0.9.18.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/core.d.mts b/dist/core.d.mts 2 | index 5d0b16ce7ad80c86f6c24f73485b8903c729d9ea..1909d7afc5f505a77f060cf267b6cfb617f0701c 100644 3 | --- a/dist/core.d.mts 4 | +++ b/dist/core.d.mts 5 | @@ -1,4 +1,4 @@ 6 | -import { TwoSlashReturn, twoslasher, TwoSlashOptions } from '@typescript/twoslash'; 7 | +import { TwoSlashReturn, twoslasher, TwoSlashOptions, TokenError, TokenTag, TokenQuery, TokenCompletion, TokenHover, TwoSlashExecuteOptions } from 'twoslashes'; 8 | import { CodeToHastOptions, ShikijiTransformerContext, ShikijiTransformer } from 'shikiji-core'; 9 | import { ElementContent, Element, Text } from 'hast'; 10 | 11 | @@ -38,30 +38,25 @@ interface TransformerTwoSlashOptions { 12 | /** 13 | * Custom renderers to decide how each info should be rendered 14 | */ 15 | - renderer?: TwoSlashRenderers; 16 | + renderer?: TwoSlashRenderer; 17 | /** 18 | * Strictly throw when there is an error 19 | * @default true 20 | */ 21 | throws?: boolean; 22 | } 23 | -interface TwoSlashRenderers { 24 | - lineError?(this: ShikijiTransformerContext, error: TwoSlashReturn['errors'][0]): ElementContent[]; 25 | - lineCustomTag?(this: ShikijiTransformerContext, tag: TwoSlashReturn['tags'][0]): ElementContent[]; 26 | - lineQuery?(this: ShikijiTransformerContext, query: TwoSlashReturn['queries'][0], targetNode?: Element | Text): ElementContent[]; 27 | - lineCompletions?(this: ShikijiTransformerContext, query: TwoSlashReturn['queries'][0]): ElementContent[]; 28 | - nodeError?(this: ShikijiTransformerContext, error: TwoSlashReturn['errors'][0], node: Element | Text): Partial; 29 | - nodeStaticInfo(this: ShikijiTransformerContext, info: TwoSlashReturn['staticQuickInfos'][0], node: Element | Text): Partial; 30 | - nodeQuery?(this: ShikijiTransformerContext, query: TwoSlashReturn['queries'][0], node: Element | Text): Partial; 31 | - nodeCompletions?(this: ShikijiTransformerContext, query: TwoSlashReturn['queries'][0], node: Element | Text): Partial; 32 | +interface TwoSlashRenderer { 33 | + lineError?(this: ShikijiTransformerContext, error: TokenError): ElementContent[]; 34 | + lineCustomTag?(this: ShikijiTransformerContext, tag: TokenTag): ElementContent[]; 35 | + lineQuery?(this: ShikijiTransformerContext, query: TokenQuery, targetNode?: Element | Text): ElementContent[]; 36 | + lineCompletions?(this: ShikijiTransformerContext, query: TokenCompletion): ElementContent[]; 37 | + nodeError?(this: ShikijiTransformerContext, error: TokenError, node: Element | Text): Partial; 38 | + nodeStaticInfo(this: ShikijiTransformerContext, info: TokenHover, node: Element | Text): Partial; 39 | + nodeQuery?(this: ShikijiTransformerContext, query: TokenQuery, node: Element | Text): Partial; 40 | + nodeCompletions?(this: ShikijiTransformerContext, query: TokenCompletion, node: Element | Text): Partial; 41 | } 42 | 43 | -/** 44 | - * The default renderer aligning with the original `shiki-twoslash` output. 45 | - */ 46 | -declare function rendererClassic(): TwoSlashRenderers; 47 | - 48 | -type CompletionItem = NonNullable[0]; 49 | +type CompletionItem = NonNullable[number]; 50 | declare const defaultCompletionIcons: Record; 51 | declare const defaultCustomTagIcons: Record; 52 | 53 | @@ -91,31 +86,52 @@ interface RendererRichOptions { 54 | /** 55 | * Custom formatter for the type info text. 56 | * Note that it might not be valid TypeScript syntax. 57 | + * 58 | + * @default defaultHoverInfoProcessor 59 | */ 60 | - formatInfo?(info: string): string; 61 | + processHoverInfo?: (info: string) => string; 62 | + /** 63 | + * Custom formatter for the docs text (can be markdown). 64 | + * 65 | + * @default undefined 66 | + */ 67 | + processHoverDocs?: (docs: string) => string; 68 | /** 69 | * Classes added to injected elements 70 | */ 71 | classExtra?: string; 72 | + /** 73 | + * Language for syntax highlight. 74 | + * @default the language of the code block 75 | + */ 76 | + lang?: string; 77 | + /** 78 | + * @deprecated Use `processHoverInfo` instead. 79 | + */ 80 | + formatInfo?(info: string): string; 81 | } 82 | /** 83 | * An alternative renderer that providers better prefixed class names, 84 | * with syntax highlight for the info text. 85 | */ 86 | -declare function rendererRich(options?: RendererRichOptions): TwoSlashRenderers; 87 | +declare function rendererRich(options?: RendererRichOptions): TwoSlashRenderer; 88 | +/** 89 | + * The default hover info processor, which will do some basic cleanup 90 | + */ 91 | +declare function defaultHoverInfoProcessor(type: string): string; 92 | + 93 | +/** 94 | + * The default renderer aligning with the original `shiki-twoslash` output. 95 | + */ 96 | +declare function rendererClassic(): TwoSlashRenderer; 97 | 98 | /** 99 | * This file is the core of the shikiji-twoslash package, 100 | * Decoupled from twoslash's implementation and allowing to introduce custom implementation or cache system. 101 | */ 102 | 103 | -declare function defaultTwoSlashOptions(): { 104 | - customTags: string[]; 105 | - defaultCompilerOptions: { 106 | - module: number; 107 | - target: number; 108 | - }; 109 | -}; 110 | -declare function createTransformerFactory(defaultTwoslasher: typeof twoslasher): (options?: TransformerTwoSlashOptions) => ShikijiTransformer; 111 | +declare function defaultTwoSlashOptions(): TwoSlashExecuteOptions; 112 | +type TwoSlashFunction = (code: string, lang?: string, options?: TwoSlashExecuteOptions) => TwoSlashReturn; 113 | +declare function createTransformerFactory(defaultTwoslasher: TwoSlashFunction, defaultRenderer?: TwoSlashRenderer): (options?: TransformerTwoSlashOptions) => ShikijiTransformer; 114 | 115 | -export { type CompletionItem, type RendererRichOptions, type TransformerTwoSlashOptions, type TwoSlashRenderers, createTransformerFactory, defaultCompletionIcons, defaultCustomTagIcons, defaultTwoSlashOptions, rendererClassic, rendererRich }; 116 | +export { type CompletionItem, type RendererRichOptions, type TransformerTwoSlashOptions, type TwoSlashRenderer, createTransformerFactory, defaultCompletionIcons, defaultCustomTagIcons, defaultHoverInfoProcessor, defaultTwoSlashOptions, rendererClassic, rendererRich }; 117 | diff --git a/dist/core.d.ts b/dist/core.d.ts 118 | index 5d0b16ce7ad80c86f6c24f73485b8903c729d9ea..1909d7afc5f505a77f060cf267b6cfb617f0701c 100644 119 | --- a/dist/core.d.ts 120 | +++ b/dist/core.d.ts 121 | @@ -1,4 +1,4 @@ 122 | -import { TwoSlashReturn, twoslasher, TwoSlashOptions } from '@typescript/twoslash'; 123 | +import { TwoSlashReturn, twoslasher, TwoSlashOptions, TokenError, TokenTag, TokenQuery, TokenCompletion, TokenHover, TwoSlashExecuteOptions } from 'twoslashes'; 124 | import { CodeToHastOptions, ShikijiTransformerContext, ShikijiTransformer } from 'shikiji-core'; 125 | import { ElementContent, Element, Text } from 'hast'; 126 | 127 | @@ -38,30 +38,25 @@ interface TransformerTwoSlashOptions { 128 | /** 129 | * Custom renderers to decide how each info should be rendered 130 | */ 131 | - renderer?: TwoSlashRenderers; 132 | + renderer?: TwoSlashRenderer; 133 | /** 134 | * Strictly throw when there is an error 135 | * @default true 136 | */ 137 | throws?: boolean; 138 | } 139 | -interface TwoSlashRenderers { 140 | - lineError?(this: ShikijiTransformerContext, error: TwoSlashReturn['errors'][0]): ElementContent[]; 141 | - lineCustomTag?(this: ShikijiTransformerContext, tag: TwoSlashReturn['tags'][0]): ElementContent[]; 142 | - lineQuery?(this: ShikijiTransformerContext, query: TwoSlashReturn['queries'][0], targetNode?: Element | Text): ElementContent[]; 143 | - lineCompletions?(this: ShikijiTransformerContext, query: TwoSlashReturn['queries'][0]): ElementContent[]; 144 | - nodeError?(this: ShikijiTransformerContext, error: TwoSlashReturn['errors'][0], node: Element | Text): Partial; 145 | - nodeStaticInfo(this: ShikijiTransformerContext, info: TwoSlashReturn['staticQuickInfos'][0], node: Element | Text): Partial; 146 | - nodeQuery?(this: ShikijiTransformerContext, query: TwoSlashReturn['queries'][0], node: Element | Text): Partial; 147 | - nodeCompletions?(this: ShikijiTransformerContext, query: TwoSlashReturn['queries'][0], node: Element | Text): Partial; 148 | +interface TwoSlashRenderer { 149 | + lineError?(this: ShikijiTransformerContext, error: TokenError): ElementContent[]; 150 | + lineCustomTag?(this: ShikijiTransformerContext, tag: TokenTag): ElementContent[]; 151 | + lineQuery?(this: ShikijiTransformerContext, query: TokenQuery, targetNode?: Element | Text): ElementContent[]; 152 | + lineCompletions?(this: ShikijiTransformerContext, query: TokenCompletion): ElementContent[]; 153 | + nodeError?(this: ShikijiTransformerContext, error: TokenError, node: Element | Text): Partial; 154 | + nodeStaticInfo(this: ShikijiTransformerContext, info: TokenHover, node: Element | Text): Partial; 155 | + nodeQuery?(this: ShikijiTransformerContext, query: TokenQuery, node: Element | Text): Partial; 156 | + nodeCompletions?(this: ShikijiTransformerContext, query: TokenCompletion, node: Element | Text): Partial; 157 | } 158 | 159 | -/** 160 | - * The default renderer aligning with the original `shiki-twoslash` output. 161 | - */ 162 | -declare function rendererClassic(): TwoSlashRenderers; 163 | - 164 | -type CompletionItem = NonNullable[0]; 165 | +type CompletionItem = NonNullable[number]; 166 | declare const defaultCompletionIcons: Record; 167 | declare const defaultCustomTagIcons: Record; 168 | 169 | @@ -91,31 +86,52 @@ interface RendererRichOptions { 170 | /** 171 | * Custom formatter for the type info text. 172 | * Note that it might not be valid TypeScript syntax. 173 | + * 174 | + * @default defaultHoverInfoProcessor 175 | */ 176 | - formatInfo?(info: string): string; 177 | + processHoverInfo?: (info: string) => string; 178 | + /** 179 | + * Custom formatter for the docs text (can be markdown). 180 | + * 181 | + * @default undefined 182 | + */ 183 | + processHoverDocs?: (docs: string) => string; 184 | /** 185 | * Classes added to injected elements 186 | */ 187 | classExtra?: string; 188 | + /** 189 | + * Language for syntax highlight. 190 | + * @default the language of the code block 191 | + */ 192 | + lang?: string; 193 | + /** 194 | + * @deprecated Use `processHoverInfo` instead. 195 | + */ 196 | + formatInfo?(info: string): string; 197 | } 198 | /** 199 | * An alternative renderer that providers better prefixed class names, 200 | * with syntax highlight for the info text. 201 | */ 202 | -declare function rendererRich(options?: RendererRichOptions): TwoSlashRenderers; 203 | +declare function rendererRich(options?: RendererRichOptions): TwoSlashRenderer; 204 | +/** 205 | + * The default hover info processor, which will do some basic cleanup 206 | + */ 207 | +declare function defaultHoverInfoProcessor(type: string): string; 208 | + 209 | +/** 210 | + * The default renderer aligning with the original `shiki-twoslash` output. 211 | + */ 212 | +declare function rendererClassic(): TwoSlashRenderer; 213 | 214 | /** 215 | * This file is the core of the shikiji-twoslash package, 216 | * Decoupled from twoslash's implementation and allowing to introduce custom implementation or cache system. 217 | */ 218 | 219 | -declare function defaultTwoSlashOptions(): { 220 | - customTags: string[]; 221 | - defaultCompilerOptions: { 222 | - module: number; 223 | - target: number; 224 | - }; 225 | -}; 226 | -declare function createTransformerFactory(defaultTwoslasher: typeof twoslasher): (options?: TransformerTwoSlashOptions) => ShikijiTransformer; 227 | +declare function defaultTwoSlashOptions(): TwoSlashExecuteOptions; 228 | +type TwoSlashFunction = (code: string, lang?: string, options?: TwoSlashExecuteOptions) => TwoSlashReturn; 229 | +declare function createTransformerFactory(defaultTwoslasher: TwoSlashFunction, defaultRenderer?: TwoSlashRenderer): (options?: TransformerTwoSlashOptions) => ShikijiTransformer; 230 | 231 | -export { type CompletionItem, type RendererRichOptions, type TransformerTwoSlashOptions, type TwoSlashRenderers, createTransformerFactory, defaultCompletionIcons, defaultCustomTagIcons, defaultTwoSlashOptions, rendererClassic, rendererRich }; 232 | +export { type CompletionItem, type RendererRichOptions, type TransformerTwoSlashOptions, type TwoSlashRenderer, createTransformerFactory, defaultCompletionIcons, defaultCustomTagIcons, defaultHoverInfoProcessor, defaultTwoSlashOptions, rendererClassic, rendererRich }; 233 | diff --git a/dist/core.mjs b/dist/core.mjs 234 | index 895ba78d536756fe0e44b1d47f716aaaffa866d2..b1ff2ce059d882e72b4079e5d57e16d441f38b98 100644 235 | --- a/dist/core.mjs 236 | +++ b/dist/core.mjs 237 | @@ -1,182 +1,5 @@ 238 | import { addClassToHast } from 'shikiji-core'; 239 | 240 | -function rendererClassic() { 241 | - return { 242 | - nodeStaticInfo(info, node) { 243 | - return { 244 | - type: "element", 245 | - tagName: "data-lsp", 246 | - properties: { 247 | - lsp: info.text 248 | - }, 249 | - children: [node] 250 | - }; 251 | - }, 252 | - nodeError(_, node) { 253 | - return { 254 | - type: "element", 255 | - tagName: "data-err", 256 | - properties: {}, 257 | - children: [node] 258 | - }; 259 | - }, 260 | - lineError(error) { 261 | - return [ 262 | - { 263 | - type: "element", 264 | - tagName: "div", 265 | - properties: { 266 | - class: "error" 267 | - }, 268 | - children: [ 269 | - { 270 | - type: "element", 271 | - tagName: "span", 272 | - properties: {}, 273 | - children: [ 274 | - { 275 | - type: "text", 276 | - value: error.renderedMessage 277 | - } 278 | - ] 279 | - }, 280 | - { 281 | - type: "element", 282 | - tagName: "span", 283 | - properties: { 284 | - class: "code" 285 | - }, 286 | - children: [ 287 | - { 288 | - type: "text", 289 | - value: String(error.code) 290 | - } 291 | - ] 292 | - } 293 | - ] 294 | - }, 295 | - { 296 | - type: "element", 297 | - tagName: "span", 298 | - properties: { 299 | - class: "error-behind" 300 | - }, 301 | - children: [ 302 | - { 303 | - type: "text", 304 | - value: error.renderedMessage 305 | - } 306 | - ] 307 | - } 308 | - ]; 309 | - }, 310 | - lineCompletions(query) { 311 | - return [ 312 | - { 313 | - type: "element", 314 | - tagName: "div", 315 | - properties: { class: "meta-line" }, 316 | - children: [ 317 | - { type: "text", value: " ".repeat(query.offset) }, 318 | - { 319 | - type: "element", 320 | - tagName: "span", 321 | - properties: { class: "inline-completions" }, 322 | - children: [{ 323 | - type: "element", 324 | - tagName: "ul", 325 | - properties: { class: "dropdown" }, 326 | - children: query.completions.filter((i) => i.name.startsWith(query.completionsPrefix || "____")).map((i) => ({ 327 | - type: "element", 328 | - tagName: "li", 329 | - properties: { 330 | - class: i.kindModifiers?.split(",").includes("deprecated") ? "deprecated" : void 0 331 | - }, 332 | - children: [{ 333 | - type: "element", 334 | - tagName: "span", 335 | - properties: {}, 336 | - children: [ 337 | - { 338 | - type: "element", 339 | - tagName: "span", 340 | - properties: { class: "result-found" }, 341 | - children: [ 342 | - { 343 | - type: "text", 344 | - value: query.completionsPrefix || "" 345 | - } 346 | - ] 347 | - }, 348 | - { 349 | - type: "text", 350 | - value: i.name.slice(query.completionsPrefix?.length || 0) 351 | - } 352 | - ] 353 | - }] 354 | - })) 355 | - }] 356 | - } 357 | - ] 358 | - } 359 | - ]; 360 | - }, 361 | - lineQuery(query, targetNode) { 362 | - const targetText = targetNode?.type === "text" ? targetNode.value : ""; 363 | - const offset = Math.max(0, (query.offset || 0) + Math.floor(targetText.length / 2) - 1); 364 | - return [ 365 | - { 366 | - type: "element", 367 | - tagName: "div", 368 | - properties: { class: "meta-line" }, 369 | - children: [ 370 | - { type: "text", value: " ".repeat(offset) }, 371 | - { 372 | - type: "element", 373 | - tagName: "span", 374 | - properties: { class: "popover" }, 375 | - children: [ 376 | - { 377 | - type: "element", 378 | - tagName: "div", 379 | - properties: { class: "arrow" }, 380 | - children: [] 381 | - }, 382 | - { 383 | - type: "text", 384 | - value: query.text || "" 385 | - } 386 | - ] 387 | - } 388 | - ] 389 | - } 390 | - ]; 391 | - }, 392 | - lineCustomTag(tag) { 393 | - return [ 394 | - { 395 | - type: "element", 396 | - tagName: "div", 397 | - properties: { class: `meta-line logger ${tag.name}-log` }, 398 | - children: [ 399 | - { 400 | - type: "element", 401 | - tagName: "span", 402 | - properties: { class: "message" }, 403 | - children: [ 404 | - { 405 | - type: "text", 406 | - value: tag.annotation || "" 407 | - } 408 | - ] 409 | - } 410 | - ] 411 | - } 412 | - ]; 413 | - } 414 | - }; 415 | -} 416 | - 417 | const module = { 418 | type: "element", 419 | tagName: "svg", 420 | @@ -547,36 +370,47 @@ function rendererRich(options = {}) { 421 | const { 422 | completionIcons = defaultCompletionIcons, 423 | customTagIcons = defaultCustomTagIcons, 424 | - formatInfo = (info) => info, 425 | + formatInfo, 426 | + processHoverInfo = formatInfo || defaultHoverInfoProcessor, 427 | + processHoverDocs = (docs) => docs, 428 | classExtra = "", 429 | jsdoc = true 430 | } = options; 431 | - function hightlightPopupContent(codeToHast, options2, info) { 432 | + function hightlightPopupContent(codeToHast, shikijiOptions, info) { 433 | if (!info.text) 434 | return []; 435 | - const themedContent = codeToHast(formatInfo(info.text), { 436 | - ...options2, 437 | + const text = processHoverInfo(info.text) ?? info.text; 438 | + if (!text.trim()) 439 | + return []; 440 | + const themedContent = codeToHast(text, { 441 | + ...shikijiOptions, 442 | + lang: options.lang || shikijiOptions.lang, 443 | transformers: [], 444 | transforms: void 0 445 | }).children[0].children[0].children; 446 | if (jsdoc && info.docs) { 447 | - themedContent.push({ 448 | - type: "element", 449 | - tagName: "div", 450 | - properties: { class: "twoslash-popup-jsdoc" }, 451 | - children: [ 452 | - { 453 | - type: "text", 454 | - value: info.docs 455 | - } 456 | - ] 457 | - }); 458 | + const docs = processHoverDocs(info.docs) ?? info.docs; 459 | + if (docs) { 460 | + themedContent.push({ 461 | + type: "element", 462 | + tagName: "div", 463 | + properties: { class: "twoslash-popup-jsdoc" }, 464 | + children: [ 465 | + { 466 | + type: "text", 467 | + value: docs 468 | + } 469 | + ] 470 | + }); 471 | + } 472 | } 473 | return themedContent; 474 | } 475 | return { 476 | nodeStaticInfo(info, node) { 477 | const themedContent = hightlightPopupContent(this.codeToHast, this.options, info); 478 | + if (!themedContent.length) 479 | + return node; 480 | return { 481 | type: "element", 482 | tagName: "span", 483 | @@ -727,7 +561,7 @@ function rendererRich(options = {}) { 484 | children: [ 485 | { 486 | type: "text", 487 | - value: error.renderedMessage 488 | + value: error.text 489 | } 490 | ] 491 | } 492 | @@ -754,7 +588,194 @@ function rendererRich(options = {}) { 493 | ] : [], 494 | { 495 | type: "text", 496 | - value: tag.annotation || "" 497 | + value: tag.text || "" 498 | + } 499 | + ] 500 | + } 501 | + ]; 502 | + } 503 | + }; 504 | +} 505 | +const regexType = /^[A-Z][a-zA-Z0-9_]*(\<[^\>]*\>)?:/; 506 | +const regexFunction = /^[a-zA-Z0-9_]*\(/; 507 | +function defaultHoverInfoProcessor(type) { 508 | + let content = type.replace(/^\(([\w-]+?)\)\s+/mg, "").replace(/\nimport .*$/, "").replace(/^(interface|namespace) \w+$/mg, "").trim(); 509 | + if (content.match(regexType)) 510 | + content = `type ${content}`; 511 | + else if (content.match(regexFunction)) 512 | + content = `function ${content}`; 513 | + return content; 514 | +} 515 | + 516 | +function rendererClassic() { 517 | + return { 518 | + nodeStaticInfo(info, node) { 519 | + return { 520 | + type: "element", 521 | + tagName: "data-lsp", 522 | + properties: { 523 | + lsp: info.text 524 | + }, 525 | + children: [node] 526 | + }; 527 | + }, 528 | + nodeError(_, node) { 529 | + return { 530 | + type: "element", 531 | + tagName: "data-err", 532 | + properties: {}, 533 | + children: [node] 534 | + }; 535 | + }, 536 | + lineError(error) { 537 | + return [ 538 | + { 539 | + type: "element", 540 | + tagName: "div", 541 | + properties: { 542 | + class: "error" 543 | + }, 544 | + children: [ 545 | + { 546 | + type: "element", 547 | + tagName: "span", 548 | + properties: {}, 549 | + children: [ 550 | + { 551 | + type: "text", 552 | + value: error.text 553 | + } 554 | + ] 555 | + }, 556 | + { 557 | + type: "element", 558 | + tagName: "span", 559 | + properties: { 560 | + class: "code" 561 | + }, 562 | + children: [ 563 | + { 564 | + type: "text", 565 | + value: String(error.code) 566 | + } 567 | + ] 568 | + } 569 | + ] 570 | + }, 571 | + { 572 | + type: "element", 573 | + tagName: "span", 574 | + properties: { 575 | + class: "error-behind" 576 | + }, 577 | + children: [ 578 | + { 579 | + type: "text", 580 | + value: error.text 581 | + } 582 | + ] 583 | + } 584 | + ]; 585 | + }, 586 | + lineCompletions(query) { 587 | + return [ 588 | + { 589 | + type: "element", 590 | + tagName: "div", 591 | + properties: { class: "meta-line" }, 592 | + children: [ 593 | + { type: "text", value: " ".repeat(query.character) }, 594 | + { 595 | + type: "element", 596 | + tagName: "span", 597 | + properties: { class: "inline-completions" }, 598 | + children: [{ 599 | + type: "element", 600 | + tagName: "ul", 601 | + properties: { class: "dropdown" }, 602 | + children: query.completions.filter((i) => i.name.startsWith(query.completionsPrefix || "____")).map((i) => ({ 603 | + type: "element", 604 | + tagName: "li", 605 | + properties: { 606 | + class: i.kindModifiers?.split(",").includes("deprecated") ? "deprecated" : void 0 607 | + }, 608 | + children: [{ 609 | + type: "element", 610 | + tagName: "span", 611 | + properties: {}, 612 | + children: [ 613 | + { 614 | + type: "element", 615 | + tagName: "span", 616 | + properties: { class: "result-found" }, 617 | + children: [ 618 | + { 619 | + type: "text", 620 | + value: query.completionsPrefix || "" 621 | + } 622 | + ] 623 | + }, 624 | + { 625 | + type: "text", 626 | + value: i.name.slice(query.completionsPrefix?.length || 0) 627 | + } 628 | + ] 629 | + }] 630 | + })) 631 | + }] 632 | + } 633 | + ] 634 | + } 635 | + ]; 636 | + }, 637 | + lineQuery(query, targetNode) { 638 | + const targetText = targetNode?.type === "text" ? targetNode.value : ""; 639 | + const offset = Math.max(0, (query.character || 0) + Math.floor(targetText.length / 2) - 1); 640 | + return [ 641 | + { 642 | + type: "element", 643 | + tagName: "div", 644 | + properties: { class: "meta-line" }, 645 | + children: [ 646 | + { type: "text", value: " ".repeat(offset) }, 647 | + { 648 | + type: "element", 649 | + tagName: "span", 650 | + properties: { class: "popover" }, 651 | + children: [ 652 | + { 653 | + type: "element", 654 | + tagName: "div", 655 | + properties: { class: "arrow" }, 656 | + children: [] 657 | + }, 658 | + { 659 | + type: "text", 660 | + value: query.text || "" 661 | + } 662 | + ] 663 | + } 664 | + ] 665 | + } 666 | + ]; 667 | + }, 668 | + lineCustomTag(tag) { 669 | + return [ 670 | + { 671 | + type: "element", 672 | + tagName: "div", 673 | + properties: { class: `meta-line logger ${tag.name}-log` }, 674 | + children: [ 675 | + { 676 | + type: "element", 677 | + tagName: "span", 678 | + properties: { class: "message" }, 679 | + children: [ 680 | + { 681 | + type: "text", 682 | + value: tag.text || "" 683 | + } 684 | + ] 685 | } 686 | ] 687 | } 688 | @@ -766,13 +787,13 @@ function rendererRich(options = {}) { 689 | function defaultTwoSlashOptions() { 690 | return { 691 | customTags: ["annotate", "log", "warn", "error"], 692 | - defaultCompilerOptions: { 693 | + compilerOptions: { 694 | module: 99, 695 | target: 99 696 | } 697 | }; 698 | } 699 | -function createTransformerFactory(defaultTwoslasher) { 700 | +function createTransformerFactory(defaultTwoslasher, defaultRenderer) { 701 | return function transformerTwoSlash(options = {}) { 702 | const { 703 | langs = ["ts", "tsx"], 704 | @@ -784,9 +805,11 @@ function createTransformerFactory(defaultTwoslasher) { 705 | }, 706 | twoslasher = defaultTwoslasher, 707 | explicitTrigger = false, 708 | - renderer = rendererClassic(), 709 | + renderer = defaultRenderer, 710 | throws = true 711 | } = options; 712 | + if (!renderer) 713 | + throw new Error("[shikiji-twoslash] Missing renderer"); 714 | const filter = options.filter || ((lang, _, options2) => langs.includes(lang) && (!explicitTrigger || /\btwoslash\b/.test(options2.meta?.__raw || ""))); 715 | return { 716 | preprocess(code, shikijiOptions) { 717 | @@ -833,6 +856,7 @@ function createTransformerFactory(defaultTwoslasher) { 718 | if (!lineEl) { 719 | if (throws) 720 | throw new Error(`[shikiji-twoslash] Cannot find line ${line} in code element`); 721 | + return; 722 | } 723 | const textNodes = lineEl.children.flatMap((i) => i.type === "element" ? i.children || [] : []); 724 | let index = 0; 725 | @@ -847,8 +871,6 @@ function createTransformerFactory(defaultTwoslasher) { 726 | }; 727 | const skipTokens = /* @__PURE__ */ new Set(); 728 | for (const error of twoslash.errors) { 729 | - if (error.line == null || error.character == null) 730 | - return; 731 | const token = locateTextToken(error.line, error.character); 732 | if (!token) 733 | continue; 734 | @@ -861,31 +883,30 @@ function createTransformerFactory(defaultTwoslasher) { 735 | insertAfterLine(error.line, renderer.lineError.call(this, error)); 736 | } 737 | for (const query of twoslash.queries) { 738 | - if (query.kind === "completions") { 739 | - const token = locateTextToken(query.line - 1, query.offset); 740 | - if (!token) 741 | - throw new Error(`[shikiji-twoslash] Cannot find token at L${query.line}:${query.offset}`); 742 | - skipTokens.add(token); 743 | - if (renderer.nodeCompletions) { 744 | - const clone = { ...token }; 745 | - Object.assign(token, renderer.nodeCompletions.call(this, query, clone)); 746 | - } 747 | - if (renderer.lineCompletions) 748 | - insertAfterLine(query.line, renderer.lineCompletions.call(this, query)); 749 | - } else if (query.kind === "query") { 750 | - const token = locateTextToken(query.line - 1, query.offset); 751 | - if (!token) 752 | - throw new Error(`[shikiji-twoslash] Cannot find token at L${query.line}:${query.offset}`); 753 | - skipTokens.add(token); 754 | - if (renderer.nodeQuery) { 755 | - const clone = { ...token }; 756 | - Object.assign(token, renderer.nodeQuery.call(this, query, clone)); 757 | - } 758 | - if (renderer.lineQuery) 759 | - insertAfterLine(query.line, renderer.lineQuery.call(this, query, token)); 760 | + const token = locateTextToken(query.line, query.character); 761 | + if (!token) 762 | + continue; 763 | + skipTokens.add(token); 764 | + if (renderer.nodeQuery) { 765 | + const clone = { ...token }; 766 | + Object.assign(token, renderer.nodeQuery.call(this, query, clone)); 767 | + } 768 | + if (renderer.lineQuery) 769 | + insertAfterLine(query.line, renderer.lineQuery.call(this, query, token)); 770 | + } 771 | + for (const completion of twoslash.completions) { 772 | + const token = locateTextToken(completion.line, completion.character); 773 | + if (!token) 774 | + continue; 775 | + skipTokens.add(token); 776 | + if (renderer.nodeCompletions) { 777 | + const clone = { ...token }; 778 | + Object.assign(token, renderer.nodeCompletions.call(this, completion, clone)); 779 | } 780 | + if (renderer.lineCompletions) 781 | + insertAfterLine(completion.line, renderer.lineCompletions.call(this, completion)); 782 | } 783 | - for (const info of twoslash.staticQuickInfos) { 784 | + for (const info of twoslash.hovers) { 785 | const token = locateTextToken(info.line, info.character); 786 | if (!token || token.type !== "text") 787 | continue; 788 | @@ -903,4 +924,4 @@ function createTransformerFactory(defaultTwoslasher) { 789 | }; 790 | } 791 | 792 | -export { createTransformerFactory, defaultCompletionIcons, defaultCustomTagIcons, defaultTwoSlashOptions, rendererClassic, rendererRich }; 793 | +export { createTransformerFactory, defaultCompletionIcons, defaultCustomTagIcons, defaultHoverInfoProcessor, defaultTwoSlashOptions, rendererClassic, rendererRich }; 794 | diff --git a/dist/index.d.mts b/dist/index.d.mts 795 | index af8685da7f9d71e5d851f118077acc66e876acb6..04c928881123f1d4c3ed22dba706cb679778ab56 100644 796 | --- a/dist/index.d.mts 797 | +++ b/dist/index.d.mts 798 | @@ -1,9 +1,12 @@ 799 | import * as shikiji_core from 'shikiji-core'; 800 | import { TransformerTwoSlashOptions } from './core.mjs'; 801 | -export { CompletionItem, RendererRichOptions, TwoSlashRenderers, createTransformerFactory, defaultCompletionIcons, defaultCustomTagIcons, defaultTwoSlashOptions, rendererClassic, rendererRich } from './core.mjs'; 802 | -import '@typescript/twoslash'; 803 | +export { CompletionItem, RendererRichOptions, TwoSlashRenderer, createTransformerFactory, defaultCompletionIcons, defaultCustomTagIcons, defaultHoverInfoProcessor, defaultTwoSlashOptions, rendererClassic, rendererRich } from './core.mjs'; 804 | +import 'twoslashes'; 805 | import 'hast'; 806 | 807 | +/** 808 | + * Factory function to create a Shikiji transformer for twoslash integrations. 809 | + */ 810 | declare const transformerTwoSlash: (options?: TransformerTwoSlashOptions) => shikiji_core.ShikijiTransformer; 811 | 812 | export { TransformerTwoSlashOptions, transformerTwoSlash }; 813 | diff --git a/dist/index.d.ts b/dist/index.d.ts 814 | index 0c96483bc9dd92c4a6b6bb6534ac3e61b360d8be..89e4d2600efb5cc41d4c19ec2e1b175f703a4dd7 100644 815 | --- a/dist/index.d.ts 816 | +++ b/dist/index.d.ts 817 | @@ -1,9 +1,12 @@ 818 | import * as shikiji_core from 'shikiji-core'; 819 | import { TransformerTwoSlashOptions } from './core.js'; 820 | -export { CompletionItem, RendererRichOptions, TwoSlashRenderers, createTransformerFactory, defaultCompletionIcons, defaultCustomTagIcons, defaultTwoSlashOptions, rendererClassic, rendererRich } from './core.js'; 821 | -import '@typescript/twoslash'; 822 | +export { CompletionItem, RendererRichOptions, TwoSlashRenderer, createTransformerFactory, defaultCompletionIcons, defaultCustomTagIcons, defaultHoverInfoProcessor, defaultTwoSlashOptions, rendererClassic, rendererRich } from './core.js'; 823 | +import 'twoslashes'; 824 | import 'hast'; 825 | 826 | +/** 827 | + * Factory function to create a Shikiji transformer for twoslash integrations. 828 | + */ 829 | declare const transformerTwoSlash: (options?: TransformerTwoSlashOptions) => shikiji_core.ShikijiTransformer; 830 | 831 | export { TransformerTwoSlashOptions, transformerTwoSlash }; 832 | diff --git a/dist/index.mjs b/dist/index.mjs 833 | index 209a5146bcbe474e9c180a93b52bbe00360b241a..082d6b90e9299c5439be8aa3d783c52ca99e62c1 100644 834 | --- a/dist/index.mjs 835 | +++ b/dist/index.mjs 836 | @@ -1,8 +1,11 @@ 837 | -import { twoslasher } from '@typescript/twoslash'; 838 | -import { createTransformerFactory } from './core.mjs'; 839 | -export { defaultCompletionIcons, defaultCustomTagIcons, defaultTwoSlashOptions, rendererClassic, rendererRich } from './core.mjs'; 840 | +import { createTwoSlasher } from 'twoslashes'; 841 | +import { createTransformerFactory, rendererClassic } from './core.mjs'; 842 | +export { defaultCompletionIcons, defaultCustomTagIcons, defaultHoverInfoProcessor, defaultTwoSlashOptions, rendererRich } from './core.mjs'; 843 | import 'shikiji-core'; 844 | 845 | -const transformerTwoSlash = createTransformerFactory(twoslasher); 846 | +const transformerTwoSlash = /* @__PURE__ */ createTransformerFactory( 847 | + /* @__PURE__ */ createTwoSlasher(), 848 | + /* @__PURE__ */ rendererClassic() 849 | +); 850 | 851 | -export { createTransformerFactory, transformerTwoSlash }; 852 | +export { createTransformerFactory, rendererClassic, transformerTwoSlash }; 853 | diff --git a/package.json b/package.json 854 | index 05959ea72ca46961fa5dcbd87fdc91fa39c69419..789e5445b919e0b4eb80a91415ed4592927dc260 100644 855 | --- a/package.json 856 | +++ b/package.json 857 | @@ -48,9 +48,16 @@ 858 | "*.css", 859 | "dist" 860 | ], 861 | + "scripts": { 862 | + "build": "unbuild", 863 | + "dev": "unbuild --stub", 864 | + "prepublishOnly": "nr build", 865 | + "test": "vitest" 866 | + }, 867 | "dependencies": { 868 | "@typescript/twoslash": "^3.2.4", 869 | - "shikiji-core": "0.9.18" 870 | + "shikiji-core": "workspace:*", 871 | + "twoslashes": "^0.0.2" 872 | }, 873 | "devDependencies": { 874 | "@iconify-json/carbon": "^1.1.27", 875 | @@ -59,10 +66,5 @@ 876 | "shiki": "^0.14.7", 877 | "shiki-twoslash": "^3.1.2", 878 | "typescript": "^5.3.3" 879 | - }, 880 | - "scripts": { 881 | - "build": "unbuild", 882 | - "dev": "unbuild --stub", 883 | - "test": "vitest" 884 | } 885 | } 886 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - docs 4 | - packages/* 5 | - examples/* 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { SourceMap, createVueLanguage, sharedTypes } from '@vue/language-core' 2 | import ts from 'typescript' 3 | import type { CreateTwoSlashOptions, Range, TwoSlashExecuteOptions, TwoSlashInstance } from 'twoslashes' 4 | import { createTwoSlasher, defaultCompilerOptions, removeCodeRanges, resolveNodePositions } from 'twoslashes' 5 | 6 | export function createTwoSlasherVue(createOptions: CreateTwoSlashOptions = {}, flag = true): TwoSlashInstance { 7 | const twoslasher = createTwoSlasher(createOptions) 8 | 9 | function twoslasherVue(code: string, extension?: string, options: TwoSlashExecuteOptions = {}) { 10 | if (extension !== 'vue') 11 | return twoslasher(code, extension, options) 12 | 13 | const lang = createVueLanguage( 14 | ts, 15 | { 16 | ...defaultCompilerOptions, 17 | ...options.compilerOptions, 18 | }, 19 | ) 20 | 21 | const fileSource = lang.createVirtualFile('index.vue', ts.ScriptSnapshot.fromString(code), 'vue')! 22 | const fileCompiled = fileSource.getEmbeddedFiles()[0] 23 | const typeHelpers = sharedTypes.getTypesCode(fileSource.vueCompilerOptions) 24 | const compiled = [ 25 | (fileCompiled as any).content.map((c: any) => Array.isArray(c) ? c[0] : c).join(''), 26 | '// ---cut-after---', 27 | typeHelpers, 28 | ].join('\n') 29 | 30 | // Pass compiled to TS file to twoslash 31 | const result = twoslasher(compiled, 'tsx', { 32 | ...options, 33 | compilerOptions: { 34 | jsx: 4 satisfies ts.JsxEmit.ReactJSX, 35 | jsxImportSource: 'vue', 36 | noImplicitAny: false, 37 | ...options.compilerOptions, 38 | }, 39 | handbookOptions: { 40 | noErrorsCutted: true, 41 | ...options.handbookOptions, 42 | keepNotations: true, 43 | }, 44 | shouldGetHoverInfo(id) { 45 | // ignore internal types 46 | return !id.startsWith('__VLS') 47 | }, 48 | }) 49 | 50 | if (!flag) 51 | return result 52 | 53 | const map = new SourceMap(fileCompiled.mappings) 54 | 55 | // Map the tokens 56 | const mappedNodes = result.nodes 57 | .map((q) => { 58 | if ('text' in q && q.text === 'any') 59 | return undefined 60 | const start = map.toSourceOffset(q.start)?.[0] 61 | const end = map.toSourceOffset(q.start + q.length)?.[0] 62 | if (start == null || end == null || start < 0 || end < 0 || start >= end) 63 | return undefined 64 | return Object.assign(q, { 65 | ...q, 66 | start, 67 | length: end - start, 68 | }) 69 | }) 70 | .filter(isNotNull) 71 | 72 | const mappedRemovals = result.meta.removals 73 | .map((r) => { 74 | const start = map.toSourceOffset(r[0])?.[0] 75 | const end = map.toSourceOffset(r[1])?.[0] 76 | if (start == null || end == null || start < 0 || end < 0 || start >= end) 77 | return undefined 78 | return [start, end] as Range 79 | }) 80 | .filter(isNotNull) 81 | 82 | const removed = removeCodeRanges(code, mappedRemovals, mappedNodes) 83 | result.code = removed.code 84 | result.nodes = resolveNodePositions(removed.nodes, result.code) 85 | 86 | return result 87 | } 88 | 89 | twoslasherVue.getCacheMap = twoslasher.getCacheMap 90 | 91 | return twoslasherVue 92 | } 93 | 94 | function isNotNull(x: T | null | undefined): x is T { 95 | return x != null 96 | } 97 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import { codeToHtml } from 'shikiji' 3 | import { createTransformerFactory, rendererRich } from 'shikiji-twoslash/core' 4 | import { createTwoSlasherVue } from '../src' 5 | 6 | const code = ` 7 | 14 | 15 | 18 | ` 19 | 20 | const styleHeader = [ 21 | '', 22 | ``, 23 | ``, 24 | '', 25 | '', 26 | ].join('\n') 27 | 28 | const twoslasherVue = createTwoSlasherVue() 29 | 30 | it('exported', () => { 31 | const result = twoslasherVue(code, 'vue') 32 | 33 | expect(result.nodes.slice(0, 10)).toMatchInlineSnapshot(` 34 | [ 35 | { 36 | "character": 9, 37 | "docs": "Takes an inner value and returns a reactive and mutable ref object, which 38 | has a single property \`.value\` that points to the inner value.", 39 | "length": 3, 40 | "line": 2, 41 | "start": 35, 42 | "target": "ref", 43 | "text": "(alias) function ref(value: T): Ref> (+1 overload) 44 | import ref", 45 | "type": "hover", 46 | }, 47 | { 48 | "character": 14, 49 | "docs": undefined, 50 | "length": 8, 51 | "line": 2, 52 | "start": 40, 53 | "target": "computed", 54 | "text": "(alias) const computed: { 55 | (getter: ComputedGetter, debugOptions?: DebuggerOptions | undefined): ComputedRef; 56 | (options: WritableComputedOptions, debugOptions?: DebuggerOptions | undefined): WritableComputedRef<...>; 57 | } 58 | import computed", 59 | "type": "hover", 60 | }, 61 | { 62 | "character": 6, 63 | "docs": undefined, 64 | "length": 5, 65 | "line": 4, 66 | "start": 69, 67 | "target": "count", 68 | "text": "const count: Ref", 69 | "type": "hover", 70 | }, 71 | { 72 | "character": 14, 73 | "docs": "Takes an inner value and returns a reactive and mutable ref object, which 74 | has a single property \`.value\` that points to the inner value.", 75 | "length": 3, 76 | "line": 4, 77 | "start": 77, 78 | "target": "ref", 79 | "text": "(alias) ref(value: number): Ref (+1 overload) 80 | import ref", 81 | "type": "hover", 82 | }, 83 | { 84 | "character": 6, 85 | "docs": undefined, 86 | "length": 6, 87 | "line": 5, 88 | "start": 90, 89 | "target": "double", 90 | "text": "const double: ComputedRef", 91 | "type": "hover", 92 | }, 93 | { 94 | "character": 7, 95 | "docs": undefined, 96 | "length": 6, 97 | "line": 5, 98 | "start": 91, 99 | "target": "double", 100 | "text": "const double: ComputedRef", 101 | "type": "query", 102 | }, 103 | { 104 | "character": 15, 105 | "docs": "Takes a getter function and returns a readonly reactive ref object for the 106 | returned value from the getter. It can also take an object with get and set 107 | functions to create a writable ref object.", 108 | "length": 8, 109 | "line": 5, 110 | "start": 99, 111 | "target": "computed", 112 | "text": "(alias) computed(getter: ComputedGetter, debugOptions?: DebuggerOptions | undefined): ComputedRef (+1 overload) 113 | import computed", 114 | "type": "hover", 115 | }, 116 | { 117 | "character": 30, 118 | "docs": undefined, 119 | "length": 5, 120 | "line": 5, 121 | "start": 114, 122 | "target": "count", 123 | "text": "const count: Ref", 124 | "type": "hover", 125 | }, 126 | { 127 | "character": 36, 128 | "docs": undefined, 129 | "length": 5, 130 | "line": 5, 131 | "start": 120, 132 | "target": "value", 133 | "text": "(property) Ref.value: number", 134 | "type": "hover", 135 | }, 136 | { 137 | "character": 3, 138 | "docs": undefined, 139 | "length": 6, 140 | "line": 9, 141 | "start": 156, 142 | "target": "button", 143 | "text": "(property) button: ButtonHTMLAttributes & ReservedProps", 144 | "type": "hover", 145 | }, 146 | ] 147 | `) 148 | }) 149 | 150 | it('highlight vue', async () => { 151 | const result = await codeToHtml(code, { 152 | lang: 'vue', 153 | theme: 'vitesse-dark', 154 | transformers: [ 155 | createTransformerFactory(twoslasherVue)({ 156 | langs: ['ts', 'tsx', 'vue'], 157 | renderer: rendererRich({ 158 | lang: 'ts', 159 | }), 160 | }), 161 | ], 162 | }) 163 | 164 | expect(styleHeader + result) 165 | .toMatchFileSnapshot('./out/example.vue.html') 166 | }) 167 | 168 | const twoslasherRaw = createTwoSlasherVue(undefined, false) 169 | it('highlight raw', async () => { 170 | const result = await codeToHtml(code, { 171 | lang: 'ts', 172 | theme: 'vitesse-dark', 173 | transformers: [ 174 | createTransformerFactory((code, _, opt) => twoslasherRaw(code, 'vue', opt))({ 175 | langs: ['ts', 'tsx', 'vue'], 176 | renderer: rendererRich({ 177 | lang: 'ts', 178 | }), 179 | }), 180 | ], 181 | }) 182 | 183 | expect(styleHeader + result) 184 | .toMatchFileSnapshot('./out/example.raw.html') 185 | }) 186 | -------------------------------------------------------------------------------- /test/out/example.vue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

 6 | <script setup lang="ts">
 7 | import { function ref<T>(value: T): Ref<UnwrapRef<T>> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which 8 | has a single property `.value` that points to the inner value.
ref
, const computed: { 9 | <T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions | undefined): ComputedRef<T>; 10 | <T>(options: WritableComputedOptions<T>, debugOptions?: DebuggerOptions | undefined): WritableComputedRef<...>; 11 | }computed } from 'vue'
12 | 13 | const const count: Ref<number>count = ref<number>(value: number): Ref<number> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which 14 | has a single property `.value` that points to the inner value.
ref
(0)
15 | const
const double: ComputedRef<number>
double
= computed<number>(getter: ComputedGetter<number>, debugOptions?: DebuggerOptions | undefined): ComputedRef<number> (+1 overload)
Takes a getter function and returns a readonly reactive ref object for the 16 | returned value from the getter. It can also take an object with get and set 17 | functions to create a writable ref object.
computed
(() => const count: Ref<number>count.Ref<number>.value: numbervalue * 2)
18 | </script> 19 | 20 | <template> 21 | <button: ButtonHTMLAttributes & ReservedPropsbutton @'click': ((payload: MouseEvent) => void) | undefinedclick="count: numbercount++">count is: {{ count: numbercount }}</button: ButtonHTMLAttributes & ReservedPropsbutton> 22 | </template> 23 |
-------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext"], 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "esModuleInterop": true, 11 | "skipDefaultLibCheck": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | --------------------------------------------------------------------------------