Feed Reader
rss.js 101 lines
Files · 10
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
export function parseFeed(xml) {
if (/<!DOCTYPE|<!ENTITY/i.test(xml))
throw new Error(
"Feeds with document types or external entities are not supported.",
);
const doc = new DOMParser().parseFromString(xml, "application/xml");
if (doc.querySelector("parsererror"))
throw new Error("The feed is not valid XML.");
const root = doc.documentElement;
if (root.localName === "rss") return parseRSS(root);
if (root.localName === "feed") return parseAtom(root);
throw new Error("Choose an RSS 2.0 or Atom feed.");
}
const children = (parent, name) =>
[...parent.children].filter((node) => node.localName === name);
const child = (parent, name) => children(parent, name)[0];
const text = (parent, name) => child(parent, name)?.textContent ?? null;
function byteCount(value) {
if (value === null || value === undefined) return null;
if (!/^\d+$/.test(value))
throw new Error("An enclosure length must be a nonnegative byte count.");
return Number(value);
}
function parseRSS(root) {
const channel = child(root, "channel");
if (!channel) throw new Error("The RSS document has no channel.");
const items = children(channel, "item").map((node) => {
const guid = child(node, "guid"),
source = child(node, "source"),
enclosure = child(node, "enclosure");
return {
guid: guid?.textContent ?? null,
guid_is_perma_link:
guid?.getAttribute("isPermaLink")?.toLowerCase() === "false" ? 0 : 1,
title: text(node, "title"),
link: text(node, "link"),
description: text(node, "description"),
author: text(node, "author"),
category: text(node, "category"),
comments: text(node, "comments"),
pub_date: text(node, "pubDate"),
source: source?.textContent ?? null,
source_url: source?.getAttribute("url") ?? null,
enclosure_url: enclosure?.getAttribute("url") ?? null,
enclosure_length: byteCount(enclosure?.getAttribute("length")),
enclosure_type: enclosure?.getAttribute("type") ?? null,
};
});
return { title: text(channel, "title"), items: usable(items) };
}
function parseAtom(root) {
const link = (node, rel) =>
children(node, "link").find(
(l) => (l.getAttribute("rel") || "alternate") === rel,
);
const rfc822 = (value) => {
const time = Date.parse(value);
return Number.isNaN(time) ? value : new Date(time).toUTCString();
};
const items = children(root, "entry").map((node) => {
const enclosure = link(node, "enclosure"),
date = text(node, "published") ?? text(node, "updated"),
author = child(node, "author");
return {
guid: text(node, "id"),
guid_is_perma_link: 0,
title: text(node, "title"),
link: link(node, "alternate")?.getAttribute("href") ?? null,
description: text(node, "summary") ?? text(node, "content"),
author: author ? text(author, "name") : null,
category: child(node, "category")?.getAttribute("term") ?? null,
comments: link(node, "replies")?.getAttribute("href") ?? null,
pub_date: date && rfc822(date),
source: null,
source_url: null,
enclosure_url: enclosure?.getAttribute("href") ?? null,
enclosure_length: byteCount(enclosure?.getAttribute("length")),
enclosure_type: enclosure?.getAttribute("type") ?? null,
};
});
return { title: text(root, "title"), items: usable(items) };
}
function usable(items) {
const kept = items.filter((item) => item.title || item.description);
if (!kept.length) throw new Error("The feed contains no items.");
return kept;
}
export function plainText(value) {
if (!value) return "";
const doc = new DOMParser().parseFromString(value, "text/html");
doc
.querySelectorAll("script, style, iframe, object")
.forEach((node) => node.remove());
return doc.body.textContent || "";
}