diff --git a/site/content/tutorials/01-introduction/01-basics/App.svelte b/site/content/tutorial/01-introduction/01-basics/App.svelte similarity index 100% rename from site/content/tutorials/01-introduction/01-basics/App.svelte rename to site/content/tutorial/01-introduction/01-basics/App.svelte diff --git a/site/content/tutorials/01-introduction/01-basics/text.md b/site/content/tutorial/01-introduction/01-basics/text.md similarity index 100% rename from site/content/tutorials/01-introduction/01-basics/text.md rename to site/content/tutorial/01-introduction/01-basics/text.md diff --git a/site/content/tutorials/01-introduction/02-adding-data/App.svelte b/site/content/tutorial/01-introduction/02-adding-data/App.svelte similarity index 100% rename from site/content/tutorials/01-introduction/02-adding-data/App.svelte rename to site/content/tutorial/01-introduction/02-adding-data/App.svelte diff --git a/site/content/tutorials/01-introduction/02-adding-data/text.md b/site/content/tutorial/01-introduction/02-adding-data/text.md similarity index 100% rename from site/content/tutorials/01-introduction/02-adding-data/text.md rename to site/content/tutorial/01-introduction/02-adding-data/text.md diff --git a/site/content/tutorials/01-introduction/03-dynamic-attributes/App.svelte b/site/content/tutorial/01-introduction/03-dynamic-attributes/App.svelte similarity index 100% rename from site/content/tutorials/01-introduction/03-dynamic-attributes/App.svelte rename to site/content/tutorial/01-introduction/03-dynamic-attributes/App.svelte diff --git a/site/content/tutorials/01-introduction/03-dynamic-attributes/text.md b/site/content/tutorial/01-introduction/03-dynamic-attributes/text.md similarity index 100% rename from site/content/tutorials/01-introduction/03-dynamic-attributes/text.md rename to site/content/tutorial/01-introduction/03-dynamic-attributes/text.md diff --git a/site/content/tutorial/01-introduction/meta.json b/site/content/tutorial/01-introduction/meta.json new file mode 100644 index 0000000000..5c8f7bc10b --- /dev/null +++ b/site/content/tutorial/01-introduction/meta.json @@ -0,0 +1,3 @@ +{ + "title": "Introduction" +} \ No newline at end of file diff --git a/site/package-lock.json b/site/package-lock.json index 052215363d..a6ac3b7e58 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -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", diff --git a/site/package.json b/site/package.json index fcc3c23e9b..d5e5131e4b 100644 --- a/site/package.json +++ b/site/package.json @@ -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" } } diff --git a/site/src/routes/api/blog/_posts.js b/site/src/routes/api/blog/_posts.js index 8b70483cd1..33c033b517 100644 --- a/site/src/routes/api/blog/_posts.js +++ b/site/src/routes/api/blog/_posts.js @@ -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], diff --git a/site/src/routes/docs/_sections.js b/site/src/routes/docs/_sections.js index 59818bc464..0fd1382652 100644 --- a/site/src/routes/docs/_sections.js +++ b/site/src/routes/docs/_sections.js @@ -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('')) { - 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], diff --git a/site/src/routes/tutorial/[slug]/_components/TableOfContents.svelte b/site/src/routes/tutorial/[slug]/_components/TableOfContents.svelte new file mode 100644 index 0000000000..21962ab03e --- /dev/null +++ b/site/src/routes/tutorial/[slug]/_components/TableOfContents.svelte @@ -0,0 +1,81 @@ + + + + + \ No newline at end of file diff --git a/site/src/routes/tutorial/[slug]/index.json.js b/site/src/routes/tutorial/[slug]/index.json.js new file mode 100644 index 0000000000..c3c84e4f42 --- /dev/null +++ b/site/src/routes/tutorial/[slug]/index.json.js @@ -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 = `${prefix} ${filename}`; + className += ' named'; + } + } + + const plang = langs[lang]; + const highlighted = PrismJS.highlight( + source, + PrismJS.languages[plang], + lang + ); + + return `
${prefix}
${highlighted}
`; + }; + + 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' })); + } +} \ No newline at end of file diff --git a/site/src/routes/tutorial/[slug]/index.svelte b/site/src/routes/tutorial/[slug]/index.svelte new file mode 100644 index 0000000000..4b164455c8 --- /dev/null +++ b/site/src/routes/tutorial/[slug]/index.svelte @@ -0,0 +1,112 @@ + + + + + + +
+
+
+ +
+ +
+ {@html chapter.html} + + {#if selected.next} + + {/if} +
+
+ +
+ TODO add the REPL +
+ +
diff --git a/site/src/routes/tutorial/_layout.svelte b/site/src/routes/tutorial/_layout.svelte new file mode 100644 index 0000000000..c424fac2e6 --- /dev/null +++ b/site/src/routes/tutorial/_layout.svelte @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/site/src/routes/tutorial/data/_tutorials.js b/site/src/routes/tutorial/data/_tutorials.js deleted file mode 100644 index eb8be09f72..0000000000 --- a/site/src/routes/tutorial/data/_tutorials.js +++ /dev/null @@ -1,4 +0,0 @@ -export function get_tutorials() { - // TODO - return []; -} \ No newline at end of file diff --git a/site/src/routes/tutorial/data/sections.json.js b/site/src/routes/tutorial/data/sections.json.js deleted file mode 100644 index 6168c67fe9..0000000000 --- a/site/src/routes/tutorial/data/sections.json.js +++ /dev/null @@ -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); -} \ No newline at end of file diff --git a/site/src/routes/tutorial/index.json.js b/site/src/routes/tutorial/index.json.js new file mode 100644 index 0000000000..6a59aa22d4 --- /dev/null +++ b/site/src/routes/tutorial/index.json.js @@ -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); +} \ No newline at end of file diff --git a/site/src/routes/tutorial/index.svelte b/site/src/routes/tutorial/index.svelte index 37b1d6d57f..738a1b33e8 100644 --- a/site/src/routes/tutorial/index.svelte +++ b/site/src/routes/tutorial/index.svelte @@ -1,26 +1,5 @@ - - - - - -
-

Tutorial

-
- + \ No newline at end of file diff --git a/site/src/utils/_process_markdown.js b/site/src/utils/_process_markdown.js deleted file mode 100644 index f760018a3b..0000000000 --- a/site/src/utils/_process_markdown.js +++ /dev/null @@ -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 }; -} \ No newline at end of file diff --git a/site/src/utils/markdown.js b/site/src/utils/markdown.js new file mode 100644 index 0000000000..cbb4e3cbcc --- /dev/null +++ b/site/src/utils/markdown.js @@ -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('')) { + 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', +}; \ No newline at end of file diff --git a/site/static/global.css b/site/static/global.css index 38d1836a6c..63f9d6ad5e 100644 --- a/site/static/global.css +++ b/site/static/global.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,