diff --git a/site/content/docs/02-template-syntax/01-svelte-components.md b/site/content/docs/02-template-syntax/01-svelte-components.md index d1f353629b..14c83177c4 100644 --- a/site/content/docs/02-template-syntax/01-svelte-components.md +++ b/site/content/docs/02-template-syntax/01-svelte-components.md @@ -52,10 +52,12 @@ In development mode (see the [compiler options](/docs/svelte-compiler#svelte-com If you export a `const`, `class` or `function`, it is readonly from outside the component. Functions are valid prop values, however, as shown below. ```svelte + @@ -302,9 +309,11 @@ bind:this={dom_node} To get a reference to a DOM node, use `bind:this`. ```svelte + @@ -198,6 +199,8 @@ If `this` is the name of a [void element](https://developer.mozilla.org/en-US/do ```svelte @@ -220,6 +223,7 @@ Unlike ``, this element may only appear at the top level of your co ```svelte ``` + +## Types + +> TYPES: svelte diff --git a/site/content/docs/03-runtime/02-svelte-store.md b/site/content/docs/03-runtime/02-svelte-store.md index cb0a8a448d..fd5ac4c8df 100644 --- a/site/content/docs/03-runtime/02-svelte-store.md +++ b/site/content/docs/03-runtime/02-svelte-store.md @@ -10,13 +10,7 @@ This makes it possible to wrap almost any other reactive state handling library ## `writable` -```js -store = writable(value?: any) -``` - -```js -store = writable(value?: any, start?: (set: (value: any) => void) => () => void) -``` +> EXPORT_SNIPPET: svelte/store#writable Function that creates a store which has values that can be set from 'outside' components. It gets created as an object with additional `set` and `update` methods. @@ -25,6 +19,7 @@ Function that creates a store which has values that can be set from 'outside' co `update` is a method that takes one argument which is a callback. The callback takes the existing store value as its argument and returns the new value to be set to the store. ```js +/// file: store.js import { writable } from 'svelte/store'; const count = writable(0); @@ -41,6 +36,7 @@ count.update((n) => n + 1); // logs '2' If a function is passed as the second argument, it will be called when the number of subscribers goes from zero to one (but not from one to two, etc). That function will be passed a `set` function which changes the value of the store. It must return a `stop` function that is called when the subscriber count goes from one to zero. ```js +/// file: store.js import { writable } from 'svelte/store'; const count = writable(0, () => { @@ -61,16 +57,15 @@ Note that the value of a `writable` is lost when it is destroyed, for example wh ## `readable` -```js -store = readable(value?: any, start?: (set: (value: any) => void) => () => void) -``` +> EXPORT_SNIPPET: svelte/store#readable Creates a store whose value cannot be set from 'outside', the first argument is the store's initial value, and the second argument to `readable` is the same as the second argument to `writable`. ```js +/// file: store.js import { readable } from 'svelte/store'; -const time = readable(null, (set) => { +const time = readable(new Date(), (set) => { set(new Date()); const interval = setInterval(() => { @@ -83,27 +78,24 @@ const time = readable(null, (set) => { ## `derived` -```js -store = derived(a, callback: (a: any) => any) -``` +> EXPORT_SNIPPET: svelte/store#derived -```js -store = derived(a, callback: (a: any, set: (value: any) => void) => void | () => void, initial_value: any) -``` +Derives a store from one or more other stores. The callback runs initially when the first subscriber subscribes and then whenever the store dependencies change. -```js -store = derived([a, ...b], callback: ([a: any, ...b: any[]]) => any) -``` +In the simplest version, `derived` takes a single store, and the callback returns a derived value. -```js -store = derived([a, ...b], callback: ([a: any, ...b: any[]], set: (value: any) => void) => void | () => void, initial_value: any) -``` +```ts +// @filename: ambient.d.ts +import { type Writable } from 'svelte/store'; -Derives a store from one or more other stores. The callback runs initially when the first subscriber subscribes and then whenever the store dependencies change. +declare global { + const a: Writable; +} -In the simplest version, `derived` takes a single store, and the callback returns a derived value. +export {}; -```js +// @filename: index.ts +// ---cut--- import { derived } from 'svelte/store'; const doubled = derived(a, ($a) => $a * 2); @@ -114,6 +106,17 @@ The callback can set a value asynchronously by accepting a second argument, `set In this case, you can also pass a third argument to `derived` — the initial value of the derived store before `set` is first called. ```js +// @filename: ambient.d.ts +import { type Writable } from 'svelte/store'; + +declare global { + const a: Writable; +} + +export {}; + +// @filename: index.ts +// ---cut--- import { derived } from 'svelte/store'; const delayed = derived( @@ -121,13 +124,24 @@ const delayed = derived( ($a, set) => { setTimeout(() => set($a), 1000); }, - 'one moment...' + 2000 ); ``` If you return a function from the callback, it will be called when a) the callback runs again, or b) the last subscriber unsubscribes. ```js +// @filename: ambient.d.ts +import { type Writable } from 'svelte/store'; + +declare global { + const frequency: Writable; +} + +export {}; + +// @filename: index.ts +// ---cut--- import { derived } from 'svelte/store'; const tick = derived( @@ -141,13 +155,26 @@ const tick = derived( clearInterval(interval); }; }, - 'one moment...' + 2000 ); ``` In both cases, an array of arguments can be passed as the first argument instead of a single store. -```js +```ts +// @filename: ambient.d.ts +import { type Writable } from 'svelte/store'; + +declare global { + const a: Writable; + const b: Writable; +} + +export {}; + +// @filename: index.ts + +// ---cut--- import { derived } from 'svelte/store'; const summed = derived([a, b], ([$a, $b]) => $a + $b); @@ -159,14 +186,12 @@ const delayed = derived([a, b], ([$a, $b], set) => { ## `readonly` -```js -readableStore = readonly(writableStore); -``` +> EXPORT_SNIPPET: svelte/store#readonly This simple helper function makes a store readonly. You can still subscribe to the changes from the original one using this new readable store. ```js -import { readonly } from 'svelte/store'; +import { readonly, writable } from 'svelte/store'; const writableStore = writable(1); const readableStore = readonly(writableStore); @@ -174,21 +199,35 @@ const readableStore = readonly(writableStore); readableStore.subscribe(console.log); writableStore.set(2); // console: 2 +// @errors: 2339 readableStore.set(2); // ERROR ``` ## `get` -```js -value: any = get(store); -``` +> EXPORT_SNIPPET: svelte/store#get Generally, you should read the value of a store by subscribing to it and using the value as it changes over time. Occasionally, you may need to retrieve the value of a store to which you're not subscribed. `get` allows you to do so. > This works by creating a subscription, reading the value, then unsubscribing. It's therefore not recommended in hot code paths. ```js +// @filename: ambient.d.ts +import { type Writable } from 'svelte/store'; + +declare global { + const store: Writable; +} + +export {}; + +// @filename: index.ts +// ---cut--- import { get } from 'svelte/store'; const value = get(store); ``` + +## Types + +> TYPES: svelte/store diff --git a/site/content/docs/03-runtime/03-svelte-motion.md b/site/content/docs/03-runtime/03-svelte-motion.md index 4cab7725cc..0351d89586 100644 --- a/site/content/docs/03-runtime/03-svelte-motion.md +++ b/site/content/docs/03-runtime/03-svelte-motion.md @@ -6,9 +6,7 @@ The `svelte/motion` module exports two functions, `tweened` and `spring`, for cr ## `tweened` -```js -store = tweened(value: any, options) -``` +> EXPORT_SNIPPET: svelte/motion#tweened Tweened stores update their values over a fixed duration. The following options are available: @@ -46,7 +44,19 @@ Out of the box, Svelte will interpolate between two numbers, two arrays or two o If the initial value is `undefined` or `null`, the first value change will take effect immediately. This is useful when you have tweened values that are based on props, and don't want any motion when the component first renders. -```js +```ts +// @filename: ambient.d.ts +declare global { + var $size: number; + var big: number; +} + +export {}; +// @filename: motion.ts +// ---cut--- +import { tweened } from 'svelte/motion'; +import { cubicOut } from 'svelte/easing'; + const size = tweened(undefined, { duration: 300, easing: cubicOut @@ -81,9 +91,7 @@ The `interpolate` option allows you to tween between _any_ arbitrary values. It ## `spring` -```js -store = spring(value: any, options) -``` +> EXPORT_SNIPPET: svelte/motion#spring A `spring` store gradually changes to its target value based on its `stiffness` and `damping` parameters. Whereas `tweened` stores change their values over a fixed duration, `spring` stores change over a duration that is determined by their existing velocity, allowing for more natural-seeming motion in many situations. The following options are available: @@ -94,6 +102,8 @@ A `spring` store gradually changes to its target value based on its `stiffness` All of the options above can be changed while the spring is in motion, and will take immediate effect. ```js +import { spring } from 'svelte/motion'; + const size = spring(100); size.stiffness = 0.3; size.damping = 0.4; @@ -105,6 +115,8 @@ As with [`tweened`](/docs/svelte-motion#tweened) stores, `set` and `update` retu Both `set` and `update` can take a second argument — an object with `hard` or `soft` properties. `{ hard: true }` sets the target value immediately; `{ soft: n }` preserves existing momentum for `n` seconds before settling. `{ soft: true }` is equivalent to `{ soft: 0.5 }`. ```js +import { spring } from 'svelte/motion'; + const coords = spring({ x: 50, y: 50 }); // updates the value immediately coords.set({ x: 100, y: 200 }, { hard: true }); @@ -135,7 +147,23 @@ coords.update( If the initial value is `undefined` or `null`, the first value change will take effect immediately, just as with `tweened` values (see above). -```js +```ts +// @filename: ambient.d.ts +declare global { + var $size: number; + var big: number; +} + +export {}; + +// @filename: motion.ts +// ---cut--- +import { spring } from 'svelte/motion'; + const size = spring(); $: $size = big ? 100 : 10; ``` + +## Types + +> TYPES: svelte/motion diff --git a/site/content/docs/03-runtime/04-svelte-transition.md b/site/content/docs/03-runtime/04-svelte-transition.md index bc33c6ee17..bd8175ea36 100644 --- a/site/content/docs/03-runtime/04-svelte-transition.md +++ b/site/content/docs/03-runtime/04-svelte-transition.md @@ -6,6 +6,8 @@ The `svelte/transition` module exports seven functions: `fade`, `blur`, `fly`, ` ## `fade` +> EXPORT_SNIPPET: svelte/transition#fade + ```svelte transition:fade={params} ``` @@ -40,6 +42,8 @@ You can see the `fade` transition in action in the [transition tutorial](/tutori ## `blur` +> EXPORT_SNIPPET: svelte/transition#blur + ```svelte transition:blur={params} ``` @@ -74,6 +78,8 @@ Animates a `blur` filter alongside an element's opacity. ## `fly` +> EXPORT_SNIPPET: svelte/transition#fly + ```svelte transition:fly={params} ``` @@ -117,6 +123,8 @@ You can see the `fly` transition in action in the [transition tutorial](/tutoria ## `slide` +> EXPORT_SNIPPET: svelte/transition#slide + ```svelte transition:slide={params} ``` @@ -154,6 +162,8 @@ Slides an element in and out. ## `scale` +> EXPORT_SNIPPET: svelte/transition#scale + ```svelte transition:scale={params} ``` @@ -191,6 +201,8 @@ Animates the opacity and scale of an element. `in` transitions animate from an e ## `draw` +> EXPORT_SNIPPET: svelte/transition#draw + ```svelte transition:draw={params} ``` @@ -236,6 +248,8 @@ The `speed` parameter is a means of setting the duration of the transition relat ## `crossfade` +> EXPORT_SNIPPET: svelte/transition#crossfade + The `crossfade` function creates a pair of [transitions](/docs/element-directives#transition-fn) called `send` and `receive`. When an element is 'sent', it looks for a corresponding element being 'received', and generates a transition that transforms the element to its counterpart's position and fades it out. When an element is 'received', the reverse happens. If there is no counterpart, the `fallback` transition is used. `crossfade` accepts the following parameters: @@ -262,3 +276,7 @@ The `crossfade` function creates a pair of [transitions](/docs/element-directive small elem {/if} ``` + +## Types + +> TYPES: svelte/transition diff --git a/site/content/docs/03-runtime/05-svelte-animate.md b/site/content/docs/03-runtime/05-svelte-animate.md index b9d71cee0b..f0ad868dd3 100644 --- a/site/content/docs/03-runtime/05-svelte-animate.md +++ b/site/content/docs/03-runtime/05-svelte-animate.md @@ -6,6 +6,8 @@ The `svelte/animate` module exports one function for use with Svelte [animations ## `flip` +> EXPORT_SNIPPET: svelte/animate#flip + ```svelte animate:flip={params} ``` @@ -39,3 +41,7 @@ You can see a full example on the [animations tutorial](/tutorial/animate) {/each} ``` + +## Types + +> TYPES: svelte/animate diff --git a/site/content/docs/03-runtime/07-actions.md b/site/content/docs/03-runtime/07-actions.md deleted file mode 100644 index dcee631fa4..0000000000 --- a/site/content/docs/03-runtime/07-actions.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Actions -draft: true ---- - -TODO diff --git a/site/content/docs/03-runtime/07-svelte-action.md b/site/content/docs/03-runtime/07-svelte-action.md new file mode 100644 index 0000000000..cfd0700bc3 --- /dev/null +++ b/site/content/docs/03-runtime/07-svelte-action.md @@ -0,0 +1,9 @@ +--- +title: svelte/action +--- + +TODO + +## Types + +> TYPES: svelte/action diff --git a/site/content/docs/04-compiler-and-api/01-svelte-compiler.md b/site/content/docs/04-compiler-and-api/01-svelte-compiler.md index 092ef6abbf..326ddeb2b4 100644 --- a/site/content/docs/04-compiler-and-api/01-svelte-compiler.md +++ b/site/content/docs/04-compiler-and-api/01-svelte-compiler.md @@ -8,23 +8,23 @@ Nonetheless, it's useful to understand how to use the compiler, since bundler pl ## `svelte.compile` -```js -result: { - js, - css, - ast, - warnings, - vars, - stats -} = svelte.compile(source: string, options?: {...}) -``` +> EXPORT_SNIPPET: svelte/compiler#compile This is where the magic happens. `svelte.compile` takes your component source code, and turns it into a JavaScript module that exports a class. ```js -import svelte from 'svelte/compiler'; +// @filename: ambient.d.ts +declare global { + var source: string +} + +export {} + +// @filename: index.ts +// ---cut--- +import { compile } from 'svelte/compiler'; -const result = svelte.compile(source, { +const result = compile(source, { // options }); ``` @@ -83,8 +83,18 @@ The following options can be passed to the compiler. None are required: The returned `result` object contains the code for your component, along with useful bits of metadata. -```js -const { js, css, ast, warnings, vars, stats } = svelte.compile(source); +```ts +// @filename: ambient.d.ts +declare global { + const source: string; +} + +export {}; + +// @filename: main.ts +import { compile } from 'svelte/compiler'; +// ---cut--- +const { js, css, ast, warnings, vars, stats } = compile(source); ``` - `js` and `css` are objects with the following properties: @@ -147,56 +157,33 @@ compiled: { ## `svelte.parse` -```js -ast: object = svelte.parse( - source: string, - options?: { - filename?: string, - customElement?: boolean - } -) -``` +> EXPORT_SNIPPET: svelte/compiler#parse The `parse` function parses a component, returning only its abstract syntax tree. Unlike compiling with the `generate: false` option, this will not perform any validation or other analysis of the component beyond parsing it. Note that the returned AST is not considered public API, so breaking changes could occur at any point in time. ```js -import svelte from 'svelte/compiler'; +// @filename: ambient.d.ts +declare global { + var source: string; +} + +export {}; + +// @filename: main.ts +// ---cut--- +import { parse } from 'svelte/compiler'; -const ast = svelte.parse(source, { filename: 'App.svelte' }); +const ast = parse(source, { filename: 'App.svelte' }); ``` ## `svelte.preprocess` +> EXPORT_SNIPPET: svelte/compiler#preprocess + A number of [community-maintained preprocessing plugins](https://sveltesociety.dev/tools#preprocessors) are available to allow you to use Svelte with tools like TypeScript, PostCSS, SCSS, and Less. You can write your own preprocessor using the `svelte.preprocess` API. -```js -result: { - code: string, - dependencies: Array -} = await svelte.preprocess( - source: string, - preprocessors: Array<{ - markup?: (input: { content: string, filename: string }) => Promise<{ - code: string, - dependencies?: Array - }>, - script?: (input: { content: string, markup: string, attributes: Record, filename: string }) => Promise<{ - code: string, - dependencies?: Array - }>, - style?: (input: { content: string, markup: string, attributes: Record, filename: string }) => Promise<{ - code: string, - dependencies?: Array - }> - }>, - options?: { - filename?: string - } -) -``` - The `preprocess` function provides convenient hooks for arbitrarily transforming component source code. For example, it can be used to convert a ` + %sveltekit.head% diff --git a/sites/svelte.dev/src/lib/server/docs/index.js b/sites/svelte.dev/src/lib/server/docs/index.js index 53c8121687..978f625e8f 100644 --- a/sites/svelte.dev/src/lib/server/docs/index.js +++ b/sites/svelte.dev/src/lib/server/docs/index.js @@ -1,14 +1,37 @@ -// import 'prism-svelte'; -// import 'prismjs/components/prism-bash.js'; -// import 'prismjs/components/prism-diff.js'; -// import 'prismjs/components/prism-typescript.js'; -import { createShikiHighlighter } from 'shiki-twoslash'; -import { SHIKI_LANGUAGE_MAP, normalizeSlugify, transform } from '../markdown'; -// import { render, replace_placeholders } from './render.js'; -// import { parse_route_id } from '../../../../../../packages/kit/src/utils/routing.js'; +import { modules } from '$lib/generated/type-info'; import { createHash } from 'crypto'; +import fs from 'fs'; import MagicString from 'magic-string'; +import { createShikiHighlighter, renderCodeToHTML, runTwoSlash } from 'shiki-twoslash'; import ts from 'typescript'; +import { SHIKI_LANGUAGE_MAP, escape, normalizeSlugify, slugify, transform } from '../markdown'; +import { replace_placeholders } from './render.js'; + +const METADATA_REGEX = /(?:|\/\/\/\s*([\w-]+):\s*(.*))\n/gm; + +const snippet_cache = new URL('../../../../node_modules/.snippets', import.meta.url).pathname; +if (!fs.existsSync(snippet_cache)) { + fs.mkdirSync(snippet_cache, { recursive: true }); +} + +const type_regex = new RegExp( + `(import\\('svelte'\\)\\.)?\\b(${modules + .flatMap((module) => module.types) + .map((type) => type.name) + .join('|')})\\b`, + 'g' +); + +const type_links = new Map(); + +modules.forEach((module) => { + const slug = slugify(module.name); + + module.types.forEach((type) => { + const link = `/docs/${slug}#type-${slugify(type.name)}`; + type_links.set(type.name, link); + }); +}); /** * @param {import('./types').DocsData} docs_data @@ -23,20 +46,23 @@ export async function get_parsed_docs(docs_data, slug) { const highlighter = await createShikiHighlighter({ theme: 'css-variables' }); + const placeholders_replaced_content = replace_placeholders(page.content); + return { ...page, content: parse({ file: page.file, - body: generate_ts_from_js(page.content), + body: generate_ts_from_js(placeholders_replaced_content), code: (source, language, current) => { const hash = createHash('sha256'); hash.update(source + language + current); const digest = hash.digest().toString('base64').replace(/\//g, '-'); - // TODO: cache - // if (fs.existsSync(`${snippet_cache}/${digest}.html`)) { - // return fs.readFileSync(`${snippet_cache}/${digest}.html`, 'utf-8'); - // } + try { + if (fs.existsSync(`${snippet_cache}/${digest}.html`)) { + return fs.readFileSync(`${snippet_cache}/${digest}.html`, 'utf-8'); + } + } catch {} /** @type {Record} */ const options = {}; @@ -44,10 +70,13 @@ export async function get_parsed_docs(docs_data, slug) { let html = ''; source = source - .replace(/^\/\/\/ (.+?): (.+)\n/gm, (_, key, value) => { - options[key] = value; - return ''; - }) + .replace( + /(?:|\/\/\/\s*([\w-]+):\s*(.*))\n/gm, + (_, key, value) => { + options[key] = value; + return ''; + } + ) .replace(/^([\-\+])?((?: )+)/gm, (match, prefix = '', spaces) => { if (prefix && language !== 'diff') return match; @@ -69,123 +98,123 @@ export async function get_parsed_docs(docs_data, slug) { version_class = 'js-version'; } - // TODO: Replace later - html = highlighter.codeToHtml(source, { lang: SHIKI_LANGUAGE_MAP[language] }); - - // if (source.includes('$env/')) { - // // TODO we're hardcoding static env vars that are used in code examples - // // in the types, which isn't... totally ideal, but will do for now - // injected.push( - // `declare module '$env/dynamic/private' { export const env: Record }`, - // `declare module '$env/dynamic/public' { export const env: Record }`, - // `declare module '$env/static/private' { export const API_KEY: string }`, - // `declare module '$env/static/public' { export const PUBLIC_BASE_URL: string }` - // ); - // } - - // if (source.includes('./$types') && !source.includes('@filename: $types.d.ts')) { - // const params = parse_route_id(options.file || `+page.${language}`) - // .params.map((param) => `${param.name}: string`) - // .join(', '); - - // injected.push( - // `// @filename: $types.d.ts`, - // `import type * as Kit from '@sveltejs/kit';`, - // `export type PageLoad = Kit.Load<{${params}}>;`, - // `export type PageServerLoad = Kit.ServerLoad<{${params}}>;`, - // `export type LayoutLoad = Kit.Load<{${params}}>;`, - // `export type LayoutServerLoad = Kit.ServerLoad<{${params}}>;`, - // `export type RequestHandler = Kit.RequestHandler<{${params}}>;`, - // `export type Action = Kit.Action<{${params}}>;`, - // `export type Actions = Kit.Actions<{${params}}>;` - // ); - // } - - // // special case — we need to make allowances for code snippets coming - // // from e.g. ambient.d.ts - // if (file.endsWith('30-modules.md')) { - // injected.push('// @errors: 7006 7031'); - // } - - // // another special case - // if (source.includes('$lib/types')) { - // injected.push(`declare module '$lib/types' { export interface User {} }`); - // } - - // if (injected.length) { - // const injected_str = injected.join('\n'); - // if (source.includes('// @filename:')) { - // source = source.replace('// @filename:', `${injected_str}\n\n// @filename:`); - // } else { - // source = source.replace( - // /^(?!\/\/ @)/m, - // `${injected_str}\n\n// @filename: index.${language}\n// ---cut---\n` - // ); - // } - // } - - // const twoslash = runTwoSlash(source, language, { - // defaultCompilerOptions: { - // allowJs: true, - // checkJs: true, - // target: 'es2021', - // }, - // }); - - // html = renderCodeToHTML( - // twoslash.code, - // 'ts', - // { twoslash: true }, - // {}, - // highlighter, - // twoslash - // ); - // } catch (e) { - // console.error(`Error compiling snippet in ${file}`); - // console.error(e.code); - // throw e; - // } - - // // we need to be able to inject the LSP attributes as HTML, not text, so we - // // turn < into &lt; - // html = html.replace( - // /]*)>(\w+)<\/data-lsp>/g, - // (match, lsp, attrs, name) => { - // if (!lsp) return name; - // return `${name}`; - // } - // ); - - // // preserve blank lines in output (maybe there's a more correct way to do this?) - // html = html.replace(/
<\/div>/g, '
'); - // } else if (language === 'diff') { - // const lines = source.split('\n').map((content) => { - // let type = null; - // if (/^[\+\-]/.test(content)) { - // type = content[0] === '+' ? 'inserted' : 'deleted'; - // content = content.slice(1); - // } - - // return { - // type, - // content: escape(content), - // }; - // }); - - // html = `
${lines
-				// 		.map((line) => {
-				// 			if (line.type) return `${line.content}\n`;
-				// 			return line.content + '\n';
-				// 		})
-				// 		.join('')}
`; - // } else { - // const plang = languages[language]; - // const highlighted = plang - // ? PrismJS.highlight(source, PrismJS.languages[plang], language) - // : source.replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c])); - - // html = `
${highlighted}
`; - // } + if (language === 'dts') { + html = renderCodeToHTML( + source, + 'ts', + { twoslash: false }, + { themeName: 'css-variables' }, + highlighter + ); + } else if (language === 'js' || language === 'ts') { + try { + const injected = []; + + if (/(svelte)/.test(source) || page.file.includes('typescript')) { + injected.push( + `// @filename: ambient.d.ts`, + `/// `, + `/// `, + `/// `, + `/// `, + `/// `, + `/// `, + `/// `, + `/// ` + ); + } + + if (page.file.includes('svelte-compiler')) { + injected.push('// @esModuleInterop'); + } + + if (page.file.includes('svelte.md')) { + injected.push('// @errors: 2304'); + } + + // Actions JSDoc examples are invalid. Too many errors, edge cases + if (page.file.includes('svelte-action')) { + injected.push('// @noErrors'); + } + + if (page.file.includes('typescript')) { + injected.push('// @errors: 2304'); + } + + if (injected.length) { + const injected_str = injected.join('\n'); + if (source.includes('// @filename:')) { + source = source.replace('// @filename:', `${injected_str}\n\n// @filename:`); + } else { + source = source.replace( + /^(?!\/\/ @)/m, + `${injected_str}\n\n// @filename: index.${language}\n` + ` // ---cut---\n` + ); + } + } + + const twoslash = runTwoSlash(source, language, { + defaultCompilerOptions: { + allowJs: true, + checkJs: true, + target: ts.ScriptTarget.ES2022 + } + }); + + html = renderCodeToHTML( + twoslash.code, + 'ts', + { twoslash: true }, + // @ts-ignore Why shiki-twoslash requires a theme name? + {}, + highlighter, + twoslash + ); + } catch (e) { + console.error(`Error compiling snippet in ${page.file}`); + console.error(e.code); + throw e; + } + + // we need to be able to inject the LSP attributes as HTML, not text, so we + // turn < into &lt; + html = html.replace( + /]*)>(\w+)<\/data-lsp>/g, + (match, lsp, attrs, name) => { + if (!lsp) return name; + return `${name}`; + } + ); + + // preserve blank lines in output (maybe there's a more correct way to do this?) + html = html.replace(/
<\/div>/g, '
'); + } else if (language === 'diff') { + const lines = source.split('\n').map((content) => { + let type = null; + if (/^[\+\-]/.test(content)) { + type = content[0] === '+' ? 'inserted' : 'deleted'; + content = content.slice(1); + } + + return { + type, + content: escape(content) + }; + }); + + html = `
${lines
+						.map((line) => {
+							if (line.type) return `${line.content}\n`;
+							return line.content + '\n';
+						})
+						.join('')}
`; + } else { + const highlighted = highlighter.codeToHtml(source, { + lang: SHIKI_LANGUAGE_MAP[language] + }); + + html = highlighted.replace(/
<\/div>/g, '
'); + } if (options.file) { html = `
${options.file}${html}
`; @@ -195,18 +224,20 @@ export async function get_parsed_docs(docs_data, slug) { html = html.replace(/class=('|")/, `class=$1${version_class} `); } - // type_regex.lastIndex = 0; + type_regex.lastIndex = 0; html = html - // .replace(type_regex, (match, prefix, name) => { - // if (options.link === 'false' || name === current) { - // // we don't want e.g. RequestHandler to link to RequestHandler - // return match; - // } - - // const link = `${name}`; - // return `${prefix || ''}${link}`; - // }) + .replace(type_regex, (match, prefix, name, pos, str) => { + const char_after = str.slice(pos + match.length, pos + match.length + 1); + + if (options.link === 'false' || name === current || /(\$|\d|\w)/.test(char_after)) { + // we don't want e.g. RequestHandler to link to RequestHandler + return match; + } + + const link = `${name}`; + return `${prefix || ''}${link}`; + }) .replace( /^(\s+)([\s\S]+?)<\/span>\n/gm, (match, intro_whitespace, content) => { @@ -226,17 +257,16 @@ export async function get_parsed_docs(docs_data, slug) { ) .replace(/\/\*…\*\//g, '…'); - // fs.writeFileSync(`${snippet_cache}/${digest}.html`, html); + fs.writeFileSync(`${snippet_cache}/${digest}.html`, html); return html; }, codespan: (text) => { return ( '' + - text + - // text.replace(type_regex, (match, prefix, name) => { - // const link = `${name}`; - // return `${prefix || ''}${link}`; - // }) + + text.replace(type_regex, (match, prefix, name) => { + const link = `${name}`; + return `${prefix || ''}${link}`; + }) + '' ); } @@ -279,9 +309,16 @@ function parse({ body, code, codespan }) { headings[level] = normalized; headings.length = level; - const slug = normalizeSlugify(raw); + const type_heading_match = /^\[TYPE\]:\s+(.+)/.exec(raw); - return `${html}`; + const slug = normalizeSlugify(type_heading_match ? `type-${type_heading_match[1]}` : raw); + + return `${html + .replace(/<\/?code>/g, '') + .replace( + /^\[TYPE\]:\s+(.+)/, + '$1' + )}`; }, code: (source, language) => code(source, language, current), codespan @@ -313,7 +350,7 @@ export function generate_ts_from_js(markdown) { return match.replace('js', 'original-js') + '\n```generated-ts\n' + ts + '\n```'; }) .replaceAll(/```svelte\n([\s\S]+?)\n```/g, (match, code) => { - if (!code.includes('/// file:')) { + if (!METADATA_REGEX.test(code)) { // No named file -> assume that the code is not meant to be shown in two versions return match; } @@ -333,7 +370,7 @@ export function generate_ts_from_js(markdown) { return ( match.replace('svelte', 'original-svelte') + '\n```generated-svelte\n' + - code.replace(outer, ``) + + code.replace(outer, ``) + '\n```' ); }); @@ -404,7 +441,7 @@ function convert_to_ts(js_code, indent = '', offset = '') { if (variable_statement.name.getText() === 'actions') { code.appendLeft(variable_statement.getEnd(), ` satisfies ${name}`); } else { - code.appendLeft(variable_statement.name.getEnd(), `: ${name}`); + code.appendLeft(variable_statement.name.getEnd(), `: ${name}${generics ?? ''}`); } modified = true; @@ -412,11 +449,11 @@ function convert_to_ts(js_code, indent = '', offset = '') { throw new Error('Unhandled @type JsDoc->TS conversion: ' + js_code); } } else if (ts.isJSDocParameterTag(tag) && ts.isFunctionDeclaration(node)) { - if (node.parameters.length !== 1) { - throw new Error( - 'Unhandled @type JsDoc->TS conversion; needs more params logic: ' + node.getText() - ); - } + // if (node.parameters.length !== 1) { + // throw new Error( + // 'Unhandled @type JsDoc->TS conversion; needs more params logic: ' + node.getText() + // ); + // } const [name] = get_type_info(tag); code.appendLeft(node.parameters[0].getEnd(), `: ${name}`); diff --git a/sites/svelte.dev/src/lib/server/docs/render.js b/sites/svelte.dev/src/lib/server/docs/render.js new file mode 100644 index 0000000000..abe1fa9c1c --- /dev/null +++ b/sites/svelte.dev/src/lib/server/docs/render.js @@ -0,0 +1,182 @@ +import { modules } from '$lib/generated/type-info.js'; + +/** @param {string} content */ +export function replace_placeholders(content) { + return content + .replace(/> EXPANDED_TYPES: (.+?)#(.+)$/gm, (_, name, id) => { + const module = modules.find((module) => module.name === name); + if (!module) throw new Error(`Could not find module ${name}`); + + const type = module.types.find((t) => t.name === id); + + return ( + type.comment + + type.children + .map((child) => { + let section = `### ${child.name}`; + + if (child.bullets) { + section += `\n\n
\n\n${child.bullets.join( + '\n' + )}\n\n
`; + } + + section += `\n\n${child.comment}`; + + if (child.children) { + section += `\n\n
\n\n${child.children + .map(stringify) + .join('\n')}\n\n
`; + } + + return section; + }) + .join('\n\n') + ); + }) + .replace(/> TYPES: (.+?)(?:#(.+))?$/gm, (_, name, id) => { + const module = modules.find((module) => module.name === name); + if (!module) throw new Error(`Could not find module ${name}`); + + if (id) { + const type = module.types.find((t) => t.name === id); + + return ( + `
${fence(type.snippet, 'dts')}` + + type.children.map(stringify).join('\n\n') + + `
` + ); + } + + return `${module.comment}\n\n${module.types + .map((t) => { + let children = t.children.map((val) => stringify(val, 'dts')).join('\n\n'); + + const markdown = `
${fence(t.snippet, 'dts')}` + children + `
`; + return `### [TYPE]: ${t.name}\n\n${t.comment}\n\n${markdown}\n\n`; + }) + .join('')}`; + }) + .replace(/> EXPORT_SNIPPET: (.+?)#(.+)?$/gm, (_, name, id) => { + const module = modules.find((module) => module.name === name); + if (!module) throw new Error(`Could not find module ${name} for EXPORT_SNIPPET clause`); + + if (!id) { + throw new Error(`id is required for module ${name}`); + } + + const exported = module.exports.filter((t) => t.name === id); + + return exported + .map((exportVal) => `
${fence(exportVal.snippet, 'dts')}
`) + .join('\n\n'); + }) + .replace('> MODULES', () => { + return modules + .map((module) => { + if (module.exports.length === 0 && !module.exempt) return ''; + + let import_block = ''; + + if (module.exports.length > 0) { + // deduplication is necessary for now, because of `error()` overload + const exports = Array.from(new Set(module.exports.map((x) => x.name))); + + let declaration = `import { ${exports.join(', ')} } from '${module.name}';`; + if (declaration.length > 80) { + declaration = `import {\n\t${exports.join(',\n\t')}\n} from '${module.name}';`; + } + + import_block = fence(declaration, 'js'); + } + + return `## ${module.name}\n\n${import_block}\n\n${module.comment}\n\n${module.exports + .map((type) => { + const markdown = + `
${fence(type.snippet)}` + + type.children.map(stringify).join('\n\n') + + `
`; + return `### ${type.name}\n\n${type.comment}\n\n${markdown}`; + }) + .join('\n\n')}`; + }) + .join('\n\n'); + }) + .replace(/> EXPORTS: (.+)/, (_, name) => { + const module = modules.find((module) => module.name === name); + if (!module) throw new Error(`Could not find module ${name} for EXPORTS: clause`); + + if (module.exports.length === 0 && !module.exempt) return ''; + + let import_block = ''; + + if (module.exports.length > 0) { + // deduplication is necessary for now, because of `error()` overload + const exports = Array.from(new Set(module.exports.map((x) => x.name))); + + let declaration = `import { ${exports.join(', ')} } from '${module.name}';`; + if (declaration.length > 80) { + declaration = `import {\n\t${exports.join(',\n\t')}\n} from '${module.name}';`; + } + + import_block = fence(declaration, 'js'); + } + + return `${import_block}\n\n${module.comment}\n\n${module.exports + .map((type) => { + const markdown = + `
${fence(type.snippet, 'dts')}` + + type.children.map((val) => stringify(val, 'dts')).join('\n\n') + + `
`; + return `### ${type.name}\n\n${type.comment}\n\n${markdown}`; + }) + .join('\n\n')}`; + }); +} + +/** + * @param {string} code + * @param {keyof typeof import('../markdown/index').SHIKI_LANGUAGE_MAP} lang + */ +function fence(code, lang = 'ts') { + return ( + '\n\n```' + + lang + + '\n' + + (['js', 'ts'].includes(lang) ? '// @noErrors\n' : '') + + code + + '\n```\n\n' + ); +} + +/** + * @param {import('./types').Type} member + * @param {keyof typeof import('../markdown').SHIKI_LANGUAGE_MAP} [lang] + */ +function stringify(member, lang = 'ts') { + const bullet_block = + member.bullets.length > 0 + ? `\n\n
\n\n${member.bullets.join('\n')}
` + : ''; + + const child_block = + member.children.length > 0 + ? `\n\n
${member.children + .map((val) => stringify(val, lang)) + .join('\n')}
` + : ''; + + return ( + `
${fence(member.snippet, lang)}` + + `
\n\n` + + bullet_block + + '\n\n' + + member.comment + .replace(/\/\/\/ type: (.+)/g, '/** @type {$1} */') + .replace(/^( )+/gm, (match, spaces) => { + return '\t'.repeat(match.length / 2); + }) + + child_block + + '\n
' + ); +} diff --git a/sites/svelte.dev/src/lib/server/markdown/index.js b/sites/svelte.dev/src/lib/server/markdown/index.js index 1635bb0b4f..0d6bbe50e1 100644 --- a/sites/svelte.dev/src/lib/server/markdown/index.js +++ b/sites/svelte.dev/src/lib/server/markdown/index.js @@ -24,6 +24,7 @@ export const SHIKI_LANGUAGE_MAP = { svelte: 'svelte', sv: 'svelte', js: 'javascript', + dts: 'typescript', css: 'css', diff: 'diff', ts: 'typescript', diff --git a/sites/svelte.dev/src/lib/utils/Tooltip.svelte b/sites/svelte.dev/src/lib/utils/Tooltip.svelte new file mode 100644 index 0000000000..56ecc6f538 --- /dev/null +++ b/sites/svelte.dev/src/lib/utils/Tooltip.svelte @@ -0,0 +1,67 @@ + + + +
+
+ {@html html} +
+
+ + diff --git a/sites/svelte.dev/src/lib/utils/hovers.js b/sites/svelte.dev/src/lib/utils/hovers.js new file mode 100644 index 0000000000..f0a079f2a5 --- /dev/null +++ b/sites/svelte.dev/src/lib/utils/hovers.js @@ -0,0 +1,60 @@ +import { onMount } from 'svelte'; +import Tooltip from './Tooltip.svelte'; + +export function setup() { + onMount(() => { + let tooltip; + let timeout; + + function over(event) { + if (event.target.tagName === 'DATA-LSP') { + clearTimeout(timeout); + + if (!tooltip) { + tooltip = new Tooltip({ + target: document.body + }); + + tooltip.$on('mouseenter', () => { + clearTimeout(timeout); + }); + + tooltip.$on('mouseleave', () => { + clearTimeout(timeout); + tooltip.$destroy(); + tooltip = null; + }); + } + + const rect = event.target.getBoundingClientRect(); + const html = event.target.getAttribute('lsp'); + + const x = (rect.left + rect.right) / 2 + window.scrollX; + const y = rect.top + window.scrollY; + + tooltip.$set({ + html, + x, + y + }); + } + } + + function out(event) { + if (event.target.tagName === 'DATA-LSP') { + timeout = setTimeout(() => { + tooltip.$destroy(); + tooltip = null; + }, 200); + } + } + + window.addEventListener('mouseover', over); + window.addEventListener('mouseout', out); + + return () => { + window.removeEventListener('mouseover', over); + window.removeEventListener('mouseout', out); + }; + }); +} diff --git a/sites/svelte.dev/src/routes/content.json/content.server.js b/sites/svelte.dev/src/routes/content.json/content.server.js index c754c5ae13..8a5c724374 100644 --- a/sites/svelte.dev/src/routes/content.json/content.server.js +++ b/sites/svelte.dev/src/routes/content.json/content.server.js @@ -1,3 +1,4 @@ +import { replace_placeholders } from '$lib/server/docs/render'; import { extract_frontmatter, normalizeSlugify, @@ -41,7 +42,7 @@ export function content() { const filepath = `${base}/${category.slug}/${file}`; // const markdown = replace_placeholders(fs.readFileSync(filepath, 'utf-8')); - const markdown = fs.readFileSync(filepath, 'utf-8'); + const markdown = replace_placeholders(fs.readFileSync(filepath, 'utf-8')); const { body, metadata } = extract_frontmatter(markdown); diff --git a/sites/svelte.dev/src/routes/docs/+layout.svelte b/sites/svelte.dev/src/routes/docs/+layout.svelte index ecd0f8a9db..f9fea90303 100644 --- a/sites/svelte.dev/src/routes/docs/+layout.svelte +++ b/sites/svelte.dev/src/routes/docs/+layout.svelte @@ -1,6 +1,7 @@ - {data.page.title} - Svelte + {data.page.title} • Docs • Svelte + + + +
{@html data.page.content}
+
+
+ previous + {#if prev} + {prev.title} + {/if} +
+ +
+ next + {#if next} + {next.title} + {/if} +
+
+ + + diff --git a/sites/svelte.dev/src/routes/docs/[slug]/OnThisPage.svelte b/sites/svelte.dev/src/routes/docs/[slug]/OnThisPage.svelte index 69afb1cc78..2c70e79fef 100644 --- a/sites/svelte.dev/src/routes/docs/[slug]/OnThisPage.svelte +++ b/sites/svelte.dev/src/routes/docs/[slug]/OnThisPage.svelte @@ -92,13 +92,13 @@ containerEl.scrollBy({ top: top - max, left: 0, - behavior: 'smooth', + behavior: 'smooth' }); } else if (bottom < min) { containerEl.scrollBy({ top: bottom - min, left: 0, - behavior: 'smooth', + behavior: 'smooth' }); } } diff --git a/sites/svelte.dev/src/routes/search/+page.svelte b/sites/svelte.dev/src/routes/search/+page.svelte index 8eefb3a976..26e3fc82b7 100644 --- a/sites/svelte.dev/src/routes/search/+page.svelte +++ b/sites/svelte.dev/src/routes/search/+page.svelte @@ -6,7 +6,7 @@ - Search • SvelteKit + Search • Svelte