feedlens

    Multi-format RSS 2.0, Atom 1.0 and JSON Feed 1.0/1.1 parsing library with source-aware links and diagnostics

    feed
    rss
    atom
    json-feed
    parser
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    8 days ago
    Downloads
    34

    #FeedLens — MoonBit 多格式 Feed 解析库

    youyong5/feedlens is a reusable MoonBit multi-format Feed parsing library for tested common behavior of RSS 2.0, Atom 1.0, and JSON Feed 1.0/1.1, including independent Atom Entry Documents. The documented gaps still prevent a full standards conformance claim. Diagnostics and the local-file CLI are supporting tools, not the primary product.

    The test-linked standard behavior matrix is the precise compatibility commitment, including supported input forms, output, diagnostics, and legal constructs that remain outside the mapped subset.

    It gives callers one source-aware, cross-format model for the common feed fields while preserving document order and distinguishing fatal parse failures from recoverable feed-quality diagnostics. It is intentionally a parser and local inspection library: it does not fetch feeds, sanitize/render HTML, or guess missing dates or a document's source URL. README.md is the GitHub landing page; this document contains the full mapping and boundary specification.

    Install the latest published module with moon add youyong5/feedlens and import "youyong5/feedlens" in the consuming package's moon.pkg. Check the registry version before using the newer document and structured-body APIs from source version 0.2.0.

    All four parsing entry points accept one leading UTF-8 BOM for XML and JSON Feed. A U+FEFF elsewhere is preserved; for JSON this reader interoperability choice does not imply that producers should emit a BOM.

    #Public model

    Feed and Entry use one vocabulary across the supported formats:

    Public fieldRSS 2.0 sourceAtom 1.0 sourceJSON Feed 1.0/1.1 source
    Feed.identifiernonefeed idfeed_url
    Feed.descriptionchannel descriptionfeed subtitledescription
    Feed.languagechannel languagefeed xml:langlanguage (1.1 only)
    Entry.languageinherited channel languageinherited feed xml:lang, or entry override/resetinherited feed language, or item override (1.1 only)
    Feed.updated_atchannel lastBuildDatefeed updatednone
    Feed.published_at, Feed.ttl_minuteschannel pubDate, ttlnonenone
    Feed.skip_hours, Feed.skip_daysdirect channel skipHours/hour (GMT 0–23), skipDays/day (English weekday)empty arraysempty arrays
    Feed.docs_url, Feed.rating, Feed.cloud, Feed.text_inputdirect channel docs, raw rating, cloud, textInput; no registration or form submissionnonenone
    Feed.rights, Feed.generatorchannel copyright, generatorfeed rights, generatornone
    Feed.next_url, Feed.expired, Feed.hubsnonenonenext_url, expired, hubs
    Entry.identifiernon-blank guid, otherwise non-blank linkentry iditem id
    Entry.linkitem link; if absent, an HTTP(S) guid with default/true isPermaLink and a valid absolute URLfirst alternate linkitem url
    Entry.descriptionitem descriptionentry summarysummary
    Entry.contentContent Module content:encodedinline text/HTML/XHTML display string; not XML/binary/external bodiescontent_html, otherwise content_text
    Entry.content_sourcenoneexternal content src, raw with parse, resolved with parse_with_sourcenone
    Entry.content_media_typenonedeclared content type spelling, if presentnone
    Entry.atom_bodynonedetailed inline or external Atom bodynone
    Entry.atom_sourcenoneoriginal feed source id/title/subtitle/updated, rights, generator (including uri/version), links, images, people and categories, separately from the aggregated entrynone
    Author.urinonefeed/entry/source author or contributor uri; raw in parse, scoped by xml:base in parse_with_sourceauthor url
    Entry.content_text, Entry.content_htmlnonenoneboth item forms independently
    Entry.comments_url, Entry.source_url, Entry.source_titleitem comments, source/@url and source channel titlenonenone
    Entry.guid_is_permalinkguid/@isPermaLink, default true on a usable guidnonenone
    Feed.managing_editor, Feed.webmasterchannel editorial and technical contact textnonenone
    Entry.rightsnoneown rights, otherwise inherited feed rights; explicit empty text blocks inheritancenone
    Entry.published_atitem pubDateentry publisheddate_published
    Entry.updated_atnoneentry updateddate_modified
    Feed.authorsnonedirect feed authorsauthors, otherwise legacy author
    Entry.authorsdirect item authorseffective entry authorsitem authors/author, otherwise feed authors
    Feed/Entry.contributorsnonedirect contributors, separate from author inheritancenone
    Feed/Entry.linksdirect channel Atom-namespace self links in Feed.links; item links not mappedordered Atom link relations and common attributes; malformed MIME and hreflang hints produce positioned diagnostics while raw values remain accessiblenone
    Author.avatarnonenoneauthor avatar URL
    Feed.categoriesdirect channel categoriesdirect feed categoriesnone in supported subset
    Feed.imagesdirect channel imagedirect feed icon/logofeed icon/favicon
    Entry.categoriesdirect item categoriesdirect entry categoriestags
    Entry.attachmentsdirect item enclosuredirect entry link rel="enclosure"item attachments
    Entry.external_urlnonenoneitem external_url, separate from url permalink
    Entry.imagesnone in supported corenone in supported coreitem image (featured preview), banner_image (detail banner)

    title and link have the same meaning across supported formats. There are no duplicate format-specific public values for these convenience fields. Atom links add the full ordered link list, while link remains the first alternate. title_kind and description_kind describe the associated string: Unspecified for RSS (which does not declare a text-construct type), Text for Atom omitted/text and JSON Feed title/summary/text content, Html for Atom/JSON Feed HTML content, and Xhtml for supported Atom XHTML constructs. XHTML text fields expose a displayable plain-text string plus title_xhtml/description_xhtml/ content_xhtml as appropriate. Each XhtmlFragment carries deterministic markup, effective language, and ordered raw base_references from Atom ancestors through the outer XHTML div; child xml:base remains in the markup. HTML values retain decoded source text for XML and JSON source strings for JSON Feed; FeedLens neither renders nor sanitizes them. content_kind follows the same rules. description is never copied into content: it remains a summary/RSS description even when no body is available.

    AtomBody is the detailed, independent Atom body union: InlineText(value,media_type) (including case-insensitive MIME text/*), InlineHtml(value) (only Atom's literal type="html"), InlineXhtml(fragment), InlineXml(XmlFragment, media_type), InlineBinary(Bytes, media_type), and External(AtomExternalContent). XmlFragment.markup is deterministic, namespace-qualified XML serialization retaining child elements, attributes, mixed text, comments, processing instructions, and meaningful whitespace; unlike XHTML it does not strip a div. Its language and ordered raw base_references retain the content construct's inherited context. The external struct has raw src, current url, optional declared media_type, and base_uri only when an absolute effective base is known. parse(input) keeps the source spelling in src and url; parse_with_source resolves url against the supplied document URL and scoped xml:base while retaining src. The already-public content_source/content_media_type convenience fields remain for existing callers; content_source follows the current resolved URL and therefore does not itself retain raw src after parse_with_source. An absolute xml:base can provide a known base_uri even for parse(input). No external content is retrieved. Bytes supplies exact decoded octets on all targets; the CLI emits padded Base64 and byte_length, never a lossy JSON string. XML and binary bodies leave legacy Entry.content = None; existing inline text/HTML/XHTML strings retain their previous meaning.

    Language tags are kept as declared, without BCP 47 canonicalization or a guessed default. Entry.language is the effective entry-level context: RSS items inherit channel language; Atom entries inherit feed xml:lang unless the entry declares its own. An explicit empty Atom xml:lang resets to None. JSON Feed 1.1 items inherit feed language unless an item declares its own; malformed or blank declarations are diagnosed and do not inherit. The 1.0 format does not declare a language field, so those keys are ignored. Language attributes on nested Atom text constructs are not projected into a single entry language; XHTML fragments expose their own effective language.

    Feed.images is an ordered collection of FeedImage values with explicit source and role: RssImage, AtomIcon, AtomLogo, JsonIcon, or JsonFavicon. The RSS image retains its required URL/title/link and optional description/declared dimensions. Atom icon and logo are kept separate, as are JSON Feed timeline icon and source-list favicon. Missing required image data or invalid optional dimensions are diagnosed. RSS dimensions are retained only when valid positive declared values (width at most 144, height at most 400); no specification defaults or actual pixel sizes are synthesized. XML images retain source order; JSON icon precedes favicon in the public array because JSON object member order is not semantically significant. No image is fetched, inspected, resized, or promoted to an attachment. parse_with_source resolves image URLs and the RSS image link; Atom icon/logo honor their own xml:base scopes. Entry.external_url is not a fallback permalink. JSON Feed item images are EntryImage values with source = JsonFeed1 and distinct JsonFeatured/ JsonBanner roles; they are separate from Entry.attachments. Their public array has a fixed featured-then-banner order, because JSON object member order is not semantically significant. RSS and Atom core do not supply equivalent item fields with these exact roles, so they remain absent there. Blank or wrong-type JSON URL values are diagnosed and omitted; malformed URI references remain raw with UnresolvedUri when parse_with_source is used.

    #Dates without local-time guessing

    The original Feed.updated_at, Entry.published_at, and Entry.updated_at strings remain unchanged. Call normalize_date(feed.format, raw_date) to get Result[NormalizedDate?, DateError]: Ok(None) means absent; Err means a present but invalid or unsupported value. A successful result provides utc (RFC 3339 UTC text), unix_seconds, nanosecond, the original offset_minutes, and precision (Minute, Second, or Fraction(digits)). This function is pure and gives the same result on Wasm, JS, and Native; it never uses the machine's local time zone.

    match @feedlens.normalize_date(feed.format, feed.entries[0].published_at) {
    Ok(Some(date)) => println(date.utc)
    Ok(None) => println("publication date missing")
    Err(_) => println("publication date invalid or unsupported")
    }

    RSS accepts the common RFC 822/1123 profile with a two- or four-digit year, optional weekday, HH:MM or HH:MM:SS, and numeric ±HHMM, UT/UTC/GMT/Z, or US EST/EDT/CST/CDT/MST/MDT/PST/PDT zones. Named weekday, month and zone tokens are case-insensitive; original strings remain intact. A stated weekday must agree with the calendar date. Atom and JSON Feed require RFC 3339 with an explicit offset or Z, and support any positive number of fractional-second digits. utc preserves every digit; nanosecond retains the first nine digits (zero-padded when needed) and truncates finer precision, while Fraction(digits) reports the declared count. Calendar dates, times, and offsets are validated. Two-digit RSS years 00–49 map to 2000–2049 and 50–99 to 1950–1999, following RFC 2822's obsolete-year interpretation. Unknown-offset -0000/-00:00 normalizes to a UTC instant with offset_known=false; announced leap seconds normalize with is_leap_second=true. Atom dates are diagnosed at parse time when invalid; malformed JSON Feed item dates keep their raw strings, produce InvalidJsonFeedDate, and do not discard otherwise usable items. RSS channel/item dates that cannot be normalized with this supported profile produce UnnormalizedRssDate, preserving source strings and items; this is not a claim that every other legal RFC 822 spelling is invalid. Non-Z military single-letter zones remain unnormalized because RFC 1123 notes that their offset signs are ambiguous; no local timezone is guessed. No date is substituted from the file timestamp, fetch time, or local clock.

    Author preserves source semantics through source, name, uri, and email. RSS core item author values are exposed only as their original email text (source = Rss2); FeedLens does not infer a name from it. A direct item Dublin Core creator under the exact http://purl.org/dc/elements/1.1/ namespace maps to name with source = DublinCore; both kinds retain document order. Atom authors are person constructs (source = Atom1) with a required name element (which may be empty) and optional uri/email. JSON Feed authors (source = JsonFeed) map name and url to name and uri; a declared name (including an empty string), a non-blank URL, or a non-blank avatar suffices; JSON Feed has no mapped email field. Category has a shared term and source marker, but preserves RSS domain separately from Atom scheme and label; those fields are never normalized into one another. JSON Feed tags use only term with source = JsonFeed1. Author and category arrays retain document order.

    Attachment exposes source (Rss2, Atom1, or JsonFeed1), the original url, optional media_type, optional size_in_bytes, optional title, and optional duration_in_seconds. Attachments retain source order and are metadata only: FeedLens never downloads or verifies the referenced resource. RSS enclosure requires non-blank url, length, and type; Atom enclosure links require href but permit absent type and length; JSON Feed attachments declare non-blank url and mime_type. When JSON Feed omits or misstates the required mime_type, the reader retains the attachment URL with media_type =None and an InvalidAttachment diagnostic; a missing/blank URL still drops that attachment. Atom's optional link title and JSON Feed's optional attachment title are preserved; only JSON Feed supplies a duration. parse keeps URL references as written; parse_with_source can resolve supported URL fields without downloading them.

    size_in_bytes is an optional Int64, accepted only for non-negative decimal integers through 9007199254740991 (the largest integer exactly representable by JavaScript's Number and by the CLI's JSON number output). Invalid, missing, or larger values never wrap: RSS enclosures are skipped when a required length is invalid; Atom/JSON Feed attachments retain their URL and receive InvalidAttachment with size None when an optional size is invalid. For JSON Feed, 1.0 and 1e3 are deliberately not accepted as integer byte counts. JSON Feed durations are optional finite, non-negative Double seconds; invalid values become None with a diagnostic. They follow ordinary IEEE 754 rounding.

    #URI references and an explicit source URL

    parse(input) retains its existing raw-reference behavior. To resolve common links, use parse_with_source(input, source_url), where source_url is the absolute HTTP(S) URL from which the document was obtained, without a fragment:

    match @feedlens.parse_with_source(input, "https://example.org/feeds/news.atom") {
    Ok(feed) => println(feed.entries[0].link.unwrap_or("(no link)"))
    Err(_) => println("feed or source URL invalid")
    }

    The opt-in source entry point resolves Feed.link, Entry.link, Entry.attachments[].url, feed-image URLs (and RSS image link), JSON item external_url, JSON item-image URLs, next/hub URLs and author avatars using RFC 3986 path, root-path, query, fragment, and dot-segment rules. RSS and JSON Feed use the caller-supplied document URL; Atom additionally applies the inherited xml:base on the feed, entry, link, and any intervening XML elements, matching the XML namespace URI rather than an arbitrary base attribute. JSON Feed's declared feed_url and home_page_url are not substituted as a fetch-location base. With a source URL, next_url is withheld and diagnosed if its resolved address equals the declared feed_url or the document URL. The public Feed.identifier remains the raw feed_url; other pagination cycles across responses remain the caller's responsibility. Without a usable base, or for a malformed URI reference, the raw field is retained and UnresolvedUri is recorded; XML diagnostics point to the relevant element, whereas JSON semantic diagnostics are (1, 1). An invalid explicit source URL is a fatal InvalidSourceUrl error. No URI is fetched. Bracketed HTTP(S) hosts require an IPv6 or RFC 3986 IPvFuture literal; malformed brackets, invalid IPv6 and the old %25ZoneID URI form are rejected. IPvFuture is syntax-checked only; FeedLens never dereferences that address mechanism.

    Identifiers (guid, Atom id, JSON Feed id) stay opaque and unchanged when usable, including the RSS link-fallback identifier. JSON Feed items with blank IDs are discarded with a diagnostic; nonblank IDs retain surrounding spaces. JSON author URLs and avatars, RSS comments/source URLs and every direct Atom link are resolved; Atom author/contributor URIs use their own element-scoped xml:base; RSS category domains, Atom category schemes and URLs inside HTML are not resolved. Atom content src uses its own xml:base scope. Existing parse callers are unaffected; callers that need the original references can continue using it.

    parse_document(input) adds a typed FeedDocument(feed) / AtomEntryDocument(entry,diagnostics) result for stand-alone Atom Entry Documents. It uses the same content and author logic as a feed entry, without inventing feed fields. parse_document_with_source applies source and entry xml:base to its links. The original parse and parse_with_source feed return types remain unchanged.

    #RSS 2.0

    • RSS 2.0 documents with an unnamespaced <rss version="2.0"> root.
    • Reads direct channel language and direct image (required URL/title/link, optional description and declared dimensions). RSS items inherit the channel language; wrong-namespace lookalikes do not count.
    • A direct <channel> child of <rss>; channel metadata and <item> elements must be direct channel children.
    • Channel title, link, and description; item guid, title, link, description, and pubDate, retained in document order.
    • Channel pubDate, lastBuildDate, copyright, generator and nonnegative ttl; item comments and source url. Invalid TTL gets a located diagnostic.
    • A direct Atom-namespace <atom:link rel="self"> on channel is retained in Feed.links with its declared href and optional metadata. Feed.link still means the RSS website link. Registered self relation IRI spelling is also recognized; wrong namespace, nested links, item links and other relations remain outside this mapping. With an explicit document URL, relative self hrefs resolve against it; RSS xml:base is not applied. An invalid href has a located diagnostic. The archived Hacker News RSS fixture actually supplies this extension; the smaller boundary sample tests scope and source resolution.
    • Direct channel skipHours and skipDays are advisory GMT polling hints, kept in source order in Feed.skip_hours and Feed.skip_days. A blank/invalid child, more than 24 hours or seven days, or a repeated container gets a located diagnostic; valid earlier values remain usable. Foreign-namespace or nested lookalikes are ignored. The library does not schedule network requests.
    • Namespace-aware matching: elements in other namespaces, or at another hierarchy level, never fill RSS core fields.
    • XML errors, unsupported roots, unsupported RSS versions, and a missing channel are fatal ParseError values.
    • Missing or whitespace-only fields are recoverable and become None. Feed.diagnostics reports each missing required channel field and an item with neither a usable title nor a description. A blank channel field diagnostic points at that field; an absent one points at <channel>. An item-content diagnostic points at <item>. A valid title-only or description-only item may have no identifier.
    • An item's identifier is its non-blank guid, falling back to its non-blank link. The identifier remains opaque and is never resolved. guid_is_permalink preserves the declared isPermaLink role (default true for a usable guid). When the item has no link, a default/true permalink guid fills Entry.link only if it is a valid absolute HTTP(S) URL. An explicit link always wins, and false/opaque guid values never become links. No URL is fetched. Unknown attribute values receive InvalidRssField.
    • RSS item source credit keeps the source channel title separately from its required url. The channel's managingEditor and webMaster contact texts remain separate from entry authors.
    • RSS core fields containing nested XML markup are not flattened: their value becomes None and UnsupportedNestedMarkup identifies the field location. Plain text, XML entities, and CDATA remain supported.
    • Supports a direct item child named content:encoded only when its resolved namespace URI is exactly http://purl.org/rss/1.0/modules/content/. Its XML-decoded text or CDATA is exposed as Entry.content with content_kind = Unspecified; it is independent from description. Nested XML is rejected with the same explicit nested-markup diagnostic policy.
    • Reads direct channel and item category elements in document order. A category's non-blank text is its term and its optional domain remains RSS-specific. Reads direct item author text as an unparsed raw email value, and direct Dublin Core creator as an author name with a distinct source. Empty or nested creators get located diagnostics; wrong-namespace and nested lookalikes are ignored. Blank core authors/categories receive source-located InvalidAuthor/InvalidCategory diagnostics.
    • Reads only direct, unnamespaced item enclosure elements. url, length, and type are required. Invalid enclosures are skipped with source-located InvalidAttachment; an invalid length never overflows.
    • The public API exposes FeedLens types only; it does not expose Milky2018/xml types.

    #Atom 1.0

    • Accepts both <feed> and independent <entry> roots in the http://www.w3.org/2005/Atom namespace, whether declared as the default namespace or through a prefix. The established parse API remains feed-only; parse_document accepts both.
    • Reads feed/entry xml:lang in the XML namespace, with feed-to-entry inheritance and explicit entry override/reset. Direct feed icon and logo remain separate image roles; each honors its own xml:base scope under parse_with_source.
    • Maps feed id, title, subtitle, updated, and alternate link to the normalized public model; maps direct entry children in document order from id, title, published, updated, summary, and alternate link.
    • A link with omitted rel, rel="alternate", or the RFC 4287 registered equivalent rel="http://www.iana.org/assignments/relation/alternate" is eligible for the convenience link; all direct links with an href are kept in the ordered links array with their declared relation, href, type, hreflang, title and length. A declared rel must be one nonempty relation name or an absolute IRI; invalid values are retained with InvalidAtomLink diagnostics. Equivalent alternate spellings share duplicate-link checks. parse_with_source resolves these links in their xml:base scope.
    • A present href is checked as an IRI reference even in parse without a document URL. Malformed values remain visible with InvalidAtomLink or, for enclosures, InvalidAttachment; empty href="" is a legal reference. Source-aware parsing can additionally report UnresolvedUri when a reference cannot resolve against its actual base.
    • Required feed fields are id, title, and updated; required entry fields are id, title, and updated. Missing or blank values are recoverable diagnostics located at the field, or its feed/entry parent when absent.
    • Reads direct feed/entry author and contributor person constructs and their direct name, uri, and email children. Contributors remain separate from authors. Effective entry authors use the first available layer: entry authors, then direct entry/source authors, then feed authors. Layers are never combined. A missing direct name gets InvalidAuthor at its element; a present empty/whitespace name stays an author and blocks fallback to another layer. If no valid layer exists, the entry gets MissingEntryField(_, "author"). A feed may omit authors only when every entry has an own valid author; a standalone entry may use its source author.
    • Entry.atom_source preserves the original feed's declared id, updated time, authors, contributors, categories, and text/HTML/XHTML title and rights, including XHTML fragments' language and base references. It does not replace the entry's own title or id. A source may omit all of these fields. Repeated source id/title/updated/rights and malformed source XHTML receive located diagnostics.
    • Reads direct feed, entry and source category elements. Atom term is required but may be the empty string; optional scheme and label stay distinct from RSS domain. A missing term receives InvalidCategory at the category element. Declared term, label, scheme, link title and enclosure title retain their source strings, including empty/whitespace values.
    • Reads only direct Atom entry link elements with rel="enclosure" or its registered IRI equivalent as attachments. This does not change alternate-link selection. Missing href skips the attachment; invalid optional length is diagnosed and omitted from an otherwise valid attachment. The original relation spelling stays in links; unrelated relation IRIs are not treated as enclosures.
    • An entry without a content element must have an alternate link. The parser tracks that presence separately from successful body extraction, so an invalid or external content element does not incorrectly trigger MissingAlternateLink.
    • Supports inline Atom content with omitted/text, html, and xhtml types as Entry.content. Text/HTML values retain XML-decoded source text and original whitespace. For XHTML, content is the concatenated display text while content_xhtml.markup retains the XML child structure and whitespace; content_kind = Xhtml prevents mistaking it for HTML or plain text.
    • External Atom content (src) is exposed through the existing content_source/content_media_type convenience fields and as an External reference with raw and optionally resolved URL, optional MIME type, and known effective absolute base. It is not fetched. An explicitly empty src="" is a valid same-document IRI reference, distinct from a missing attribute; href="" in Atom links/enclosures follows the same rule. Nonempty external content or invalid whitespace-only src/media type receives a positioned diagnostic; a missing summary receives MissingSummaryForExternalContent.
    • Other case-insensitive text/* MIME types retain decoded text and their exact declared media-type spelling as InlineText; MIME text/html is not mislabeled as Atom's special type="html". XML media types ending /xml or +xml expose InlineXml without stripping a wrapper. Other non-composite MIME types are Base64-decoded to InlineBinary(Bytes, media_type); bad encoding produces UnsupportedAtomContent("invalid Atom Base64 content"), not a text body. Binary content lacking summary gets MissingSummaryForBinaryContent. MIME token/major-type checks are applied, but parameters are not fully validated against the MIME grammar.
    • Atom title, subtitle, summary, and inline content accept a single div in the exact XHTML namespace URI for type="xhtml", regardless of prefix. The outer div is not part of markup; descendant elements/attributes/namespaces, mixed text, comments, processing instructions, and meaningful in-div whitespace are retained in deterministic XML serialization. language and base_references retain effective xml:lang and the inherited xml:base declaration chain even when declared on the removed div. Missing/wrong-namespace/multiple div roots or non-whitespace text outside div are diagnosed at the Atom field; no false complete value is exposed. Nested XML inside text/html constructs remains diagnosed. The serialization preserves XML content semantics, not original byte spelling of entities, prefixes, quote style, or attribute order independent of the parser. It is not sanitized; callers must apply safe rendering, escaping, and URI policy themselves. Neither entry point rewrites links inside XHTML markup.

    For the fixed Atom XHTML acceptance sample, actual public values include:

    feed.title = Some("Plan A & B") feed.title_kind = Xhtml feed.title_xhtml.markup = "Plan <h:em xmlns:h=\"http://www.w3.org/1999/xhtml\">A</h:em> &amp; B" entry.title = Some("Race review") entry.content = Some("Lap 12 & rain.") entry.content_kind = Xhtml entry.content_xhtml.markup = "<h:p xmlns:h=\"http://www.w3.org/1999/xhtml\">Lap <h:strong xmlns:h=\"http://www.w3.org/1999/xhtml\">12</h:strong> &amp; rain.</h:p>" entry.atom_body = Some(InlineXhtml(...))

    XhtmlFragment.base_references retains raw scoped declarations rather than guessing an absolute base from a local filename. Even with parse_with_source, callers must combine that chain with the known source URL and nested child xml:base before interpreting relative links in the fragment. The removed outer div's non-inherited style/class attributes are not projected.

    For the fixed four-form Atom body sample, the public union has External(src="entry.xml", url="entry.xml",media_type=Some("application/xml"), base_uri=Some("https://example.test/feeds/media/")), InlineText("**Text & more**", "TEXT/MARKDOWN"), an InlineXml fragment containing <n0:root ... n0:key="A&amp;B">one<n0:child .../>two</n0:root>, and InlineBinary with bytes [1, 2, 3, 255] and media type IMAGE/PNG. The CLI exposes these respectively as kind = external, inline_text, inline_xml, and inline_binary; the last has base64 = "AQID/w==" and byte_length = 4. The CLI uses parse, so the external url is raw here; the known absolute xml:base supplies base_uri. parse_with_source resolves relative src from the caller's real document URL. All fragments and decoded bytes are untrusted; FeedLens performs no sanitization, XML-media validation, network access, or HTML rendering.

    #JSON Feed 1.0 and 1.1

    • Accepts only root objects whose version is exactly https://jsonfeed.org/version/1 or https://jsonfeed.org/version/1.1. JSON is selected whenever the first non-whitespace input character is not <; XML dispatch remains unchanged.
    • title must be a string and items must be an array. Invalid JSON, a non-object root, absent/wrong-type version, absent/wrong-type title, absent/wrong-type items, and unsupported versions are fatal JSON-specific ParseError values, never InvalidXml.
    • Maps title, description, home_page_url, feed_url, icon, and favicon; maps item id, url, external_url, image, banner_image, title, summary, date_published, date_modified, content_text, and content_html. Version 1.1 also maps feed/item language. Text fields are Text; content_html is Html and is retained unchanged. Malformed string dates receive InvalidJsonFeedDate diagnostics and keep their raw text; normalize_date also exposes normalized instants. No URI resolution occurs during parse.
    • url stays the item permalink, while external_url points to an outside page; a missing permalink is not replaced by the external URL. Featured and banner images are presentation hints, never attachments. JSON Feed 1.0 does not define language, so 1.0 keys of that name are ignored.
    • Both content fields are legal and independently exposed as Entry.content_html and Entry.content_text; Entry.content remains the HTML-preferred convenience field. An item must have one valid content field. A malformed/missing item id or content discards that item with InvalidJsonFeedItem; a JSON number id is retained as its JSON spelling. Repeated usable item IDs receive InvalidJsonFeedItem(_, "duplicate id") without discarding entries; comparison is case-sensitive and uses the exact public identifier, including number-to-string recovery and any declared surrounding spaces. Optional malformed item fields are diagnosed and become None.
    • Supports authors arrays and deprecated author objects at feed/item level. If authors is present it wins, even over author; undeclared item authors inherit feed authors. A JSON Feed author needs a non-blank name, url, or avatar; URL and avatar remain separate public fields.
    • next_url, expired, and ordered hubs are parsed but never followed or subscribed to. Equal raw feed_url/next_url values are diagnosed; with an explicit source URL, resolved equality to feed_url or the current document also diagnoses and clears next_url.
    • Maps item tags to ordered categories. Empty tags and malformed optional fields are diagnosed and skipped. Unknown extension keys are ignored.
    • Maps each valid item attachments array element in order, preserving URL, MIME type, optional title, integer byte size, and floating-point duration. A malformed attachment is diagnosed without discarding its valid item. Invalid optional metadata is omitted; unknown attachment extension keys are ignored.
    • JSON syntax errors retain a parser position. The core JSON DOM does not retain semantic-field spans, so JSON Feed field/item diagnostics use document position (1, 1); XML diagnostics continue to point to their source elements.

    #Not supported

    • Attachment/image retrieval or verification; JSON Feed pagination following and extension fields; network retrieval and unscoped URI/IRI fields listed above.
    • RSS versions other than 2.0, and RSS core elements placed in a namespace.
    • Atom external-content retrieval, comprehensive MIME-parameter validation, full XHTML/XML rendering or sanitization, and resolution inside HTML/XHTML or unsupported constructs. Some full-document ordering constraints, IRI/date grammar checks, and optional extension metadata remain outside the mapped behavior.

    #Example

    match @feedlens.parse(input) {
    Ok(feed) => {
    let entry = feed.entries[0]
    println(entry.identifier.unwrap_or("(no identifier)"))
    match entry.atom_body {
    Some(@feedlens.InlineBinary(bytes, media_type)) =>
    println(media_type + ": " + bytes.length().to_string() + " bytes")
    Some(@feedlens.External(reference)) =>
    println("external, not fetched: " + reference.url)
    _ => ()
    }
    }
    Err(_) => println("feed parsing failed")
    }

    #Local-file CLI

    cmd/main is a separate executable package. The reusable root package remains the cross-target parser; only the CLI package imports local-file IO and process exit support. It reads one UTF-8 local file and never performs a network request.

    On Windows, from the module root, run the source-traceable offline projections of WP Tavern's three real public subscriptions:

    moon run --target native cmd/main samples/real_wptavern_rss.xml moon run --target native cmd/main samples/real_wptavern_atom.xml moon run --target native cmd/main --json samples/real_wptavern_json.json moon run --target native cmd/main --json samples/atom1_xhtml.xml moon run --target native cmd/main --json samples/atom1_content_forms.xml moon run --target native cmd/main --json samples/rss2_optional_channel.xml moon run --target native cmd/main --json samples/jsonfeed10_legacy_ids.json moon run --target native cmd/main -- --help

    The extra -- in the help command is required by Moon 0.1.20260920 so that --help reaches FeedLens rather than being consumed by moon run itself.

    Normal output is a short, human-readable summary:

    FeedLens — MoonBit multi-format Feed parser library Format: RSS 2.0 Title: WP Tavern Link: https://wptavern.com Entries: 2 1. identifier: https://wptavern.com/?post_type=podcast&p=209016 title: #237 – Robert Abela on WordPress Security and the Battle Against AI Threats link: https://wptavern.com/podcast/237-robert-abela-on-wordpress-security-and-the-battle-against-ai-threats/ attachments: 1

    --json writes exactly one JSON value to stdout. It contains the normalized Feed/Entry fields, text kinds, XHTML fragment objects and detailed Atom body variants (or null), ordered author/category/attachment arrays, and ordered diagnostics with positions. Missing values are JSON null. This is FeedLens's normalized inspection format, not a conversion to a JSON Feed document. Errors (bad arguments, unreadable files, or fatal parse errors) go to stderr and return exit code 2; --help and successful parsing return 0. Recoverable feed diagnostics are written into normal output and still return 0. In --json mode, no status/error text is written to stdout.

    The CLI is exercised on the Native backend in this environment. File IO and process exit use verified moonbitlang/x@0.5.5 APIs only in the executable package. The parser now also imports the pure moonbitlang/x/time package for date validation; it does not import file IO and remains checked/tested on Wasm, JS, and Native. A local file path is never treated as a source URL, so the CLI uses parse, not parse_with_source.

    #XML dependency

    This module pins Milky2018/xml@0.5.0. The newer 0.6.1 was downloaded and evaluated first, but cannot compile with this MoonBit toolchain because its source uses the removed Int32 type. 0.5.0 provides the same NamespaceReader API and passes this project's verification, so it is the newest compatible version actually installed here. Matching uses resolved namespace URIs instead of tag prefixes.

    #Verification samples

    samples/ contains compact synthetic compatibility/boundary cases and three real-feed offline projections with source URLs, capture date, response hashes, reduction policy, and baseline findings. compatibility_test.mbt and real_samples_test.mbt embed checked-in XML/JSON samples so Wasm, JS, and Native tests need no filesystem or network access. samples/DISPLAY_FIELD_INVENTORY.md distinguishes display fields in the original captured feeds from specification-defined fields exercised only by synthetic fixtures. examples/consumer imports the public library without the CLI or XML implementation; run moon run --target native examples/consumer or powershell -ExecutionPolicy Bypass -File tools/check_consumer.ps1.

    For an end-to-end Native CLI check (including file IO, exit codes, stderr, and PowerShell's standard ConvertFrom-Json), run:

    powershell -ExecutionPolicy Bypass -File tools/check_cli.ps1

    For a development-only independent comparison, use uv run --with feedparser==6.0.11 python tools/check_compatibility.py. Run uv run --with feedparser==6.0.11 python tools/check_xhtml_reference.py for the separately declared Atom XHTML text/type intersection. Both scripts read local samples only; the XHTML script also runs the local Native CLI. The script checks the real-sample mirrors and compares the older independent XML reference only on the declared field intersection in samples/reference_intersection.json; it is not a MoonBit dependency. See samples/COMPATIBILITY.md for actual reference version/results and intentional normalization differences, including RSS link fallback for a missing guid. Run uv run --with feedparser==6.0.11 python tools/check_real_cross_format.py for the two saved real-world entries across three formats and the independent Atom Entry fixture. The JSON side uses Python's standard JSON and date parsers; all inputs are local, editorially reduced samples.

    GitHub-hosted Linux CI uses the latest downloadable MoonBit toolchain and has an offline independent-reference job. Record the tested local MoonBit version alongside the relevant CI run for any submission.

    AtomBody

    pub enum AtomBody {
    InlineText(String, String)
    InlineHtml(String)
    InlineXhtml(XhtmlFragment)
    InlineXml(XmlFragment, String)
    InlineBinary(Bytes, String)
    External(AtomExternalContent)
    } derive(Eq,
    Debug
    )

    Detailed Atom body representation. Bytes contains exact decoded octets on Wasm, JS, and Native. MIME spelling is kept.

    AtomBody::equal

    fn AtomBody::equal(AtomBody, AtomBody) -> Bool

    AtomBody::not_equal

    fn AtomBody::not_equal(x : AtomBody, y : AtomBody) -> Bool

    AtomBody::to_repr

    AtomExternalContent

    pub struct AtomExternalContent {
    src : String
    url : String
    media_type : String?
    base_uri : String?
    } derive(Eq,
    Debug
    )

    An Atom out-of-line body reference. src is the original IRI reference; url is that reference after optional source resolution. base_uri is present only when an absolute effective base is known. No fetch occurs.

    AtomExternalContent::equal

    AtomExternalContent::not_equal

    pub struct AtomLink {
    relation : String
    href : String
    media_type : String?
    hreflang : String?
    title : String?
    length : Int64?
    } derive(Eq,
    Debug
    )

    An Atom link, including relations beyond the convenience alternate link and enclosure attachment fields. RSS channel Atom self extensions also use this type. A missing rel is represented as alternate.

    AtomLink::equal

    fn AtomLink::equal(AtomLink, AtomLink) -> Bool

    AtomLink::not_equal

    fn AtomLink::not_equal(x : AtomLink, y : AtomLink) -> Bool

    AtomLink::to_repr

    AtomSource

    pub struct AtomSource {
    identifier : String?
    title : String?
    title_kind : TextKind
    title_xhtml : XhtmlFragment?
    subtitle : String?
    subtitle_kind : TextKind
    subtitle_xhtml : XhtmlFragment?
    updated_at : String?
    rights : String?
    rights_kind : TextKind
    rights_xhtml : XhtmlFragment?
    generator : String?
    generator_uri : String?
    generator_version : String?
    authors : Array[Author]
    contributors : Array[Author]
    categories : Array[Category]
    link : String?
    links : Array[AtomLink]
    images : Array[FeedImage]
    } derive(Eq,
    Debug
    )

    Metadata retained from an Atom entry's original feed when it is copied into another feed. Missing members remain absent: atom:source may carry only some of its original feed's metadata.

    AtomSource::equal

    fn AtomSource::equal(AtomSource, AtomSource) -> Bool

    AtomSource::not_equal

    fn AtomSource::not_equal(x : AtomSource, y : AtomSource) -> Bool

    Attachment

    pub struct Attachment {
    source : FeedFormat
    url : String
    media_type : String?
    size_in_bytes : Int64?
    title : String?
    duration_in_seconds : Double?
    } derive(Eq,
    Debug
    )

    Metadata for a related resource. size_in_bytes is limited to the largest integer exactly representable by all three backends and JSON consumers. title is available from Atom links and JSON Feed; duration is supplied by JSON Feed only.

    Attachment::equal

    fn Attachment::equal(Attachment, Attachment) -> Bool

    Attachment::not_equal

    fn Attachment::not_equal(x : Attachment, y : Attachment) -> Bool

    Author

    pub struct Author {
    source : AuthorSource
    name : String?
    uri : String?
    email : String?
    avatar : String?
    } derive(Eq,
    Debug
    )

    An RSS raw author email, Dublin Core creator name, Atom person construct, or JSON Feed author. RSS uses email, Dublin Core uses name, and JSON Feed maps its url to uri.

    Author::equal

    fn Author::equal(Author, Author) -> Bool

    Author::not_equal

    fn Author::not_equal(x : Author, y : Author) -> Bool

    Author::to_repr

    AuthorSource

    pub enum AuthorSource {
    Rss2
    DublinCore
    Atom1
    JsonFeed
    } derive(Eq,
    Debug
    )

    The source syntax that defines an author's fields.

    AuthorSource::equal

    AuthorSource::not_equal

    fn AuthorSource::not_equal(x : AuthorSource, y : AuthorSource) -> Bool

    Category

    pub struct Category {
    source : FeedFormat
    term : String
    domain : String?
    scheme : String?
    label : String?
    } derive(Eq,
    Debug
    )

    A category whose optional metadata retains its source-specific meaning. domain belongs only to RSS; scheme and label belong only to Atom. JSON Feed tags use only term.

    Category::equal

    fn Category::equal(Category, Category) -> Bool

    Category::not_equal

    fn Category::not_equal(x : Category, y : Category) -> Bool

    Category::to_repr

    DateError

    pub enum DateError {
    InvalidSyntax
    InvalidCalendarDate
    InvalidOffset
    UnsupportedPrecision
    UnsupportedTimeZone
    OutOfRange
    } derive(Eq,
    Debug
    )

    DateError::equal

    fn DateError::equal(DateError, DateError) -> Bool

    DateError::not_equal

    fn DateError::not_equal(x : DateError, y : DateError) -> Bool

    DatePrecision

    pub enum DatePrecision {
    Minute
    Second
    Fraction(Int)
    } derive(Eq,
    Debug
    )

    The precision actually declared by a source date. UTC rendering always includes seconds, but this marker distinguishes an RSS minute-only value. Fraction(digits) retains the full count beyond nanosecond precision.

    DatePrecision::equal

    DatePrecision::not_equal

    fn DatePrecision::not_equal(x : DatePrecision, y : DatePrecision) -> Bool

    Diagnostic

    pub struct Diagnostic {
    kind : DiagnosticKind
    line : Int
    column : Int
    } derive(Eq,
    Debug
    )

    A recovered problem that does not prevent the rest of an RSS feed from being used. line and column are one-indexed source locations. A blank channel field points to its element; an absent one points to <channel>.

    Diagnostic::equal

    fn Diagnostic::equal(Diagnostic, Diagnostic) -> Bool

    Diagnostic::not_equal

    fn Diagnostic::not_equal(x : Diagnostic, y : Diagnostic) -> Bool

    DiagnosticKind

    pub enum DiagnosticKind {
    MissingChannelField(String)
    MissingItemIdentifier(Int)
    MissingItemContent(Int)
    MissingFeedField(String)
    MissingEntryField(Int, String)
    UnsupportedNestedMarkup(String)
    UnsupportedTextConstruct(String)
    UnsupportedAtomContent(String)
    MissingAlternateLink(Int)
    MissingSummaryForExternalContent(Int)
    MissingSummaryForBinaryContent(Int)
    DuplicateAtomField(String)
    InvalidAuthor(String)
    InvalidCategory(String)
    InvalidAttachment(String)
    InvalidLanguage(String)
    InvalidFeedImage(String)
    InvalidRssField(String)
    UnnormalizedRssDate(String)
    InvalidAtomLink(String)
    InvalidAtomGeneratorUri(String)
    InvalidAtomId(String)
    InvalidAtomDate(String)
    InvalidAtomOrder(String)
    UnresolvedUri(String)
    InvalidJsonFeedField(String)
    InvalidJsonFeedItem(Int, String)
    InvalidJsonFeedDate(Int, String)
    } derive(Eq,
    Debug
    )

    The kind of recoverable feed data problem.

    DiagnosticKind::equal

    DiagnosticKind::not_equal

    fn DiagnosticKind::not_equal(x : DiagnosticKind, y : DiagnosticKind) -> Bool

    Entry

    pub struct Entry {
    identifier : String?
    title : String?
    title_kind : TextKind
    title_xhtml : XhtmlFragment?
    link : String?
    external_url : String?
    language : String?
    description : String?
    description_kind : TextKind
    description_xhtml : XhtmlFragment?
    content : String?
    content_text : String?
    content_html : String?
    content_kind : TextKind
    content_xhtml : XhtmlFragment?
    content_source : String?
    content_media_type : String?
    atom_body : AtomBody?
    atom_source : AtomSource?
    links : Array[AtomLink]
    comments_url : String?
    source_url : String?
    source_title : String?
    guid_is_permalink : Bool?
    rights : String?
    rights_kind : TextKind
    rights_xhtml : XhtmlFragment?
    authors : Array[Author]
    contributors : Array[Author]
    categories : Array[Category]
    attachments : Array[Attachment]
    images : Array[EntryImage]
    published_at : String?
    updated_at : String?
    } derive(Eq,
    Debug
    )

    A normalized feed entry. Fields retain decoded source text without HTML rendering or sanitization.

    Entry::equal

    fn Entry::equal(Entry, Entry) -> Bool

    Entry::not_equal

    fn Entry::not_equal(x : Entry, y : Entry) -> Bool

    Entry::to_repr

    EntryImage

    pub struct EntryImage {
    source : FeedFormat
    role : EntryImageRole
    url : String
    } derive(Eq,
    Debug
    )

    An item image hint, separate from downloadable attachments.

    EntryImage::equal

    fn EntryImage::equal(EntryImage, EntryImage) -> Bool

    EntryImage::not_equal

    fn EntryImage::not_equal(x : EntryImage, y : EntryImage) -> Bool

    EntryImageRole

    pub enum EntryImageRole {
    JsonFeatured
    JsonBanner
    } derive(Eq,
    Debug
    )

    JSON Feed item images serve distinct reader presentation roles.

    EntryImageRole::equal

    EntryImageRole::not_equal

    fn EntryImageRole::not_equal(x : EntryImageRole, y : EntryImageRole) -> Bool

    Feed

    pub struct Feed {
    format : FeedFormat
    identifier : String?
    title : String?
    title_kind : TextKind
    title_xhtml : XhtmlFragment?
    link : String?
    language : String?
    description : String?
    description_kind : TextKind
    description_xhtml : XhtmlFragment?
    updated_at : String?
    published_at : String?
    ttl_minutes : Int?
    docs_url : String?
    rating : String?
    cloud : RssCloud?
    text_input : RssTextInput?
    skip_hours : Array[Int]
    skip_days : Array[String]
    rights : String?
    rights_kind : TextKind
    rights_xhtml : XhtmlFragment?
    generator : String?
    generator_uri : String?
    generator_version : String?
    managing_editor : String?
    webmaster : String?
    next_url : String?
    expired : Bool?
    hubs : Array[Hub]
    links : Array[AtomLink]
    authors : Array[Author]
    contributors : Array[Author]
    categories : Array[Category]
    images : Array[FeedImage]
    entries : Array[Entry]
    diagnostics : Array[Diagnostic]
    } derive(Eq,
    Debug
    )

    A normalized feed and its entries, in document order.

    Feed::equal

    fn Feed::equal(Feed, Feed) -> Bool

    Feed::not_equal

    fn Feed::not_equal(x : Feed, y : Feed) -> Bool

    Feed::to_repr

    FeedFormat

    pub enum FeedFormat {
    Rss2
    Atom1
    JsonFeed1
    } derive(Eq,
    Debug
    )

    The feed syntax accepted by this library.

    FeedFormat::equal

    fn FeedFormat::equal(FeedFormat, FeedFormat) -> Bool

    FeedFormat::not_equal

    fn FeedFormat::not_equal(x : FeedFormat, y : FeedFormat) -> Bool

    FeedImage

    pub struct FeedImage {
    source : FeedFormat
    role : FeedImageRole
    url : String
    title : String?
    link : String?
    description : String?
    width : Int?
    height : Int?
    } derive(Eq,
    Debug
    )

    URL and optional RSS image metadata. Dimensions are declared pixel values, not inferred image dimensions; no image resource is downloaded.

    FeedImage::equal

    fn FeedImage::equal(FeedImage, FeedImage) -> Bool

    FeedImage::not_equal

    fn FeedImage::not_equal(x : FeedImage, y : FeedImage) -> Bool

    FeedImageRole

    pub enum FeedImageRole {
    RssImage
    AtomIcon
    AtomLogo
    JsonIcon
    JsonFavicon
    } derive(Eq,
    Debug
    )

    A feed-level visual identity. Roles are source-specific; in particular an Atom logo and JSON favicon are not interchangeable.

    FeedImageRole::equal

    FeedImageRole::not_equal

    fn FeedImageRole::not_equal(x : FeedImageRole, y : FeedImageRole) -> Bool

    Hub

    pub struct Hub {
    transport_type : String
    url : String
    } derive(Eq,
    Debug
    )

    JSON Feed hub declaration. Parsing does not subscribe or send notifications.

    Hub::equal

    fn Hub::equal(Hub, Hub) -> Bool

    Hub::not_equal

    fn Hub::not_equal(x : Hub, y : Hub) -> Bool

    Hub::to_repr

    NormalizedDate

    pub struct NormalizedDate {
    utc : String
    unix_seconds : Int64
    nanosecond : Int
    offset_minutes : Int
    offset_known : Bool
    is_leap_second : Bool
    precision : DatePrecision
    } derive(Eq,
    Debug
    )

    A validated instant. utc preserves declared fractional-second digits; unix_seconds and nanosecond expose seconds and nanoseconds without local time zones. utc retains any finer digits for callers that need them. nanosecond truncates any sub-nanosecond digits; utc keeps them intact. offset_known is false for RFC 3339 -00:00 or RSS -0000: UTC is known but the source's local offset is not. In that case offset_minutes is zero only for UTC arithmetic; it does not assert a local offset. is_leap_second distinguishes an announced 23:59:60 from the preceding second: POSIX/Unix time has no separate leap-second value, so unix_seconds uses the preceding 23:59:59 second in that case.

    NormalizedDate::equal

    NormalizedDate::not_equal

    fn NormalizedDate::not_equal(x : NormalizedDate, y : NormalizedDate) -> Bool

    ParseError

    pub enum ParseError {
    InvalidXml(String, Int, Int)
    UnsupportedRoot(String, String?)
    UnsupportedRssVersion(String?)
    MissingChannel
    InvalidRssStructure(String)
    InvalidAtomStructure(String)
    InvalidJson(String, Int, Int)
    InvalidJsonFeed(String)
    UnsupportedJsonFeedVersion(String?)
    InvalidSourceUrl(String)
    } derive(Eq,
    Debug
    )

    A fatal error that prevents producing a feed.

    ParseError::equal

    fn ParseError::equal(ParseError, ParseError) -> Bool

    ParseError::not_equal

    fn ParseError::not_equal(x : ParseError, y : ParseError) -> Bool

    ParsedDocument

    pub enum ParsedDocument {
    FeedDocument(Feed)
    AtomEntryDocument(Entry, Array[Diagnostic])
    } derive(Eq,
    Debug
    )

    A complete feed or a stand-alone Atom Entry Document. An entry document has its own diagnostics and does not invent enclosing feed metadata.

    ParsedDocument::equal

    ParsedDocument::not_equal

    fn ParsedDocument::not_equal(x : ParsedDocument, y : ParsedDocument) -> Bool

    RssCloud

    pub struct RssCloud {
    domain : String
    port : Int
    path : String
    register_procedure : String
    protocol : String
    } derive(Eq,
    Debug
    )

    RSS 2.0 cloud registration descriptor. Its values are metadata only.

    RssCloud::equal

    fn RssCloud::equal(RssCloud, RssCloud) -> Bool

    RssCloud::not_equal

    fn RssCloud::not_equal(x : RssCloud, y : RssCloud) -> Bool

    RssCloud::to_repr

    RssTextInput

    pub struct RssTextInput {
    title : String
    description : String
    name : String
    link : String
    } derive(Eq,
    Debug
    )

    RSS 2.0 textInput form descriptor. The link is resolved only when a document source URL is supplied.

    RssTextInput::equal

    RssTextInput::not_equal

    fn RssTextInput::not_equal(x : RssTextInput, y : RssTextInput) -> Bool

    TextKind

    pub enum TextKind {
    Unspecified
    Text
    Html
    Xhtml
    } derive(Eq,
    Debug
    )

    How a text field was declared by its source format.

    TextKind::equal

    fn TextKind::equal(TextKind, TextKind) -> Bool

    TextKind::not_equal

    fn TextKind::not_equal(x : TextKind, y : TextKind) -> Bool

    TextKind::to_repr

    XhtmlAttribute

    pub struct XhtmlAttribute {
    local_name : String
    namespace_uri : String?
    value : String
    } derive(Eq,
    Debug
    )

    An attribute declared on the outer Atom XHTML div. Names are expanded into namespace URI and local name so prefixes do not affect callers.

    XhtmlAttribute::equal

    XhtmlAttribute::not_equal

    fn XhtmlAttribute::not_equal(x : XhtmlAttribute, y : XhtmlAttribute) -> Bool

    XhtmlFragment

    pub struct XhtmlFragment {
    markup : String
    language : String?
    base_references : Array[String]
    container_attributes : Array[XhtmlAttribute]
    } derive(Eq,
    Debug
    )

    An Atom XHTML fragment. markup omits the required outer XHTML div and uses deterministic namespace-qualified XML serialization. language is the effective xml:lang at that div; base_references retains the ordered xml:base declarations from the feed/entry, text construct, and div. The latter are raw references, not guessed absolute URLs. container_attributes retains the discarded div's attributes in source order, including xml:lang and xml:base; markup and attributes are unsanitized.

    XhtmlFragment::equal

    XhtmlFragment::not_equal

    fn XhtmlFragment::not_equal(x : XhtmlFragment, y : XhtmlFragment) -> Bool

    XmlFragment

    pub struct XmlFragment {
    markup : String
    language : String?
    base_references : Array[String]
    } derive(Eq,
    Debug
    )

    An XML media-type Atom body, serialized with stable namespace-qualified names. Unlike XHTML, no wrapper element is removed. This is unsanitized.

    XmlFragment::equal

    fn XmlFragment::equal(XmlFragment, XmlFragment) -> Bool

    XmlFragment::not_equal

    fn XmlFragment::not_equal(x : XmlFragment, y : XmlFragment) -> Bool

    normalize_date

    fn normalize_date(format : FeedFormat, raw : String?) -> Result[NormalizedDate?, DateError]

    Validate an optional source date without changing the raw feed field. RSS uses the supported RFC 822/1123 profile; Atom and JSON Feed use RFC 3339. Atom additionally requires uppercase T and Z. A missing date is Ok(None), while malformed dates are Err.

    parse

    fn parse(input : String) -> Result[Feed, ParseError]

    Parse one RSS 2.0, Atom 1.0, or JSON Feed 1.0/1.1 document.

    Inputs whose first non-whitespace character is < are treated as XML; all other inputs are treated as JSON. XML failures and JSON syntax/structural failures return Err. Recoverable field problems return Ok with diagnostics. An optional leading U+FEFF byte order mark is ignored for both formats; U+FEFF elsewhere remains part of the input.

    parse_document

    fn parse_document(input : String) -> Result[ParsedDocument, ParseError]

    Parse a feed or an independent Atom Entry Document. The established parse function remains feed-only and keeps its Result[Feed, ParseError] signature. Stand-alone entries use the same Atom field, content, and diagnostic rules as entries inside feeds.

    parse_document_with_source

    fn parse_document_with_source(input : String, source_url : String) -> Result[ParsedDocument, ParseError]

    Resolve links and external content in either document shape using the supplied HTTP(S) document URL and Atom xml:base scopes.

    parse_with_source

    fn parse_with_source(input : String, source_url : String) -> Result[Feed, ParseError]

    Parse a document and resolve documented feed/entry links, attachment URLs, feed-image URLs, and JSON Feed item external/image URLs against an explicit HTTP(S) source URL. Atom xml:base is inherited by nested elements. Identifiers and HTML content are never rewritten. parse(String) remains the raw-reference entry point.