vocs 2.0.8 → 2.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/internal/twoslash/inline-cache.d.ts.map +1 -1
- package/dist/internal/twoslash/inline-cache.js +27 -20
- package/dist/internal/twoslash/inline-cache.js.map +1 -1
- package/dist/react/internal/CodeToHtml.client.d.ts +27 -0
- package/dist/react/internal/CodeToHtml.client.d.ts.map +1 -0
- package/dist/react/internal/CodeToHtml.client.js +138 -0
- package/dist/react/internal/CodeToHtml.client.js.map +1 -0
- package/dist/react/internal/CodeToHtml.d.ts +1 -7
- package/dist/react/internal/CodeToHtml.d.ts.map +1 -1
- package/dist/react/internal/CodeToHtml.js +1 -56
- package/dist/react/internal/CodeToHtml.js.map +1 -1
- package/dist/react/internal/TwoslashHover.client.d.ts.map +1 -1
- package/dist/react/internal/TwoslashHover.client.js +7 -1
- package/dist/react/internal/TwoslashHover.client.js.map +1 -1
- package/dist/styles/twoslash.css +32 -0
- package/package.json +1 -1
- package/src/internal/twoslash/inline-cache.test.ts +26 -0
- package/src/internal/twoslash/inline-cache.ts +28 -26
- package/src/react/internal/CodeToHtml.client.tsx +176 -0
- package/src/react/internal/CodeToHtml.tsx +1 -74
- package/src/react/internal/TwoslashHover.client.tsx +8 -1
- package/src/styles/twoslash.css +32 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inline-cache.d.ts","sourceRoot":"","sources":["../../../src/internal/twoslash/inline-cache.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"inline-cache.d.ts","sourceRoot":"","sources":["../../../src/internal/twoslash/inline-cache.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAEhF,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C;;;;;;;;;;;;;;;;;;GAkBG;AAEH,8EAA8E;AAC9E,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;CACX,CAAA;AAED,OAAO,QAAQ,gBAAgB,CAAC;IAC9B,UAAU,2BAA2B;QACnC,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;QAC5B,OAAO,CAAC,EAAE,mBAAmB,CAAA;QAC7B,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;KACrC;CACF;AAUD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,MAAM,CAElF;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG;IACrD,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,SAAS,GAAG,IAAI,CAAA;CAC5B,CAWA;AAuBD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,sBAAsB,CACpC,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAAO,GAChF;IAAE,UAAU,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,CAkJ1D;AA+BD;;;GAGG;AACH,wBAAgB,OAAO,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAIjE;AAWD,qEAAqE;AACrE,wBAAgB,WAAW,IAAI;IAAE,UAAU,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,CAGtF;AAED,iFAAiF;AACjF,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAExC;AAED,iDAAiD;AACjD,wBAAgB,KAAK,IAAI,IAAI,CAE5B"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
2
|
import LZString from 'lz-string';
|
|
3
|
-
import { getObjectHash } from 'twoslash/core';
|
|
4
3
|
import { FilePatcher } from './file-patcher.js';
|
|
5
4
|
/**
|
|
6
5
|
* Tag used for the ephemeral source-map comment. This comment is injected into
|
|
@@ -25,6 +24,15 @@ export function extractSourceMapComment(code) {
|
|
|
25
24
|
}
|
|
26
25
|
return { code, sourceMap };
|
|
27
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Cache payload format version. Kept at `1` even though the cache key changed
|
|
29
|
+
* from `optionsHash:lang:code` to `lang:code` (see `cacheHash`): stale
|
|
30
|
+
* payloads from the old key naturally fail the hash check on `preprocess` and
|
|
31
|
+
* are transparently re-seeded on the next write, so a version bump (which
|
|
32
|
+
* would force a one-shot invalidation of every committed cache comment) is
|
|
33
|
+
* unnecessary.
|
|
34
|
+
*/
|
|
35
|
+
const CACHE_VERSION = 1;
|
|
28
36
|
const CODE_INLINE_CACHE_KEY = '@twoslash-cache';
|
|
29
37
|
const CODE_INLINE_CACHE_REGEX = new RegExp(`// ${CODE_INLINE_CACHE_KEY}: (.*)(?:\n|$)`, 'g');
|
|
30
38
|
/** Matches a cache comment anchored to the start of a code block body. */
|
|
@@ -44,25 +52,24 @@ export function stripInlineCacheComments(code) {
|
|
|
44
52
|
export function createInlineTypesCache(options = {}) {
|
|
45
53
|
const { remove, ignoreCache } = options;
|
|
46
54
|
const patcher = new FilePatcher();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
function cacheHash(code, lang, options) {
|
|
55
|
+
// Key the cache on the (already fully-composed) snippet code and language
|
|
56
|
+
// only — deliberately NOT on the twoslash options. The whole point of the
|
|
57
|
+
// inline cache is to travel with the repo across machines and CI, but
|
|
58
|
+
// `twoslashOptions` frequently contain environment-specific or volatile
|
|
59
|
+
// values (absolute paths, config that branches on whether built `.d.ts`
|
|
60
|
+
// files exist, etc). Hashing them in makes the committed cache miss on any
|
|
61
|
+
// machine other than the one that seeded it. This matches the filesystem
|
|
62
|
+
// types cache, which also keys on code alone.
|
|
63
|
+
function cacheHash(code, lang) {
|
|
57
64
|
return crypto
|
|
58
65
|
.createHash('sha256')
|
|
59
|
-
.update(`${
|
|
66
|
+
.update(`${lang ?? ''}:${code}`)
|
|
60
67
|
.digest('hex');
|
|
61
68
|
}
|
|
62
|
-
function stringifyCachePayload(data, code, lang
|
|
69
|
+
function stringifyCachePayload(data, code, lang) {
|
|
63
70
|
const payload = {
|
|
64
|
-
v:
|
|
65
|
-
hash: cacheHash(code, lang
|
|
71
|
+
v: CACHE_VERSION,
|
|
72
|
+
hash: cacheHash(code, lang),
|
|
66
73
|
data: LZString.compressToBase64(JSON.stringify(data)),
|
|
67
74
|
};
|
|
68
75
|
return JSON.stringify(payload);
|
|
@@ -72,7 +79,7 @@ export function createInlineTypesCache(options = {}) {
|
|
|
72
79
|
return null;
|
|
73
80
|
try {
|
|
74
81
|
const payload = JSON.parse(cache);
|
|
75
|
-
if (payload.v ===
|
|
82
|
+
if (payload.v === CACHE_VERSION) {
|
|
76
83
|
return {
|
|
77
84
|
payload,
|
|
78
85
|
twoslash: () => {
|
|
@@ -132,7 +139,7 @@ export function createInlineTypesCache(options = {}) {
|
|
|
132
139
|
};
|
|
133
140
|
}
|
|
134
141
|
const typesCache = {
|
|
135
|
-
preprocess(code, lang,
|
|
142
|
+
preprocess(code, lang, _options, meta) {
|
|
136
143
|
if (!meta)
|
|
137
144
|
return;
|
|
138
145
|
let rawCache = '';
|
|
@@ -148,7 +155,7 @@ export function createInlineTypesCache(options = {}) {
|
|
|
148
155
|
const shouldLoadCache = !ignoreCache && !remove;
|
|
149
156
|
if (shouldLoadCache) {
|
|
150
157
|
const cache = resolveCachePayload(cacheString);
|
|
151
|
-
if (cache?.payload.hash === cacheHash(code, lang
|
|
158
|
+
if (cache?.payload.hash === cacheHash(code, lang)) {
|
|
152
159
|
const twoslash = cache.twoslash();
|
|
153
160
|
if (twoslash)
|
|
154
161
|
meta.__cache = twoslash;
|
|
@@ -164,13 +171,13 @@ export function createInlineTypesCache(options = {}) {
|
|
|
164
171
|
read(_code, _lang, _options, meta) {
|
|
165
172
|
return meta?.__cache ?? null;
|
|
166
173
|
},
|
|
167
|
-
write(code, data, lang,
|
|
174
|
+
write(code, data, lang, _options, meta) {
|
|
168
175
|
if (remove) {
|
|
169
176
|
meta?.__patch?.('');
|
|
170
177
|
return;
|
|
171
178
|
}
|
|
172
179
|
const twoslashShiki = simplifyTwoslashReturn(data);
|
|
173
|
-
const cacheStr = `// ${CODE_INLINE_CACHE_KEY}: ${stringifyCachePayload(twoslashShiki, code, lang
|
|
180
|
+
const cacheStr = `// ${CODE_INLINE_CACHE_KEY}: ${stringifyCachePayload(twoslashShiki, code, lang)}`;
|
|
174
181
|
meta?.__patch?.(cacheStr);
|
|
175
182
|
},
|
|
176
183
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inline-cache.js","sourceRoot":"","sources":["../../../src/internal/twoslash/inline-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC,OAAO,QAAQ,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"inline-cache.js","sourceRoot":"","sources":["../../../src/internal/twoslash/inline-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazily highlights code on the client.
|
|
3
|
+
*
|
|
4
|
+
* Twoslash hover popups embed a code snippet (the resolved type) for every
|
|
5
|
+
* hoverable token. Highlighting these with Shiki on the server forces RSC to
|
|
6
|
+
* render and serialize a fully highlighted snippet for every token across every
|
|
7
|
+
* page at build time — which dominates build memory and time even though the
|
|
8
|
+
* popups are only ever seen on hover.
|
|
9
|
+
*
|
|
10
|
+
* Instead we render the snippet as plain text in the RSC payload and highlight
|
|
11
|
+
* it on the client. Because the popup only mounts when it opens, the Shiki
|
|
12
|
+
* bundle is dynamically imported and the snippet highlighted on demand.
|
|
13
|
+
*/
|
|
14
|
+
export declare function CodeToHtml(props: CodeToHtml.Props): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export declare namespace CodeToHtml {
|
|
16
|
+
type Props = {
|
|
17
|
+
code: string;
|
|
18
|
+
lang: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Returns the highlighted HTML for a snippet if it has already been computed. */
|
|
22
|
+
export declare function getCached(code: string, lang: string): string | undefined;
|
|
23
|
+
/** Highlights a snippet (memoized), loading the Shiki bundle on first use. */
|
|
24
|
+
export declare function highlight(code: string, lang: string): Promise<string>;
|
|
25
|
+
/** Eagerly loads the Shiki bundle so the first real highlight is fast. */
|
|
26
|
+
export declare function prewarm(): void;
|
|
27
|
+
//# sourceMappingURL=CodeToHtml.client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CodeToHtml.client.d.ts","sourceRoot":"","sources":["../../../src/react/internal/CodeToHtml.client.tsx"],"names":[],"mappings":"AAKA;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,2CA2BjD;AAmCD,yBAAiB,UAAU,CAAC;IAC1B,KAAY,KAAK,GAAG;QAClB,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;CACF;AA0DD,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,sBAEnD;AAED,8EAA8E;AAC9E,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,mBAQzD;AAED,0EAA0E;AAC1E,wBAAgB,OAAO,SAEtB"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { config } from 'virtual:vocs/config';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
/**
|
|
6
|
+
* Lazily highlights code on the client.
|
|
7
|
+
*
|
|
8
|
+
* Twoslash hover popups embed a code snippet (the resolved type) for every
|
|
9
|
+
* hoverable token. Highlighting these with Shiki on the server forces RSC to
|
|
10
|
+
* render and serialize a fully highlighted snippet for every token across every
|
|
11
|
+
* page at build time — which dominates build memory and time even though the
|
|
12
|
+
* popups are only ever seen on hover.
|
|
13
|
+
*
|
|
14
|
+
* Instead we render the snippet as plain text in the RSC payload and highlight
|
|
15
|
+
* it on the client. Because the popup only mounts when it opens, the Shiki
|
|
16
|
+
* bundle is dynamically imported and the snippet highlighted on demand.
|
|
17
|
+
*/
|
|
18
|
+
export function CodeToHtml(props) {
|
|
19
|
+
const { code, lang } = props;
|
|
20
|
+
// Initialize from the cache so a snippet that was already highlighted (e.g.
|
|
21
|
+
// pre-highlighted by the popover before opening, or shown previously) renders
|
|
22
|
+
// highlighted immediately without a plain-text flash.
|
|
23
|
+
const [html, setHtml] = useState(() => getCached(code, lang) ?? null);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (html)
|
|
26
|
+
return;
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
highlight(code, lang)
|
|
29
|
+
.then((result) => {
|
|
30
|
+
if (!cancelled)
|
|
31
|
+
setHtml(result);
|
|
32
|
+
})
|
|
33
|
+
.catch(() => { });
|
|
34
|
+
return () => {
|
|
35
|
+
cancelled = true;
|
|
36
|
+
};
|
|
37
|
+
}, [code, lang, html]);
|
|
38
|
+
if (html)
|
|
39
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: highlighted by Shiki
|
|
40
|
+
return _jsx("div", { dangerouslySetInnerHTML: { __html: html } });
|
|
41
|
+
// While highlighting, render a skeleton that mirrors the snippet's line
|
|
42
|
+
// structure so the popup is shown immediately without a layout shift.
|
|
43
|
+
return _jsx(Skeleton, { code: code });
|
|
44
|
+
}
|
|
45
|
+
function Skeleton(props) {
|
|
46
|
+
const lines = props.code.split('\n');
|
|
47
|
+
return (_jsx("div", { "data-v-code-skeleton": true, "aria-hidden": "true", children: _jsxs("pre", { className: "shiki", "data-v-overflow-fade": true, children: [_jsx("code", { children: lines.map((line, index) => {
|
|
48
|
+
const indent = line.length - line.trimStart().length;
|
|
49
|
+
const length = line.trim().length;
|
|
50
|
+
return (_jsx("span", { className: "line", children: length > 0 && (_jsx("span", { "data-v-skeleton-bar": true, style: {
|
|
51
|
+
marginLeft: `${indent}ch`,
|
|
52
|
+
width: `${Math.min(length, 48)}ch`,
|
|
53
|
+
} })) }, index));
|
|
54
|
+
}) }), _jsx("div", { "data-v-overflow-sentinel": true })] }) }));
|
|
55
|
+
}
|
|
56
|
+
let highlighterPromise;
|
|
57
|
+
function getHighlighter() {
|
|
58
|
+
if (!highlighterPromise)
|
|
59
|
+
highlighterPromise = (async () => {
|
|
60
|
+
const { bundledLanguages, createHighlighter, hastToHtml } = await import('shiki/bundle/web');
|
|
61
|
+
const { codeHighlight } = config;
|
|
62
|
+
const { langAlias = {}, themes } = codeHighlight;
|
|
63
|
+
// Note: `langAlias` is intentionally not passed to the highlighter.
|
|
64
|
+
// Passing a custom `langAlias` registers languages under their alias name
|
|
65
|
+
// when lazily loaded (e.g. `loadLanguage('ts')` registers as `ts` instead
|
|
66
|
+
// of `typescript`), which then makes `codeToHast({ lang })` fail to resolve
|
|
67
|
+
// the grammar. We resolve aliases to their base language ourselves below.
|
|
68
|
+
const highlighter = await createHighlighter({
|
|
69
|
+
themes: Object.values(themes),
|
|
70
|
+
langs: [],
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
async codeToHtml(code, lang) {
|
|
74
|
+
const base = langAlias[lang] ?? lang;
|
|
75
|
+
const resolvedLang = base in bundledLanguages ? base : 'txt';
|
|
76
|
+
if (!highlighter.getLoadedLanguages().includes(resolvedLang))
|
|
77
|
+
await highlighter.loadLanguage(resolvedLang);
|
|
78
|
+
const hast = highlighter.codeToHast(code, {
|
|
79
|
+
defaultColor: 'light-dark()',
|
|
80
|
+
lang: resolvedLang,
|
|
81
|
+
rootStyle: false,
|
|
82
|
+
meta: { 'data-v-overflow-fade': true },
|
|
83
|
+
themes,
|
|
84
|
+
transformers: [transformerShrinkIndent()],
|
|
85
|
+
});
|
|
86
|
+
const pre = hast.children[0];
|
|
87
|
+
if (pre && pre.type === 'element' && pre.tagName === 'pre')
|
|
88
|
+
pre.children.push({
|
|
89
|
+
type: 'element',
|
|
90
|
+
tagName: 'div',
|
|
91
|
+
properties: { 'data-v-overflow-sentinel': true },
|
|
92
|
+
children: [],
|
|
93
|
+
});
|
|
94
|
+
return hastToHtml(hast);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
})();
|
|
98
|
+
return highlighterPromise;
|
|
99
|
+
}
|
|
100
|
+
const cache = new Map();
|
|
101
|
+
function cacheKey(code, lang) {
|
|
102
|
+
return `${lang}\n${code}`;
|
|
103
|
+
}
|
|
104
|
+
/** Returns the highlighted HTML for a snippet if it has already been computed. */
|
|
105
|
+
export function getCached(code, lang) {
|
|
106
|
+
return cache.get(cacheKey(code, lang));
|
|
107
|
+
}
|
|
108
|
+
/** Highlights a snippet (memoized), loading the Shiki bundle on first use. */
|
|
109
|
+
export async function highlight(code, lang) {
|
|
110
|
+
const key = cacheKey(code, lang);
|
|
111
|
+
const cached = cache.get(key);
|
|
112
|
+
if (cached !== undefined)
|
|
113
|
+
return cached;
|
|
114
|
+
const highlighter = await getHighlighter();
|
|
115
|
+
const html = await highlighter.codeToHtml(code, lang);
|
|
116
|
+
cache.set(key, html);
|
|
117
|
+
return html;
|
|
118
|
+
}
|
|
119
|
+
/** Eagerly loads the Shiki bundle so the first real highlight is fast. */
|
|
120
|
+
export function prewarm() {
|
|
121
|
+
void getHighlighter();
|
|
122
|
+
}
|
|
123
|
+
function transformerShrinkIndent() {
|
|
124
|
+
return {
|
|
125
|
+
name: 'indent',
|
|
126
|
+
span(hast) {
|
|
127
|
+
const child = hast.children[0];
|
|
128
|
+
if (!child)
|
|
129
|
+
return;
|
|
130
|
+
if (child.type !== 'text')
|
|
131
|
+
return;
|
|
132
|
+
if (!child.value)
|
|
133
|
+
return;
|
|
134
|
+
hast.children[0] = { type: 'text', value: child.value.replace(/\s\s/g, ' ') };
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=CodeToHtml.client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CodeToHtml.client.js","sourceRoot":"","sources":["../../../src/react/internal/CodeToHtml.client.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export declare namespace CodeToHtml {
|
|
3
|
-
type Props = {
|
|
4
|
-
code: string;
|
|
5
|
-
lang: string;
|
|
6
|
-
};
|
|
7
|
-
}
|
|
1
|
+
export { CodeToHtml } from './CodeToHtml.client.js';
|
|
8
2
|
//# sourceMappingURL=CodeToHtml.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeToHtml.d.ts","sourceRoot":"","sources":["../../../src/react/internal/CodeToHtml.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CodeToHtml.d.ts","sourceRoot":"","sources":["../../../src/react/internal/CodeToHtml.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA"}
|
|
@@ -1,57 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { config } from 'virtual:vocs/config';
|
|
3
|
-
import { langs as virtualLangs } from 'virtual:vocs/langs';
|
|
4
|
-
import { bundledLanguages, createHighlighter, hastToHtml, makeSingletonHighlighter, } from 'shiki/bundle/web';
|
|
5
|
-
const getHighlighter = makeSingletonHighlighter(createHighlighter);
|
|
6
|
-
export async function CodeToHtml(props) {
|
|
7
|
-
const { code, lang } = props;
|
|
8
|
-
const { codeHighlight } = config;
|
|
9
|
-
const { langAlias = {}, themes } = codeHighlight;
|
|
10
|
-
const highlighter = await getHighlighter({
|
|
11
|
-
themes: import.meta.env.DEV ? ['none'] : Object.values(themes),
|
|
12
|
-
langs: import.meta.env.DEV
|
|
13
|
-
? ['txt']
|
|
14
|
-
: [...Object.keys(bundledLanguages), ...virtualLangs],
|
|
15
|
-
langAlias,
|
|
16
|
-
});
|
|
17
|
-
const loadedLangs = highlighter.getLoadedLanguages();
|
|
18
|
-
const resolvedLang = loadedLangs.includes(lang) ? lang : 'txt';
|
|
19
|
-
const hast = highlighter.codeToHast(code, {
|
|
20
|
-
defaultColor: 'light-dark()',
|
|
21
|
-
lang: import.meta.env.DEV ? 'txt' : resolvedLang,
|
|
22
|
-
rootStyle: false,
|
|
23
|
-
meta: {
|
|
24
|
-
'data-v-overflow-fade': true,
|
|
25
|
-
},
|
|
26
|
-
...(import.meta.env.DEV ? { theme: 'none' } : { themes }),
|
|
27
|
-
transformers: [transformerShrinkIndent()],
|
|
28
|
-
});
|
|
29
|
-
// Add overflow sentinel
|
|
30
|
-
const pre = hast.children[0];
|
|
31
|
-
if (pre && pre.type === 'element' && pre.tagName === 'pre')
|
|
32
|
-
pre.children.push({
|
|
33
|
-
type: 'element',
|
|
34
|
-
tagName: 'div',
|
|
35
|
-
properties: { 'data-v-overflow-sentinel': true },
|
|
36
|
-
children: [],
|
|
37
|
-
});
|
|
38
|
-
const html = hastToHtml(hast);
|
|
39
|
-
// biome-ignore lint/security/noDangerouslySetInnerHtml: _
|
|
40
|
-
return _jsx("div", { dangerouslySetInnerHTML: { __html: html } });
|
|
41
|
-
}
|
|
42
|
-
function transformerShrinkIndent() {
|
|
43
|
-
return {
|
|
44
|
-
name: 'indent',
|
|
45
|
-
span(hast) {
|
|
46
|
-
const child = hast.children[0];
|
|
47
|
-
if (!child)
|
|
48
|
-
return;
|
|
49
|
-
if (child.type !== 'text')
|
|
50
|
-
return;
|
|
51
|
-
if (!child.value)
|
|
52
|
-
return;
|
|
53
|
-
hast.children[0] = { type: 'text', value: child.value.replace(/\s\s/g, ' ') };
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
}
|
|
1
|
+
export { CodeToHtml } from './CodeToHtml.client.js';
|
|
57
2
|
//# sourceMappingURL=CodeToHtml.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeToHtml.js","sourceRoot":"","sources":["../../../src/react/internal/CodeToHtml.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CodeToHtml.js","sourceRoot":"","sources":["../../../src/react/internal/CodeToHtml.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TwoslashHover.client.d.ts","sourceRoot":"","sources":["../../../src/react/internal/TwoslashHover.client.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"TwoslashHover.client.d.ts","sourceRoot":"","sources":["../../../src/react/internal/TwoslashHover.client.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAInC,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,CAAC,KAAK,2CAqCvD;AAED,yBAAiB,aAAa,CAAC;IAC7B,KAAY,KAAK,GAAG;QAClB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAC9B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;QACzB,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;QAC1B,OAAO,EAAE,KAAK,CAAC,SAAS,CAAA;KACzB,CAAA;IAED,SAAgB,eAAe;uBACK,cAAc,GAAG,IAAI;MAwBxD;CACF"}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Popover } from '@base-ui/react/popover';
|
|
4
|
-
import { useCallback } from 'react';
|
|
4
|
+
import { useCallback, useEffect } from 'react';
|
|
5
|
+
import { prewarm } from './CodeToHtml.client.js';
|
|
5
6
|
export function TwoslashHover(props) {
|
|
6
7
|
const { className = '', children, trigger } = props;
|
|
7
8
|
const { ref } = TwoslashHover.useOverflowFade();
|
|
8
9
|
const open = className?.includes('twoslash-query-persisted');
|
|
10
|
+
// Warm the highlighter so the code inside the popup highlights as quickly as
|
|
11
|
+
// possible (the popup shows a skeleton placeholder until then).
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
prewarm();
|
|
14
|
+
}, []);
|
|
9
15
|
return (_jsxs(Popover.Root, { ...(open ? { open } : {}), children: [_jsx(Popover.Trigger, { "data-v-twoslash-trigger": true, openOnHover: true, delay: 0, children: _jsx("span", { children: trigger }) }), _jsx(Popover.Portal, { children: _jsx(Popover.Positioner, { align: "start", side: "bottom", sideOffset: 4, collisionAvoidance: { side: 'none' }, children: _jsxs(Popover.Popup, { className: className, initialFocus: false, children: [_jsx(Popover.Arrow, { className: "vocs:data-[side=bottom]:top-[-8px] vocs:data-[side=left]:right-[-13px] vocs:data-[side=left]:rotate-90 vocs:data-[side=right]:left-[-13px] vocs:data-[side=right]:-rotate-90 vocs:data-[side=top]:bottom-[-8px] vocs:data-[side=top]:rotate-180", children: _jsx(ArrowSvg, {}) }), _jsx("div", { "data-v-content": true, ref: ref, children: children })] }) }) })] }));
|
|
10
16
|
}
|
|
11
17
|
(function (TwoslashHover) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TwoslashHover.client.js","sourceRoot":"","sources":["../../../src/react/internal/TwoslashHover.client.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAEhD,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"TwoslashHover.client.js","sourceRoot":"","sources":["../../../src/react/internal/TwoslashHover.client.tsx"],"names":[],"mappings":"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"}
|
package/dist/styles/twoslash.css
CHANGED
|
@@ -63,6 +63,38 @@
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
[data-v-code-skeleton] [data-v-skeleton-bar] {
|
|
67
|
+
display: inline-block;
|
|
68
|
+
height: 0.72em;
|
|
69
|
+
vertical-align: middle;
|
|
70
|
+
border-radius: 0.1875rem;
|
|
71
|
+
background-color: var(--vocs-background-color-surfaceTint);
|
|
72
|
+
background-image: linear-gradient(
|
|
73
|
+
90deg,
|
|
74
|
+
transparent 0%,
|
|
75
|
+
color-mix(in srgb, var(--vocs-color-gray10) 22%, transparent) 50%,
|
|
76
|
+
transparent 100%
|
|
77
|
+
);
|
|
78
|
+
background-repeat: no-repeat;
|
|
79
|
+
background-size: 200% 100%;
|
|
80
|
+
animation: vocs-skeleton-shimmer 1.25s ease-in-out infinite;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@keyframes vocs-skeleton-shimmer {
|
|
84
|
+
from {
|
|
85
|
+
background-position: 180% 0;
|
|
86
|
+
}
|
|
87
|
+
to {
|
|
88
|
+
background-position: -80% 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@media (prefers-reduced-motion: reduce) {
|
|
93
|
+
[data-v-code-skeleton] [data-v-skeleton-bar] {
|
|
94
|
+
animation: none;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
.twoslash-popup-docs {
|
|
67
99
|
@apply vocs:max-h-40 vocs:overflow-y-scroll;
|
|
68
100
|
|
package/package.json
CHANGED
|
@@ -141,6 +141,32 @@ describe('inline types cache', () => {
|
|
|
141
141
|
expect(cacheLines()).toHaveLength(1)
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
+
it('hits regardless of twoslash options (portable across environments)', () => {
|
|
145
|
+
const code = 'const a: string = "x"'
|
|
146
|
+
const { file, from, to } = createMarkdown(code)
|
|
147
|
+
const data = { nodes: [], code: 'compiled-output' }
|
|
148
|
+
|
|
149
|
+
// Seed with one set of options (e.g. machine A, with `paths`).
|
|
150
|
+
{
|
|
151
|
+
const { typesCache, patcher } = createInlineTypesCache()
|
|
152
|
+
const meta = { sourceMap: { path: file, from, to } } as never
|
|
153
|
+
const options = { compilerOptions: { paths: { foo: ['/abs/a'] } } }
|
|
154
|
+
typesCache.preprocess?.(code, 'ts', options as never, meta)
|
|
155
|
+
typesCache.write(code, data as never, 'ts', options as never, meta)
|
|
156
|
+
patcher.patch(file)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Read with different options (e.g. machine B / Vercel, without `paths`).
|
|
160
|
+
{
|
|
161
|
+
const { typesCache } = createInlineTypesCache()
|
|
162
|
+
const body = readBody(file, from)
|
|
163
|
+
const meta = { sourceMap: { path: file, from, to } } as never
|
|
164
|
+
const otherOptions = { compilerOptions: {} }
|
|
165
|
+
typesCache.preprocess?.(body, 'ts', otherOptions as never, meta)
|
|
166
|
+
expect(typesCache.read(body, 'ts', otherOptions as never, meta)).toEqual(data)
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
144
170
|
it('invalidates the cache when the hash no longer matches', () => {
|
|
145
171
|
const code = 'const a: string = "x"'
|
|
146
172
|
const { file, from, to } = createMarkdown(code)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto'
|
|
2
2
|
import type { TwoslashShikiReturn, TwoslashTypesCache } from '@shikijs/twoslash'
|
|
3
3
|
import LZString from 'lz-string'
|
|
4
|
-
import { getObjectHash, type TwoslashExecuteOptions } from 'twoslash/core'
|
|
5
4
|
import { FilePatcher } from './file-patcher.js'
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -73,6 +72,16 @@ type CachePayload = {
|
|
|
73
72
|
data: string
|
|
74
73
|
}
|
|
75
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Cache payload format version. Kept at `1` even though the cache key changed
|
|
77
|
+
* from `optionsHash:lang:code` to `lang:code` (see `cacheHash`): stale
|
|
78
|
+
* payloads from the old key naturally fail the hash check on `preprocess` and
|
|
79
|
+
* are transparently re-seeded on the next write, so a version bump (which
|
|
80
|
+
* would force a one-shot invalidation of every committed cache comment) is
|
|
81
|
+
* unnecessary.
|
|
82
|
+
*/
|
|
83
|
+
const CACHE_VERSION = 1
|
|
84
|
+
|
|
76
85
|
const CODE_INLINE_CACHE_KEY = '@twoslash-cache'
|
|
77
86
|
const CODE_INLINE_CACHE_REGEX = new RegExp(`// ${CODE_INLINE_CACHE_KEY}: (.*)(?:\n|$)`, 'g')
|
|
78
87
|
/** Matches a cache comment anchored to the start of a code block body. */
|
|
@@ -97,32 +106,25 @@ export function createInlineTypesCache(
|
|
|
97
106
|
const { remove, ignoreCache } = options
|
|
98
107
|
const patcher = new FilePatcher()
|
|
99
108
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
function cacheHash(code: string, lang?: string, options?: TwoslashExecuteOptions): string {
|
|
109
|
+
// Key the cache on the (already fully-composed) snippet code and language
|
|
110
|
+
// only — deliberately NOT on the twoslash options. The whole point of the
|
|
111
|
+
// inline cache is to travel with the repo across machines and CI, but
|
|
112
|
+
// `twoslashOptions` frequently contain environment-specific or volatile
|
|
113
|
+
// values (absolute paths, config that branches on whether built `.d.ts`
|
|
114
|
+
// files exist, etc). Hashing them in makes the committed cache miss on any
|
|
115
|
+
// machine other than the one that seeded it. This matches the filesystem
|
|
116
|
+
// types cache, which also keys on code alone.
|
|
117
|
+
function cacheHash(code: string, lang?: string): string {
|
|
111
118
|
return crypto
|
|
112
119
|
.createHash('sha256')
|
|
113
|
-
.update(`${
|
|
120
|
+
.update(`${lang ?? ''}:${code}`)
|
|
114
121
|
.digest('hex')
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
function stringifyCachePayload(
|
|
118
|
-
data: TwoslashShikiReturn,
|
|
119
|
-
code: string,
|
|
120
|
-
lang?: string,
|
|
121
|
-
options?: TwoslashExecuteOptions,
|
|
122
|
-
): string {
|
|
124
|
+
function stringifyCachePayload(data: TwoslashShikiReturn, code: string, lang?: string): string {
|
|
123
125
|
const payload: CachePayload = {
|
|
124
|
-
v:
|
|
125
|
-
hash: cacheHash(code, lang
|
|
126
|
+
v: CACHE_VERSION,
|
|
127
|
+
hash: cacheHash(code, lang),
|
|
126
128
|
data: LZString.compressToBase64(JSON.stringify(data)),
|
|
127
129
|
}
|
|
128
130
|
return JSON.stringify(payload)
|
|
@@ -135,7 +137,7 @@ export function createInlineTypesCache(
|
|
|
135
137
|
if (!cache) return null
|
|
136
138
|
try {
|
|
137
139
|
const payload = JSON.parse(cache) as CachePayload
|
|
138
|
-
if (payload.v ===
|
|
140
|
+
if (payload.v === CACHE_VERSION) {
|
|
139
141
|
return {
|
|
140
142
|
payload,
|
|
141
143
|
twoslash: () => {
|
|
@@ -200,7 +202,7 @@ export function createInlineTypesCache(
|
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
const typesCache: TwoslashTypesCache = {
|
|
203
|
-
preprocess(code, lang,
|
|
205
|
+
preprocess(code, lang, _options, meta) {
|
|
204
206
|
if (!meta) return
|
|
205
207
|
|
|
206
208
|
let rawCache = ''
|
|
@@ -218,7 +220,7 @@ export function createInlineTypesCache(
|
|
|
218
220
|
const shouldLoadCache = !ignoreCache && !remove
|
|
219
221
|
if (shouldLoadCache) {
|
|
220
222
|
const cache = resolveCachePayload(cacheString)
|
|
221
|
-
if (cache?.payload.hash === cacheHash(code, lang
|
|
223
|
+
if (cache?.payload.hash === cacheHash(code, lang)) {
|
|
222
224
|
const twoslash = cache.twoslash()
|
|
223
225
|
if (twoslash) meta.__cache = twoslash
|
|
224
226
|
}
|
|
@@ -234,13 +236,13 @@ export function createInlineTypesCache(
|
|
|
234
236
|
read(_code, _lang, _options, meta) {
|
|
235
237
|
return meta?.__cache ?? null
|
|
236
238
|
},
|
|
237
|
-
write(code, data, lang,
|
|
239
|
+
write(code, data, lang, _options, meta) {
|
|
238
240
|
if (remove) {
|
|
239
241
|
meta?.__patch?.('')
|
|
240
242
|
return
|
|
241
243
|
}
|
|
242
244
|
const twoslashShiki = simplifyTwoslashReturn(data)
|
|
243
|
-
const cacheStr = `// ${CODE_INLINE_CACHE_KEY}: ${stringifyCachePayload(twoslashShiki, code, lang
|
|
245
|
+
const cacheStr = `// ${CODE_INLINE_CACHE_KEY}: ${stringifyCachePayload(twoslashShiki, code, lang)}`
|
|
244
246
|
meta?.__patch?.(cacheStr)
|
|
245
247
|
},
|
|
246
248
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { config } from 'virtual:vocs/config'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lazily highlights code on the client.
|
|
8
|
+
*
|
|
9
|
+
* Twoslash hover popups embed a code snippet (the resolved type) for every
|
|
10
|
+
* hoverable token. Highlighting these with Shiki on the server forces RSC to
|
|
11
|
+
* render and serialize a fully highlighted snippet for every token across every
|
|
12
|
+
* page at build time — which dominates build memory and time even though the
|
|
13
|
+
* popups are only ever seen on hover.
|
|
14
|
+
*
|
|
15
|
+
* Instead we render the snippet as plain text in the RSC payload and highlight
|
|
16
|
+
* it on the client. Because the popup only mounts when it opens, the Shiki
|
|
17
|
+
* bundle is dynamically imported and the snippet highlighted on demand.
|
|
18
|
+
*/
|
|
19
|
+
export function CodeToHtml(props: CodeToHtml.Props) {
|
|
20
|
+
const { code, lang } = props
|
|
21
|
+
// Initialize from the cache so a snippet that was already highlighted (e.g.
|
|
22
|
+
// pre-highlighted by the popover before opening, or shown previously) renders
|
|
23
|
+
// highlighted immediately without a plain-text flash.
|
|
24
|
+
const [html, setHtml] = useState<string | null>(() => getCached(code, lang) ?? null)
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (html) return
|
|
28
|
+
let cancelled = false
|
|
29
|
+
highlight(code, lang)
|
|
30
|
+
.then((result) => {
|
|
31
|
+
if (!cancelled) setHtml(result)
|
|
32
|
+
})
|
|
33
|
+
.catch(() => {})
|
|
34
|
+
return () => {
|
|
35
|
+
cancelled = true
|
|
36
|
+
}
|
|
37
|
+
}, [code, lang, html])
|
|
38
|
+
|
|
39
|
+
if (html)
|
|
40
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: highlighted by Shiki
|
|
41
|
+
return <div dangerouslySetInnerHTML={{ __html: html }} />
|
|
42
|
+
|
|
43
|
+
// While highlighting, render a skeleton that mirrors the snippet's line
|
|
44
|
+
// structure so the popup is shown immediately without a layout shift.
|
|
45
|
+
return <Skeleton code={code} />
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function Skeleton(props: { code: string }) {
|
|
49
|
+
const lines = props.code.split('\n')
|
|
50
|
+
return (
|
|
51
|
+
<div data-v-code-skeleton aria-hidden="true">
|
|
52
|
+
{/* `shiki` class mirrors the real highlighted markup so shared styles
|
|
53
|
+
(e.g. code padding) apply identically and there is no layout shift. */}
|
|
54
|
+
<pre className="shiki" data-v-overflow-fade>
|
|
55
|
+
<code>
|
|
56
|
+
{lines.map((line, index) => {
|
|
57
|
+
const indent = line.length - line.trimStart().length
|
|
58
|
+
const length = line.trim().length
|
|
59
|
+
return (
|
|
60
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: static, never reordered
|
|
61
|
+
<span className="line" key={index}>
|
|
62
|
+
{length > 0 && (
|
|
63
|
+
<span
|
|
64
|
+
data-v-skeleton-bar
|
|
65
|
+
style={{
|
|
66
|
+
marginLeft: `${indent}ch`,
|
|
67
|
+
width: `${Math.min(length, 48)}ch`,
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
</span>
|
|
72
|
+
)
|
|
73
|
+
})}
|
|
74
|
+
</code>
|
|
75
|
+
<div data-v-overflow-sentinel />
|
|
76
|
+
</pre>
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export namespace CodeToHtml {
|
|
82
|
+
export type Props = {
|
|
83
|
+
code: string
|
|
84
|
+
lang: string
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let highlighterPromise:
|
|
89
|
+
| Promise<{
|
|
90
|
+
codeToHtml: (code: string, lang: string) => Promise<string>
|
|
91
|
+
}>
|
|
92
|
+
| undefined
|
|
93
|
+
|
|
94
|
+
function getHighlighter() {
|
|
95
|
+
if (!highlighterPromise)
|
|
96
|
+
highlighterPromise = (async () => {
|
|
97
|
+
const { bundledLanguages, createHighlighter, hastToHtml } = await import('shiki/bundle/web')
|
|
98
|
+
const { codeHighlight } = config
|
|
99
|
+
const { langAlias = {}, themes } = codeHighlight
|
|
100
|
+
// Note: `langAlias` is intentionally not passed to the highlighter.
|
|
101
|
+
// Passing a custom `langAlias` registers languages under their alias name
|
|
102
|
+
// when lazily loaded (e.g. `loadLanguage('ts')` registers as `ts` instead
|
|
103
|
+
// of `typescript`), which then makes `codeToHast({ lang })` fail to resolve
|
|
104
|
+
// the grammar. We resolve aliases to their base language ourselves below.
|
|
105
|
+
const highlighter = await createHighlighter({
|
|
106
|
+
themes: Object.values(themes) as never,
|
|
107
|
+
langs: [],
|
|
108
|
+
})
|
|
109
|
+
return {
|
|
110
|
+
async codeToHtml(code: string, lang: string) {
|
|
111
|
+
const base = langAlias[lang] ?? lang
|
|
112
|
+
const resolvedLang = base in bundledLanguages ? base : 'txt'
|
|
113
|
+
if (!highlighter.getLoadedLanguages().includes(resolvedLang))
|
|
114
|
+
await highlighter.loadLanguage(resolvedLang as never)
|
|
115
|
+
const hast = highlighter.codeToHast(code, {
|
|
116
|
+
defaultColor: 'light-dark()',
|
|
117
|
+
lang: resolvedLang,
|
|
118
|
+
rootStyle: false,
|
|
119
|
+
meta: { 'data-v-overflow-fade': true },
|
|
120
|
+
themes,
|
|
121
|
+
transformers: [transformerShrinkIndent()],
|
|
122
|
+
})
|
|
123
|
+
const pre = hast.children[0]
|
|
124
|
+
if (pre && pre.type === 'element' && pre.tagName === 'pre')
|
|
125
|
+
pre.children.push({
|
|
126
|
+
type: 'element',
|
|
127
|
+
tagName: 'div',
|
|
128
|
+
properties: { 'data-v-overflow-sentinel': true },
|
|
129
|
+
children: [],
|
|
130
|
+
})
|
|
131
|
+
return hastToHtml(hast)
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
})()
|
|
135
|
+
return highlighterPromise
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const cache = new Map<string, string>()
|
|
139
|
+
|
|
140
|
+
function cacheKey(code: string, lang: string) {
|
|
141
|
+
return `${lang}\n${code}`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Returns the highlighted HTML for a snippet if it has already been computed. */
|
|
145
|
+
export function getCached(code: string, lang: string) {
|
|
146
|
+
return cache.get(cacheKey(code, lang))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Highlights a snippet (memoized), loading the Shiki bundle on first use. */
|
|
150
|
+
export async function highlight(code: string, lang: string) {
|
|
151
|
+
const key = cacheKey(code, lang)
|
|
152
|
+
const cached = cache.get(key)
|
|
153
|
+
if (cached !== undefined) return cached
|
|
154
|
+
const highlighter = await getHighlighter()
|
|
155
|
+
const html = await highlighter.codeToHtml(code, lang)
|
|
156
|
+
cache.set(key, html)
|
|
157
|
+
return html
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Eagerly loads the Shiki bundle so the first real highlight is fast. */
|
|
161
|
+
export function prewarm() {
|
|
162
|
+
void getHighlighter()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function transformerShrinkIndent() {
|
|
166
|
+
return {
|
|
167
|
+
name: 'indent',
|
|
168
|
+
span(hast: { children: { type: string; value?: string }[] }) {
|
|
169
|
+
const child = hast.children[0]
|
|
170
|
+
if (!child) return
|
|
171
|
+
if (child.type !== 'text') return
|
|
172
|
+
if (!child.value) return
|
|
173
|
+
hast.children[0] = { type: 'text', value: child.value.replace(/\s\s/g, ' ') }
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -1,74 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { langs as virtualLangs } from 'virtual:vocs/langs'
|
|
3
|
-
import {
|
|
4
|
-
bundledLanguages,
|
|
5
|
-
createHighlighter,
|
|
6
|
-
hastToHtml,
|
|
7
|
-
makeSingletonHighlighter,
|
|
8
|
-
type ShikiTransformer,
|
|
9
|
-
} from 'shiki/bundle/web'
|
|
10
|
-
|
|
11
|
-
const getHighlighter = makeSingletonHighlighter(createHighlighter)
|
|
12
|
-
|
|
13
|
-
export async function CodeToHtml(props: CodeToHtml.Props) {
|
|
14
|
-
const { code, lang } = props
|
|
15
|
-
const { codeHighlight } = config
|
|
16
|
-
const { langAlias = {}, themes } = codeHighlight
|
|
17
|
-
|
|
18
|
-
const highlighter = await getHighlighter({
|
|
19
|
-
themes: import.meta.env.DEV ? ['none'] : (Object.values(themes) as never),
|
|
20
|
-
langs: import.meta.env.DEV
|
|
21
|
-
? ['txt']
|
|
22
|
-
: ([...Object.keys(bundledLanguages), ...virtualLangs] as never),
|
|
23
|
-
langAlias,
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
const loadedLangs = highlighter.getLoadedLanguages()
|
|
27
|
-
const resolvedLang = loadedLangs.includes(lang) ? lang : 'txt'
|
|
28
|
-
|
|
29
|
-
const hast = highlighter.codeToHast(code, {
|
|
30
|
-
defaultColor: 'light-dark()',
|
|
31
|
-
lang: import.meta.env.DEV ? 'txt' : resolvedLang,
|
|
32
|
-
rootStyle: false,
|
|
33
|
-
meta: {
|
|
34
|
-
'data-v-overflow-fade': true,
|
|
35
|
-
},
|
|
36
|
-
...(import.meta.env.DEV ? { theme: 'none' } : { themes }),
|
|
37
|
-
transformers: [transformerShrinkIndent()],
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
// Add overflow sentinel
|
|
41
|
-
const pre = hast.children[0]
|
|
42
|
-
if (pre && pre.type === 'element' && pre.tagName === 'pre')
|
|
43
|
-
pre.children.push({
|
|
44
|
-
type: 'element',
|
|
45
|
-
tagName: 'div',
|
|
46
|
-
properties: { 'data-v-overflow-sentinel': true },
|
|
47
|
-
children: [],
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
const html = hastToHtml(hast)
|
|
51
|
-
|
|
52
|
-
// biome-ignore lint/security/noDangerouslySetInnerHtml: _
|
|
53
|
-
return <div dangerouslySetInnerHTML={{ __html: html }} />
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export namespace CodeToHtml {
|
|
57
|
-
export type Props = {
|
|
58
|
-
code: string
|
|
59
|
-
lang: string
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function transformerShrinkIndent(): ShikiTransformer {
|
|
64
|
-
return {
|
|
65
|
-
name: 'indent',
|
|
66
|
-
span(hast) {
|
|
67
|
-
const child = hast.children[0]
|
|
68
|
-
if (!child) return
|
|
69
|
-
if (child.type !== 'text') return
|
|
70
|
-
if (!child.value) return
|
|
71
|
-
hast.children[0] = { type: 'text', value: child.value.replace(/\s\s/g, ' ') }
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
}
|
|
1
|
+
export { CodeToHtml } from './CodeToHtml.client.js'
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { Popover } from '@base-ui/react/popover'
|
|
4
4
|
import type * as React from 'react'
|
|
5
|
-
import { useCallback } from 'react'
|
|
5
|
+
import { useCallback, useEffect } from 'react'
|
|
6
|
+
import { prewarm } from './CodeToHtml.client.js'
|
|
6
7
|
|
|
7
8
|
export function TwoslashHover(props: TwoslashHover.Props) {
|
|
8
9
|
const { className = '', children, trigger } = props
|
|
@@ -11,6 +12,12 @@ export function TwoslashHover(props: TwoslashHover.Props) {
|
|
|
11
12
|
|
|
12
13
|
const open = className?.includes('twoslash-query-persisted')
|
|
13
14
|
|
|
15
|
+
// Warm the highlighter so the code inside the popup highlights as quickly as
|
|
16
|
+
// possible (the popup shows a skeleton placeholder until then).
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
prewarm()
|
|
19
|
+
}, [])
|
|
20
|
+
|
|
14
21
|
return (
|
|
15
22
|
<Popover.Root {...(open ? { open } : {})}>
|
|
16
23
|
<Popover.Trigger data-v-twoslash-trigger openOnHover delay={0}>
|
package/src/styles/twoslash.css
CHANGED
|
@@ -63,6 +63,38 @@
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
[data-v-code-skeleton] [data-v-skeleton-bar] {
|
|
67
|
+
display: inline-block;
|
|
68
|
+
height: 0.72em;
|
|
69
|
+
vertical-align: middle;
|
|
70
|
+
border-radius: 0.1875rem;
|
|
71
|
+
background-color: var(--vocs-background-color-surfaceTint);
|
|
72
|
+
background-image: linear-gradient(
|
|
73
|
+
90deg,
|
|
74
|
+
transparent 0%,
|
|
75
|
+
color-mix(in srgb, var(--vocs-color-gray10) 22%, transparent) 50%,
|
|
76
|
+
transparent 100%
|
|
77
|
+
);
|
|
78
|
+
background-repeat: no-repeat;
|
|
79
|
+
background-size: 200% 100%;
|
|
80
|
+
animation: vocs-skeleton-shimmer 1.25s ease-in-out infinite;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@keyframes vocs-skeleton-shimmer {
|
|
84
|
+
from {
|
|
85
|
+
background-position: 180% 0;
|
|
86
|
+
}
|
|
87
|
+
to {
|
|
88
|
+
background-position: -80% 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@media (prefers-reduced-motion: reduce) {
|
|
93
|
+
[data-v-code-skeleton] [data-v-skeleton-bar] {
|
|
94
|
+
animation: none;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
.twoslash-popup-docs {
|
|
67
99
|
@apply vocs:max-h-40 vocs:overflow-y-scroll;
|
|
68
100
|
|