import { stringifyEntities } from 'stringify-entities'; import * as fs from 'fs/promises'; import config from '$lib/config'; /** * @param {Record} properties */ function renderProperties(properties) { if (Object.keys(properties).length === 0) return ''; return ' ' + Object.entries(properties) .filter(([k, v]) => k !== '' && v !== '') .map(([k, v]) => v ? `${k}=${typeof v === 'string' ? `"${stringifyEntities(v)}"` : v.toString()}` : k ).join(' '); } /** * @param {ASTMap} ast * @returns {string} */ // todo: obliterate from orbit export function renderASTMap(ast) { switch (ast.type) { case 'root': return ast.children.map(c => renderASTMap(c)).join('') case 'element': if (ast.tagName === 'a') { ast.properties.target = '_blank'; ast.properties.rel = 'noreferrer noopener'; } if (ast.properties.id && ast.properties.id.includes('cohost-blogger-ignore')) return ''; return `<${ast.tagName}${renderProperties(ast.properties)}>${ast.children.map(c => renderASTMap(c)).join('')}`; case 'text': return stringifyEntities(ast.value); default: return ''; } } const COHOST_API_URI = 'https://cohost.org/api/v1/trpc/'; /** * @param {string} route * @param {Record} input */ export async function trpcRequest(route, input) { const url = new URL(COHOST_API_URI + route); if (input) url.searchParams.set('input', JSON.stringify(input)); const data = await (await fetch(url)).json(); return data; } const PAGES_PER_POST = 20; /** * @param {number} page * @returns {Promise} */ export async function fetchAllPosts(page = 0) { const data = await trpcRequest('posts.getPostsTagged', { projectHandle: config.handle, tagSlug: config.tag, page: page }); let posts = data.result.data.items; if (data.result.data.nItems >= PAGES_PER_POST) { posts = [...posts, ...(await fetchAllPosts(page + 1))] } return posts; } /** * @returns {Promise} */ async function getPostsUncached() { return await fetchAllPosts(); //return JSON.parse(await fs.readFile('src/testPosts.json', 'utf8')).filter(post => post.tags.includes('cohost-blogger')); } // this technically only stores the preview data - the posts on the actual pages are always fetched // however there is no way to fetch a specified amount of info, so cache it is let postCache = { /** @type {Post[]} **/ posts: [], refreshed: -1 } const CACHE_INVALID_PERIOD = 60 * 1000; /** * @returns {Promise} */ export async function getPosts() { const timeSinceCache = Date.now() - postCache.refreshed; if (timeSinceCache > CACHE_INVALID_PERIOD) { postCache.posts = await getPostsUncached(); postCache.refreshed = Date.now(); } return postCache.posts; } /** * @param {Post} post */ export function getPostImages(post) { return post.blocks.filter(block => block.type === 'attachment').map(block => block.attachment); } const COMMENT_REGEX = /^\s*\s*$/; /** * @param {Post} post * @returns {Record} */ export function getPostMetadata(post) { return post.blocks .filter(block => block.type === 'markdown') .map(block => block.markdown.content) .map(text => COMMENT_REGEX.exec(text)).filter(res => res !== null).map(res => res[1]) .reduce((lines, comment) => [...lines, ...comment.split('\n')], []) .map(line => line.trim()) .filter(line => line.length > 0) .reduce((properties, line) => { properties[line.split(':')[0].trim()] = line.split(':')[1].trim(); return properties; }, {}); } /** * @param {Post} post * @returns {Date | null} */ export function getPostPublishDate(post) { const meta = getPostMetadata(post); if (meta['published-at']) return new Date(meta['published-at']); if (post.publishedAt) return new Date(post.publishedAt); return null; } /** * @param {Post} post * @returns {string} */ export function getPostSlug(post) { return getPostMetadata(post).slug || post.filename; }