# feeds — syndication feed FORMAT toolkit

Syndication feed FORMAT toolkit: parse, detect, convert, and merge web feeds — RSS 2.0, Atom 1.0, and JSON Feed 1.1. Pure hand-rolled lightweight XML/JSON parsing (zero dependencies, Node built-ins...


Parse, detect, convert, and merge web feeds across the three syndication formats:
**RSS 2.0**, **Atom 1.0**, and **JSON Feed 1.1**.

Pure JavaScript — **zero npm dependencies**, Node built-ins only. The XML reader is a small,
forgiving, hand-rolled tag-scanner (no `xml2js` / `fast-xml-parser` / `cheerio`). It handles
CDATA, XML/HTML entities (named + numeric), namespaced tags (`atom:`, `dc:`, `content:encoded`,
`media:`), and both RFC-822 (RSS) and ISO-8601 (Atom/JSON) dates.

## Design

Everything routes through **one canonical item model**, so any format converts to any other by
passing through the middle:

```
parseRss  ─┐                        ┌─ toRss
parseAtom ─┼──-> normalized items ──┼─ toAtom
parseJson ─┘   (title, link, id,    └─ toJsonFeed
                summary, content,
                author, published,
                updated, categories,
                enclosures)
```

Normalized item shape:

```js
{
  id, title, link,
  summary, content,
  author,
  published,      // ISO-8601 string | null
  updated,        // ISO-8601 string | null
  publishedMs,    // epoch ms | null  (sort key)
  categories,     // string[]
  enclosures,     // [{ url, type, length }]
  source,         // string | null
}
```

## Tools (14)

| Tool | Input | Returns |
|---|---|---|
| `parse` | `{ feed, options? }` | auto-detects format, then `{ format, meta, items }` |
| `parseRss` | `{ feed, options? }` | parse an RSS 2.0 / RDF string → `{ format, meta, items }` |
| `parseAtom` | `{ feed, options? }` | parse an Atom 1.0 string → `{ format, meta, items }` |
| `parseJsonFeed` | `{ feed, options? }` | parse a JSON Feed 1.x string/object → `{ format, meta, items }` |
| `detect` | `{ feed }` | `{ format: 'rss'\|'atom'\|'json'\|'unknown' }` |
| `toRss` | `{ items, meta? }` | `{ format:'rss', xml }` — valid RSS 2.0 |
| `toAtom` | `{ items, meta? }` | `{ format:'atom', xml }` — valid Atom 1.0 |
| `toJsonFeed` | `{ items, meta? }` | `{ format:'json', feed }` — JSON Feed 1.1 object |
| `merge` | `{ feeds, options? }` | combine parsed feeds/strings into one date-sorted, de-duped list |
| `normalizeItems` | `{ items }` | coerce loose objects into the canonical item shape |
| `sortItems` | `{ items, options? }` | sort by date (`order: 'desc'` default, or `'asc'`) |
| `latest` | `{ items \| feed, count? }` | the `count` most-recent items (default 10) |
| `extractLinks` | `{ feed }` | unique set of all URLs (home + item links + enclosures) |
| `extractCategories` | `{ feed }` | unique sorted set of all tags/categories |

`options`: `{ limit }` (parsers, cap item count) · `{ order: 'desc'\|'asc' }` (sort/merge) ·
`{ dedupe: boolean }` (merge, on by default — keys on `id` then `link` then `title`).

Every tool takes ONE args object (so an HTTP POST body maps 1:1). Invalid input throws a
`TypeError`/`Error`; an undetectable feed format throws an error tagged `status: 422`.

## Usage

```js
import feeds from './index.js';
const A = feeds.adapters;

// 1. Parse whatever format you were handed
const parsed = A.parse({ feed: someXmlOrJsonString });   // { format, meta, items }

// 2. Convert RSS → JSON Feed
const rss = A.parseRss({ feed: rssXml });
const { feed } = A.toJsonFeed({ items: rss.items, meta: rss.meta });

// 3. Aggregate several sources into one river, newest-first
const river = A.merge({ feeds: [rssXml, atomXml, jsonFeedString] });
const top5 = A.latest({ items: river.items, count: 5 });

// 4. Re-emit an aggregated Atom feed
const { xml } = A.toAtom({ items: top5.items, meta: { title: 'My River' } });
```

## DRY boundary

This pack owns feed **SYNDICATION FORMATS** — reading and writing the RSS / Atom / JSON Feed
grammar and converting between them. It does **not** fetch over the network and it does **not**
scrape arbitrary HTML: raw HTML extraction, link discovery, `sitemap.xml`, and `robots.txt` belong
to the **`scraping`** pack. If you need to *turn a webpage into a feed*, scrape with `scraping`,
shape the results, then hand them to `feeds` (`normalizeItems` → `toRss`/`toAtom`/`toJsonFeed`).
Numeric/date math stays in `numbers`; this pack only parses/emits the dates a feed standard requires.


---
Source: shared/engines/adapters/domain/feeds/README.md
Canonical: https://docs.leumas.tech/p/adapters/domain/feeds
