make a start on tutorial

pull/2143/head
Rich Harris 7 years ago
parent bab62df434
commit c779162b29

@ -0,0 +1,3 @@
{
"title": "Introduction"
}

@ -1579,7 +1579,7 @@
"dev": true
},
"eslint-plugin-svelte3": {
"version": "git+https://github.com/sveltejs/eslint-plugin-svelte3.git#cf43fd9a1498bdffe7c5b4927eb785d63beb73a4",
"version": "git+https://github.com/sveltejs/eslint-plugin-svelte3.git#651d7e3695b1731251ab3a501d1067b561ede09f",
"from": "git+https://github.com/sveltejs/eslint-plugin-svelte3.git#semver:*",
"dev": true
},
@ -4291,9 +4291,9 @@
}
},
"sapper": {
"version": "0.26.0-alpha.9",
"resolved": "https://registry.npmjs.org/sapper/-/sapper-0.26.0-alpha.9.tgz",
"integrity": "sha512-3CnC8rQWgVv8IfqSgWV+MXfn+f/ZKMISQBwPsSBBX0x+vBckfmLZ31omKDRDyVnit1WgzFW4mXW/I3PdqRVMZw==",
"version": "0.26.0-alpha.10",
"resolved": "https://registry.npmjs.org/sapper/-/sapper-0.26.0-alpha.10.tgz",
"integrity": "sha512-S1XdAA0gxEPT3Ikh3jsLKAAbV3EnDd80sppCeUJ5wHCfXTRiP6STxe5PLYZ4Ym8uYU7Iez+6cGLTkfP0ZLPRQw==",
"dev": true,
"requires": {
"html-minifier": "^3.5.21",

@ -52,7 +52,7 @@
"rollup-plugin-replace": "^2.1.0",
"rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^4.0.4",
"sapper": "^0.26.0-alpha.9",
"sapper": "^0.26.0-alpha.10",
"svelte": "^3.0.0-beta.8"
}
}

