diff --git a/sites/svelte.dev/.prettierrc b/sites/svelte.dev/.prettierrc index 1cb44838c1..cc9aa8e796 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 d51bc14b81..497081233f 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/server/blog/marked.js b/sites/svelte.dev/src/lib/server/blog/marked.js index 6741bb5f2d..ab18871010 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/tutorial/get-tutorial-data.js b/sites/svelte.dev/src/lib/server/tutorial/get-tutorial-data.js new file mode 100644 index 0000000000..77d153317d --- /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 0000000000..2da22f7659 --- /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 0000000000..2ec5bd99fb --- /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 222f0b3065..0c7eeaec6a 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 5c79a2c47f..4a14f4a962 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/tutorial/+layout.js b/sites/svelte.dev/src/routes/tutorial/+layout.js deleted file mode 100644 index 027240c0b5..0000000000 --- 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 210060b178..0000000000 --- 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 81a8307aa2..ccafa42ec9 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 f7c5f71759..0000000000 --- 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 0000000000..9cb550decc --- /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 9cee2aa90a..c9b310f71b 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/svelte.config.js b/sites/svelte.dev/svelte.config.js index 68268902e0..f86cd1a197 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} */