mirror of https://github.com/sveltejs/svelte
feat(site-2): Local tutorial (#8427)
* feat(docs): Local tutorial * Refactor some stuff * Better error handling * Fix search imports * Prerender tutorial * try prerendering hack * fix super stupid display hidden bug * Shorten the rendered URL * Shorten URL code even morepull/8453/head
parent
99611ad82e
commit
35e7a852fc
@ -1,5 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"useTabs": true
|
||||
"useTabs": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
|
@ -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,
|
||||
})),
|
||||
}));
|
||||
}
|
@ -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+)<span class="token comment">([\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 `<span class="token comment wrapped" style="--indent: ${indent}ch">${
|
||||
line ?? ''
|
||||
}</span>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
)
|
||||
.replace(/\/\*…\*\//g, '…');
|
||||
|
||||
return html;
|
||||
},
|
||||
codespan: (text) => '<code>' + text + '</code>',
|
||||
});
|
||||
|
||||
return { ...tutorial, content };
|
||||
}
|
@ -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[];
|
@ -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 };
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
<script>
|
||||
import { setContext } from 'svelte';
|
||||
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
|
||||
setContext('tutorial', { sections: data.tutorials });
|
||||
</script>
|
||||
|
||||
<slot />
|
@ -1,5 +1,7 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export function load() {
|
||||
throw redirect(301, '/tutorial/basics');
|
||||
}
|
@ -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 };
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
Loading…
Reference in new issue