export interface AtomAuthor {
name: string;
email?: string;
}
export interface AtomEntry {
id: string;
title: string;
href: string;
updatedAt: string;
summary?: string;
contentHtml?: string;
author?: AtomAuthor;
}
export interface AtomFeedInput {
id: string;
title: string;
subtitle?: string;
selfHref: string;
alternateHref?: string;
updatedAt?: string;
entries: AtomEntry[];
}
export function escapeXml(input: string): string {
if (!input) return "";
return input
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
export function toIsoUtc(input: string | Date | null | undefined): string {
if (input instanceof Date) {
const t = input.getTime();
return Number.isFinite(t) ? new Date(t).toISOString() : "1970-01-01T00:00:00.000Z";
}
if (typeof input === "string" && input) {
const t = Date.parse(input);
if (Number.isFinite(t)) return new Date(t).toISOString();
}
return "1970-01-01T00:00:00.000Z";
}
function pickFeedUpdated(input: AtomFeedInput): string {
if (input.updatedAt) return toIsoUtc(input.updatedAt);
if (input.entries.length > 0) {
let newest = -Infinity;
for (const e of input.entries) {
const t = Date.parse(e.updatedAt);
if (Number.isFinite(t) && t > newest) newest = t;
}
if (newest > -Infinity) return new Date(newest).toISOString();
}
return new Date().toISOString();
}
function renderAuthor(a: AtomAuthor): string {
const lines = [` <name>${escapeXml(a.name || "unknown")}</name>`];
if (a.email) lines.push(` <email>${escapeXml(a.email)}</email>`);
return ` <author>\n${lines.join("\n")}\n </author>`;
}
function renderEntry(e: AtomEntry): string {
const parts = [
" <entry>",
` <id>${escapeXml(e.id)}</id>`,
` <title>${escapeXml(e.title || "(untitled)")}</title>`,
` <link rel="alternate" href="${escapeXml(e.href)}"/>`,
` <updated>${toIsoUtc(e.updatedAt)}</updated>`,
` <published>${toIsoUtc(e.updatedAt)}</published>`,
];
if (e.author) {
parts.push(
` <author>`,
` <name>${escapeXml(e.author.name || "unknown")}</name>`,
...(e.author.email
? [` <email>${escapeXml(e.author.email)}</email>`]
: []),
` </author>`
);
}
if (e.summary) {
parts.push(
` <summary type="text">${escapeXml(e.summary)}</summary>`
);
}
if (e.contentHtml) {
parts.push(
` <content type="html">${escapeXml(e.contentHtml)}</content>`
);
}
parts.push(" </entry>");
return parts.join("\n");
}
export function renderAtomFeed(input: AtomFeedInput): string {
const updated = pickFeedUpdated(input);
const lines: string[] = [
'<?xml version="1.0" encoding="utf-8"?>',
'<feed xmlns="http://www.w3.org/2005/Atom">',
` <id>${escapeXml(input.id)}</id>`,
` <title>${escapeXml(input.title)}</title>`,
];
if (input.subtitle) {
lines.push(` <subtitle>${escapeXml(input.subtitle)}</subtitle>`);
}
lines.push(` <updated>${updated}</updated>`);
lines.push(` <link rel="self" href="${escapeXml(input.selfHref)}"/>`);
if (input.alternateHref) {
lines.push(
` <link rel="alternate" href="${escapeXml(input.alternateHref)}"/>`
);
}
for (const entry of input.entries) {
lines.push(renderEntry(entry));
}
lines.push("</feed>");
return lines.join("\n") + "\n";
}
export const ATOM_CONTENT_TYPE = "application/atom+xml; charset=utf-8";
export const __internal = {
escapeXml,
toIsoUtc,
pickFeedUpdated,
renderAuthor,
renderEntry,
renderAtomFeed,
ATOM_CONTENT_TYPE,
};
|