diff --git a/apps/svelte.dev/package.json b/apps/svelte.dev/package.json
index 3c5b747365..74b90edc89 100644
--- a/apps/svelte.dev/package.json
+++ b/apps/svelte.dev/package.json
@@ -64,7 +64,7 @@
"@supabase/supabase-js": "^2.43.4",
"@sveltejs/adapter-vercel": "^5.4.3",
"@sveltejs/enhanced-img": "^0.3.4",
- "@sveltejs/kit": "^2.6.3",
+ "@sveltejs/kit": "^2.7.0",
"@sveltejs/site-kit": "workspace:*",
"@sveltejs/vite-plugin-svelte": "4.0.0-next.6",
"@types/cookie": "^0.6.0",
diff --git a/packages/site-kit/src/lib/components/Text.svelte b/packages/site-kit/src/lib/components/Text.svelte
index b29497c034..d4ab644baa 100644
--- a/packages/site-kit/src/lib/components/Text.svelte
+++ b/packages/site-kit/src/lib/components/Text.svelte
@@ -99,7 +99,7 @@
margin-top: 5rem;
}
- code,
+ code:not(pre *),
kbd {
white-space: pre-wrap;
padding: 0.2rem 0.4rem;
diff --git a/packages/site-kit/src/lib/docs/Tooltip.svelte b/packages/site-kit/src/lib/docs/Tooltip.svelte
index 47150d6676..5c82a7ba2d 100644
--- a/packages/site-kit/src/lib/docs/Tooltip.svelte
+++ b/packages/site-kit/src/lib/docs/Tooltip.svelte
@@ -1,31 +1,46 @@
@@ -34,35 +49,63 @@
{onmouseleave}
role="tooltip"
class="tooltip-container"
+ class:visible
+ style:width
style:left="{x}px"
style:top="{y}px"
- style:--offset="{Math.min(-10, window.innerWidth - (x + width + 10))}px"
+ style:--offset="{offset}px"
>
- {@html html}
+
+ {@html html}
+
diff --git a/packages/site-kit/src/lib/docs/hover.ts b/packages/site-kit/src/lib/docs/hover.ts
index 458c6c8371..7e6b4de6b9 100644
--- a/packages/site-kit/src/lib/docs/hover.ts
+++ b/packages/site-kit/src/lib/docs/hover.ts
@@ -1,56 +1,87 @@
import { mount, onMount, unmount } from 'svelte';
import Tooltip from './Tooltip.svelte';
+const CLASSNAME = 'highlight';
+
export function setupDocsHovers() {
onMount(() => {
let tooltip: any;
+ let hovered: HTMLSpanElement | null = null;
let timeout: NodeJS.Timeout;
+ function clear() {
+ if (!tooltip) return;
+
+ unmount(tooltip);
+ hovered?.classList.remove(CLASSNAME);
+ tooltip = hovered = null;
+ }
+
function over(event: MouseEvent) {
- const target = event.target as HTMLElement;
+ if (event.buttons !== 0) return; // probably selecting
+
+ let target = event.target as HTMLSpanElement;
+
+ if (!target.classList?.contains('twoslash-hover')) {
+ return;
+ }
+
+ clearTimeout(timeout);
- if (target.classList?.contains('twoslash-hover')) {
- clearTimeout(timeout);
+ if (target === hovered) return;
- if (tooltip) {
- unmount(tooltip);
- tooltip = null;
+ clear();
+
+ const container = target.querySelector('.twoslash-popup-container')!;
+
+ const code = container.querySelector('.twoslash-popup-code pre code');
+ if (code && code.children.length === 2) {
+ // for reasons I don't really understand, generated types are duplicated.
+ // this is the easiest way to fix it
+ const [a, b] = code.children;
+ if (a.outerHTML === b.outerHTML) {
+ b.remove();
}
+ }
- const rect = target?.getBoundingClientRect();
- const html = target?.innerHTML;
+ const html = container.innerHTML;
+ if (html) {
+ const rect = target.getBoundingClientRect();
const x = (rect.left + rect.right) / 2 + window.scrollX;
const y = rect.top + window.scrollY;
- if (html) {
- tooltip = mount(Tooltip, {
- target: document.body,
- props: {
- html,
- x,
- y,
- onmouseenter: () => {
- clearTimeout(timeout);
- },
- onmouseleave: () => {
- clearTimeout(timeout);
- unmount(tooltip);
- tooltip = null;
- }
+ tooltip = mount(Tooltip, {
+ target: document.body,
+ props: {
+ html,
+ x,
+ y,
+ onmouseenter: () => {
+ clearTimeout(timeout);
+ },
+ onmouseleave: () => {
+ clearTimeout(timeout);
+ timeout = setTimeout(clear, 0);
}
- });
- }
+ }
+ });
+
+ hovered = target;
+ hovered.classList.add(CLASSNAME);
}
}
function out(event: MouseEvent) {
- const target = event.target as HTMLElement;
- if (target.classList?.contains('twoslash-hover')) {
- timeout = setTimeout(() => {
- unmount(tooltip);
- tooltip = null;
- }, 200);
+ let target = event.target as HTMLElement | null;
+
+ while (target) {
+ if (target.classList.contains('twoslash-hover')) {
+ timeout = setTimeout(clear, 0);
+ return;
+ }
+
+ target = target.parentElement;
}
}
diff --git a/packages/site-kit/src/lib/markdown/renderer.ts b/packages/site-kit/src/lib/markdown/renderer.ts
index eb260ca9bd..d5355076bd 100644
--- a/packages/site-kit/src/lib/markdown/renderer.ts
+++ b/packages/site-kit/src/lib/markdown/renderer.ts
@@ -3,6 +3,7 @@ import { createHash, Hash } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import ts from 'typescript';
+import * as marked from 'marked';
import { codeToHtml, createCssVariablesTheme } from 'shiki';
import { transformerTwoslash } from '@shikijs/twoslash';
import { SHIKI_LANGUAGE_MAP, slugify, smart_quotes, transform } from './utils';
@@ -185,11 +186,11 @@ const snippets = await create_snippet_cache();
export async function render_content_markdown(
filename: string,
body: string,
- options: { check?: boolean },
+ options?: { check?: boolean },
twoslashBanner?: TwoslashBanner
) {
const headings: string[] = [];
- const { check = true } = options;
+ const { check = true } = options ?? {};
return await transform(body, {
async walkTokens(token) {
@@ -675,6 +676,66 @@ async function syntax_highlight({
});
html = html.replace(/ {27,}/g, () => redactions.shift()!);
+
+ if (check) {
+ // munge the twoslash output so that it renders sensibly. the order of operations
+ // here is important — we need to work backwards, to avoid corrupting the offsets
+ const replacements: Array<{ start: number; end: number; content: string }> = [];
+
+ for (const match of html.matchAll(/