feat(site-2): Make blog logic consistent with other content (#8447)

pull/8453/head
Puru Vijay 1 year ago committed by GitHub
parent b569018fef
commit 050c1031b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

File diff suppressed because it is too large Load Diff

@ -29,7 +29,7 @@
"@resvg/resvg-js": "^2.4.1",
"@sveltejs/adapter-vercel": "^2.4.1",
"@sveltejs/kit": "^1.15.0",
"@sveltejs/site-kit": "^3.3.6",
"@sveltejs/site-kit": "^3.3.7",
"@sveltejs/vite-plugin-svelte": "^2.0.4",
"@types/marked": "^4.0.8",
"@types/prismjs": "^1.26.0",

@ -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
};
});
}
return {
...post,
content: transform(post.content, {
/**
* @param {string} html
*/
heading(html) {
const title = html
.replace(/<\/?code>/g, '')
.replace(/&quot;/g, '"')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>');
/**
* @param {string} slug
* @returns {import('./types').BlogPost}
*/
export function get_post(slug) {
for (const file of fs.readdirSync(`${base}`)) {
if (!file.endsWith('.md')) continue;
if (file.slice(11, -3) !== slug) continue;
return title;
},
code: (source, language) => {
let html = '';
const { date, date_formatted } = get_date_and_slug(file);
source = source
.replace(/^([\-\+])?((?: )+)/gm, (match, prefix = '', spaces) => {
if (prefix && language !== 'diff') return match;
const content = fs.readFileSync(`${base}/${file}`, 'utf-8');
const { metadata, body } = extract_frontmatter(content);
// 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, '*/');
return {
date,
date_formatted,
title: metadata.title,
description: metadata.description,
author: {
name: metadata.author,
url: metadata.authorURL
},
draft: !!metadata.draft,
content: transform(body)
};
}
}
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);
}
/** @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}'`);
return {
type,
content: escape(content)
};
});
const [, date, slug] = match;
const [y, m, d] = date.split('-');
const date_formatted = `${months[+m - 1]} ${+d} ${y}`;
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] });
return { date, date_formatted, slug };
}
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;
const months = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ');
return `<span class="token comment wrapped" style="--indent: ${indent}ch">${
line ?? ''
}</span>`;
})
.join('');
}
)
.replace(/\/\*…\*\//g, '…');
}
function format_date(date) {}
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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
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);
}

@ -3,6 +3,7 @@ export interface BlogPost {
description: string;
date: string;
date_formatted: string;
slug: string;
author: {
name: string;
url?: string;
@ -11,6 +12,8 @@ export interface BlogPost {
content: string;
}
export type BlogData = BlogPost[];
export interface BlogPostSummary {
slug: string;
title: string;

@ -3,26 +3,13 @@
// import 'prismjs/components/prism-diff.js';
// import 'prismjs/components/prism-typescript.js';
import { createShikiHighlighter } from 'shiki-twoslash';
import { normalizeSlugify, transform } from '../markdown';
import { SHIKI_LANGUAGE_MAP, normalizeSlugify, transform } from '../markdown';
// import { render, replace_placeholders } from './render.js';
// import { parse_route_id } from '../../../../../../packages/kit/src/utils/routing.js';
import { createHash } from 'crypto';
import MagicString from 'magic-string';
import ts from 'typescript';
const languages = {
bash: 'bash',
env: 'bash',
html: 'html',
svelte: 'svelte',
sv: 'svelte',
js: 'javascript',
css: 'css',
diff: 'diff',
ts: 'typescript',
'': ''
};
/**
* @param {import('./types').DocsData} docs_data
* @param {string} slug
@ -83,24 +70,7 @@ export async function get_parsed_docs(docs_data, slug) {
}
// TODO: Replace later
html = highlighter.codeToHtml(source, { lang: languages[language] });
// if (language === 'dts') {
// // @ts-ignore
// html = renderCodeToHTML(source, 'ts', { twoslash: false }, {}, highlighter);
// } else if (language === 'js' || language === 'ts') {
// try {
// const injected = [];
// if (
// source.includes('$app/') ||
// source.includes('$service-worker') ||
// source.includes('@sveltejs/kit/')
// ) {
// injected.push(
// `// @filename: ambient-kit.d.ts`,
// `/// <reference types="@sveltejs/kit" />`
// );
// }
html = highlighter.codeToHtml(source, { lang: SHIKI_LANGUAGE_MAP[language] });
// if (source.includes('$env/')) {
// // TODO we're hardcoding static env vars that are used in code examples

@ -1,19 +1,6 @@
// @ts-check
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',
'': ''
};
import { SHIKI_LANGUAGE_MAP, transform } from '../markdown';
/**
* @param {import('./types').FAQData} faq_data
@ -55,7 +42,7 @@ export async function get_parsed_faq(faq_data) {
})
.replace(/\*\\\//g, '*/');
html = highlighter.codeToHtml(source, { lang: languages[language] });
html = highlighter.codeToHtml(source, { lang: SHIKI_LANGUAGE_MAP[language] });
html = html
.replace(

@ -17,6 +17,19 @@ const escapeReplacements = {
*/
const getEscapeReplacement = (ch) => escapeReplacements[ch];
export const SHIKI_LANGUAGE_MAP = {
bash: 'bash',
env: 'bash',
html: 'svelte',
svelte: 'svelte',
sv: 'svelte',
js: 'javascript',
css: 'css',
diff: 'diff',
ts: 'typescript',
'': ''
};
/**
* @param {string} html
* @param {boolean} encode

@ -1,18 +1,5 @@
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',
'': ''
};
import { SHIKI_LANGUAGE_MAP, transform } from '../markdown';
/**
* @param {import('./types').TutorialData} tutorial_data
@ -58,7 +45,7 @@ export async function get_parsed_tutorial(tutorial_data, slug) {
})
.replace(/\*\\\//g, '*/');
html = highlighter.codeToHtml(source, { lang: languages[language] });
html = highlighter.codeToHtml(source, { lang: SHIKI_LANGUAGE_MAP[language] });
html = html
.replace(

@ -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())
};
}

@ -1,10 +1,11 @@
import { get_post } from '$lib/server/blog/index.js';
import { get_processed_blog_post } from '$lib/server/blog';
import { get_blog_data } from '$lib/server/blog/get-blog-data';
import { error } from '@sveltejs/kit';
export const prerender = true;
export async function load({ params }) {
const post = get_post(params.slug);
const post = get_processed_blog_post(get_blog_data(), params.slug);
if (!post) {
throw error(404);

@ -1,10 +1,11 @@
import satori from 'satori';
import { get_blog_data } from '$lib/server/blog/get-blog-data';
import { get_processed_blog_post } from '$lib/server/blog/index.js';
import { Resvg } from '@resvg/resvg-js';
import OverpassRegular from './Overpass-Regular.ttf';
import { html as toReactNode } from 'satori-html';
import { get_post } from '$lib/server/blog/index.js';
import { error } from '@sveltejs/kit';
import satori from 'satori';
import { html as toReactNode } from 'satori-html';
import Card from './Card.svelte';
import OverpassRegular from './Overpass-Regular.ttf';
const height = 630;
const width = 1200;
@ -13,7 +14,7 @@ export const prerender = true;
/** @type {import('./$types').RequestHandler} */
export const GET = async ({ params, url }) => {
const post = get_post(params.slug);
const post = get_processed_blog_post(get_blog_data(), params.slug);
if (!post) {
throw error(404);

@ -1,4 +1,4 @@
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;
@ -58,7 +58,7 @@ const get_rss = (posts) =>
.trim();
export async function GET() {
const posts = get_index();
const posts = get_blog_list(get_blog_data());
return new Response(get_rss(posts), {
headers: {

Loading…
Cancel
Save