mirror of https://github.com/sveltejs/svelte
feat(site-2): Make blog logic consistent with other content (#8447)
parent
b569018fef
commit
050c1031b7
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,60 @@
|
||||
// @ts-check
|
||||
import fs from 'fs';
|
||||
import { extract_frontmatter } from '../markdown';
|
||||
|
||||
const BLOG_NAME_REGEX = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
|
||||
const BASE = '../../site/content/blog';
|
||||
|
||||
/** @returns {import('./types').BlogData} */
|
||||
export function get_blog_data(base = BASE) {
|
||||
/** @type {import('./types').BlogData} */
|
||||
const blog_posts = [];
|
||||
|
||||
for (const file of fs.readdirSync(base).reverse()) {
|
||||
if (!BLOG_NAME_REGEX.test(file)) continue;
|
||||
|
||||
const { date, date_formatted, slug } = get_date_and_slug(file);
|
||||
const { metadata, body } = extract_frontmatter(fs.readFileSync(`${base}/${file}`, 'utf-8'));
|
||||
|
||||
blog_posts.push({
|
||||
date,
|
||||
date_formatted,
|
||||
content: body,
|
||||
description: metadata.description,
|
||||
draft: metadata.draft === 'true',
|
||||
slug,
|
||||
title: metadata.title,
|
||||
author: {
|
||||
name: metadata.author,
|
||||
url: metadata.authorURL
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return blog_posts;
|
||||
}
|
||||
|
||||
/** @param {import('./types').BlogData} blog_data */
|
||||
export function get_blog_list(blog_data) {
|
||||
return blog_data.map(({ slug, date, title, description, draft }) => ({
|
||||
slug,
|
||||
date,
|
||||
title,
|
||||
description,
|
||||
draft
|
||||
}));
|
||||
}
|
||||
|
||||
/** @param {string} filename */
|
||||
function get_date_and_slug(filename) {
|
||||
const match = BLOG_NAME_REGEX.exec(filename);
|
||||
if (!match) throw new Error(`Invalid filename for blog: '${filename}'`);
|
||||
|
||||
const [, date, slug] = match;
|
||||
const [y, m, d] = date.split('-');
|
||||
const date_formatted = `${months[+m - 1]} ${+d} ${y}`;
|
||||
|
||||
return { date, date_formatted, slug };
|
||||
}
|
||||
|
||||
const months = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ');
|
@ -1,75 +1,96 @@
|
||||
import fs from 'fs';
|
||||
import { extract_frontmatter } from '../markdown';
|
||||
import { transform } from './marked';
|
||||
|
||||
const base = '../../site/content/blog';
|
||||
// @ts-check
|
||||
import { createShikiHighlighter } from 'shiki-twoslash';
|
||||
import { SHIKI_LANGUAGE_MAP, escape, transform } from '../markdown';
|
||||
|
||||
/**
|
||||
* @returns {import('./types').BlogPostSummary[]}
|
||||
* @param {import('./types').BlogData} blog_data
|
||||
* @param {string} slug
|
||||
*/
|
||||
export function get_index() {
|
||||
return fs
|
||||
.readdirSync(base)
|
||||
.reverse()
|
||||
.map((file) => {
|
||||
if (!file.endsWith('.md')) return;
|
||||
export async function get_processed_blog_post(blog_data, slug) {
|
||||
const post = blog_data.find((post) => post.slug === slug);
|
||||
|
||||
const { date, slug } = get_date_and_slug(file);
|
||||
if (!post) return null;
|
||||
|
||||
const content = fs.readFileSync(`${base}/${file}`, 'utf-8');
|
||||
const { metadata } = extract_frontmatter(content);
|
||||
const highlighter = await createShikiHighlighter({ theme: 'css-variables' });
|
||||
|
||||
return {
|
||||
slug,
|
||||
date,
|
||||
title: metadata.title,
|
||||
description: metadata.description,
|
||||
draft: !!metadata.draft
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} slug
|
||||
* @returns {import('./types').BlogPost}
|
||||
...post,
|
||||
content: transform(post.content, {
|
||||
/**
|
||||
* @param {string} html
|
||||
*/
|
||||
export function get_post(slug) {
|
||||
for (const file of fs.readdirSync(`${base}`)) {
|
||||
if (!file.endsWith('.md')) continue;
|
||||
if (file.slice(11, -3) !== slug) continue;
|
||||
heading(html) {
|
||||
const title = html
|
||||
.replace(/<\/?code>/g, '')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
const { date, date_formatted } = get_date_and_slug(file);
|
||||
return title;
|
||||
},
|
||||
code: (source, language) => {
|
||||
let html = '';
|
||||
|
||||
const content = fs.readFileSync(`${base}/${file}`, 'utf-8');
|
||||
const { metadata, body } = extract_frontmatter(content);
|
||||
source = source
|
||||
.replace(/^([\-\+])?((?: )+)/gm, (match, prefix = '', spaces) => {
|
||||
if (prefix && language !== 'diff') return match;
|
||||
|
||||
return {
|
||||
date,
|
||||
date_formatted,
|
||||
title: metadata.title,
|
||||
description: metadata.description,
|
||||
author: {
|
||||
name: metadata.author,
|
||||
url: metadata.authorURL
|
||||
},
|
||||
draft: !!metadata.draft,
|
||||
content: transform(body)
|
||||
};
|
||||
// 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, '*/');
|
||||
|
||||
/** @param {string} filename */
|
||||
function get_date_and_slug(filename) {
|
||||
const match = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/.exec(filename);
|
||||
if (!match) throw new Error(`Invalid filename for blog: '${filename}'`);
|
||||
if (language === 'diff') {
|
||||
const lines = source.split('\n').map((content) => {
|
||||
let type = null;
|
||||
if (/^[\+\-]/.test(content)) {
|
||||
type = content[0] === '+' ? 'inserted' : 'deleted';
|
||||
content = content.slice(1);
|
||||
}
|
||||
|
||||
const [, date, slug] = match;
|
||||
const [y, m, d] = date.split('-');
|
||||
const date_formatted = `${months[+m - 1]} ${+d} ${y}`;
|
||||
return {
|
||||
type,
|
||||
content: escape(content)
|
||||
};
|
||||
});
|
||||
|
||||
return { date, date_formatted, slug };
|
||||
}
|
||||
html = `<pre class="language-diff"><code>${lines
|
||||
.map((line) => {
|
||||
if (line.type) return `<span class="${line.type}">${line.content}\n</span>`;
|
||||
return line.content + '\n';
|
||||
})
|
||||
.join('')}</code></pre>`;
|
||||
} else {
|
||||
html = highlighter.codeToHtml(source, { lang: SHIKI_LANGUAGE_MAP[language] });
|
||||
|
||||
const months = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ');
|
||||
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;
|
||||
|
||||
function format_date(date) {}
|
||||
return `<span class="token comment wrapped" style="--indent: ${indent}ch">${
|
||||
line ?? ''
|
||||
}</span>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
)
|
||||
.replace(/\/\*…\*\//g, '…');
|
||||
}
|
||||
|
||||
return html;
|
||||
},
|
||||
codespan: (text) => '<code>' + text + '</code>'
|
||||
})
|
||||
};
|
||||
}
|
||||
|
@ -1,187 +0,0 @@
|
||||
import PrismJS from 'prismjs';
|
||||
import 'prismjs/components/prism-bash.js';
|
||||
import 'prismjs/components/prism-diff.js';
|
||||
import 'prismjs/components/prism-typescript.js';
|
||||
import 'prism-svelte';
|
||||
import { marked } from 'marked';
|
||||
|
||||
const escape_test = /[&<>"']/;
|
||||
const escape_replace = /[&<>"']/g;
|
||||
const escape_test_no_encode = /[<>"']|&(?!#?\w+;)/;
|
||||
const escape_replace_no_encode = /[<>"']|&(?!#?\w+;)/g;
|
||||
const escape_replacements = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
const get_escape_replacement = (ch) => escape_replacements[ch];
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @param {boolean} [encode]
|
||||
*/
|
||||
export function escape(html, encode) {
|
||||
if (encode) {
|
||||
if (escape_test.test(html)) {
|
||||
return html.replace(escape_replace, get_escape_replacement);
|
||||
}
|
||||
} else {
|
||||
if (escape_test_no_encode.test(html)) {
|
||||
return html.replace(escape_replace_no_encode, get_escape_replacement);
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
const prism_languages = {
|
||||
bash: 'bash',
|
||||
env: 'bash',
|
||||
html: 'markup',
|
||||
svelte: 'svelte',
|
||||
js: 'javascript',
|
||||
css: 'css',
|
||||
diff: 'diff',
|
||||
ts: 'typescript',
|
||||
'': ''
|
||||
};
|
||||
|
||||
/** @type {Partial<import('marked').Renderer>} */
|
||||
const default_renderer = {
|
||||
code(code, infostring, escaped) {
|
||||
const lang = (infostring || '').match(/\S*/)[0];
|
||||
|
||||
const prism_language = prism_languages[lang];
|
||||
|
||||
if (prism_language) {
|
||||
const highlighted = PrismJS.highlight(code, PrismJS.languages[prism_language], lang);
|
||||
return `<div class="code-block"><pre class="language-${prism_language}"><code>${highlighted}</code></pre></div>`;
|
||||
}
|
||||
|
||||
return (
|
||||
'<div class="code-block"><pre><code>' +
|
||||
(escaped ? code : escape(code, true)) +
|
||||
'</code></pre></div>'
|
||||
);
|
||||
},
|
||||
|
||||
blockquote(quote) {
|
||||
return '<blockquote>\n' + quote + '</blockquote>\n';
|
||||
},
|
||||
|
||||
html(html) {
|
||||
return html;
|
||||
},
|
||||
|
||||
heading(text, level) {
|
||||
return '<h' + level + '>' + text + '</h' + level + '>\n';
|
||||
},
|
||||
|
||||
hr() {
|
||||
return '<hr>\n';
|
||||
},
|
||||
|
||||
list(body, ordered, start) {
|
||||
const type = ordered ? 'ol' : 'ul',
|
||||
startatt = ordered && start !== 1 ? ' start="' + start + '"' : '';
|
||||
return '<' + type + startatt + '>\n' + body + '</' + type + '>\n';
|
||||
},
|
||||
|
||||
listitem(text) {
|
||||
return '<li>' + text + '</li>\n';
|
||||
},
|
||||
|
||||
checkbox(checked) {
|
||||
return '<input ' + (checked ? 'checked="" ' : '') + 'disabled="" type="checkbox"' + '' + '> ';
|
||||
},
|
||||
|
||||
paragraph(text) {
|
||||
return '<p>' + text + '</p>\n';
|
||||
},
|
||||
|
||||
table(header, body) {
|
||||
if (body) body = '<tbody>' + body + '</tbody>';
|
||||
|
||||
return '<table>\n' + '<thead>\n' + header + '</thead>\n' + body + '</table>\n';
|
||||
},
|
||||
|
||||
tablerow(content) {
|
||||
return '<tr>\n' + content + '</tr>\n';
|
||||
},
|
||||
|
||||
tablecell(content, flags) {
|
||||
const type = flags.header ? 'th' : 'td';
|
||||
const tag = flags.align ? '<' + type + ' align="' + flags.align + '">' : '<' + type + '>';
|
||||
return tag + content + '</' + type + '>\n';
|
||||
},
|
||||
|
||||
// span level renderer
|
||||
strong(text) {
|
||||
return '<strong>' + text + '</strong>';
|
||||
},
|
||||
|
||||
em(text) {
|
||||
return '<em>' + text + '</em>';
|
||||
},
|
||||
|
||||
codespan(text) {
|
||||
return '<code>' + text + '</code>';
|
||||
},
|
||||
|
||||
br() {
|
||||
return '<br>';
|
||||
},
|
||||
|
||||
del(text) {
|
||||
return '<del>' + text + '</del>';
|
||||
},
|
||||
|
||||
link(href, title, text) {
|
||||
if (href === null) {
|
||||
return text;
|
||||
}
|
||||
let out = '<a href="' + escape(href) + '"';
|
||||
if (title) {
|
||||
out += ' title="' + title + '"';
|
||||
}
|
||||
out += '>' + text + '</a>';
|
||||
return out;
|
||||
},
|
||||
|
||||
image(href, title, text) {
|
||||
if (href === null) {
|
||||
return text;
|
||||
}
|
||||
|
||||
let out = '<img src="' + href + '" alt="' + text + '"';
|
||||
if (title) {
|
||||
out += ' title="' + title + '"';
|
||||
}
|
||||
out += '>';
|
||||
return out;
|
||||
},
|
||||
|
||||
text(text) {
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} markdown
|
||||
* @param {Partial<import('marked').Renderer>} renderer
|
||||
*/
|
||||
export function transform(markdown, renderer = {}) {
|
||||
marked.use({
|
||||
renderer: {
|
||||
// we have to jump through these hoops because of marked's API design choices —
|
||||
// options are global, and merged in confusing ways. You can't do e.g.
|
||||
// `new Marked(options).parse(markdown)`
|
||||
...default_renderer,
|
||||
...renderer
|
||||
}
|
||||
});
|
||||
|
||||
return marked(markdown);
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { get_index } from '$lib/server/blog';
|
||||
import { get_blog_data, get_blog_list } from '$lib/server/blog/get-blog-data';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function load() {
|
||||
return {
|
||||
posts: get_index()
|
||||
posts: get_blog_list(get_blog_data())
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in new issue