Skip to content

Commit

Permalink
feat(site-2): Local tutorial (#8427)
Browse files Browse the repository at this point in the history
* 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 more
  • Loading branch information
PuruVJ authored Mar 29, 2023
1 parent 99611ad commit 35e7a85
Show file tree
Hide file tree
Showing 16 changed files with 283 additions and 75 deletions.
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

1 comment on commit 35e7a85

@vercel
Copy link

@vercel vercel bot commented on 35e7a85 Mar 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

svelte-dev-2 – ./

svelte-dev-2-svelte.vercel.app
svelte-dev-2.vercel.app
svelte-dev-2-git-sites-svelte.vercel.app

Please sign in to comment.