import * as fs from 'fs'; import * as crypto from 'crypto'; import showdown from 'showdown'; import { footnotes, highlight, prettify, spoiler, video } from '$lib/showdown'; import { dither } from '$lib/img'; interface Metadata { title: string; hero?: string; series?: string; part?: number; } export interface Post extends Metadata { type: 'post'; slug: string; date: string; readtime: number; html?: string; } export interface Note extends Metadata { type: 'note'; slug: string; date: string; html?: string; } export type Article = Post | Note; const converter = new showdown.Converter({ tables: true, strikethrough: true, // `highlight` needs to be before `prettify` extensions: [footnotes, highlight, prettify, spoiler, video] }); const calculateReadingTime = (text: string) => { const wordsPerMinute = 200; const words = text .trim() .split(/\s+/) .filter((word) => word.length > 0).length; return Math.ceil(words / wordsPerMinute) || 1; }; const getContentData = (data: string, includeReadTime = true) => { // because there is no metadata extension, we need to read the lines const lines = data.split('\n'); const metadataEnd = lines.indexOf('---', 1); const body = lines.slice(metadataEnd + 1, lines.length).join('\n'); const metadata: Metadata = Object.fromEntries( lines.slice(1, metadataEnd).map((x: string) => x.split(/:(.*)/s).map((x) => x.trim())) ) as Metadata; const result = { ...metadata, html: converter.makeHtml(body) }; if (includeReadTime) { return { ...result, readtime: Math.ceil(calculateReadingTime(body)) }; } return result; }; const parseFilename = (file: string) => { const f = file.split('/')[1].split('.')[0].split('-'); const [date, slug] = [f.splice(0, 3).join('-'), f.splice(0, 1)[0]]; if (slug !== '[cid]') return [date, slug]; const file_buffer = fs.readFileSync(file); const sum = crypto.createHash('sha256'); sum.update(file_buffer); return [date, sum.digest('hex')]; }; const getPostFileList = () => [ ...fs.readdirSync('_posts').map((x) => `_posts/${x}`), ...(process.env.SHOW_DRAFTS ? fs.readdirSync('_drafts').map((x) => `_drafts/${x}`) : []) ]; const getNoteFileList = () => { try { return fs.readdirSync('_notes').map((x) => `_notes/${x}`); } catch (e) { return []; } }; const getPosts = (): Post[] => { const series: Record = {}; return getPostFileList() .map((file) => { const [date, slug] = parseFilename(file); const post = getContentData(fs.readFileSync(file, { encoding: 'utf-8' }), true); if (post.series) { if (post.series in series) series[post.series].push(date); else series[post.series] = [date]; } return { ...post, slug, date, type: 'post' } as Post; }) .map((post) => { if (post.series) { const part = series[post.series].findIndex((p) => p === post.date); post.part = part; } return post; }); }; const getNotes = (): Note[] => { const series: Record = {}; return getNoteFileList().map((file) => { const [date, slug] = parseFilename(file); const note = getContentData(fs.readFileSync(file, { encoding: 'utf-8' }), false); if (note.series) { if (note.series in series) series[note.series].push(date); else series[note.series] = [date]; } return { ...note, slug, date, type: 'note' } as Note; }); }; const getPost = (snail: string) => getPosts().find((p) => p.slug === snail)!; const getNote = (snail: string) => getNotes().find((n) => n.slug === snail)!; const processHTML = (html: string) => { html = html.replace( /(.*?)]*)>/gi, (match, imgName, altText, attrs) => { const found = Object.entries(dither).find(([path, _]) => path.endsWith(imgName))?.[1]; if (found) { return `${altText}`; } return match; } ); const footnoteContents: Record = {}; html = html.replace( /