From aea25115c38241b96ce0f625220e0cd779ffd3f9 Mon Sep 17 00:00:00 2001 From: brokenbones Date: Fri, 7 Jun 2024 19:40:52 +0800 Subject: [PATCH] feat(twoslash): add line query rendering option for twoslash renderer (#695) Co-authored-by: Anthony Fu --- packages/twoslash/src/renderer-rich.ts | 64 +++++++++++++++++++ packages/twoslash/style-rich.css | 10 ++- .../twoslash/test/out/rich/line-query.html | 48 ++++++++++++++ packages/twoslash/test/rich.test.ts | 38 +++++++++++ 4 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 packages/twoslash/test/out/rich/line-query.html diff --git a/packages/twoslash/src/renderer-rich.ts b/packages/twoslash/src/renderer-rich.ts index f183ae8a..a326edec 100644 --- a/packages/twoslash/src/renderer-rich.ts +++ b/packages/twoslash/src/renderer-rich.ts @@ -81,6 +81,14 @@ export interface RendererRichOptions { */ renderMarkdownInline?: (this: ShikiTransformerContextCommon, markdown: string, context: string) => ElementContent[] + /** + * The way query should be rendered. + * - `'popup'`: Render the query in the absolute popup + * - `'line'`: Render the query line after the line of code + * @default 'popup' + */ + queryRendering?: 'popup' | 'line' + /** * Extensions for the genreated HAST tree. */ @@ -206,6 +214,7 @@ export function rendererRich(options: RendererRichOptions = {}): TwoslashRendere classExtra = '', jsdoc = true, errorRendering = 'line', + queryRendering = 'popup', renderMarkdown = renderMarkdownPassThrough, renderMarkdownInline = renderMarkdownPassThrough, hast, @@ -354,6 +363,22 @@ export function rendererRich(options: RendererRichOptions = {}): TwoslashRendere const themedContent = highlightPopupContent.call(this, query) + if (queryRendering !== 'popup') { + return extend( + hast?.queryToken, + { + type: 'element', + tagName: 'span', + properties: { + class: 'twoslash-hover', + }, + children: [ + node, + ], + }, + ) + } + const popup = extend( hast?.queryPopup, { @@ -567,6 +592,45 @@ export function rendererRich(options: RendererRichOptions = {}): TwoslashRendere ] }, + lineQuery(query, node) { + if (queryRendering !== 'line') + return [] + + const themedContent = highlightPopupContent.call(this, query) + const targetNode = node?.type === 'element' ? node.children[0] : undefined + const targetText = targetNode?.type === 'text' ? targetNode.value : '' + const offset = Math.max(0, (query.character || 0) + Math.floor(targetText.length / 2) - 2) + + return [ + { + type: 'element', + tagName: 'div', + properties: { + class: ['twoslash-meta-line twoslash-query-line', classExtra].filter(Boolean).join(' '), + }, + children: [ + { type: 'text', value: ' '.repeat(offset) }, + { + type: 'element', + tagName: 'span', + properties: { + class: ['twoslash-popup-container', classExtra].filter(Boolean).join(' '), + }, + children: [ + { + type: 'element', + tagName: 'div', + properties: { class: 'twoslash-popup-arrow' }, + children: [], + }, + ...themedContent, + ], + }, + ], + }, + ] + }, + lineError(error) { if (errorRendering !== 'line') return [] diff --git a/packages/twoslash/style-rich.css b/packages/twoslash/style-rich.css index 8df27bfa..9a97e2bf 100644 --- a/packages/twoslash/style-rich.css +++ b/packages/twoslash/style-rich.css @@ -70,7 +70,8 @@ .twoslash .twoslash-hover:hover .twoslash-popup-container, .twoslash .twoslash-error-hover:hover .twoslash-popup-container, -.twoslash .twoslash-query-presisted .twoslash-popup-container { +.twoslash .twoslash-query-presisted .twoslash-popup-container, +.twoslash .twoslash-query-line .twoslash-popup-container { opacity: 1; pointer-events: auto; } @@ -132,6 +133,13 @@ font-family: var(--twoslash-code-font); } +/* ===== Query Line ===== */ +.twoslash .twoslash-query-line .twoslash-popup-container { + position: relative; + margin-bottom: 1.4em; + transform: translateY(0.6em); +} + /* ===== Error Line ===== */ .twoslash .twoslash-error-line { position: relative; diff --git a/packages/twoslash/test/out/rich/line-query.html b/packages/twoslash/test/out/rich/line-query.html new file mode 100644 index 00000000..078aa06a --- /dev/null +++ b/packages/twoslash/test/out/rich/line-query.html @@ -0,0 +1,48 @@ + + + +
interface Todo {
+  /** The title of the todo item */
+  Todo.title: string
The title of the todo item
title
: string;
+} + +const const todo: Readonly<Todo>todo: type Readonly<T> = { readonly [P in keyof T]: T[P]; }
Make all properties in T readonly
Readonly
<Todo> = {
+ title: "Delete inactive users".String.toUpperCase(): string
Converts all the alphabetic characters in a string to uppercase.
toUpperCase
(),
title: string
The title of the todo item
}; + +const todo: Readonly<Todo>todo.title = "Hello";
Cannot assign to 'title' because it is a read-only property.
+var Number: NumberConstructor
An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
Number
.p
  • parseFloat
  • parseInt
  • prototype
NumberConstructor.parseInt(string: string, radix?: number | undefined): number
Converts A string to an integer.
@paramstring A string to convert into a number.@paramradix A value between 2 and 36 that specifies the base of the number in `string`. +If this argument is not supplied, strings with a prefix of '0x' are considered hexadecimal. +All other strings are considered decimal.
arseInt
(const todo: Readonly<Todo>todo.title: string
The title of the todo item
title
, 10);
+
+ +
+ +
diff --git a/packages/twoslash/test/rich.test.ts b/packages/twoslash/test/rich.test.ts index c32ec0ac..0b95a630 100644 --- a/packages/twoslash/test/rich.test.ts +++ b/packages/twoslash/test/rich.test.ts @@ -186,3 +186,41 @@ const c = 1 + colorToggle, ).toMatchFileSnapshot('./out/rich/custom-tags.html') }) + +it('line-query', async () => { + const code = ` +// @errors: 2540 +interface Todo { + /** The title of the todo item */ + title: string; +} + +const todo: Readonly = { + title: "Delete inactive users".toUpperCase(), +// ^? +}; + +todo.title = "Hello"; + +Number.parseInt(todo.title, 10); +// ^| +`.trim() + + const htmlWithSeparateLine = await codeToHtml(code, { + lang: 'ts', + themes: { + dark: 'vitesse-dark', + light: 'vitesse-light', + }, + defaultColor: false, + transformers: [ + transformerTwoslash({ + renderer: rendererRich({ + queryRendering: 'line', + }), + }), + ], + }) + + expect(styleTag + htmlWithSeparateLine + colorToggle).toMatchFileSnapshot('./out/rich/line-query.html') +})