Skip to content

feat(site-2): Local tutorial #8427

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Mar 29, 2023
3 changes: 2 additions & 1 deletion sites/svelte.dev/.prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"singleQuote": true,
"printWidth": 100,
"useTabs": true
"useTabs": true,
"trailingComma": "es5"
}
4 changes: 2 additions & 2 deletions sites/svelte.dev/src/lib/components/ReplWidget.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -64,7 +64,7 @@
const components = process_example(data.files);

repl.set({
components
components,
});
}
});
Expand Down
12 changes: 6 additions & 6 deletions sites/svelte.dev/src/lib/server/blog/marked.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ const escape_replacements = {
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
"'": '&#39;',
};
const get_escape_replacement = (ch) => escape_replacements[ch];

/**
* @param {string} html
* @param {boolean} encode
* @param {boolean} [encode]
*/
export function escape(html, encode) {
if (encode) {
Expand All @@ -45,7 +45,7 @@ const prism_languages = {
css: 'css',
diff: 'diff',
ts: 'typescript',
'': ''
'': '',
};

/** @type {Partial<import('marked').Renderer>} */
Expand Down Expand Up @@ -165,7 +165,7 @@ const default_renderer = {

text(text) {
return text;
}
},
};

/**
Expand All @@ -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);
Expand Down
81 changes: 81 additions & 0 deletions sites/svelte.dev/src/lib/server/tutorial/get-tutorial-data.js
Original file line number Diff line number Diff line change
@@ -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,
})),
}));
}
89 changes: 89 additions & 0 deletions sites/svelte.dev/src/lib/server/tutorial/index.js
Original file line number Diff line number Diff line change
@@ -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(/&quot;/g, '"')
.replace(/&lt;/g, '<')
.replace(/&gt;/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 };
}
24 changes: 24 additions & 0 deletions sites/svelte.dev/src/lib/server/tutorial/types.d.ts
Original file line number Diff line number Diff line change
@@ -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 change: 0 additions & 1 deletion sites/svelte.dev/src/routes/docs/+layout.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion sites/svelte.dev/src/routes/docs/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 0 additions & 6 deletions sites/svelte.dev/src/routes/tutorial/+layout.js

This file was deleted.

10 changes: 0 additions & 10 deletions sites/svelte.dev/src/routes/tutorial/+layout.svelte

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { redirect } from '@sveltejs/kit';

export const prerender = true;

export function load() {
throw redirect(301, '/tutorial/basics');
}
17 changes: 0 additions & 17 deletions sites/svelte.dev/src/routes/tutorial/[slug]/+page.js

This file was deleted.

20 changes: 20 additions & 0 deletions sites/svelte.dev/src/routes/tutorial/[slug]/+page.server.js
Original file line number Diff line number Diff line change
@@ -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