import DOMPurify from "dompurify"; import { marked, type Tokens } from "marked"; import { parse as parseYaml } from "yaml"; import { ReferenceClient } from "@permaweb/references"; import { ARWEAVE_GATEWAY, MANIFEST_REFERENCE_NAME, MANIFEST_TX_ID } from "./config"; import { parseManifest, type Frontmatter, type ManifestPost } from "./types"; const formatDate = (date: string): string => new Date(date).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }); marked.setOptions({ gfm: true, breaks: false }); const escapeHtml = (value: string): string => value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); const getTweetEmbedUrl = (value: string): string | null => { let url: URL; try { url = new URL(value.trim()); } catch { return null; } if (url.protocol !== "https:" && url.protocol !== "http:") return null; const hostname = url.hostname.toLowerCase().replace(/^www\./, ""); const isTweetHost = hostname === "x.com" || hostname === "twitter.com" || hostname === "mobile.twitter.com"; if (!isTweetHost) return null; const [handle, statusSegment, statusId] = url.pathname.split("/").filter(Boolean); if (!handle || !statusSegment || !statusId) return null; if (!/^[a-z0-9_]{1,15}$/i.test(handle)) return null; if (statusSegment !== "status" && statusSegment !== "statuses") return null; if (!/^\d+$/.test(statusId)) return null; return `https://twitter.com/${handle}/status/${statusId}`; }; marked.use({ renderer: { paragraph(this, token: Tokens.Paragraph): string { const tweetUrl = getTweetEmbedUrl(token.text); if (!tweetUrl) { return `

${this.parser.parseInline(token.tokens)}

\n`; } const href = escapeHtml(tweetUrl); const link = `${href}`; return `
${link}
\n`; } } }); export const arweaveUrl = (txId: string): string => `${ARWEAVE_GATEWAY}/${txId}`; export const getReadableDate = (date?: string | null): string | null => { if (!date) return null; const parsed = Date.parse(date); if (Number.isNaN(parsed)) return null; return formatDate(date); }; export const loadManifest = async (): Promise => { return (await loadBestManifest()).posts; }; const timestampMs = (value: unknown): number => { if (typeof value !== "string" || !value) return 0; const parsed = Date.parse(value); return Number.isNaN(parsed) ? 0 : parsed; }; const manifestFreshness = (payload: unknown, posts: ManifestPost[]): number => { const root = isObject(payload) ? payload : {}; return Math.max( timestampMs(root.updated), timestampMs(root.date), timestampMs(root.generatedAt), ...posts.map((post) => Math.max(timestampMs(post.updated), timestampMs(post.publishedAt))) ); }; const fetchManifest = async (manifestTxId: string): Promise<{ txId: string; posts: ManifestPost[]; freshness: number; }> => { const response = await fetch(arweaveUrl(manifestTxId)); if (!response.ok) { throw new Error(`Failed to load manifest (${response.status})`); } const payload: unknown = await response.json(); const posts = parseManifest(payload); return { txId: manifestTxId, posts, freshness: manifestFreshness(payload, posts) }; }; const resolveManifestCandidates = async (): Promise => { const candidates: string[] = []; if (MANIFEST_REFERENCE_NAME) { try { const names = new ReferenceClient({ gateway: ARWEAVE_GATEWAY }); const value = await names.resolveName(MANIFEST_REFERENCE_NAME); if (typeof value === "string" && value.length > 0) candidates.push(value); else throw new Error(`Reference ${MANIFEST_REFERENCE_NAME} did not resolve to a manifest id`); } catch (error) { console.warn( `Could not resolve manifest reference ${MANIFEST_REFERENCE_NAME}; trying configured manifest`, error ); } } if (MANIFEST_TX_ID) candidates.push(MANIFEST_TX_ID); return [...new Set(candidates)]; }; const loadBestManifest = async () => { const candidates = await resolveManifestCandidates(); if (!candidates.length) throw new Error("No manifest reference or fallback manifest configured"); const loaded = []; for (const candidate of candidates) { try { loaded.push(await fetchManifest(candidate)); } catch (error) { console.warn(`Could not load manifest ${candidate}; trying other configured candidates`, error); } } if (!loaded.length) throw new Error("No configured manifest could be loaded"); loaded.sort((a, b) => b.freshness - a.freshness); return loaded[0]; }; export const resolveManifestTxId = async (): Promise => { return (await loadBestManifest()).txId; }; const isObject = (value: unknown): value is Record => typeof value === "object" && value !== null; const asStringOrUndefined = (value: unknown): string | undefined => typeof value === "string" ? value : undefined; const asBannerTxId = (value: unknown): string | undefined => { if (typeof value === "string") return value; if (!isObject(value)) return undefined; return ( asStringOrUndefined(value.txId) ?? asStringOrUndefined(value.id) ?? asStringOrUndefined(value.src) ?? asStringOrUndefined(value.url) ); }; const asStringArray = (value: unknown): string[] => Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : []; const parseFrontmatter = (input: unknown): Frontmatter => { if (!isObject(input)) return {}; return { title: asStringOrUndefined(input.title), desc: asStringOrUndefined(input.desc), description: asStringOrUndefined(input.description), excerpt: asStringOrUndefined(input.excerpt), slug: asStringOrUndefined(input.slug), banner: asBannerTxId(input.banner), date: asStringOrUndefined(input.date), updated: asStringOrUndefined(input.updated), tags: asStringArray(input.tags), categories: asStringArray(input.categories) }; }; export interface PostContent { html: string; frontmatter: Frontmatter; } const FRONTMATTER_PATTERN = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/; const splitFrontmatter = ( markdown: string ): { frontmatter: Frontmatter; content: string } => { const match = markdown.match(FRONTMATTER_PATTERN); if (!match) { return { frontmatter: {}, content: markdown }; } const parsed = parseYaml(match[1]); const frontmatter = parseFrontmatter(parsed); return { frontmatter, content: markdown.slice(match[0].length) }; }; export const loadPostContent = async (txId: string): Promise => { const response = await fetch(arweaveUrl(txId)); if (!response.ok) { throw new Error(`Failed to load post (${response.status})`); } const markdown = await response.text(); const { frontmatter, content } = splitFrontmatter(markdown); const html = await marked.parse(content); return { html: DOMPurify.sanitize(html), frontmatter }; };