init
This commit is contained in:
185
src/lib.ts
Normal file
185
src/lib.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import DOMPurify from "dompurify";
|
||||
import { marked } from "marked";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import {
|
||||
AO_PROCESS_ID,
|
||||
AO_URL,
|
||||
ARWEAVE_GATEWAY,
|
||||
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
|
||||
});
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const asObject = (value: unknown): Record<string, unknown> | null =>
|
||||
typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
|
||||
|
||||
const asString = (value: unknown): string | null =>
|
||||
typeof value === "string" && value.length > 0 ? value : null;
|
||||
|
||||
const getLatestManifestFromAo = async (): Promise<string | null> => {
|
||||
try {
|
||||
const pushResponse = await fetch(`${AO_URL}/${AO_PROCESS_ID}~process@1.0/push`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"signing-format": "ans104",
|
||||
"accept-bundle": "true",
|
||||
"require-codec": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
Type: "Message",
|
||||
"Data-Protocol": "ao",
|
||||
Variant: "ao.N.1",
|
||||
Action: "Get",
|
||||
target: AO_PROCESS_ID,
|
||||
data: "1984"
|
||||
})
|
||||
});
|
||||
if (!pushResponse.ok) return null;
|
||||
|
||||
const pushPayload = (await pushResponse.json()) as unknown;
|
||||
const slot = asString(asObject(pushPayload)?.slot);
|
||||
if (!slot) return null;
|
||||
|
||||
const computeResponse = await fetch(`${AO_URL}/${AO_PROCESS_ID}~process@1.0/compute=${slot}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"signing-format": "ans104",
|
||||
"accept-bundle": "true",
|
||||
"require-codec": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
target: AO_PROCESS_ID,
|
||||
data: "1984"
|
||||
})
|
||||
});
|
||||
if (!computeResponse.ok) return null;
|
||||
|
||||
const computePayload = (await computeResponse.json()) as unknown;
|
||||
const body = asObject(asObject(asObject(computePayload)?.results)?.json)?.body;
|
||||
const parsedBody = typeof body === "string" ? asObject(JSON.parse(body)) : asObject(body);
|
||||
const messages = Array.isArray(parsedBody?.Messages) ? parsedBody.Messages : [];
|
||||
|
||||
for (const message of messages) {
|
||||
const tags = Array.isArray(asObject(message)?.Tags) ? (asObject(message)?.Tags as unknown[]) : [];
|
||||
for (const tag of tags) {
|
||||
const tagObject = asObject(tag);
|
||||
const name = asString(tagObject?.name) ?? asString(tagObject?.Name);
|
||||
const value = asString(tagObject?.value) ?? asString(tagObject?.Value);
|
||||
if (name === "LatestManifestId" && value) return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const loadManifest = async (): Promise<ManifestPost[]> => {
|
||||
const manifestTxId = (await getLatestManifestFromAo()) ?? MANIFEST_TX_ID;
|
||||
const response = await fetch(arweaveUrl(manifestTxId));
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load manifest (${response.status})`);
|
||||
}
|
||||
|
||||
const payload: unknown = await response.json();
|
||||
return parseManifest(payload);
|
||||
};
|
||||
|
||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||
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<PostContent> => {
|
||||
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
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user