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(/
([^]+?)<\/div>/g)) { + const content = await render_content_markdown('', match[1], { check: false }); + + replacements.push({ + start: match.index, + end: match.index + match[0].length, + content: '
' + content + '
' + }); + } + + while (replacements.length > 0) { + const { start, end, content } = replacements.pop()!; + html = html.slice(0, start) + content + html.slice(end); + } + + for (const match of html.matchAll( + /([^]+?)<\/span>([^]+?)<\/span><\/span>/g + )) { + const tag = match[1]; + let value = match[2]; + + let content = `${tag}`; + + if (tag === '@param' || tag === '@throws') { + const words = value.split(' '); + let param = words.shift()!; + value = words.join(' '); + + if (tag === '@throws') { + if (param[0] !== '{' || param[param.length - 1] !== '}') { + throw new Error('TODO robustify @throws handling'); + } + + param = param.slice(1, -1); + } + + content += `${param} `; + } + + content += marked.parseInline(value); + content += ''; + + replacements.push({ + start: match.index, + end: match.index + match[0].length, + content: '
' + content + '
' + }); + } + + while (replacements.length > 0) { + const { start, end, content } = replacements.pop()!; + html = html.slice(0, start) + content + html.slice(end); + } + } } catch (e) { console.error((e as Error).message); console.warn(prelude + redacted); diff --git a/packages/site-kit/src/lib/styles/tokens.css b/packages/site-kit/src/lib/styles/tokens.css index 10707a6793..c8946afac6 100644 --- a/packages/site-kit/src/lib/styles/tokens.css +++ b/packages/site-kit/src/lib/styles/tokens.css @@ -132,7 +132,7 @@ --sk-text-4: hsl(0, 0%, 45%); --sk-text-translucent: hsla(0, 0%, 100%, 0.9); --sk-scrollbar: rgba(255, 255, 255, 0.3); - --sk-shadow: drop-shadow(0px 0px 0 1px var(--sk-back-4)); + --sk-shadow: drop-shadow(1px 2px 16px rgb(0 0 0 / 0.5)); --sk-theme-1-variant: hsl(15, 100%, 40%); --sk-theme-2-variant: hsl(240, 8%, 35%); diff --git a/packages/site-kit/src/lib/styles/utils/twoslash.css b/packages/site-kit/src/lib/styles/utils/twoslash.css index 2fefa12605..74004015fe 100644 --- a/packages/site-kit/src/lib/styles/utils/twoslash.css +++ b/packages/site-kit/src/lib/styles/utils/twoslash.css @@ -1,6 +1,16 @@ .twoslash-hover { position: relative; + &.highlight::after { + /* ensure contiguous hover area */ + content: ''; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: calc(100% + 1rem); + } + .twoslash-popup-container { display: none; } @@ -10,3 +20,19 @@ visibility: hidden; height: 0; } + +.twoslash-popup-container { + display: flex; + flex-direction: column; + font: var(--sk-font-mono); + gap: 1rem; +} + +.twoslash-popup-code { + background: transparent !important; + margin: 0 !important; + font: var(--sk-font-mono); + display: block; + width: 100%; + padding: 0 !important; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e296097944..b197e877bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,7 +73,7 @@ importers: version: 1.22.0(typescript@5.5.4) '@sveltejs/amp': specifier: ^1.1.3 - version: 1.1.3(@sveltejs/kit@2.6.3(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1))) + version: 1.1.3(@sveltejs/kit@2.7.0(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1))) '@sveltejs/repl': specifier: workspace:* version: link:../../packages/repl @@ -91,7 +91,7 @@ importers: version: 3.1.0 '@vercel/speed-insights': specifier: ^1.0.0 - version: 1.0.11(@sveltejs/kit@2.6.3(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264) + version: 1.0.11(@sveltejs/kit@2.7.0(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264) '@webcontainer/api': specifier: ^1.1.5 version: 1.1.9 @@ -149,13 +149,13 @@ importers: version: 2.43.4 '@sveltejs/adapter-vercel': specifier: ^5.4.3 - version: 5.4.3(@sveltejs/kit@2.6.3(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1))) + version: 5.4.3(@sveltejs/kit@2.7.0(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1))) '@sveltejs/enhanced-img': specifier: ^0.3.4 version: 0.3.4(rollup@4.21.2)(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)) '@sveltejs/kit': - specifier: ^2.6.3 - version: 2.6.3(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)) + specifier: ^2.7.0 + version: 2.7.0(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)) '@sveltejs/site-kit': specifier: workspace:* version: link:../../packages/site-kit @@ -1101,8 +1101,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@polka/url@1.0.0-next.25': - resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + '@polka/url@1.0.0-next.28': + resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} '@replit/codemirror-lang-svelte@6.0.0': resolution: {integrity: sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==} @@ -1380,6 +1380,15 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 vite: ^5.0.3 + '@sveltejs/kit@2.7.0': + resolution: {integrity: sha512-4XyY1SCB/Eyz8E9G7SEBKViysYwVtDftuA7kyQ5bmuFNPWC1KZC4988rMvaJxhH2BbCTsbLPjNOZwiEGXt8/2g==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.3 + '@sveltejs/package@2.3.1': resolution: {integrity: sha512-JvR2J4ost1oCn1CSdqenYRwGX/1RX+7LN+VZ71aPnz3JAlIFaEKQd1pBxlb+OSQTfeugJO0W39gB9voAbBO5ow==} engines: {node: ^16.14 || >=18} @@ -2694,6 +2703,9 @@ packages: set-cookie-parser@2.6.0: resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + set-cookie-parser@2.7.0: + resolution: {integrity: sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ==} + sharp@0.33.4: resolution: {integrity: sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==} engines: {libvips: '>=8.15.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3934,7 +3946,7 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 - '@polka/url@1.0.0-next.25': {} + '@polka/url@1.0.0-next.28': {} '@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.28.0)(@lezer/common@1.2.2))(@codemirror/lang-css@6.2.1(@codemirror/view@6.28.0))(@codemirror/lang-html@6.4.9)(@codemirror/lang-javascript@6.2.2)(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.28.0)(@lezer/common@1.2.2)(@lezer/highlight@1.2.1)(@lezer/javascript@1.4.17)(@lezer/lr@1.4.1)': dependencies: @@ -4170,18 +4182,18 @@ snapshots: '@sveltejs/kit': 2.6.3(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.260)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.260)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)) import-meta-resolve: 4.1.0 - '@sveltejs/adapter-vercel@5.4.3(@sveltejs/kit@2.6.3(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))': + '@sveltejs/adapter-vercel@5.4.3(@sveltejs/kit@2.7.0(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))': dependencies: - '@sveltejs/kit': 2.6.3(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)) + '@sveltejs/kit': 2.7.0(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)) '@vercel/nft': 0.27.2 esbuild: 0.21.5 transitivePeerDependencies: - encoding - supports-color - '@sveltejs/amp@1.1.3(@sveltejs/kit@2.6.3(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))': + '@sveltejs/amp@1.1.3(@sveltejs/kit@2.7.0(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))': dependencies: - '@sveltejs/kit': 2.6.3(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)) + '@sveltejs/kit': 2.7.0(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)) '@sveltejs/enhanced-img@0.3.4(rollup@4.21.2)(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1))': dependencies: @@ -4211,7 +4223,7 @@ snapshots: tiny-glob: 0.2.9 vite: 5.4.7(@types/node@20.14.2)(lightningcss@1.25.1) - '@sveltejs/kit@2.6.3(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1))': + '@sveltejs/kit@2.7.0(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1))': dependencies: '@sveltejs/vite-plugin-svelte': 4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)) '@types/cookie': 0.6.0 @@ -4223,7 +4235,7 @@ snapshots: magic-string: 0.30.11 mrmime: 2.0.0 sade: 1.8.1 - set-cookie-parser: 2.6.0 + set-cookie-parser: 2.7.0 sirv: 2.0.4 svelte: 5.0.0-next.264 tiny-glob: 0.2.9 @@ -4382,9 +4394,9 @@ snapshots: - encoding - supports-color - '@vercel/speed-insights@1.0.11(@sveltejs/kit@2.6.3(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)': + '@vercel/speed-insights@1.0.11(@sveltejs/kit@2.7.0(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)': optionalDependencies: - '@sveltejs/kit': 2.6.3(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)) + '@sveltejs/kit': 2.7.0(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)))(svelte@5.0.0-next.264)(vite@5.4.7(@types/node@20.14.2)(lightningcss@1.25.1)) svelte: 5.0.0-next.264 '@vitest/expect@2.1.2': @@ -5546,6 +5558,8 @@ snapshots: set-cookie-parser@2.6.0: {} + set-cookie-parser@2.7.0: {} + sharp@0.33.4: dependencies: color: 4.2.3 @@ -5597,7 +5611,7 @@ snapshots: sirv@2.0.4: dependencies: - '@polka/url': 1.0.0-next.25 + '@polka/url': 1.0.0-next.28 mrmime: 2.0.0 totalist: 3.0.1