diff --git a/sites/svelte.dev/.prettierrc b/sites/svelte.dev/.prettierrc index 1cb44838c1a7..cc9aa8e79621 100644 --- a/sites/svelte.dev/.prettierrc +++ b/sites/svelte.dev/.prettierrc @@ -1,5 +1,6 @@ { "singleQuote": true, "printWidth": 100, - "useTabs": true + "useTabs": true, + "trailingComma": "es5" } diff --git a/sites/svelte.dev/src/lib/components/ReplWidget.svelte b/sites/svelte.dev/src/lib/components/ReplWidget.svelte index d51bc14b81fd..497081233f11 100644 --- a/sites/svelte.dev/src/lib/components/ReplWidget.svelte +++ b/sites/svelte.dev/src/lib/components/ReplWidget.svelte @@ -42,7 +42,7 @@ return { name: file.slice(0, dot), type: file.slice(dot + 1), - source + source, }; }) .filter((x) => x.type === 'svelte' || x.type === 'js') @@ -64,7 +64,7 @@ const components = process_example(data.files); repl.set({ - components + components, }); } }); diff --git a/sites/svelte.dev/src/lib/components/ScreenToggle.svelte b/sites/svelte.dev/src/lib/components/ScreenToggle.svelte index 399f96bd715c..14a9341fb069 100644 --- a/sites/svelte.dev/src/lib/components/ScreenToggle.svelte +++ b/sites/svelte.dev/src/lib/components/ScreenToggle.svelte @@ -20,8 +20,8 @@ display: flex; justify-content: center; align-items: center; - border-top: 1px solid var(--second); - background-color: white; + border-top: 1px solid var(--sk-theme-2); + background-color: var(--sk-back-4); } button { @@ -29,15 +29,15 @@ width: 4em; height: 1em; padding: 0.3em 0.4em; - border-radius: var(--border-r); + border-radius: var(--sk-border-radius); line-height: 1em; box-sizing: content-box; - color: #888; - border: 1px solid var(--back-light); + color: var(--sk-text-3); + border: 1px solid var(--sk-back-3); } .selected { - background-color: var(--prime); + background-color: var(--sk-theme-1); color: white; } diff --git a/sites/svelte.dev/src/lib/server/blog/marked.js b/sites/svelte.dev/src/lib/server/blog/marked.js index 6741bb5f2d8a..ab188710107a 100644 --- a/sites/svelte.dev/src/lib/server/blog/marked.js +++ b/sites/svelte.dev/src/lib/server/blog/marked.js @@ -14,13 +14,13 @@ const escape_replacements = { '<': '<', '>': '>', '"': '"', - "'": ''' + "'": ''', }; const get_escape_replacement = (ch) => escape_replacements[ch]; /** * @param {string} html - * @param {boolean} encode + * @param {boolean} [encode] */ export function escape(html, encode) { if (encode) { @@ -45,7 +45,7 @@ const prism_languages = { css: 'css', diff: 'diff', ts: 'typescript', - '': '' + '': '', }; /** @type {Partial} */ @@ -165,7 +165,7 @@ const default_renderer = { text(text) { return text; - } + }, }; /** @@ -179,8 +179,8 @@ export function transform(markdown, renderer = {}) { // options are global, and merged in confusing ways. You can't do e.g. // `new Marked(options).parse(markdown)` ...default_renderer, - ...renderer - } + ...renderer, + }, }); return marked(markdown); diff --git a/sites/svelte.dev/src/lib/server/examples/get-examples.js b/sites/svelte.dev/src/lib/server/examples/get-examples.js new file mode 100644 index 000000000000..09b06723df40 --- /dev/null +++ b/sites/svelte.dev/src/lib/server/examples/get-examples.js @@ -0,0 +1,72 @@ +// @ts-check +import fs from 'node:fs'; + +const base = '../../site/content/examples/'; + +/** + * @returns {import('./types').ExamplesData} + */ +export function get_examples_data() { + const examples = []; + + for (const subdir of fs.readdirSync(base)) { + // Exclude embeds + if (subdir.endsWith('99-embeds')) continue; + + const section = { + title: '', // Initialise with empty + slug: subdir.split('-').slice(1).join('-'), + examples: [], + }; + + if (!(fs.statSync(`${base}/${subdir}`).isDirectory() || subdir.endsWith('meta.json'))) continue; + + if (!subdir.endsWith('meta.json')) + section.title = JSON.parse(fs.readFileSync(`${base}/${subdir}/meta.json`, 'utf-8')).title; + + for (const section_dir of fs.readdirSync(`${base}/${subdir}`)) { + const match = /\d{2}-(.+)/.exec(section_dir); + if (!match) continue; + + const slug = match[1]; + + const example_base_dir = `${base}/${subdir}/${section_dir}`; + + // Get title for + const example_title = JSON.parse( + fs.readFileSync(`${example_base_dir}/meta.json`, 'utf-8') + ).title; + + const files = []; + for (const file of fs + .readdirSync(example_base_dir) + .filter((file) => !file.endsWith('meta.json'))) { + files.push({ + filename: file, + type: file.split('.').at(-1), + content: fs.readFileSync(`${example_base_dir}/${file}`, 'utf-8'), + }); + } + + section.examples.push({ title: example_title, slug, files }); + } + + examples.push(section); + } + + return examples; +} + +/** + * @param {import('./types').ExamplesData} examples_data + * @returns {import('./types').ExamplesList} + */ +export function get_examples_list(examples_data) { + return examples_data.map((section) => ({ + title: section.title, + examples: section.examples.map((example) => ({ + title: example.title, + slug: example.slug, + })), + })); +} diff --git a/sites/svelte.dev/src/lib/server/examples/index.js b/sites/svelte.dev/src/lib/server/examples/index.js new file mode 100644 index 000000000000..822d07263665 --- /dev/null +++ b/sites/svelte.dev/src/lib/server/examples/index.js @@ -0,0 +1,11 @@ +/** + * @param {import('./types').ExamplesData} examples_data + * @param {string} slug + */ +export function get_example(examples_data, slug) { + const example = examples_data + .find((section) => section.examples.find((example) => example.slug === slug)) + ?.examples.find((example) => example.slug === slug); + + return example; +} diff --git a/sites/svelte.dev/src/lib/server/examples/types.d.ts b/sites/svelte.dev/src/lib/server/examples/types.d.ts new file mode 100644 index 000000000000..5fe93a0ca5bf --- /dev/null +++ b/sites/svelte.dev/src/lib/server/examples/types.d.ts @@ -0,0 +1,25 @@ +export type ExamplesData = { + title: string; + slug: string; + examples: { + title: string; + slug: string; + files: { + content: string; + type: 'svelte' | 'js'; + filename: string; + }[]; + }[]; +}[]; + +export interface Example { + title: string; + slug: string; +} + +export interface ExampleSection { + title: string; + examples: Example[]; +} + +export type ExamplesList = ExampleSection[]; diff --git a/sites/svelte.dev/src/lib/server/tutorial/get-tutorial-data.js b/sites/svelte.dev/src/lib/server/tutorial/get-tutorial-data.js new file mode 100644 index 000000000000..77d153317dd1 --- /dev/null +++ b/sites/svelte.dev/src/lib/server/tutorial/get-tutorial-data.js @@ -0,0 +1,81 @@ +// @ts-check +import fs from 'node:fs'; +import { extract_frontmatter } from '../markdown/index.js'; + +const base = '../../site/content/tutorial/'; + +/** + * @returns {import('./types').TutorialData} + */ +export function get_tutorial_data() { + const tutorials = []; + + for (const subdir of fs.readdirSync(base)) { + const section = { + title: '', // Initialise with empty + slug: subdir.split('-').slice(1).join('-'), + tutorials: [], + }; + + if (!(fs.statSync(`${base}/${subdir}`).isDirectory() || subdir.endsWith('meta.json'))) continue; + + if (!subdir.endsWith('meta.json')) + section.title = JSON.parse(fs.readFileSync(`${base}/${subdir}/meta.json`, 'utf-8')).title; + + for (const section_dir of fs.readdirSync(`${base}/${subdir}`)) { + const match = /\d{2}-(.+)/.exec(section_dir); + if (!match) continue; + + const slug = match[1]; + + const tutorial_base_dir = `${base}/${subdir}/${section_dir}`; + + // Read the file, get frontmatter + const contents = fs.readFileSync(`${tutorial_base_dir}/text.md`, 'utf-8'); + const { metadata, body } = extract_frontmatter(contents); + + // Get the contents of the apps. + const completion_states_data = { initial: [], complete: [] }; + for (const app_dir of fs.readdirSync(tutorial_base_dir)) { + if (!app_dir.startsWith('app-')) continue; + + const app_dir_path = `${tutorial_base_dir}/${app_dir}`; + const app_contents = fs.readdirSync(app_dir_path, 'utf-8'); + + for (const file of app_contents) { + completion_states_data[app_dir === 'app-a' ? 'initial' : 'complete'].push({ + name: file, + type: file.split('.').at(-1), + content: fs.readFileSync(`${app_dir_path}/${file}`, 'utf-8'), + }); + } + } + + section.tutorials.push({ + title: metadata.title, + slug, + content: body, + dir: `${subdir}/${section_dir}`, + ...completion_states_data, + }); + } + + tutorials.push(section); + } + + return tutorials; +} + +/** + * @param {import('./types').TutorialData} tutorial_data + * @returns {import('./types').TutorialsList} + */ +export function get_tutorial_list(tutorial_data) { + return tutorial_data.map((section) => ({ + title: section.title, + tutorials: section.tutorials.map((tutorial) => ({ + title: tutorial.title, + slug: tutorial.slug, + })), + })); +} diff --git a/sites/svelte.dev/src/lib/server/tutorial/index.js b/sites/svelte.dev/src/lib/server/tutorial/index.js new file mode 100644 index 000000000000..2da22f765973 --- /dev/null +++ b/sites/svelte.dev/src/lib/server/tutorial/index.js @@ -0,0 +1,89 @@ +import { createShikiHighlighter } from 'shiki-twoslash'; +import { transform } from '../markdown'; + +const languages = { + bash: 'bash', + env: 'bash', + html: 'svelte', + svelte: 'svelte', + sv: 'svelte', + js: 'javascript', + css: 'css', + diff: 'diff', + ts: 'typescript', + '': '', +}; + +/** + * @param {import('./types').TutorialData} tutorial_data + * @param {string} slug + */ +export async function get_parsed_tutorial(tutorial_data, slug) { + const tutorial = tutorial_data + .find(({ tutorials }) => tutorials.find((t) => t.slug === slug)) + ?.tutorials?.find((t) => t.slug === slug); + + if (!tutorial) return null; + + const body = tutorial.content; + + const highlighter = await createShikiHighlighter({ theme: 'css-variables' }); + + const content = transform(body, { + /** + * @param {string} html + */ + heading(html) { + const title = html + .replace(/<\/?code>/g, '') + .replace(/"/g, '"') + .replace(/</g, '<') + .replace(/>/g, '>'); + + return title; + }, + code: (source, language) => { + let html = ''; + + source = source + .replace(/^([\-\+])?((?: )+)/gm, (match, prefix = '', spaces) => { + if (prefix && language !== 'diff') return match; + + // for no good reason at all, marked replaces tabs with spaces + let tabs = ''; + for (let i = 0; i < spaces.length; i += 4) { + tabs += ' '; + } + return prefix + tabs; + }) + .replace(/\*\\\//g, '*/'); + + html = highlighter.codeToHtml(source, { lang: languages[language] }); + + html = html + .replace( + /^(\s+)([\s\S]+?)<\/span>\n/gm, + (match, intro_whitespace, content) => { + // we use some CSS trickery to make comments break onto multiple lines while preserving indentation + const lines = (intro_whitespace + content).split('\n'); + return lines + .map((line) => { + const match = /^(\s*)(.*)/.exec(line); + const indent = (match[1] ?? '').replace(/\t/g, ' ').length; + + return `${ + line ?? '' + }`; + }) + .join(''); + } + ) + .replace(/\/\*…\*\//g, '…'); + + return html; + }, + codespan: (text) => '' + text + '', + }); + + return { ...tutorial, content }; +} diff --git a/sites/svelte.dev/src/lib/server/tutorial/types.d.ts b/sites/svelte.dev/src/lib/server/tutorial/types.d.ts new file mode 100644 index 000000000000..2ec5bd99fb1c --- /dev/null +++ b/sites/svelte.dev/src/lib/server/tutorial/types.d.ts @@ -0,0 +1,24 @@ +export type TutorialData = { + title: string; + slug: string; + tutorials: { + title: string; + slug: string; + dir: string; + content: string; + initial: { name: string; type: string; content: string }[]; + complete: { name: string; type: string; content: string }[]; + }[]; +}[]; + +export interface Tutorial { + title: string; + slug: string; +} + +export interface TutorialSection { + title: string; + tutorials: Tutorial[]; +} + +export type TutorialsList = TutorialSection[]; diff --git a/sites/svelte.dev/src/routes/docs/+layout.server.js b/sites/svelte.dev/src/routes/docs/+layout.server.js index 222f0b30650b..0c7eeaec6a8a 100644 --- a/sites/svelte.dev/src/routes/docs/+layout.server.js +++ b/sites/svelte.dev/src/routes/docs/+layout.server.js @@ -6,7 +6,6 @@ export const prerender = true; const base_dir = '../../site/content/docs/'; -/** @type {import('./$types').LayoutServerLoad} */ export function load() { const sections = fs.readdirSync(base_dir).map((file) => { const { title } = extract_frontmatter(fs.readFileSync(`${base_dir}/${file}`, 'utf-8')).metadata; diff --git a/sites/svelte.dev/src/routes/docs/+layout.svelte b/sites/svelte.dev/src/routes/docs/+layout.svelte index 5c79a2c47fd4..4a14f4a96294 100644 --- a/sites/svelte.dev/src/routes/docs/+layout.svelte +++ b/sites/svelte.dev/src/routes/docs/+layout.svelte @@ -181,7 +181,7 @@ } .content :global(code) { - padding: 0.4rem; + /* padding: 0.4rem; */ margin: 0 0.2rem; top: -0.1rem; background: var(--sk-back-4); diff --git a/sites/svelte.dev/src/routes/examples/+layout.js b/sites/svelte.dev/src/routes/examples/+layout.js deleted file mode 100644 index e0904af00483..000000000000 --- a/sites/svelte.dev/src/routes/examples/+layout.js +++ /dev/null @@ -1,6 +0,0 @@ -import { PUBLIC_API_BASE } from '$env/static/public'; - -export async function load({ fetch }) { - const examples = await fetch(`${PUBLIC_API_BASE}/docs/svelte/examples`).then((r) => r.json()); - return { examples }; -} diff --git a/sites/svelte.dev/src/routes/examples/+layout.svelte b/sites/svelte.dev/src/routes/examples/+layout.svelte deleted file mode 100644 index 75bd0e2cfda6..000000000000 --- a/sites/svelte.dev/src/routes/examples/+layout.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/sites/svelte.dev/src/routes/examples/+page.js b/sites/svelte.dev/src/routes/examples/+page.js index ac962514a06d..6c39a1de348e 100644 --- a/sites/svelte.dev/src/routes/examples/+page.js +++ b/sites/svelte.dev/src/routes/examples/+page.js @@ -1,5 +1,7 @@ import { redirect } from '@sveltejs/kit'; +export const prerender = true; + /** @type {import('./$types').PageLoad} */ export function load() { throw redirect(301, 'examples/hello-world'); diff --git a/sites/svelte.dev/src/routes/examples/[slug]/+page.js b/sites/svelte.dev/src/routes/examples/[slug]/+page.js deleted file mode 100644 index e2a1472cea1c..000000000000 --- a/sites/svelte.dev/src/routes/examples/[slug]/+page.js +++ /dev/null @@ -1,17 +0,0 @@ -import { PUBLIC_API_BASE } from '$env/static/public'; - -/** @type {import('./$types').PageLoad} */ -export async function load({ fetch, params, setHeaders }) { - const example = await fetch(`${PUBLIC_API_BASE}/docs/svelte/examples/${params.slug}`, { - credentials: 'omit' - }); - - setHeaders({ - 'cache-control': 'public, max-age=60' - }); - - return { - example: await example.json(), - slug: params.slug - }; -} diff --git a/sites/svelte.dev/src/routes/examples/[slug]/+page.server.js b/sites/svelte.dev/src/routes/examples/[slug]/+page.server.js new file mode 100644 index 000000000000..6bc280e0983f --- /dev/null +++ b/sites/svelte.dev/src/routes/examples/[slug]/+page.server.js @@ -0,0 +1,17 @@ +import { get_example } from '$lib/server/examples'; +import { get_examples_data, get_examples_list } from '$lib/server/examples/get-examples'; + +export const prerender = true; + +export async function load({ params }) { + const examples_data = get_examples_data(); + + const examples_list = get_examples_list(examples_data); + const example = get_example(examples_data, params.slug); + + return { + examples_list, + example, + slug: params.slug, + }; +} diff --git a/sites/svelte.dev/src/routes/examples/[slug]/+page.svelte b/sites/svelte.dev/src/routes/examples/[slug]/+page.svelte index 6a12d3d0a361..c27de536b7c2 100644 --- a/sites/svelte.dev/src/routes/examples/[slug]/+page.svelte +++ b/sites/svelte.dev/src/routes/examples/[slug]/+page.svelte @@ -1,16 +1,12 @@ - {data.example.name} {data.example.name ? '•' : ''} Svelte Examples + {data.example.title} {data.example.title ? '•' : ''} Svelte Examples @@ -44,7 +38,11 @@

Examples

- +
{#each sections as section}
  • - {section.name} + {section.title} {#each section.examples as example}
    @@ -31,7 +31,7 @@ src="/examples/thumbnails/{example.slug}.jpg" /> - {example.name} + {example.title} {#if example.slug === active_section} REPL @@ -109,7 +109,7 @@ } .thumbnail { - background-color: var(--sk-back-1); + background-color: #fff; object-fit: contain; width: 5rem; height: 5rem; diff --git a/sites/svelte.dev/src/routes/tutorial/+layout.js b/sites/svelte.dev/src/routes/tutorial/+layout.js deleted file mode 100644 index 027240c0b5c9..000000000000 --- a/sites/svelte.dev/src/routes/tutorial/+layout.js +++ /dev/null @@ -1,6 +0,0 @@ -import { PUBLIC_API_BASE } from '$env/static/public'; - -export async function load({ fetch }) { - const tutorials = await fetch(`${PUBLIC_API_BASE}/docs/svelte/tutorial`).then((r) => r.json()); - return { tutorials }; -} diff --git a/sites/svelte.dev/src/routes/tutorial/+layout.svelte b/sites/svelte.dev/src/routes/tutorial/+layout.svelte deleted file mode 100644 index 210060b178a3..000000000000 --- a/sites/svelte.dev/src/routes/tutorial/+layout.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/sites/svelte.dev/src/routes/tutorial/+page.js b/sites/svelte.dev/src/routes/tutorial/+page.server.js similarity index 77% rename from sites/svelte.dev/src/routes/tutorial/+page.js rename to sites/svelte.dev/src/routes/tutorial/+page.server.js index 81a8307aa221..ccafa42ec9a2 100644 --- a/sites/svelte.dev/src/routes/tutorial/+page.js +++ b/sites/svelte.dev/src/routes/tutorial/+page.server.js @@ -1,5 +1,7 @@ import { redirect } from '@sveltejs/kit'; +export const prerender = true; + export function load() { throw redirect(301, '/tutorial/basics'); } diff --git a/sites/svelte.dev/src/routes/tutorial/[slug]/+page.js b/sites/svelte.dev/src/routes/tutorial/[slug]/+page.js deleted file mode 100644 index f7c5f7175977..000000000000 --- a/sites/svelte.dev/src/routes/tutorial/[slug]/+page.js +++ /dev/null @@ -1,17 +0,0 @@ -import { redirect } from '@sveltejs/kit'; -import { PUBLIC_API_BASE } from '$env/static/public'; - -export async function load({ fetch, params, setHeaders }) { - // TODO: Use local data - const tutorial = await fetch(`${PUBLIC_API_BASE}/docs/svelte/tutorial/${params.slug}`); - - if (!tutorial.ok) { - throw redirect(301, '/tutorial/basics'); - } - - setHeaders({ - 'cache-control': 'public, max-age=60', - }); - - return { tutorial: await tutorial.json(), slug: params.slug }; -} diff --git a/sites/svelte.dev/src/routes/tutorial/[slug]/+page.server.js b/sites/svelte.dev/src/routes/tutorial/[slug]/+page.server.js new file mode 100644 index 000000000000..9cb550decca3 --- /dev/null +++ b/sites/svelte.dev/src/routes/tutorial/[slug]/+page.server.js @@ -0,0 +1,20 @@ +import { get_parsed_tutorial } from '$lib/server/tutorial'; +import { get_tutorial_data, get_tutorial_list } from '$lib/server/tutorial/get-tutorial-data'; +import { error } from '@sveltejs/kit'; + +export const prerender = true; + +export async function load({ params }) { + const tutorial_data = get_tutorial_data(); + const tutorials_list = get_tutorial_list(tutorial_data); + + const tutorial = await get_parsed_tutorial(tutorial_data, params.slug); + + if (!tutorial) throw error(404); + + return { + tutorials_list, + tutorial, + slug: params.slug, + }; +} diff --git a/sites/svelte.dev/src/routes/tutorial/[slug]/+page.svelte b/sites/svelte.dev/src/routes/tutorial/[slug]/+page.svelte index 9cee2aa90aaa..c9b310f71b77 100644 --- a/sites/svelte.dev/src/routes/tutorial/[slug]/+page.svelte +++ b/sites/svelte.dev/src/routes/tutorial/[slug]/+page.svelte @@ -1,31 +1,32 @@ - {selected.section.name} / {selected.chapter.name} • Svelte Tutorial + {selected.section.title} / {selected.chapter.title} • Svelte Tutorial - - + + @@ -106,10 +107,10 @@
    - +
    -
    +
    {@html data.tutorial.content}
    @@ -152,6 +153,16 @@ {/if}
    + +

    + {#each data.tutorials_list as { tutorials }} + {#each tutorials as { slug }} + + + {/each} + {/each} +

    + diff --git a/sites/svelte.dev/static/examples/thumbnails/svelte-document.jpg b/sites/svelte.dev/static/examples/thumbnails/svelte-document.jpg new file mode 100644 index 000000000000..4449279e50fe Binary files /dev/null and b/sites/svelte.dev/static/examples/thumbnails/svelte-document.jpg differ diff --git a/sites/svelte.dev/svelte.config.js b/sites/svelte.dev/svelte.config.js index 68268902e0d8..f86cd1a1976a 100644 --- a/sites/svelte.dev/svelte.config.js +++ b/sites/svelte.dev/svelte.config.js @@ -1,3 +1,4 @@ +// @ts-check import adapter from '@sveltejs/adapter-auto'; /** @type {import('@sveltejs/kit').Config} */