diff --git a/README.md b/README.md
index f83910b..69a9e04 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,18 @@
# @jsonic/feed
-A [Jsonic](https://jsonic.senecajs.org) plugin (built on
-[`@jsonic/xml`](https://github.com/jsonicjs/xml)) that parses
-syndication feeds — **RSS 0.90, 0.91, 0.92, 1.0, 2.0** and **Atom 0.3,
-1.0** — into a typed structure. By default every dialect is normalised
-to an Atom-shaped result; pass `format: 'native'` to keep the source
-dialect's structure, or `format: 'raw'` to get back the underlying
-XML element tree from `@jsonic/xml`.
+A [Jsonic](https://jsonic.senecajs.org) plugin — built on
+[`@jsonic/xml`](https://github.com/jsonicjs/xml) — that parses
+syndication feeds (**RSS 0.90, 0.91, 0.92, 1.0, 2.0** and **Atom 0.3,
+1.0**) into a typed structure. By default every dialect is normalised
+to an Atom-shaped result, so the same downstream code can consume
+feeds from any source.
The same parser is available in two languages:
-| Language | Package | Source |
-| ---------- | -------------------------------------------------------------- | ----------------------------------- |
-| TypeScript | [`@jsonic/feed`](https://npmjs.com/package/@jsonic/feed) | [`src/feed.ts`](src/feed.ts) |
-| Go | [`github.com/jsonicjs/feed/go`](https://github.com/jsonicjs/feed/tree/main/go) | [`go/feed.go`](go/feed.go) |
+| Language | Package | Source | Docs |
+| ---------- | -------------------------------------------------------------- | ---------------------------- | --------------------------------- |
+| TypeScript | [`@jsonic/feed`](https://npmjs.com/package/@jsonic/feed) | [`src/feed.ts`](src/feed.ts) | this file |
+| Go | [`github.com/jsonicjs/feed/go`](https://github.com/jsonicjs/feed/tree/main/go) | [`go/feed.go`](go/feed.go) | [`go/README.md`](go/README.md) |
[](https://npmjs.com/package/@jsonic/feed)
[](https://github.com/jsonicjs/feed/actions/workflows/build.yml)
@@ -23,30 +22,45 @@ The same parser is available in two languages:
| ---------------------------------------------------- | --------------------------------------------------------------------------------------- |
-The documentation below is organised along the
-[Diátaxis](https://diataxis.fr) quadrants:
+> **Go users:** the [`go/README.md`](go/README.md) is a Go-only view
+> of these same docs. The rest of this file covers both languages
+> side by side.
-- [Quick start](#quick-start) — tutorial
-- [How-to guides](#how-to-guides) — task recipes
-- [Reference](#reference) — API surface
-- [Format mapping](#format-mapping) — explanation
+This documentation follows the four [Diátaxis](https://diataxis.fr)
+modes:
+- [Tutorial](#tutorial) — work through a first feed parse
+- [How-to guides](#how-to-guides) — short recipes for specific tasks
+- [Reference](#reference) — types, options, mapping tables
+- [Explanation](#explanation) — design rationale and trade-offs
-## Quick start
+
+---
+
+## Tutorial
+
+This walkthrough takes you from an empty project to a parsed feed in
+under a minute. By the end you will have a working `Feed`-equipped
+parser, recognise the shape of its output, and know where to look in
+the rest of the docs.
### TypeScript
+Install the plugin and its peer dependencies:
+
```bash
npm install @jsonic/feed jsonic @jsonic/xml
```
+Create `index.ts`:
+
```typescript
import { Jsonic } from 'jsonic'
import { Feed } from '@jsonic/feed'
const j = Jsonic.make().use(Feed)
-const atom = j(`
+const result = j(`
My Blog
@@ -62,17 +76,29 @@ const atom = j(`
`)
-// atom.format === 'atom'
-// atom.title.value === 'My Blog'
-// atom.entries[0].id === 'https://example.com/1'
+console.log(result.title.value) // 'My Blog'
+console.log(result.entries[0].id) // 'https://example.com/1'
+console.log(result.entries[0].links[0]) // { href: '...', rel: 'alternate' }
```
+The input was RSS 2.0 but `result` is in **Atom shape**:
+`title` is an `AtomText` (`{ type, value }`), `entries[0].id` came
+from RSS's ``, and the `` became an Atom link with
+`rel: 'alternate'`. The plugin handles every supported dialect this
+way, so the rest of your code never has to branch on the source
+format.
+
### Go
+Initialise a module and pull in the plugin:
+
```bash
+go mod init example
go get github.com/jsonicjs/feed/go
```
+Create `main.go`:
+
```go
package main
@@ -87,7 +113,8 @@ func main() {
if err := j.UseDefaults(feed.Feed, feed.Defaults); err != nil {
panic(err)
}
- got, err := j.Parse(`
+ got, err := j.Parse(`
+ My BlogHello1
@@ -98,33 +125,37 @@ func main() {
}
f := got.(feed.AtomFeed)
fmt.Println(f.Title.Value, "/", f.Entries[0].ID)
+ // My Blog / 1
}
```
+`got` is `any`; type-assert it to `feed.AtomFeed` (the default), or
+to `feed.Rss2Feed` / `feed.Rss1Feed` when you opt into the native
+shape (see the next section).
+
+
+---
## How-to guides
-### Keep the source dialect's structure (no Atom conversion)
+### How to keep the source dialect's structure
-TypeScript:
+When you need RSS-specific fields like `ttl`, `cloud`, or `skipDays`
+that the Atom shape does not carry, ask for the native form:
```typescript
-import { Jsonic } from 'jsonic'
import { Feed, type Rss2Feed } from '@jsonic/feed'
-
const j = Jsonic.make().use(Feed, { format: 'native' })
const native = j(rssSource) as Rss2Feed
-// native.format === 'rss', native.version === '2.0'
+// native.ttl, native.cloud, native.skipDays
```
-Go:
-
```go
j := jsonic.Make()
j.UseDefaults(feed.Feed, feed.Defaults, map[string]any{"format": "native"})
got, _ := j.Parse(rssSource)
native := got.(feed.Rss2Feed)
-// native.Format == "rss", native.Version == "2.0"
+// native.TTL, native.Cloud, native.SkipDays
```
The native return type is a discriminated union on `format`:
@@ -135,9 +166,11 @@ The native return type is a discriminated union on `format`:
| RSS 2.0 / 0.92 / 0.91 | `Rss2Feed` | `'rss'` | `'2.0'` / `'0.92'` / `'0.91'` |
| RSS 1.0 / 0.90 | `Rss1Feed` | `'rdf'` | `'1.0'` / `'0.90'` |
-### Get the raw XML tree
+### How to access the raw XML tree
-TypeScript:
+When even the native shape is not enough — for example you need a
+non-standard namespace extension like `` — drop down
+to the raw element tree from `@jsonic/xml`:
```typescript
const j = Jsonic.make().use(Feed, { format: 'raw' })
@@ -145,22 +178,18 @@ const tree = j(rssSource)
// tree.localName === 'rss', tree.children === [...]
```
-Go:
-
```go
j := jsonic.Make()
j.UseDefaults(feed.Feed, feed.Defaults, map[string]any{"format": "raw"})
got, _ := j.Parse(rssSource)
tree := got.(map[string]any)
-// tree["localName"] == "rss", tree["children"].([]any) == [...]
+// tree["localName"] == "rss"; tree["children"].([]any)
```
-This is the element tree produced by `@jsonic/xml` with no further
-processing, useful when you want to handle non-standard extensions.
+### How to detect a feed's dialect without converting
-### Detect a dialect without converting
-
-TypeScript:
+Use `format: 'raw'` to get the underlying XML tree, then call
+`detect`:
```typescript
import { Feed, detect } from '@jsonic/feed'
@@ -169,74 +198,64 @@ const { dialect, version } = detect(j(rssSource))
// e.g. { dialect: 'rss', version: 'rss20' }
```
-Go:
-
```go
j := jsonic.Make()
j.UseDefaults(feed.Feed, feed.Defaults, map[string]any{"format": "raw"})
got, _ := j.Parse(rssSource)
det := feed.Detect(got)
-// e.g. feed.Detection{Dialect: "rss", Version: "rss20"}
+// e.g. {Dialect: "rss", Version: "rss20"}
```
+### How to read fields that the Atom conversion drops
-## Reference
+The default conversion is lossy by design (see
+[Explanation](#what-conversion-loses) below). If your application
+needs both the convenient Atom shape *and* a stray RSS-only field,
+parse twice into the same source — once for each format — or use
+`format: 'raw'` plus your own extraction. The recommended path is to
+parse `'native'` and convert in your own code only when you need the
+Atom shape.
-### TypeScript
-```typescript
-const Feed: Plugin
-
-function detect(root: XmlElement): { dialect: FeedDialect; version: FeedVersion }
+---
-type FeedOptions = {
- format?: 'atom' | 'native' | 'raw' // default: 'atom'
-}
+## Reference
-type FeedResult = AtomFeed | Rss2Feed | Rss1Feed | XmlElement
+### Plugin registration
-type FeedDialect = 'atom' | 'rss' | 'rdf' | 'unknown'
+**TypeScript**
-type FeedVersion =
- | 'atom10' | 'atom03'
- | 'rss20' | 'rss092' | 'rss091u' | 'rss091n'
- | 'rss10' | 'rss090'
- | 'unknown'
+```typescript
+import { Feed } from '@jsonic/feed'
+const j = Jsonic.make().use(Feed, options?)
+const result = j(src)
```
-Use the plugin via `Jsonic.make().use(Feed, options?)`. After
-registration, invoke the jsonic instance as a function on a feed XML
-source string; it returns the converted feed (or the raw `XmlElement`
-tree, when `options.format === 'raw'`).
-
-| Option | Type | Default | Effect |
-|----------|-------------------------------|----------|-------------------------------------------------------|
-| `format` | `'atom' \| 'native' \| 'raw'` | `'atom'` | Output shape: normalised Atom, dialect-native, or raw XML tree |
-
-### Go
+**Go**
```go
-func Feed(j *jsonic.Jsonic, opts map[string]any) error
-func Detect(root any) Detection
+j := jsonic.Make()
+err := j.UseDefaults(feed.Feed, feed.Defaults, opts)
+result, err := j.Parse(src)
+```
-var Defaults = map[string]any{ "format": "atom" }
+### Options
-type Detection struct {
- Dialect string `json:"dialect"`
- Version string `json:"version"`
-}
-```
+| Key | Type | Default | Effect |
+|----------|-------------------------------|----------|-------------------------------------------------------|
+| `format` | `'atom' \| 'native' \| 'raw'` | `'atom'` | Output shape: normalised Atom, dialect-native, or raw XML element tree |
-Register with `j.UseDefaults(feed.Feed, feed.Defaults, opts)` where
-`opts` is a `map[string]any` overriding the defaults. `Parse` then
-returns `(any, error)`; type-assert the result to `feed.AtomFeed`,
-`feed.Rss2Feed`, or `feed.Rss1Feed` based on the `format` option.
+### Result types
-| Key | Type | Default | Effect |
-|----------|----------|----------|-------------------------------------------------------|
-| `format` | `string` | `"atom"` | Output shape: `"atom"`, `"native"`, or `"raw"` |
+The `format` option determines which type the parser returns:
-Atom shape (the default output) follows RFC 4287 closely:
+| `format` | TypeScript | Go |
+|------------|---------------------------------------------|---------------------------------------------|
+| `'atom'` | `AtomFeed` | `feed.AtomFeed` |
+| `'native'` | `AtomFeed \| Rss2Feed \| Rss1Feed` | `feed.AtomFeed` / `feed.Rss2Feed` / `feed.Rss1Feed` |
+| `'raw'` | `XmlElement` (from `@jsonic/xml`) | `map[string]any` |
+
+### Atom shape (RFC 4287)
```typescript
type AtomFeed = {
@@ -281,79 +300,154 @@ type AtomGenerator = { uri?: string; version?: string; value: string }
type AtomContent = { type: string; src?: string; value?: string }
```
-The Go structs (`AtomFeed`, `AtomEntry`, `Rss2Feed`, `Rss2Item`,
-`Rss1Feed`, `Rss1Item`, …) carry equivalent JSON tags so they marshal
-to the same shape. See [`go/feed.go`](go/feed.go) for the full set.
-
-
-## Format mapping
-
-When converting any RSS dialect to Atom, the plugin makes the following
-best-effort mappings:
-
-| RSS source | Atom target |
-|-------------------------------|----------------------------------------------------|
-| `channel/title` | `feed.title` (`type: 'text'`) |
-| `channel/description` | `feed.subtitle` (`type: 'text'`) |
-| `channel/link` | `feed.id` and `feed.links[]` (`rel: 'alternate'`) |
-| `channel/copyright` | `feed.rights` |
-| `channel/lastBuildDate` | `feed.updated` |
-| `channel/pubDate` | `feed.updated` (fallback) |
-| `channel/managingEditor` | `feed.authors[0]` (parsed `email (Name)`) |
-| `channel/generator` | `feed.generator.value` |
-| `channel/image/url` | `feed.logo` |
-| `item/guid` | `entry.id` |
-| `item/link` (no `guid`) | `entry.id` (fallback) and `entry.links[]` |
-| `item/description` | `entry.summary` (`type: 'html'`) |
-| `item/pubDate` | `entry.published` and `entry.updated` |
-| `item/author` | `entry.authors[0]` |
-| `item/enclosure` | `entry.links[]` with `rel: 'enclosure'` |
-| `item/comments` | `entry.links[]` with `rel: 'replies'` |
-| `item/category` | `entry.categories[].term` (+ `scheme` from domain) |
-
-For RDF (RSS 1.0/0.90):
-
-| RDF source | Atom target |
-|-------------------------------|----------------------------------------------------|
-| `channel/@rdf:about` | `feed.id` |
-| `channel/title` | `feed.title` |
-| `channel/description` | `feed.subtitle` |
-| `channel/link` | `feed.links[]` (`rel: 'alternate'`) |
-| `image/url` | `feed.logo` |
-| `item/@rdf:about` | `entry.id` |
-| `item/title` | `entry.title` |
-| `item/link` | `entry.links[]` (`rel: 'alternate'`) |
-
-For Atom 0.3 → Atom 1.0 the legacy element names are renamed:
-`tagline → subtitle`, `modified → updated`, `issued → published`,
-`copyright → rights`.
-
-
-## Tests
-
-The TypeScript and Go test suites share fixtures from
-[`test/specs/`](test/specs/) — each base name has a `.xml` input and one
-or more expected JSON outputs (`.atom.json`, `.native.json`,
-`.detect.json`). Both languages enumerate the directory and JSON-compare
-the parser result to the expected output, so adding a new fixture is
-covered by both immediately.
-
-Both suites also run against a focused subset of well-formed feeds
-vendored from [`kurtmckee/feedparser`](https://github.com/kurtmckee/feedparser)
-under BSD 2-Clause (see
-[THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md)) at
-[`test/feedparser-wellformed/`](test/feedparser-wellformed/).
+The Go structs (`AtomFeed`, `AtomEntry`, …) carry equivalent JSON
+tags so they marshal to the same shape. See
+[`go/feed.go`](go/feed.go) for the full set, including the native
+RSS 2.0 (`Rss2Feed`, `Rss2Item`, …) and RSS 1.0 (`Rss1Feed`,
+`Rss1Item`, …) types.
+### Detection helper
-## Acknowledgments
+```typescript
+function detect(root: XmlElement):
+ { dialect: 'atom' | 'rss' | 'rdf' | 'unknown'
+ version: 'atom10' | 'atom03' | 'rss20' | 'rss092' |
+ 'rss091u' | 'rss091n' | 'rss10' | 'rss090' |
+ 'unknown' }
+```
+
+```go
+func Detect(root any) Detection
+type Detection struct { Dialect, Version string }
+```
+
+### Mapping: RSS 2.x / 0.92 / 0.91 → Atom
+
+| RSS source | Atom target |
+|---------------------------|----------------------------------------------------|
+| `channel/title` | `feed.title` (`type: 'text'`) |
+| `channel/description` | `feed.subtitle` (`type: 'text'`) |
+| `channel/link` | `feed.id` and `feed.links[]` (`rel: 'alternate'`) |
+| `channel/copyright` | `feed.rights` |
+| `channel/lastBuildDate` | `feed.updated` |
+| `channel/pubDate` | `feed.updated` (fallback) |
+| `channel/managingEditor` | `feed.authors[0]` (parsed as `email (Name)`) |
+| `channel/generator` | `feed.generator.value` |
+| `channel/image/url` | `feed.logo` |
+| `item/guid` | `entry.id` |
+| `item/link` (no `guid`) | `entry.id` (fallback) and `entry.links[]` |
+| `item/description` | `entry.summary` (`type: 'html'`) |
+| `item/pubDate` | `entry.published` and `entry.updated` |
+| `item/author` | `entry.authors[0]` |
+| `item/enclosure` | `entry.links[]` with `rel: 'enclosure'` |
+| `item/comments` | `entry.links[]` with `rel: 'replies'` |
+| `item/category` | `entry.categories[].term` (+ `scheme` from domain) |
+
+### Mapping: RDF (RSS 1.0 / 0.90) → Atom
+
+| RDF source | Atom target |
+|-----------------------|---------------------------------------------------|
+| `channel/@rdf:about` | `feed.id` |
+| `channel/title` | `feed.title` |
+| `channel/description` | `feed.subtitle` |
+| `channel/link` | `feed.links[]` (`rel: 'alternate'`) |
+| `image/url` | `feed.logo` |
+| `item/@rdf:about` | `entry.id` |
+| `item/title` | `entry.title` |
+| `item/link` | `entry.links[]` (`rel: 'alternate'`) |
+
+### Atom 0.3 → 1.0 element renames
+
+| Atom 0.3 | Atom 1.0 |
+|-------------|-------------|
+| `tagline` | `subtitle` |
+| `modified` | `updated` |
+| `issued` | `published` |
+| `copyright` | `rights` |
+
+
+---
+
+## Explanation
+
+### Why default to Atom?
+
+Atom 1.0 (RFC 4287) is a strict superset of what every flavour of
+RSS expresses, with consistent typed elements (`AtomText` carries
+its content type, `AtomLink` carries `rel`/`type`/`length`, dates
+are well-defined). RSS, by contrast, is a small family of related
+formats with overlapping but inconsistent shapes — RSS 0.91 has no
+`guid`, 0.92 added `enclosure` and `category`, 1.0 is RDF, 2.0
+added `cloud` and `ttl`. Picking one shape for downstream code to
+target avoids per-dialect branching, and Atom is the obvious
+candidate because it can carry everything the others express.
+
+The result is that 95% of feed-consuming code can ignore the source
+dialect entirely. The remaining 5% — applications that genuinely
+need RSS-only metadata — opt into `format: 'native'`.
+
+### What conversion loses
+
+Mapping RSS to Atom is not bijective. The default conversion drops:
+
+- `ttl`, `cloud`, `skipHours`, `skipDays` (RSS 2.x channel-level
+ scheduling hints — Atom has no equivalent)
+- `guid/@isPermaLink` (true/false flag; the value becomes `entry.id`
+ but the boolean is dropped)
+- `image/title`, `image/link`, `image/width`, `image/height`
+ (Atom's `logo` is just a URL)
+- `textInput` (an obsolete RSS UI element with no Atom counterpart)
+- `category/@domain` becomes `category.scheme`, which is the
+ intended mapping but loses the original RSS naming
+
+If any of these matter, parse with `format: 'native'` and read the
+dialect-specific structure directly.
+
+### Composition with `@jsonic/xml`
+
+The plugin layers on top of [`@jsonic/xml`](https://github.com/jsonicjs/xml)
+in three tiers:
+
+```
+src ──► @jsonic/xml ──► native parser ──► Atom converter
+ (XmlElement) (Rss2Feed/...) (AtomFeed)
+ format:'raw' format:'native' format:'atom' (default)
+```
+
+Each tier is exposed by a `format` option, so you can stop at
+whichever level your application needs. Internally, the Feed plugin
+calls `jsonic.use(Xml)` itself and registers a `bc` (before-close)
+hook on the `xml` rule that runs after `@jsonic/xml`'s own
+`@xml-bc`. The hook gates on `r.child.node` — the same idiom
+`@xml-bc` uses — so it runs exactly once even when the grammar's
+trailing-whitespace recursion fires `bc` again.
-Conformance testing uses third-party corpora under permissive licenses
-(see [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for full
-attribution):
+### Cross-language parity through shared fixtures
+
+The TypeScript and Go implementations are kept in sync through
+[`test/specs/`](test/specs/): each `.xml` ships with a
+`.detect.json`, `.atom.json`, and an optional
+`.native.json`. Both test suites enumerate the directory,
+parse each input, and JSON-deep-compare the result against the
+expectation after a marshal/unmarshal round-trip (which collapses
+property-ordering and pointer-vs-value differences). Adding a
+fixture covers both languages immediately.
+
+A subset of the well-formed feed corpus from
+[`kurtmckee/feedparser`](https://github.com/kurtmckee/feedparser)
+is also vendored at
+[`test/feedparser-wellformed/`](test/feedparser-wellformed/) under
+BSD 2-Clause; both languages run the same no-error and targeted
+value checks against it.
+
+
+---
+
+## Acknowledgments
- [kurtmckee/feedparser](https://github.com/kurtmckee/feedparser) by
- Kurt McKee and Mark Pilgrim — a focused subset of well-formed feed
- samples is vendored at `test/feedparser-wellformed/`.
+ Kurt McKee and Mark Pilgrim — vendored well-formed corpus, BSD
+ 2-Clause. See [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md).
## License
diff --git a/go/README.md b/go/README.md
new file mode 100644
index 0000000..1ff332d
--- /dev/null
+++ b/go/README.md
@@ -0,0 +1,378 @@
+# feed (Go)
+
+A Go port of [`@jsonic/feed`](https://github.com/jsonicjs/feed) — a
+[Jsonic](https://github.com/jsonicjs/jsonic) plugin (built on
+[`xml`](https://github.com/jsonicjs/xml)) that parses syndication
+feeds (**RSS 0.90, 0.91, 0.92, 1.0, 2.0** and **Atom 0.3, 1.0**)
+into typed Go structs. By default every dialect is normalised to an
+Atom-shaped result, so the same downstream code can consume feeds
+from any source.
+
+For the full project — including the TypeScript implementation,
+shared test fixtures, and language-agnostic explanation — see the
+[main README](../README.md).
+
+This README follows the four [Diátaxis](https://diataxis.fr) modes:
+
+- [Tutorial](#tutorial) — work through a first feed parse
+- [How-to guides](#how-to-guides) — short recipes for specific tasks
+- [Reference](#reference) — types, options, mapping tables
+- [Explanation](#explanation) — design rationale and trade-offs
+
+
+---
+
+## Tutorial
+
+This walkthrough takes you from an empty Go module to a parsed feed.
+
+Initialise a module and pull in the plugin:
+
+```bash
+go mod init example
+go get github.com/jsonicjs/feed/go
+```
+
+Create `main.go`:
+
+```go
+package main
+
+import (
+ "fmt"
+
+ jsonic "github.com/jsonicjs/jsonic/go"
+ feed "github.com/jsonicjs/feed/go"
+)
+
+func main() {
+ j := jsonic.Make()
+ if err := j.UseDefaults(feed.Feed, feed.Defaults); err != nil {
+ panic(err)
+ }
+ got, err := j.Parse(`
+
+
+ My Blog
+ https://example.com/
+ Posts
+
+ Hello
+ https://example.com/1
+ https://example.com/1
+
+
+ `)
+ if err != nil {
+ panic(err)
+ }
+ f := got.(feed.AtomFeed)
+ fmt.Println(f.Title.Value) // My Blog
+ fmt.Println(f.Entries[0].ID) // https://example.com/1
+ fmt.Println(f.Entries[0].Links[0]) // {https://example.com/1 alternate ...}
+}
+```
+
+The input was RSS 2.0 but `f` is an `AtomFeed`: `Title` is an
+`*AtomText` (carrying its content type), the `` became
+`Entries[0].ID`, and the `` became an `AtomLink` with
+`Rel: "alternate"`. The plugin handles every supported dialect this
+way, so the rest of your code never has to branch on the source
+format.
+
+`Parse` returns `(any, error)`. With the default `format`, the
+concrete type is `feed.AtomFeed`; with `format: "native"` it is
+`feed.Rss2Feed`, `feed.Rss1Feed`, or `feed.AtomFeed` depending on
+the input dialect; with `format: "raw"` it is `map[string]any` (the
+raw element tree from `@jsonic/xml`).
+
+
+---
+
+## How-to guides
+
+### How to keep the source dialect's structure
+
+When you need RSS-specific fields like `TTL`, `Cloud`, or
+`SkipDays` that the Atom shape does not carry, ask for the native
+form:
+
+```go
+j := jsonic.Make()
+j.UseDefaults(feed.Feed, feed.Defaults, map[string]any{"format": "native"})
+got, _ := j.Parse(rssSource)
+native := got.(feed.Rss2Feed)
+// native.TTL, native.Cloud, native.SkipDays
+```
+
+The native return type depends on the input dialect:
+
+| Input dialect | Type assertion target |
+|-----------------------|-----------------------|
+| Atom 1.0 / Atom 0.3 | `feed.AtomFeed` |
+| RSS 2.0 / 0.92 / 0.91 | `feed.Rss2Feed` |
+| RSS 1.0 / 0.90 | `feed.Rss1Feed` |
+
+Switch on the `Format` field if the dialect is not known up front:
+
+```go
+switch v := got.(type) {
+case feed.AtomFeed:
+ // v.Format == "atom"
+case feed.Rss2Feed:
+ // v.Format == "rss"
+case feed.Rss1Feed:
+ // v.Format == "rdf"
+}
+```
+
+### How to access the raw XML tree
+
+When even the native shape is not enough — for example you need a
+non-standard namespace extension like `` — drop down
+to the raw element tree from `@jsonic/xml`:
+
+```go
+j := jsonic.Make()
+j.UseDefaults(feed.Feed, feed.Defaults, map[string]any{"format": "raw"})
+got, _ := j.Parse(rssSource)
+tree := got.(map[string]any)
+// tree["localName"] == "rss"
+// tree["children"].([]any) == [...]
+// tree["attributes"].(map[string]string) == {"version": "2.0"}
+```
+
+### How to detect a feed's dialect without converting
+
+Use `format: "raw"` to get the underlying XML tree, then call
+`feed.Detect`:
+
+```go
+j := jsonic.Make()
+j.UseDefaults(feed.Feed, feed.Defaults, map[string]any{"format": "raw"})
+got, _ := j.Parse(rssSource)
+det := feed.Detect(got)
+// e.g. feed.Detection{Dialect: "rss", Version: "rss20"}
+```
+
+### How to handle parse errors
+
+`Parse` returns a `*jsonic.JsonicError` for both XML-level errors
+(unterminated tag, mismatched close, etc.) and feed-level errors
+(unrecognised root element):
+
+```go
+got, err := j.Parse(src)
+if err != nil {
+ var je *jsonic.JsonicError
+ if errors.As(err, &je) {
+ // structured error with row/col, error code, source snippet
+ log.Printf("feed parse failed: %v", je)
+ }
+ return err
+}
+```
+
+### How to read fields that the Atom conversion drops
+
+The default conversion is lossy by design (see
+[Explanation](#what-conversion-loses) below). The recommended path
+is to register the plugin with `format: "native"` and convert in
+your own code only when you need the Atom shape.
+
+
+---
+
+## Reference
+
+### Plugin registration
+
+```go
+func Feed(j *jsonic.Jsonic, opts map[string]any) error
+var Defaults = map[string]any{ "format": "atom" }
+```
+
+Register via `UseDefaults`:
+
+```go
+j := jsonic.Make()
+err := j.UseDefaults(feed.Feed, feed.Defaults, opts)
+result, err := j.Parse(src)
+```
+
+`UseDefaults` merges `opts` over `Defaults`, so caller options
+override defaults. Pass `nil` (or omit) for defaults-only.
+
+### Options
+
+| Key | Type | Default | Effect |
+|----------|----------|----------|-------------------------------------------------|
+| `format` | `string` | `"atom"` | `"atom"`, `"native"`, or `"raw"` (see below) |
+
+### Result types by `format`
+
+| `format` | Concrete type returned by `Parse` |
+|------------|--------------------------------------------------------|
+| `"atom"` | `feed.AtomFeed` |
+| `"native"` | `feed.AtomFeed` / `feed.Rss2Feed` / `feed.Rss1Feed` |
+| `"raw"` | `map[string]any` (the `@jsonic/xml` element tree) |
+
+### Atom shape (RFC 4287)
+
+```go
+type AtomFeed struct {
+ Format string `json:"format"`
+ Version string `json:"version"`
+ ID string `json:"id,omitempty"`
+ Title *AtomText `json:"title,omitempty"`
+ Updated string `json:"updated,omitempty"`
+ Authors []AtomPerson `json:"authors,omitempty"`
+ Contributors []AtomPerson `json:"contributors,omitempty"`
+ Categories []AtomCategory `json:"categories,omitempty"`
+ Generator *AtomGenerator `json:"generator,omitempty"`
+ Icon string `json:"icon,omitempty"`
+ Logo string `json:"logo,omitempty"`
+ Rights *AtomText `json:"rights,omitempty"`
+ Subtitle *AtomText `json:"subtitle,omitempty"`
+ Links []AtomLink `json:"links,omitempty"`
+ Entries []AtomEntry `json:"entries"`
+}
+
+type AtomEntry struct {
+ ID string `json:"id,omitempty"`
+ Title *AtomText `json:"title,omitempty"`
+ Updated string `json:"updated,omitempty"`
+ Published string `json:"published,omitempty"`
+ Authors []AtomPerson `json:"authors,omitempty"`
+ Contributors []AtomPerson `json:"contributors,omitempty"`
+ Categories []AtomCategory `json:"categories,omitempty"`
+ Content *AtomContent `json:"content,omitempty"`
+ Links []AtomLink `json:"links,omitempty"`
+ Rights *AtomText `json:"rights,omitempty"`
+ Summary *AtomText `json:"summary,omitempty"`
+ Source *AtomEntrySource `json:"source,omitempty"`
+}
+
+type AtomText struct { Type, Value string }
+type AtomPerson struct { Name, URI, Email string }
+type AtomLink struct { Href, Rel, Type, Hreflang, Title string; Length int }
+type AtomCategory struct { Term, Scheme, Label string }
+type AtomGenerator struct { URI, Version, Value string }
+type AtomContent struct { Type, Src, Value string }
+```
+
+`AtomEntrySource` is a slim variant of `AtomFeed` (no `Entries`)
+used for the Atom-source-element that an RSS `/` maps
+to.
+
+### Native types
+
+```go
+type Rss2Feed struct { /* RSS 0.91 / 0.92 / 2.0 channel + items */ }
+type Rss2Item struct { /* one in RSS 2.x */ }
+type Rss1Feed struct { /* RSS 0.90 / 1.0 RDF channel + items */ }
+type Rss1Item struct { /* one in RSS 1.0 */ }
+```
+
+See [`feed.go`](feed.go) for the full field list.
+
+### Detection helper
+
+```go
+func Detect(root any) Detection
+type Detection struct {
+ Dialect string `json:"dialect"`
+ Version string `json:"version"`
+}
+```
+
+`Dialect` is one of `"atom"`, `"rss"`, `"rdf"`, `"unknown"`.
+`Version` is one of `"atom10"`, `"atom03"`, `"rss20"`, `"rss092"`,
+`"rss091u"`, `"rss091n"`, `"rss10"`, `"rss090"`, `"unknown"`.
+
+### Mapping tables
+
+For the full RSS-to-Atom and RDF-to-Atom mapping tables, see the
+[Reference section of the main README](../README.md#reference).
+
+
+---
+
+## Explanation
+
+### Why default to Atom?
+
+Atom 1.0 (RFC 4287) is a strict superset of what every flavour of
+RSS expresses, with consistent typed elements (`AtomText` carries
+its content type, `AtomLink` carries `rel`/`type`/`length`, dates
+are well-defined). RSS, by contrast, is a small family of related
+formats with overlapping but inconsistent shapes. Picking one shape
+for downstream code to target avoids per-dialect branching, and
+Atom is the obvious candidate because it can carry everything the
+others express.
+
+### What conversion loses
+
+Mapping RSS to Atom is not bijective. The default conversion drops:
+
+- `Ttl`, `Cloud`, `SkipHours`, `SkipDays` (RSS 2.x channel-level
+ scheduling hints — Atom has no equivalent)
+- `Guid.IsPermaLink` (the value becomes `Entries[i].ID` but the
+ boolean is dropped)
+- `Image.Title`, `Image.Link`, `Image.Width`, `Image.Height`
+ (Atom's `Logo` is just a URL)
+- `TextInput` (an obsolete RSS UI element with no Atom counterpart)
+
+If any of these matter, parse with `format: "native"` and read the
+dialect-specific structure directly.
+
+### Composition with the `xml` plugin
+
+The plugin layers on top of
+[`github.com/jsonicjs/xml/go`](https://github.com/jsonicjs/xml) in
+three tiers:
+
+```
+src ──► xml plugin ───► native parser ───► Atom converter
+ (map[string]any) (Rss2Feed/...) (AtomFeed)
+ format:"raw" format:"native" format:"atom" (default)
+```
+
+Each tier is exposed by a `format` option, so you can stop at
+whichever level your application needs. Internally, `Feed` calls
+`j.UseDefaults(xml.Xml, xml.Defaults)` itself and registers a
+`bc` (before-close) action on the `xml` rule that runs after the
+xml plugin's own `@xml-bc`. The hook gates on `r.Child.Node` — the
+same idiom `@xml-bc` uses — so it runs exactly once even when the
+grammar's trailing-whitespace recursion fires `bc` again.
+
+### JSON-shape parity with the TypeScript implementation
+
+The Go structs carry JSON tags chosen so that
+`json.Marshal(result)` produces the same shape the TypeScript
+parser produces with `JSON.stringify`. This is what makes the
+shared fixtures in
+[`../test/specs/`](../test/specs/) work for both languages: each
+test JSON-marshal-unmarshals the parser output and deep-compares it
+to the language-agnostic expected `*.atom.json` / `*.native.json`.
+
+
+---
+
+## Testing
+
+```bash
+go test ./...
+```
+
+The Go test suite runs the cross-language fixtures in
+[`../test/specs/`](../test/specs/) and the vendored well-formed
+corpus in
+[`../test/feedparser-wellformed/`](../test/feedparser-wellformed/).
+
+
+---
+
+## License
+
+MIT. Copyright (c) 2021-2025 Richard Rodger and contributors.