@ -1,19 +1,10 @@
import fs from 'fs';
import path from 'path';
import process_markdown from '../../../utils/_process_markdown.js';
import { extract_frontmatter, langs } from '../../../utils/markdown.js';
import marked from 'marked';
import PrismJS from 'prismjs';
import 'prismjs/components/prism-bash';
// map lang to prism-language-attr
const prismLang = {
bash: 'bash',
html: 'markup',
js: 'javascript',
css: 'css',
};
export default function() {
return fs
.readdirSync('content/blog')
@ -22,7 +13,7 @@ export default function() {
const markdown = fs.readFileSync(`content/blog/${file}`, 'utf-8');
const { content, metadata } = process_markdown(markdown);
const { content, metadata } = extract_frontmatter(markdown);
const date = new Date(`${metadata.pubdate} EDT`); // cheeky hack
metadata.dateString = date.toDateString();
@ -30,7 +21,7 @@ export default function() {
const renderer = new marked.Renderer();
renderer.code = (source, lang) => {
const plang = prismLang[lang];
const plang = langs[lang];
const highlighted = PrismJS.highlight(
source,
PrismJS.languages[plang],

@ -1,20 +1,10 @@
import fs from 'fs';
import path from 'path';
import * as fleece from 'golden-fleece';
import process_markdown from '../../utils/_process_markdown.js';
import { extract_frontmatter, extract_metadata, langs } from '../../utils/markdown.js';
import marked from 'marked';
import PrismJS from 'prismjs';
import 'prismjs/components/prism-bash';
// map lang to prism-language-attr
const prismLang = {
bash: 'bash',
html: 'markup',
js: 'javascript',
css: 'css',
};
const escaped = {
'"': '"',
"'": ''',
@ -45,24 +35,6 @@ const blockTypes = [
'tablecell'
];
function extractMeta(line, lang) {
try {
if (lang === 'html' && line.startsWith('<!--') && line.endsWith('-->')) {
return fleece.evaluate(line.slice(4, -3).trim());
}
if (
lang === 'js' ||
(lang === 'json' && line.startsWith('/*') && line.endsWith('*/'))
) {
return fleece.evaluate(line.slice(2, -2).trim());
}
} catch (err) {
// TODO report these errors, don't just squelch them
return null;
}
}
// https://github.com/darkskyapp/string-hash/blob/master/index.js
function getHash(str) {
let hash = 5381;
@ -81,7 +53,7 @@ export default function() {
.map(file => {
const markdown = fs.readFileSync(`content/guide/${file}`, 'utf-8');
const { content, metadata } = process_markdown(markdown);
const { content, metadata } = extract_frontmatter(markdown);
const subsections = [];
const groups = [];
@ -97,7 +69,7 @@ export default function() {
const lines = source.split('\n');
const meta = extractMeta(lines[0], lang);
const meta = extract_metadata(lines[0], lang);
let prefix = '';
let className = 'code-block';
@ -124,7 +96,7 @@ export default function() {
if (meta && meta.hidden) return '';
const plang = prismLang[lang];
const plang = langs[lang];
const highlighted = PrismJS.highlight(
source,
PrismJS.languages[plang],

@ -0,0 +1,81 @@
<script>
import { goto } from '@sapper/app';
import Icon from '../../../../components/Icon.svelte';
export let sections;
export let slug;
export let selected;
function navigate(e) {
goto(`tutorial/${e.target.value}`);
}
</script>
<style>
nav {
display: grid;
grid-template-columns: 2.5em 1fr 2.5em;
border-bottom: 1px solid #eee;
}
div {
position: relative;
padding: 1em 0.5em;
font-weight: 300;
font-size: var(--h6);
color: var(--text);
cursor: pointer;
}
a {
display: block;
padding: 0.7em 0;
text-align: center;
}
a.disabled, a.disabled:hover, a.disabled:active {
color: var(--second);
}
span {
white-space: nowrap;
color: var(--prime);
}
strong {
color: var(--heading);
}
select {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0.0001;
}
</style>
<nav>
<a rel="prefetch" href="tutorial/{(selected.prev || selected).slug}" class:disabled={!selected.prev}>
<Icon name="arrow-left" />
</a>
<div>
<span><strong>{selected.section.title} /</strong> {selected.chapter.title}</span>
<select value={slug} on:change={navigate}>
{#each sections as section, i}
<optgroup label="{i + 1}. {section.title}">
{#each section.chapters as chapter, i}
<option value={chapter.slug}>{String.fromCharCode(i + 97)}. {chapter.title}</option>
{/each}
</optgroup>
{/each}
</select>
</div>
<a rel="prefetch" href="tutorial/{(selected.next || selected).slug}" class:disabled={!selected.next}>
<Icon name="arrow-right" />
</a>
</nav>

@ -0,0 +1,95 @@
import * as fs from 'fs';
import marked from 'marked';
import PrismJS from 'prismjs';
import { extract_frontmatter, extract_metadata, langs } from '../../../utils/markdown';
const cache = new Map();
function find_tutorial(slug) {
const sections = fs.readdirSync(`content/tutorial`);
for (const section of sections) {
const chapters = fs.readdirSync(`content/tutorial/${section}`).filter(dir => /^\d+/.test(dir));
for (const chapter of chapters) {
if (slug === chapter.replace(/^\d+-/, '')) {
return { section, chapter };
}
}
}
}
function get_tutorial(slug) {
const found = find_tutorial(slug);
if (!found) return found;
const dir = `content/tutorial/${found.section}/${found.chapter}`;
const markdown = fs.readFileSync(`${dir}/text.md`, 'utf-8');
const files = fs.readdirSync(dir).filter(file => file[0] !== '.' && file !== 'text.md');
const { content } = extract_frontmatter(markdown);
const renderer = new marked.Renderer();
renderer.code = (source, lang) => {
source = source.replace(/^ +/gm, match =>
match.split(' ').join('\t')
);
const lines = source.split('\n');
const meta = extract_metadata(lines[0], lang);
let prefix = '';
let className = 'code-block';
if (meta) {
source = lines.slice(1).join('\n');
const filename = meta.filename || (lang === 'html' && 'App.svelte');
if (filename) {
prefix = `<span class='filename'>${prefix} ${filename}</span>`;
className += ' named';
}
}
const plang = langs[lang];
const highlighted = PrismJS.highlight(
source,
PrismJS.languages[plang],
lang
);
return `<div class='${className}'>${prefix}<pre class='language-${plang}'><code>${highlighted}</code></pre></div>`;
};
const html = marked(content, { renderer });
return {
html,
files: files.map(file => ({
file,
contents: fs.readFileSync(`${dir}/${file}`, 'utf-8')
}))
};
}
export function get(req, res) {
const { slug } = req.params;
if (!cache.has(slug) || process.env.NODE_ENV !== 'production') {
cache.set(slug, JSON.stringify(get_tutorial(slug)));
}
const json = cache.get(slug);
res.set({
'Content-Type': 'application/json'
});
if (json) {
res.end(json);
} else {
res.statusCode = 404;
res.end(JSON.stringify({ message: 'not found' }));
}
}

@ -0,0 +1,112 @@
<script context="module">
export async function preload({ params }) {
const chapter = await this.fetch(`tutorial/${params.slug}.json`).then(r => r.json());
return {
slug: params.slug,
chapter
};
}
</script>
<script>
import TableOfContents from './_components/TableOfContents.svelte';
import Icon from '../../../components/Icon.svelte';
import { getContext } from 'svelte';
export let slug;
export let chapter;
const { sections } = getContext('tutorial');
const lookup = new Map();
let prev;
sections.forEach(section => {
section.chapters.forEach(chapter => {
const obj = {
slug: chapter.slug,
section,
chapter,
prev
};
lookup.set(chapter.slug, obj);
if (process.browser) { // pending https://github.com/sveltejs/svelte/issues/2135
if (prev) prev.next = obj;
prev = obj;
}
});
});
$: selected = lookup.get(slug);
</script>
<style>
.tutorial-outer {
position: relative;
height: calc(100vh - var(--nav-h));
overflow: hidden;
padding: 0;
margin: 0 calc(var(--side-nav) * -1);
box-sizing: border-box;
display: grid;
grid-template-columns: 400px 1fr;
}
.tutorial-text {
display: flex;
flex-direction: column;
height: 100%;
border-right: 1px solid var(--second);
}
.tutorial-repl {
}
.table-of-contents {
background-color: white;
}
.chapter-markup {
padding: 1em;
overflow: auto;
flex: 1;
height: 0;
}
.chapter-markup :global(h2) {
font-size: var(--h3);
color: var(--second);
margin: 3.2rem 0 1.6rem 0;
line-height: 1;
}
.next {
display: block;
text-align: right;
}
</style>
<div class="tutorial-outer">
<div class="tutorial-text">
<div class="table-of-contents">
<TableOfContents {sections} {slug} {selected}/>
</div>
<div class="chapter-markup">
{@html chapter.html}
{#if selected.next}
<a class="next" href="tutorial/{selected.next.slug}">Next <Icon name="arrow-right" /></a>
{/if}
</div>
</div>
<div class="tutorial-repl">
TODO add the REPL
</div>
</div>

@ -0,0 +1,15 @@
<script context="module">
export async function preload() {
const sections = await this.fetch(`tutorial.json`).then(r => r.json());
return { sections };
}
</script>
<script>
import { setContext } from 'svelte';
export let sections;
setContext('tutorial', { sections });
</script>
<slot></slot>

@ -1,4 +0,0 @@
export function get_tutorials() {
// TODO
return [];
}

@ -1,15 +0,0 @@
import get_tutorials from './_tutorials.js';
let json;
export function get(req, res) {
if (!json || process.env.NODE_ENV !== 'production') {
json = JSON.stringify(get_tutorials());
}
res.set({
'Content-Type': 'application/json'
});
res.end(json);
}

@ -0,0 +1,48 @@
import * as fs from 'fs';
import { extract_frontmatter } from '../../utils/markdown';
let json;
function get_sections() {
const slugs = new Set();
const sections = fs.readdirSync(`content/tutorial`)
.filter(dir => /^\d+/.test(dir))
.map(dir => {
const meta = JSON.parse(fs.readFileSync(`content/tutorial/${dir}/meta.json`, 'utf-8'));
return {
title: meta.title,
chapters: fs.readdirSync(`content/tutorial/${dir}`)
.filter(dir => /^\d+/.test(dir))
.map(tutorial => {
const md = fs.readFileSync(`content/tutorial/${dir}/${tutorial}/text.md`, 'utf-8');
const { metadata, content } = extract_frontmatter(md);
const slug = tutorial.replace(/^\d+-/, '');
if (slugs.has(slug)) throw new Error(`Duplicate slug: ${slug}`);
slugs.add(slug);
return {
slug,
title: metadata.title
};
})
}
});
return sections;
}
export function get(req, res) {
if (!json || process.env.NODE_ENV !== 'production') {
json = JSON.stringify(get_sections());
}
res.set({
'Content-Type': 'application/json'
});
res.end(json);
}

@ -1,26 +1,5 @@
<script context="module">
export function preload() {
return {};
this.redirect(301, 'tutorial/basics');
}
</script>
<script>
export let sections;
</script>
<style>
.tutorial-outer {
position: relative;
height: calc(100vh - var(--nav-h));
overflow: hidden;
background-color: var(--back);
padding: 0;
margin: 0 calc(var(--side-nav) * -1);
box-sizing: border-box;
}
</style>
<div class="tutorial-outer">
<h1>Tutorial</h1>
</div>
</script>

@ -1,15 +0,0 @@
export default function process_markdown(markdown) {
const match = /---\r?\n([\s\S]+?)\r?\n---/.exec(markdown);
const frontMatter = match[1];
const content = markdown.slice(match[0].length);
const metadata = {};
frontMatter.split('\n').forEach(pair => {
const colonIndex = pair.indexOf(':');
metadata[pair.slice(0, colonIndex).trim()] = pair
.slice(colonIndex + 1)
.trim();
});
return { metadata, content };
}

@ -0,0 +1,43 @@
import * as fleece from 'golden-fleece';
export function extract_frontmatter(markdown) {
const match = /---\r?\n([\s\S]+?)\r?\n---/.exec(markdown);
const frontMatter = match[1];
const content = markdown.slice(match[0].length);
const metadata = {};
frontMatter.split('\n').forEach(pair => {
const colonIndex = pair.indexOf(':');
metadata[pair.slice(0, colonIndex).trim()] = pair
.slice(colonIndex + 1)
.trim();
});
return { metadata, content };
}
export function extract_metadata(line, lang) {
try {
if (lang === 'html' && line.startsWith('<!--') && line.endsWith('-->')) {
return fleece.evaluate(line.slice(4, -3).trim());
}
if (
lang === 'js' ||
(lang === 'json' && line.startsWith('/*') && line.endsWith('*/'))
) {
return fleece.evaluate(line.slice(2, -2).trim());
}
} catch (err) {
// TODO report these errors, don't just squelch them
return null;
}
}
// map lang to prism-language-attr
export const langs = {
bash: 'bash',
html: 'markup',
js: 'javascript',
css: 'css',
};

@ -380,7 +380,7 @@ button[outline] {
transform: scaleX(1);
} */
a:hover > .icon { stroke: var(--flash) }
a:hover:not(.disabled) > .icon { stroke: var(--flash) }
/* lists ---------------------------------- */
.listify ol,

Loading…
Cancel
Save