cohost-blogger/src/lib/cohost.js

146 lines
4.0 KiB
JavaScript

import { stringifyEntities } from 'stringify-entities';
import * as fs from 'fs/promises';
import config from '$lib/config';
/**
* @param {Record<string, any>} 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('')}</${ast.tagName}>`;
case 'text':
return stringifyEntities(ast.value);
default:
return '';
}
}
const COHOST_API_URI = 'https://cohost.org/api/v1/trpc/';
/**
* @param {string} route
* @param {Record<string, any>} 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<Post[]>}
*/
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<Post[]>}
*/
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<Post[]>}
*/
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*@cohost-blogger-meta\s+([\S\s]+)\s*-->\s*$/;
/**
* @param {Post} post
* @returns {Record<string, string>}
*/
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;
}