├── benchmarks ├── .gitattributes ├── cross-language │ ├── requirements.txt │ ├── Gemfile │ ├── composer.json │ ├── parsing-feedjira.rb │ ├── parsing-feedparser.py │ ├── parsing-simplepie.php │ ├── parsing-feedsmith.ts │ ├── go.mod │ ├── parsing-gofeed.go │ └── parsing.sh └── javascript │ └── package.json ├── compatibility ├── .gitattributes ├── .gitignore ├── bundler │ ├── cjs │ │ ├── package.json │ │ ├── vite.config.ts │ │ └── index.cjs │ └── esm │ │ ├── package.json │ │ ├── vite.config.ts │ │ └── index.ts ├── javascript │ ├── cjs │ │ ├── package.json │ │ ├── index.cjs │ │ └── index.js │ └── esm │ │ ├── package.json │ │ ├── index.js │ │ └── index.mjs ├── explicit-modules │ ├── esm-package │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── README.md │ │ ├── index.cts │ │ └── index.mts │ ├── cjs-package │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── README.md │ │ ├── index.cts │ │ └── index.mts │ └── mixed-package │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── index.cts │ │ ├── index.mts │ │ ├── index.ts │ │ └── README.md ├── typescript │ ├── legacy-cjs │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── index.ts │ ├── modern-cjs │ │ ├── package.json │ │ ├── tsconfig.node.json │ │ ├── tsconfig.bundler.json │ │ ├── tsconfig.node16.json │ │ ├── tsconfig.nodenext.json │ │ ├── tsconfig.base.json │ │ └── index.ts │ └── modern-esm │ │ ├── package.json │ │ ├── tsconfig.node.json │ │ ├── tsconfig.bundler.json │ │ ├── tsconfig.node16.json │ │ ├── tsconfig.nodenext.json │ │ ├── tsconfig.base.json │ │ ├── bun.lock │ │ └── index.ts ├── package.json ├── test.sh └── README.md ├── biome.json ├── commitlint.json ├── docs ├── benchmarks.md ├── .vitepress │ ├── Dockerfile │ └── nginx.conf ├── generating │ └── index.md ├── parsing │ ├── detecting.md │ └── dates.md └── reference │ ├── namespaces │ ├── rdf.md │ ├── app.md │ ├── yt.md │ ├── pingback.md │ ├── feedpress.md │ ├── cc.md │ ├── geo.md │ ├── rawvoice.md │ ├── blogchannel.md │ ├── source.md │ ├── georss.md │ ├── content.md │ ├── admin.md │ ├── media.md │ ├── wfw.md │ ├── trackback.md │ ├── slash.md │ ├── opensearch.md │ ├── arxiv.md │ ├── creativecommons.md │ ├── spotify.md │ ├── itunes.md │ ├── acast.md │ ├── psc.md │ ├── googleplay.md │ ├── thr.md │ ├── sy.md │ ├── podcast.md │ ├── atom.md │ ├── dcterms.md │ ├── dc.md │ └── prism.md │ └── index.md ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── compatibility.yml │ ├── test.yml │ ├── lint.yml │ └── release.yml ├── src ├── namespaces │ ├── wfw │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── creativecommons │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── trackback │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── admin │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── geo │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── blogchannel │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── pingback │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── feedpress │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── yt │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── slash │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ ├── utils.ts │ │ │ └── utils.test.ts │ │ └── parse │ │ │ └── utils.ts │ ├── psc │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── sy │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ ├── utils.ts │ │ │ └── utils.test.ts │ │ └── parse │ │ │ └── utils.ts │ ├── app │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── atom │ │ ├── common │ │ │ └── types.ts │ │ ├── parse │ │ │ └── utils.ts │ │ └── generate │ │ │ └── utils.ts │ ├── arxiv │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── content │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ ├── utils.ts │ │ │ └── utils.test.ts │ │ └── parse │ │ │ └── utils.ts │ ├── cc │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── rdf │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── thr │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── opensearch │ │ ├── common │ │ │ └── types.ts │ │ ├── generate │ │ │ └── utils.ts │ │ └── parse │ │ │ └── utils.ts │ ├── googleplay │ │ ├── common │ │ │ └── types.ts │ │ └── generate │ │ │ └── utils.ts │ ├── acast │ │ ├── common │ │ │ └── types.ts │ │ └── generate │ │ │ └── utils.ts │ ├── georss │ │ └── common │ │ │ └── types.ts │ ├── spotify │ │ └── common │ │ │ └── types.ts │ ├── source │ │ └── common │ │ │ └── types.ts │ ├── rawvoice │ │ └── common │ │ │ └── types.ts │ └── itunes │ │ └── common │ │ └── types.ts ├── opml │ ├── generate │ │ ├── config.ts │ │ └── index.ts │ ├── references │ │ ├── category.opml │ │ ├── category.json │ │ ├── directory.opml │ │ ├── places.opml │ │ ├── script.opml │ │ ├── directory.json │ │ ├── script.json │ │ └── places.json │ ├── parse │ │ ├── config.ts │ │ └── index.ts │ └── common │ │ └── types.ts ├── feeds │ ├── rss │ │ ├── generate │ │ │ ├── config.ts │ │ │ └── index.ts │ │ ├── detect │ │ │ └── index.ts │ │ ├── references │ │ │ ├── rss-090.json │ │ │ ├── rss-090.xml │ │ │ ├── rss-091.json │ │ │ └── rss-091.xml │ │ └── parse │ │ │ ├── index.ts │ │ │ └── config.ts │ ├── atom │ │ ├── generate │ │ │ ├── config.ts │ │ │ └── index.ts │ │ ├── detect │ │ │ └── index.ts │ │ └── parse │ │ │ ├── index.ts │ │ │ └── config.ts │ ├── json │ │ ├── generate │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── parse │ │ │ └── index.ts │ │ ├── detect │ │ │ └── index.ts │ │ └── common │ │ │ └── types.ts │ └── rdf │ │ ├── detect │ │ └── index.ts │ │ ├── parse │ │ ├── config.ts │ │ └── index.ts │ │ ├── references │ │ ├── rdf-09.json │ │ ├── rdf-09.xml │ │ ├── rdf-10.json │ │ └── rdf-10.xml │ │ └── common │ │ └── types.ts ├── types.ts └── common │ └── parse.ts ├── tsconfig.json ├── lefthook.json ├── .gitignore ├── LICENSE └── package.json /benchmarks/.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | -------------------------------------------------------------------------------- /compatibility/.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["kvalita/biome"] 3 | } 4 | -------------------------------------------------------------------------------- /benchmarks/cross-language/requirements.txt: -------------------------------------------------------------------------------- 1 | feedparser~=6.0 2 | -------------------------------------------------------------------------------- /compatibility/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | *.tsbuildinfo 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /benchmarks/cross-language/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'feedjira', '~> 3.2' 3 | -------------------------------------------------------------------------------- /commitlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["./node_modules/kvalita/configs/commitlint.json"] 3 | } 4 | -------------------------------------------------------------------------------- /benchmarks/cross-language/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "simplepie/simplepie": "^1.9" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/benchmarks.md: -------------------------------------------------------------------------------- 1 | --- 2 | prev: Quick Start 3 | next: Parsing › Overview 4 | --- 5 | 6 | -------------------------------------------------------------------------------- /compatibility/bundler/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feedsmith/compatibility-bundler-cjs", 3 | "private": true, 4 | "type": "commonjs" 5 | } 6 | -------------------------------------------------------------------------------- /compatibility/bundler/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feedsmith/compatibility-bundler-esm", 3 | "private": true, 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /compatibility/javascript/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feedsmith/compatibility-javascript-cjs", 3 | "private": true, 4 | "type": "commonjs" 5 | } 6 | -------------------------------------------------------------------------------- /compatibility/javascript/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feedsmith/compatibility-javascript-esm", 3 | "private": true, 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/esm-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feedsmith/compatibility-explicit-esm", 3 | "private": true, 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/cjs-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feedsmith/compatibility-explicit-cjs", 3 | "private": true, 4 | "type": "commonjs" 5 | } 6 | -------------------------------------------------------------------------------- /compatibility/typescript/legacy-cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feedsmith/compatibility-typescript-legacy-cjs", 3 | "private": true, 4 | "type": "commonjs" 5 | } 6 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feedsmith/compatibility-typescript-modern-cjs", 3 | "private": true, 4 | "type": "commonjs" 5 | } 6 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feedsmith/compatibility-typescript-modern-esm", 3 | "private": true, 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_style = space 5 | indent_size = 2 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bun 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | target-branch: main 8 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/mixed-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feedsmith/compatibility-explicit-mixed", 3 | "private": true, 4 | "type": "commonjs" 5 | } 6 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-cjs/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "moduleResolution": "node" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-esm/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-cjs/tsconfig.bundler.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "moduleResolution": "bundler" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-cjs/tsconfig.node16.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "node16", 5 | "moduleResolution": "node16" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-esm/tsconfig.bundler.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "moduleResolution": "bundler" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-esm/tsconfig.node16.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "node16", 5 | "moduleResolution": "node16" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/namespaces/wfw/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace WfwNs { 3 | export type Item = { 4 | comment?: string 5 | commentRss?: string 6 | } 7 | } 8 | // #endregion reference 9 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-cjs/tsconfig.nodenext.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "nodenext", 5 | "moduleResolution": "nodenext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-esm/tsconfig.nodenext.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "nodenext", 5 | "moduleResolution": "nodenext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/opml/generate/config.ts: -------------------------------------------------------------------------------- 1 | import { XMLBuilder } from 'fast-xml-parser' 2 | import { builderConfig } from '../../common/config.js' 3 | 4 | export const builder = new XMLBuilder({ 5 | ...builderConfig, 6 | }) 7 | -------------------------------------------------------------------------------- /src/feeds/rss/generate/config.ts: -------------------------------------------------------------------------------- 1 | import { XMLBuilder } from 'fast-xml-parser' 2 | import { builderConfig } from '../../../common/config.js' 3 | 4 | export const builder = new XMLBuilder({ 5 | ...builderConfig, 6 | }) 7 | -------------------------------------------------------------------------------- /src/namespaces/creativecommons/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace CreativeCommonsNs { 3 | export type ItemOrFeed = { 4 | licenses?: Array 5 | } 6 | } 7 | // #endregion reference 8 | -------------------------------------------------------------------------------- /src/namespaces/trackback/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace TrackbackNs { 3 | export type Item = { 4 | ping?: string 5 | abouts?: Array 6 | } 7 | } 8 | // #endregion reference 9 | -------------------------------------------------------------------------------- /src/feeds/atom/generate/config.ts: -------------------------------------------------------------------------------- 1 | import { XMLBuilder } from 'fast-xml-parser' 2 | import { builderConfig } from '../../../common/config.js' 3 | 4 | export const builder = new XMLBuilder({ 5 | ...builderConfig, 6 | }) 7 | -------------------------------------------------------------------------------- /src/namespaces/admin/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace AdminNs { 3 | export type Feed = { 4 | errorReportsTo?: string 5 | generatorAgent?: string 6 | } 7 | } 8 | // #endregion reference 9 | -------------------------------------------------------------------------------- /src/namespaces/geo/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace GeoNs { 3 | export type ItemOrFeed = { 4 | lat?: number 5 | long?: number 6 | alt?: number 7 | } 8 | } 9 | // #endregion reference 10 | -------------------------------------------------------------------------------- /src/namespaces/blogchannel/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace BlogChannelNs { 3 | export type Feed = { 4 | blogRoll?: string 5 | blink?: string 6 | mySubscriptions?: string 7 | } 8 | } 9 | // #endregion reference 10 | -------------------------------------------------------------------------------- /src/namespaces/pingback/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace PingbackNs { 3 | export type Feed = { 4 | to?: string 5 | } 6 | 7 | export type Item = { 8 | server?: string 9 | target?: string 10 | } 11 | } 12 | // #endregion reference 13 | -------------------------------------------------------------------------------- /src/namespaces/feedpress/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace FeedPressNs { 3 | export type Feed = { 4 | link?: string 5 | newsletterId?: string 6 | locale?: string 7 | podcastId?: string 8 | cssFile?: string 9 | } 10 | } 11 | // #endregion reference 12 | -------------------------------------------------------------------------------- /benchmarks/cross-language/parsing-feedjira.rb: -------------------------------------------------------------------------------- 1 | require 'feedjira' 2 | 3 | def main 4 | dir_path, feed_type = ARGV 5 | 6 | Dir.glob(File.join(dir_path, "*.#{feed_type}")).each do |file_path| 7 | content = File.read(file_path) 8 | Feedjira.parse(content) 9 | end 10 | end 11 | 12 | main 13 | -------------------------------------------------------------------------------- /src/namespaces/yt/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace YtNs { 3 | export type Item = { 4 | videoId?: string 5 | channelId?: string 6 | } 7 | 8 | export type Feed = { 9 | channelId?: string 10 | playlistId?: string 11 | } 12 | } 13 | // #endregion reference 14 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-cjs/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2023", 4 | "esModuleInterop": true, 5 | "strict": true, 6 | "skipLibCheck": false, 7 | "declaration": true, 8 | "outDir": "dist" 9 | }, 10 | "include": ["*.ts", "*.cts", "*.mts"] 11 | } 12 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-esm/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2023", 4 | "esModuleInterop": true, 5 | "strict": true, 6 | "skipLibCheck": false, 7 | "declaration": true, 8 | "outDir": "dist" 9 | }, 10 | "include": ["*.ts", "*.cts", "*.mts"] 11 | } 12 | -------------------------------------------------------------------------------- /src/namespaces/slash/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace SlashNs { 3 | export type HitParade = Array 4 | 5 | export type Item = { 6 | section?: string 7 | department?: string 8 | comments?: number 9 | hitParade?: HitParade 10 | } 11 | } 12 | // #endregion reference 13 | -------------------------------------------------------------------------------- /src/namespaces/psc/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace PscNs { 3 | export type Chapter = { 4 | start: string 5 | title: string 6 | href?: string 7 | image?: string 8 | } 9 | 10 | export type Item = { 11 | chapters?: Array 12 | } 13 | } 14 | // #endregion reference 15 | -------------------------------------------------------------------------------- /src/namespaces/sy/common/types.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike } from '../../../common/types.js' 2 | 3 | // #region reference 4 | export namespace SyNs { 5 | export type Feed = { 6 | updatePeriod?: string 7 | updateFrequency?: number 8 | updateBase?: TDate 9 | } 10 | } 11 | // #endregion reference 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "nodenext", 5 | "declaration": true, 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "isolatedModules": true 11 | }, 12 | "include": ["src"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type { DeepPartial } from './common/types.js' 2 | export type { Atom } from './feeds/atom/common/types.js' 3 | export type { Json } from './feeds/json/common/types.js' 4 | export type { Rdf } from './feeds/rdf/common/types.js' 5 | export type { Rss } from './feeds/rss/common/types.js' 6 | export type { Opml } from './opml/common/types.js' 7 | -------------------------------------------------------------------------------- /lefthook.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "node_modules/kvalita/configs/lefthook-biome.json", 4 | "node_modules/kvalita/configs/lefthook-typescript.json", 5 | "node_modules/kvalita/configs/lefthook-commitlint.json" 6 | ], 7 | "pre-push": { 8 | "commands": { 9 | "test": { 10 | "run": "bun test" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/namespaces/app/common/types.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike } from '../../../common/types.js' 2 | 3 | // #region reference 4 | export namespace AppNs { 5 | export type Control = { 6 | draft?: boolean 7 | } 8 | 9 | export type Entry = { 10 | edited?: TDate 11 | control?: Control 12 | } 13 | } 14 | // #endregion reference 15 | -------------------------------------------------------------------------------- /compatibility/typescript/legacy-cjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2023", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": false, 9 | "declaration": true, 10 | "outDir": "dist" 11 | }, 12 | "include": ["*.ts", "*.cts", "*.mts"] 13 | } 14 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-esm/bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "feedsmith-tests-typescript-modern-esm", 6 | "dependencies": { 7 | "feedsmith": "link:../../../feedsmith" 8 | } 9 | } 10 | }, 11 | "packages": { 12 | "feedsmith": ["feedsmith@link:../../../feedsmith", {}] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/cjs-package/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2023", 4 | "module": "nodenext", 5 | "moduleResolution": "nodenext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": false, 9 | "declaration": true, 10 | "outDir": "dist" 11 | }, 12 | "include": ["*.ts", "*.cts", "*.mts"] 13 | } 14 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/esm-package/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2023", 4 | "module": "nodenext", 5 | "moduleResolution": "nodenext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": false, 9 | "declaration": true, 10 | "outDir": "dist" 11 | }, 12 | "include": ["*.ts", "*.cts", "*.mts"] 13 | } 14 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/mixed-package/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2023", 4 | "module": "nodenext", 5 | "moduleResolution": "nodenext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": false, 9 | "declaration": true, 10 | "outDir": "dist" 11 | }, 12 | "include": ["*.ts", "*.cts", "*.mts"] 13 | } 14 | -------------------------------------------------------------------------------- /compatibility/bundler/cjs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import { defineConfig } from 'vite' 4 | 5 | const __dirname = dirname(fileURLToPath(import.meta.url)) 6 | 7 | export default defineConfig({ 8 | build: { 9 | rollupOptions: { 10 | input: resolve(__dirname, 'index.cjs'), 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /compatibility/bundler/esm/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | import { defineConfig } from 'vite' 4 | 5 | const __dirname = dirname(fileURLToPath(import.meta.url)) 6 | 7 | export default defineConfig({ 8 | build: { 9 | rollupOptions: { 10 | input: resolve(__dirname, 'index.ts'), 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /src/feeds/json/generate/index.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike, DeepPartial, JsonGenerateMain } from '../../../common/types.js' 2 | import type { Json } from '../common/types.js' 3 | import { generateFeed } from './utils.js' 4 | 5 | export const generate: JsonGenerateMain, DeepPartial>> = ( 6 | value, 7 | ) => { 8 | return generateFeed(value as Json.Feed) 9 | } 10 | -------------------------------------------------------------------------------- /src/namespaces/atom/common/types.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike } from '../../../common/types.js' 2 | import type { Atom } from '../../../feeds/atom/common/types.js' 3 | 4 | // #region reference 5 | export namespace AtomNs { 6 | export type Entry = Partial> 7 | 8 | export type Feed = Partial> 9 | } 10 | // #endregion reference 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System 2 | Thumbs.db 3 | .DS_Store 4 | 5 | # Dependency 6 | node_modules 7 | 8 | # Build 9 | dist 10 | coverage 11 | 12 | # Debug 13 | tsconfig.tsbuildinfo 14 | 15 | # Docs 16 | docs/.vitepress/cache 17 | docs/.vitepress/dist 18 | 19 | # Benchmarks 20 | benchmarks/files 21 | benchmarks/**/vendor 22 | benchmarks/**/go.sum 23 | benchmarks/**/composer.lock 24 | benchmarks/**/Gemfile.lock 25 | benchmarks/**/parsing-gofeed 26 | -------------------------------------------------------------------------------- /docs/.vitepress/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM oven/bun:alpine AS builder 2 | RUN apk add --no-cache git 3 | WORKDIR /app 4 | COPY package.json bun.lock ./ 5 | RUN bun install --frozen-lockfile --ignore-scripts 6 | COPY . . 7 | RUN bun run docs:build 8 | 9 | FROM nginx:alpine 10 | COPY --from=builder /app/docs/.vitepress/dist /usr/share/nginx/html 11 | COPY docs/.vitepress/nginx.conf /etc/nginx/conf.d/default.conf 12 | 13 | CMD ["nginx", "-g", "daemon off;"] 14 | -------------------------------------------------------------------------------- /src/namespaces/arxiv/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace ArxivNs { 3 | export type PrimaryCategory = { 4 | term?: string 5 | scheme?: string 6 | label?: string 7 | } 8 | 9 | export type Author = { 10 | affiliation?: string 11 | } 12 | 13 | export type Entry = { 14 | comment?: string 15 | journalRef?: string 16 | doi?: string 17 | primaryCategory?: PrimaryCategory 18 | } 19 | } 20 | // #endregion reference 21 | -------------------------------------------------------------------------------- /src/namespaces/content/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace ContentNs { 3 | export type Item = { 4 | // Spec (https://web.resource.org/rss/1.0/modules/content/) also mentions content:items, 5 | // but it is not clear what it is used for. Also, it's not widely used so its implementation 6 | // will be skipped for now. If it's requested in the future, it can be added here. 7 | encoded?: string 8 | } 9 | } 10 | // #endregion reference 11 | -------------------------------------------------------------------------------- /src/namespaces/cc/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace CcNs { 3 | export type ItemOrFeed = { 4 | license?: string 5 | morePermissions?: string 6 | attributionName?: string 7 | attributionURL?: string 8 | useGuidelines?: string 9 | permits?: string 10 | requires?: string 11 | prohibits?: string 12 | jurisdiction?: string 13 | legalcode?: string 14 | deprecatedOn?: string 15 | } 16 | } 17 | // #endregion reference 18 | -------------------------------------------------------------------------------- /benchmarks/cross-language/parsing-feedparser.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import glob 4 | import feedparser 5 | 6 | def main(): 7 | dir_path, feed_type = sys.argv[1], sys.argv[2] 8 | file_pattern = os.path.join(dir_path, f"*.{feed_type}") 9 | 10 | for file_path in glob.glob(file_pattern): 11 | if os.path.isfile(file_path): 12 | with open(file_path, 'r', encoding='utf-8') as f: 13 | content = f.read() 14 | feedparser.parse(content) 15 | 16 | main() 17 | -------------------------------------------------------------------------------- /src/opml/references/category.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Illustrating the category attribute with cuisine 5 | Sun, 25 Feb 2024 15:45:00 GMT 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/opml/references/category.json: -------------------------------------------------------------------------------- 1 | { 2 | "head": { 3 | "title": "Illustrating the category attribute with cuisine", 4 | "dateCreated": "Sun, 25 Feb 2024 15:45:00 GMT" 5 | }, 6 | "body": { 7 | "outlines": [ 8 | { 9 | "text": "Ramen from Ichiran is the most authentic tonkotsu experience outside Japan.", 10 | "category": "/Food/Cuisine/Japanese,/Tourism/Asia/Japan", 11 | "created": "Sun, 25 Feb 2024 15:42:18 GMT" 12 | } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/namespaces/content/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { generateCdataString, isObject, trimObject } from '../../../common/utils.js' 3 | import type { ContentNs } from '../common/types.js' 4 | 5 | export const generateItem: GenerateUtil = (item) => { 6 | if (!isObject(item)) { 7 | return 8 | } 9 | 10 | const value = { 11 | 'content:encoded': generateCdataString(item?.encoded), 12 | } 13 | 14 | return trimObject(value) 15 | } 16 | -------------------------------------------------------------------------------- /src/namespaces/rdf/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace RdfNs { 3 | export type About = { 4 | about?: string 5 | } 6 | 7 | /** @internal General RDF element kept for potential future use when all RDF data is needed. */ 8 | export type Element = { 9 | about?: string 10 | resource?: string 11 | id?: string 12 | nodeId?: string 13 | parseType?: string 14 | datatype?: string 15 | type?: string 16 | value?: Array 17 | } 18 | } 19 | // #endregion reference 20 | -------------------------------------------------------------------------------- /src/namespaces/thr/common/types.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike } from '../../../common/types.js' 2 | 3 | // #region reference 4 | export namespace ThrNs { 5 | export type InReplyTo = { 6 | ref: string 7 | href?: string 8 | type?: string 9 | source?: string 10 | } 11 | 12 | export type Link = { 13 | count?: number 14 | updated?: TDate 15 | } 16 | 17 | export type Item = { 18 | total?: number 19 | inReplyTos?: Array 20 | } 21 | } 22 | // #endregion reference 23 | -------------------------------------------------------------------------------- /benchmarks/cross-language/parsing-simplepie.php: -------------------------------------------------------------------------------- 1 | set_raw_data($fileData); 18 | $feed->init(); 19 | } 20 | } 21 | 22 | main(); 23 | -------------------------------------------------------------------------------- /src/namespaces/opensearch/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace OpenSearchNs { 3 | export type Query = { 4 | role: string 5 | searchTerms?: string 6 | count?: number 7 | startIndex?: number 8 | startPage?: number 9 | language?: string 10 | inputEncoding?: string 11 | outputEncoding?: string 12 | } 13 | 14 | export type Feed = { 15 | totalResults?: number 16 | startIndex?: number 17 | itemsPerPage?: number 18 | queries?: Array 19 | } 20 | } 21 | // #endregion reference 22 | -------------------------------------------------------------------------------- /benchmarks/cross-language/parsing-feedsmith.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync, readFileSync } from 'node:fs' 2 | import { join } from 'node:path' 3 | import { parseFeed } from '../../src/index' 4 | 5 | const main = () => { 6 | const [, , dirPath, feedType] = process.argv 7 | const fileList = readdirSync(dirPath).filter((f) => f.endsWith(`.${feedType}`)) 8 | 9 | for (const file of fileList) { 10 | const filePath = join(dirPath, file) 11 | const fileData = readFileSync(filePath, 'utf-8') 12 | 13 | parseFeed(fileData) 14 | } 15 | } 16 | 17 | main() 18 | -------------------------------------------------------------------------------- /src/namespaces/wfw/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { generateCdataString, isObject, trimObject } from '../../../common/utils.js' 3 | import type { WfwNs } from '../common/types.js' 4 | 5 | export const generateItem: GenerateUtil = (item) => { 6 | if (!isObject(item)) { 7 | return 8 | } 9 | 10 | const value = { 11 | 'wfw:comment': generateCdataString(item.comment), 12 | 'wfw:commentRss': generateCdataString(item.commentRss), 13 | } 14 | 15 | return trimObject(value) 16 | } 17 | -------------------------------------------------------------------------------- /docs/.vitepress/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | gzip on; 3 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 4 | 5 | listen 80; 6 | server_name _; 7 | index index.html; 8 | 9 | location / { 10 | root /usr/share/nginx/html; 11 | try_files $uri $uri.html $uri/ =404; 12 | error_page 404 /404.html; 13 | error_page 403 /404.html; 14 | 15 | location ~* ^/assets/ { 16 | expires 1y; 17 | add_header Cache-Control "public, immutable"; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /benchmarks/cross-language/go.mod: -------------------------------------------------------------------------------- 1 | module parsing 2 | 3 | go 1.24.0 4 | 5 | require github.com/mmcdole/gofeed v1.3.0 6 | 7 | require ( 8 | github.com/PuerkitoBio/goquery v1.10.3 // indirect 9 | github.com/andybalholm/cascadia v1.3.3 // indirect 10 | github.com/json-iterator/go v1.1.12 // indirect 11 | github.com/mmcdole/goxpp v1.1.1 // indirect 12 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 13 | github.com/modern-go/reflect2 v1.0.2 // indirect 14 | golang.org/x/net v0.46.0 // indirect 15 | golang.org/x/text v0.30.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /src/namespaces/geo/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { generateNumber, isObject, trimObject } from '../../../common/utils.js' 3 | import type { GeoNs } from '../common/types.js' 4 | 5 | export const generateItemOrFeed: GenerateUtil = (geo) => { 6 | if (!isObject(geo)) { 7 | return 8 | } 9 | 10 | const value = { 11 | 'geo:lat': generateNumber(geo.lat), 12 | 'geo:long': generateNumber(geo.long), 13 | 'geo:alt': generateNumber(geo.alt), 14 | } 15 | 16 | return trimObject(value) 17 | } 18 | -------------------------------------------------------------------------------- /src/namespaces/creativecommons/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { generateCdataString, isObject, trimArray, trimObject } from '../../../common/utils.js' 3 | import type { CreativeCommonsNs } from '../common/types.js' 4 | 5 | export const generateItemOrFeed: GenerateUtil = (itemOrFeed) => { 6 | if (!isObject(itemOrFeed)) { 7 | return 8 | } 9 | 10 | const value = { 11 | 'creativeCommons:license': trimArray(itemOrFeed.licenses, generateCdataString), 12 | } 13 | 14 | return trimObject(value) 15 | } 16 | -------------------------------------------------------------------------------- /src/namespaces/trackback/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { generateCdataString, isObject, trimArray, trimObject } from '../../../common/utils.js' 3 | import type { TrackbackNs } from '../common/types.js' 4 | 5 | export const generateItem: GenerateUtil = (item) => { 6 | if (!isObject(item)) { 7 | return 8 | } 9 | 10 | const value = { 11 | 'trackback:ping': generateCdataString(item.ping), 12 | 'trackback:about': trimArray(item.abouts, generateCdataString), 13 | } 14 | 15 | return trimObject(value) 16 | } 17 | -------------------------------------------------------------------------------- /compatibility/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feedsmith/compatibility", 3 | "private": true, 4 | "workspaces": [ 5 | "typescript/modern-esm", 6 | "typescript/modern-cjs", 7 | "typescript/legacy-cjs", 8 | "explicit-modules/esm-package", 9 | "explicit-modules/cjs-package", 10 | "explicit-modules/mixed-package", 11 | "javascript/esm", 12 | "javascript/cjs", 13 | "bundler/esm", 14 | "bundler/cjs" 15 | ], 16 | "devDependencies": { 17 | "@types/bun": "^1.3.1", 18 | "feedsmith": "file:..", 19 | "typescript": "^5.9.3", 20 | "vite": "^7.1.12" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /compatibility/javascript/esm/index.js: -------------------------------------------------------------------------------- 1 | import { generateRssFeed, parseFeed } from 'feedsmith' 2 | 3 | const rssXml = ` 4 | 5 | 6 | Test Feed 7 | https://example.com 8 | Test Description 9 | 10 | ` 11 | 12 | const result = parseFeed(rssXml) 13 | 14 | const feedData = { 15 | title: 'Generated Feed', 16 | link: 'https://example.com', 17 | description: 'Generated description', 18 | items: [], 19 | } 20 | 21 | const generatedRss = generateRssFeed(feedData) 22 | -------------------------------------------------------------------------------- /compatibility/javascript/esm/index.mjs: -------------------------------------------------------------------------------- 1 | import { generateRssFeed, parseFeed } from 'feedsmith' 2 | 3 | const rssXml = ` 4 | 5 | 6 | Test Feed 7 | https://example.com 8 | Test Description 9 | 10 | ` 11 | 12 | const result = parseFeed(rssXml) 13 | 14 | const feedData = { 15 | title: 'Generated Feed', 16 | link: 'https://example.com', 17 | description: 'Generated description', 18 | items: [], 19 | } 20 | 21 | const generatedRss = generateRssFeed(feedData) 22 | -------------------------------------------------------------------------------- /src/feeds/atom/detect/index.ts: -------------------------------------------------------------------------------- 1 | import { isNonEmptyString } from '../../../common/utils.js' 2 | 3 | export const detect = (value: unknown): value is string => { 4 | if (!isNonEmptyString(value)) { 5 | return false 6 | } 7 | 8 | const hasFeedElement = /(?:^|[\s>])<(?:\w+:)?feed[\s>]/im.test(value) 9 | 10 | if (!hasFeedElement) { 11 | return false 12 | } 13 | 14 | const hasAtomNamespace = value.includes('http://www.w3.org/2005/Atom') 15 | const hasAtomElements = /(<(?:\w+:)?(entry|title|link|id|updated|summary)[\s>])/i.test(value) 16 | 17 | return hasAtomNamespace || hasAtomElements 18 | } 19 | -------------------------------------------------------------------------------- /src/feeds/rss/detect/index.ts: -------------------------------------------------------------------------------- 1 | import { isNonEmptyString } from '../../../common/utils.js' 2 | 3 | export const detect = (value: unknown): value is string => { 4 | if (!isNonEmptyString(value)) { 5 | return false 6 | } 7 | 8 | const hasRssElement = /(?:^|[\s>])<(?:\w+:)?rss[\s>]/im.test(value) 9 | 10 | if (!hasRssElement) { 11 | return false 12 | } 13 | 14 | const hasVersionAttribute = /version\s*=\s*["'][^"']*["']/i.test(value) 15 | const hasRssElements = /(<(channel|item|title|link|description|pubDate|guid)[\s>])/i.test(value) 16 | 17 | return hasVersionAttribute || hasRssElements 18 | } 19 | -------------------------------------------------------------------------------- /compatibility/bundler/cjs/index.cjs: -------------------------------------------------------------------------------- 1 | const { generateRssFeed, parseFeed } = require('feedsmith') 2 | 3 | const rssXml = ` 4 | 5 | 6 | Test Feed 7 | https://example.com 8 | Test Description 9 | 10 | ` 11 | 12 | const result = parseFeed(rssXml) 13 | 14 | const feedData = { 15 | title: 'Generated Feed', 16 | link: 'https://example.com', 17 | description: 'Generated description', 18 | items: [], 19 | } 20 | 21 | const generatedRss = generateRssFeed(feedData) 22 | -------------------------------------------------------------------------------- /compatibility/javascript/cjs/index.cjs: -------------------------------------------------------------------------------- 1 | const { generateRssFeed, parseFeed } = require('feedsmith') 2 | 3 | const rssXml = ` 4 | 5 | 6 | Test Feed 7 | https://example.com 8 | Test Description 9 | 10 | ` 11 | 12 | const result = parseFeed(rssXml) 13 | 14 | const feedData = { 15 | title: 'Generated Feed', 16 | link: 'https://example.com', 17 | description: 'Generated description', 18 | items: [], 19 | } 20 | 21 | const generatedRss = generateRssFeed(feedData) 22 | -------------------------------------------------------------------------------- /compatibility/javascript/cjs/index.js: -------------------------------------------------------------------------------- 1 | const { generateRssFeed, parseFeed } = require('feedsmith') 2 | 3 | const rssXml = ` 4 | 5 | 6 | Test Feed 7 | https://example.com 8 | Test Description 9 | 10 | ` 11 | 12 | const result = parseFeed(rssXml) 13 | 14 | const feedData = { 15 | title: 'Generated Feed', 16 | link: 'https://example.com', 17 | description: 'Generated description', 18 | items: [], 19 | } 20 | 21 | const generatedRss = generateRssFeed(feedData) 22 | -------------------------------------------------------------------------------- /src/namespaces/atom/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | parseEntry as parseAtomEntry, 4 | parseFeed as parseAtomFeed, 5 | } from '../../../feeds/atom/parse/utils.js' 6 | import type { AtomNs } from '../common/types.js' 7 | 8 | export const retrieveEntry: ParsePartialUtil> = (value) => { 9 | return parseAtomEntry(value, { prefix: 'atom:', asNamespace: true }) 10 | } 11 | 12 | export const retrieveFeed: ParsePartialUtil> = (value) => { 13 | return parseAtomFeed(value, { prefix: 'atom:', asNamespace: true }) 14 | } 15 | -------------------------------------------------------------------------------- /src/namespaces/content/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseSingularOf, 5 | parseString, 6 | retrieveText, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { ContentNs } from '../common/types.js' 10 | 11 | export const retrieveItem: ParsePartialUtil = (value) => { 12 | if (!isObject(value)) { 13 | return 14 | } 15 | 16 | const item = { 17 | encoded: parseSingularOf(value['content:encoded'], (value) => parseString(retrieveText(value))), 18 | } 19 | 20 | return trimObject(item) 21 | } 22 | -------------------------------------------------------------------------------- /src/namespaces/googleplay/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace GooglePlayNs { 3 | export type Image = { 4 | href: string 5 | } 6 | 7 | export type Item = { 8 | author?: string 9 | description?: string 10 | explicit?: boolean | 'clean' 11 | block?: boolean 12 | image?: Image 13 | } 14 | 15 | export type Feed = { 16 | author?: string 17 | description?: string 18 | explicit?: boolean | 'clean' 19 | block?: boolean 20 | image?: Image 21 | newFeedUrl?: string 22 | email?: string 23 | categories?: Array 24 | } 25 | } 26 | // #endregion reference 27 | -------------------------------------------------------------------------------- /benchmarks/cross-language/parsing-gofeed.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "github.com/mmcdole/gofeed" 8 | ) 9 | 10 | func main() { 11 | dirPath := os.Args[1] 12 | feedType := os.Args[2] 13 | pattern := fmt.Sprintf("*.%s", feedType) 14 | globPattern := filepath.Join(dirPath, pattern) 15 | 16 | matches, _ := filepath.Glob(globPattern) 17 | fp := gofeed.NewParser() 18 | 19 | for _, path := range matches { 20 | info, err := os.Stat(path) 21 | if err != nil || info.IsDir() { 22 | continue 23 | } 24 | 25 | content, _ := os.ReadFile(path) 26 | fp.ParseString(string(content)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/namespaces/blogchannel/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { generateCdataString, isObject, trimObject } from '../../../common/utils.js' 3 | import type { BlogChannelNs } from '../common/types.js' 4 | 5 | export const generateFeed: GenerateUtil = (feed) => { 6 | if (!isObject(feed)) { 7 | return 8 | } 9 | 10 | const value = { 11 | 'blogChannel:blogRoll': generateCdataString(feed.blogRoll), 12 | 'blogChannel:blink': generateCdataString(feed.blink), 13 | 'blogChannel:mySubscriptions': generateCdataString(feed.mySubscriptions), 14 | } 15 | 16 | return trimObject(value) 17 | } 18 | -------------------------------------------------------------------------------- /src/opml/parse/config.ts: -------------------------------------------------------------------------------- 1 | import { XMLParser } from 'fast-xml-parser' 2 | import { parserConfig } from '../../common/config.js' 3 | 4 | export const stopNodes = [ 5 | 'opml.head.title', 6 | 'opml.head.dateCreated', 7 | 'opml.head.dateModified', 8 | 'opml.head.ownerName', 9 | 'opml.head.ownerEmail', 10 | 'opml.head.ownerId', 11 | 'opml.head.docs', 12 | 'opml.head.expansionState', 13 | 'opml.head.vertScrollState', 14 | 'opml.head.windowTop', 15 | 'opml.head.windowLeft', 16 | 'opml.head.windowBottom', 17 | 'opml.head.windowRight', 18 | '*.outline.outline', 19 | ] 20 | 21 | export const parser = new XMLParser({ 22 | ...parserConfig, 23 | stopNodes, 24 | }) 25 | -------------------------------------------------------------------------------- /src/namespaces/admin/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { 3 | generatePlainString, 4 | generateRdfResource, 5 | isObject, 6 | trimObject, 7 | } from '../../../common/utils.js' 8 | import type { AdminNs } from '../common/types.js' 9 | 10 | export const generateFeed: GenerateUtil = (feed) => { 11 | if (!isObject(feed)) { 12 | return 13 | } 14 | 15 | const value = { 16 | 'admin:errorReportsTo': generateRdfResource(feed.errorReportsTo, generatePlainString), 17 | 'admin:generatorAgent': generateRdfResource(feed.generatorAgent, generatePlainString), 18 | } 19 | 20 | return trimObject(value) 21 | } 22 | -------------------------------------------------------------------------------- /src/namespaces/acast/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace AcastNs { 3 | export type Signature = { 4 | key?: string 5 | algorithm?: string 6 | value?: string 7 | } 8 | 9 | export type Network = { 10 | id?: string 11 | slug?: string 12 | value?: string 13 | } 14 | 15 | export type Feed = { 16 | showId?: string 17 | showUrl?: string 18 | signature?: Signature 19 | settings?: string 20 | network?: Network 21 | importedFeed?: string 22 | } 23 | 24 | export type Item = { 25 | episodeId?: string 26 | showId?: string 27 | episodeUrl?: string 28 | settings?: string 29 | } 30 | } 31 | // #endregion reference 32 | -------------------------------------------------------------------------------- /src/namespaces/creativecommons/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseArrayOf, 5 | parseString, 6 | retrieveText, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { CreativeCommonsNs } from '../common/types.js' 10 | 11 | export const retrieveItemOrFeed: ParsePartialUtil = (value) => { 12 | if (!isObject(value)) { 13 | return 14 | } 15 | 16 | const itemOrFeed = { 17 | licenses: parseArrayOf(value['creativecommons:license'], (value) => 18 | parseString(retrieveText(value)), 19 | ), 20 | } 21 | 22 | return trimObject(itemOrFeed) 23 | } 24 | -------------------------------------------------------------------------------- /src/feeds/rdf/detect/index.ts: -------------------------------------------------------------------------------- 1 | import { isNonEmptyString } from '../../../common/utils.js' 2 | 3 | export const detect = (value: unknown): value is string => { 4 | if (!isNonEmptyString(value)) { 5 | return false 6 | } 7 | 8 | const hasRdfElement = /(?:^|[\s>])<(?:\w+:)?rdf[\s>]/im.test(value) 9 | 10 | if (!hasRdfElement) { 11 | return false 12 | } 13 | 14 | const hasRdfNamespace = value.includes('http://www.w3.org/1999/02/22-rdf-syntax-ns#') 15 | const hasRssNamespace = value.includes('http://purl.org/rss/1.0/') 16 | const hasRdfElements = /(<(?:\w+:)?(channel|item|title|link|description)[\s>])/i.test(value) 17 | 18 | return hasRdfNamespace || hasRssNamespace || hasRdfElements 19 | } 20 | -------------------------------------------------------------------------------- /src/feeds/rdf/parse/config.ts: -------------------------------------------------------------------------------- 1 | import { XMLParser } from 'fast-xml-parser' 2 | import { parserConfig } from '../../../common/config.js' 3 | 4 | export const stopNodes = [ 5 | 'rdf:rdf.channel.title', 6 | 'rdf:rdf.channel.link', 7 | 'rdf:rdf.channel.description', 8 | 'rdf:rdf.image.title', 9 | 'rdf:rdf.image.link', 10 | 'rdf:rdf.image.url', 11 | 'rdf:rdf.item.title', 12 | 'rdf:rdf.item.link', 13 | 'rdf:rdf.item.description', 14 | 'rdf:rdf.textinput.title', 15 | 'rdf:rdf.textinput.description', 16 | 'rdf:rdf.textinput.name', 17 | 'rdf:rdf.textinput.link', 18 | // TODO: What about the namespaces? 19 | ] 20 | 21 | export const parser = new XMLParser({ 22 | ...parserConfig, 23 | stopNodes, 24 | }) 25 | -------------------------------------------------------------------------------- /src/feeds/atom/generate/index.ts: -------------------------------------------------------------------------------- 1 | import { locales } from '../../../common/config.js' 2 | import type { DateLike, DeepPartial, XmlGenerateMain } from '../../../common/types.js' 3 | import { generateXml } from '../../../common/utils.js' 4 | import type { Atom } from '../common/types.js' 5 | import { builder } from './config.js' 6 | import { generateFeed } from './utils.js' 7 | 8 | export const generate: XmlGenerateMain, DeepPartial>> = ( 9 | value, 10 | options, 11 | ) => { 12 | const generated = generateFeed(value as Atom.Feed) 13 | 14 | if (!generated) { 15 | throw new Error(locales.invalidInputAtom) 16 | } 17 | 18 | return generateXml(builder, generated, options) 19 | } 20 | -------------------------------------------------------------------------------- /src/namespaces/sy/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike, GenerateUtil } from '../../../common/types.js' 2 | import { 3 | generateCdataString, 4 | generateNumber, 5 | generateRfc3339Date, 6 | isObject, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { SyNs } from '../common/types.js' 10 | 11 | export const generateFeed: GenerateUtil> = (feed) => { 12 | if (!isObject(feed)) { 13 | return 14 | } 15 | 16 | const value = { 17 | 'sy:updatePeriod': generateCdataString(feed.updatePeriod), 18 | 'sy:updateFrequency': generateNumber(feed.updateFrequency), 19 | 'sy:updateBase': generateRfc3339Date(feed.updateBase), 20 | } 21 | 22 | return trimObject(value) 23 | } 24 | -------------------------------------------------------------------------------- /src/opml/parse/index.ts: -------------------------------------------------------------------------------- 1 | import { locales } from '../../common/config.js' 2 | import type { DeepPartial } from '../../common/types.js' 3 | import type { MainOptions, Opml } from '../common/types.js' 4 | import { parser } from './config.js' 5 | import { parseDocument } from './utils.js' 6 | 7 | export const parse = = ReadonlyArray>( 8 | value: string, 9 | options?: MainOptions, 10 | ): DeepPartial> => { 11 | const object = parser.parse(value) 12 | const parsed = parseDocument(object, options) 13 | 14 | if (!parsed) { 15 | throw new Error(locales.invalidOpmlFormat) 16 | } 17 | 18 | return parsed as DeepPartial> 19 | } 20 | -------------------------------------------------------------------------------- /src/namespaces/wfw/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseSingularOf, 5 | parseString, 6 | retrieveText, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { WfwNs } from '../common/types.js' 10 | 11 | export const retrieveItem: ParsePartialUtil = (value) => { 12 | if (!isObject(value)) { 13 | return 14 | } 15 | 16 | const item = { 17 | comment: parseSingularOf(value['wfw:comment'], (value) => parseString(retrieveText(value))), 18 | commentRss: parseSingularOf(value['wfw:commentrss'], (value) => 19 | parseString(retrieveText(value)), 20 | ), 21 | } 22 | 23 | return trimObject(item) 24 | } 25 | -------------------------------------------------------------------------------- /src/namespaces/georss/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace GeoRssNs { 3 | export type Point = { 4 | lat: number 5 | lng: number 6 | } 7 | 8 | export type Line = { 9 | points: Array 10 | } 11 | 12 | export type Polygon = { 13 | points: Array 14 | } 15 | 16 | export type Box = { 17 | lowerCorner: Point 18 | upperCorner: Point 19 | } 20 | 21 | export type ItemOrFeed = { 22 | point?: Point 23 | line?: Line 24 | polygon?: Polygon 25 | box?: Box 26 | featureTypeTag?: string 27 | relationshipTag?: string 28 | featureName?: string 29 | elev?: number 30 | floor?: number 31 | radius?: number 32 | } 33 | } 34 | // #endregion reference 35 | -------------------------------------------------------------------------------- /src/namespaces/atom/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike, GenerateUtil } from '../../../common/types.js' 2 | import type { Atom } from '../../../feeds/atom/common/types.js' 3 | import { 4 | generateEntry as generateAtomEntry, 5 | generateFeed as generateAtomFeed, 6 | } from '../../../feeds/atom/generate/utils.js' 7 | import type { AtomNs } from '../common/types.js' 8 | 9 | export const generateEntry: GenerateUtil> = (entry) => { 10 | return generateAtomEntry(entry as Atom.Entry, { prefix: 'atom:', asNamespace: true }) 11 | } 12 | 13 | export const generateFeed: GenerateUtil> = (feed) => { 14 | return generateAtomFeed(feed as Atom.Feed, { prefix: 'atom:', asNamespace: true })?.feed 15 | } 16 | -------------------------------------------------------------------------------- /src/feeds/rss/generate/index.ts: -------------------------------------------------------------------------------- 1 | import { locales } from '../../../common/config.js' 2 | import type { DateLike, DeepPartial, XmlGenerateMain } from '../../../common/types.js' 3 | import { generateXml } from '../../../common/utils.js' 4 | import type { Rss } from '../common/types.js' 5 | import { builder } from './config.js' 6 | import { generateFeed } from './utils.js' 7 | 8 | export const generate: XmlGenerateMain< 9 | Rss.Feed, 10 | DeepPartial> 11 | > = (value, options) => { 12 | const generated = generateFeed(value as Rss.Feed) 13 | 14 | if (!generated) { 15 | throw new Error(locales.invalidInputRss) 16 | } 17 | 18 | return generateXml(builder, generated, options) 19 | } 20 | -------------------------------------------------------------------------------- /src/namespaces/slash/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { 3 | generateCdataString, 4 | generateCsvOf, 5 | generateNumber, 6 | isObject, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { SlashNs } from '../common/types.js' 10 | 11 | export const generateItem: GenerateUtil = (item) => { 12 | if (!isObject(item)) { 13 | return 14 | } 15 | 16 | const value = { 17 | 'slash:section': generateCdataString(item.section), 18 | 'slash:department': generateCdataString(item.department), 19 | 'slash:comments': generateNumber(item.comments), 20 | 'slash:hit_parade': generateCsvOf(item.hitParade, generateNumber), 21 | } 22 | 23 | return trimObject(value) 24 | } 25 | -------------------------------------------------------------------------------- /src/namespaces/spotify/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace SpotifyNs { 3 | export type Limit = { 4 | recentCount?: number 5 | } 6 | 7 | export type Partner = { 8 | id: string 9 | } 10 | 11 | export type Sandbox = { 12 | enabled: boolean 13 | } 14 | 15 | export type FeedAccess = { 16 | partner?: Partner 17 | sandbox?: Sandbox 18 | } 19 | 20 | export type Entitlement = { 21 | name: string 22 | } 23 | 24 | export type ItemAccess = { 25 | entitlement?: Entitlement 26 | } 27 | 28 | export type Feed = { 29 | limit?: Limit 30 | countryOfOrigin?: string 31 | access?: FeedAccess 32 | } 33 | 34 | export type Item = { 35 | access?: ItemAccess 36 | } 37 | } 38 | // #endregion reference 39 | -------------------------------------------------------------------------------- /src/namespaces/feedpress/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { generateCdataString, isObject, trimObject } from '../../../common/utils.js' 3 | import type { FeedPressNs } from '../common/types.js' 4 | 5 | export const generateFeed: GenerateUtil = (feed) => { 6 | if (!isObject(feed)) { 7 | return 8 | } 9 | 10 | const value = { 11 | 'feedpress:link': generateCdataString(feed.link), 12 | 'feedpress:newsletterId': generateCdataString(feed.newsletterId), 13 | 'feedpress:locale': generateCdataString(feed.locale), 14 | 'feedpress:podcastId': generateCdataString(feed.podcastId), 15 | 'feedpress:cssFile': generateCdataString(feed.cssFile), 16 | } 17 | 18 | return trimObject(value) 19 | } 20 | -------------------------------------------------------------------------------- /src/feeds/json/parse/index.ts: -------------------------------------------------------------------------------- 1 | import { locales } from '../../../common/config.js' 2 | import type { DeepPartial, ParseOptions } from '../../../common/types.js' 3 | import { parseJsonObject } from '../../../common/utils.js' 4 | import { detectJsonFeed } from '../../../index.js' 5 | import type { Json } from '../common/types.js' 6 | import { parseFeed } from './utils.js' 7 | 8 | export const parse = (value: unknown, options?: ParseOptions): DeepPartial> => { 9 | const json = parseJsonObject(value) 10 | 11 | if (!detectJsonFeed(json)) { 12 | throw new Error(locales.invalidFeedFormat) 13 | } 14 | 15 | const parsed = parseFeed(json, options) 16 | 17 | if (!parsed) { 18 | throw new Error(locales.invalidFeedFormat) 19 | } 20 | 21 | return parsed 22 | } 23 | -------------------------------------------------------------------------------- /src/namespaces/geo/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseNumber, 5 | parseSingularOf, 6 | retrieveText, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { GeoNs } from '../common/types.js' 10 | 11 | export const retrieveItemOrFeed: ParsePartialUtil = (value) => { 12 | if (!isObject(value)) { 13 | return 14 | } 15 | 16 | const geo = { 17 | lat: parseSingularOf(value['geo:lat'], (value) => parseNumber(retrieveText(value))), 18 | long: parseSingularOf(value['geo:long'], (value) => parseNumber(retrieveText(value))), 19 | alt: parseSingularOf(value['geo:alt'], (value) => parseNumber(retrieveText(value))), 20 | } 21 | 22 | return trimObject(geo) 23 | } 24 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/esm-package/README.md: -------------------------------------------------------------------------------- 1 | # Explicit Modules - ESM Package 2 | 3 | This test validates that feedsmith works correctly with explicit TypeScript module extensions in an ESM package context. 4 | 5 | ## Files 6 | 7 | - **test.mts** - Explicitly ESM file (natural in ESM package) 8 | - Uses `import` syntax 9 | - Tests feedsmith's "import" export condition 10 | - Should load `index.d.ts` types 11 | 12 | - **test.cts** - Explicitly CommonJS file (overrides package type) 13 | - Uses `import` syntax (compiled to require) 14 | - Tests feedsmith's "require" export condition 15 | - Should load `index.d.cts` types 16 | 17 | ## Use Case 18 | 19 | This pattern is used in: 20 | - Projects with mixed ESM/CJS files 21 | - Gradual ESM migration scenarios 22 | - Packages that need both module types available 23 | -------------------------------------------------------------------------------- /src/feeds/json/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike, GenerateUtil } from '../../../common/types.js' 2 | import { generateRfc3339Date, trimArray, trimObject } from '../../../common/utils.js' 3 | import type { Json } from '../common/types.js' 4 | 5 | export const generateItem: GenerateUtil> = (item) => { 6 | const value = { 7 | ...item, 8 | date_published: generateRfc3339Date(item?.date_published), 9 | date_modified: generateRfc3339Date(item?.date_modified), 10 | } 11 | 12 | return trimObject(value) 13 | } 14 | 15 | export const generateFeed: GenerateUtil> = (feed) => { 16 | const value = { 17 | version: 'https://jsonfeed.org/version/1.1', 18 | ...feed, 19 | items: trimArray(feed?.items, generateItem), 20 | } 21 | 22 | return trimObject(value) 23 | } 24 | -------------------------------------------------------------------------------- /src/namespaces/admin/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseSingularOf, 5 | parseString, 6 | retrieveRdfResourceOrText, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { AdminNs } from '../common/types.js' 10 | 11 | export const retrieveFeed: ParsePartialUtil = (value) => { 12 | if (!isObject(value)) { 13 | return 14 | } 15 | 16 | const feed = { 17 | errorReportsTo: parseSingularOf(value['admin:errorreportsto'], (value) => 18 | retrieveRdfResourceOrText(value, parseString), 19 | ), 20 | generatorAgent: parseSingularOf(value['admin:generatoragent'], (value) => 21 | retrieveRdfResourceOrText(value, parseString), 22 | ), 23 | } 24 | 25 | return trimObject(feed) 26 | } 27 | -------------------------------------------------------------------------------- /src/namespaces/trackback/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseArrayOf, 5 | parseSingularOf, 6 | parseString, 7 | retrieveRdfResourceOrText, 8 | trimObject, 9 | } from '../../../common/utils.js' 10 | import type { TrackbackNs } from '../common/types.js' 11 | 12 | export const retrieveItem: ParsePartialUtil = (value) => { 13 | if (!isObject(value)) { 14 | return 15 | } 16 | 17 | const item = { 18 | ping: parseSingularOf(value['trackback:ping'], (value) => 19 | retrieveRdfResourceOrText(value, parseString), 20 | ), 21 | abouts: parseArrayOf(value['trackback:about'], (value) => 22 | retrieveRdfResourceOrText(value, parseString), 23 | ), 24 | } 25 | 26 | return trimObject(item) 27 | } 28 | -------------------------------------------------------------------------------- /src/feeds/json/detect/index.ts: -------------------------------------------------------------------------------- 1 | import { isNonEmptyString, isObject, parseJsonObject } from '../../../common/utils.js' 2 | import { createCaseInsensitiveGetter } from '../parse/utils.js' 3 | 4 | export const detect = (value: unknown): value is object => { 5 | const json = parseJsonObject(value) 6 | 7 | if (!isObject(json)) { 8 | return false 9 | } 10 | 11 | const get = createCaseInsensitiveGetter(json) 12 | const version = get('version') 13 | 14 | if (isNonEmptyString(version)) { 15 | return version.includes('jsonfeed.org/version/') 16 | } 17 | 18 | const hasTitle = isNonEmptyString(get('title')) 19 | const hasItems = Array.isArray(get('items')) 20 | const hasJsonFeedProps = !!(get('home_page_url') || get('feed_url') || get('authors')) 21 | 22 | return hasTitle && (hasItems || hasJsonFeedProps) 23 | } 24 | -------------------------------------------------------------------------------- /src/feeds/rss/references/rss-090.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sample Feed", 3 | "link": "http://example.org/", 4 | "description": "For documentation only", 5 | "image": { 6 | "url": "http://example.org/banner.png", 7 | "title": "Example banner", 8 | "link": "http://example.org/" 9 | }, 10 | "items": [ 11 | { 12 | "title": "First item title", 13 | "link": "http://example.org/item/1", 14 | "description": "Watch out for nasty tricks" 15 | }, 16 | { 17 | "title": "Second item title", 18 | "link": "http://example.org/item/2", 19 | "description": "Watch out for nasty tricks" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/namespaces/pingback/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { generateCdataString, isObject, trimObject } from '../../../common/utils.js' 3 | import type { PingbackNs } from '../common/types.js' 4 | 5 | export const generateItem: GenerateUtil = (item) => { 6 | if (!isObject(item)) { 7 | return 8 | } 9 | 10 | const value = { 11 | 'pingback:server': generateCdataString(item.server), 12 | 'pingback:target': generateCdataString(item.target), 13 | } 14 | 15 | return trimObject(value) 16 | } 17 | 18 | export const generateFeed: GenerateUtil = (feed) => { 19 | if (!isObject(feed)) { 20 | return 21 | } 22 | 23 | const value = { 24 | 'pingback:to': generateCdataString(feed.to), 25 | } 26 | 27 | return trimObject(value) 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/compatibility.yml: -------------------------------------------------------------------------------- 1 | name: Compatibility 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_call: 9 | 10 | jobs: 11 | compatibility: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Bun 18 | uses: oven-sh/setup-bun@v2 19 | 20 | - name: Install dependencies 21 | run: bun install --frozen-lockfile --ignore-scripts 22 | 23 | - name: Build package 24 | run: bun run build 25 | 26 | - name: Install compatibility test dependencies 27 | working-directory: compatibility 28 | run: bun install --frozen-lockfile --ignore-scripts 29 | 30 | - name: Run compatibility tests 31 | working-directory: compatibility 32 | run: ./test.sh 33 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/cjs-package/README.md: -------------------------------------------------------------------------------- 1 | # Explicit Modules - CJS Package 2 | 3 | This test validates that feedsmith works correctly with explicit TypeScript module extensions in a CommonJS package context. 4 | 5 | ## Files 6 | 7 | - **test.cts** - Explicitly CommonJS file (natural in CJS package) 8 | - Uses `import` syntax (compiled to require) 9 | - Tests feedsmith's "require" export condition 10 | - Should load `index.d.cts` types 11 | 12 | - **test.mts** - Explicitly ESM file (overrides package type) 13 | - Uses `import` syntax 14 | - Tests feedsmith's "import" export condition 15 | - Should load `index.d.ts` types 16 | 17 | ## Use Case 18 | 19 | This pattern is used in: 20 | - Legacy CJS projects adopting ESM gradually 21 | - Projects that need to support both module types 22 | - Build tool configuration files that need specific module types 23 | -------------------------------------------------------------------------------- /benchmarks/javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "dependencies": { 5 | "@extractus/feed-extractor": "^7.1.7", 6 | "@gaphub/feed": "^5.2.4", 7 | "@rowanmanning/feed-parser": "^2.1.1", 8 | "@ulisesgascon/rss-feed-parser": "^1.0.1", 9 | "benchmark": "^2.1.4", 10 | "feedme": "^2.0.2", 11 | "feedparser": "^2.2.10", 12 | "podcast-feed-parser": "^1.0.4", 13 | "rss-parser": "^3.13.0", 14 | "tinybench": "^6.0.0", 15 | "node-opml-parser": "^1.0.0", 16 | "opml": "^0.5.7", 17 | "opml-generator": "^1.1.1", 18 | "opml-to-json": "^1.0.1", 19 | "opmlparser": "^0.8.0" 20 | }, 21 | "devDependencies": { 22 | "@types/benchmark": "^2.1.5", 23 | "@types/bun": "^1.3.3", 24 | "@types/feedme": "^2.0.4", 25 | "@types/feedparser": "^2.2.8", 26 | "typescript": "^5.9.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/opml/generate/index.ts: -------------------------------------------------------------------------------- 1 | import { locales } from '../../common/config.js' 2 | import type { DateLike, DeepPartial, XmlGenerateOptions } from '../../common/types.js' 3 | import { generateXml } from '../../common/utils.js' 4 | import type { MainOptions, Opml } from '../common/types.js' 5 | import { builder } from './config.js' 6 | import { generateDocument } from './utils.js' 7 | 8 | export const generate = = [], F extends boolean = false>( 9 | value: F extends true ? DeepPartial> : Opml.Document, 10 | options?: XmlGenerateOptions, F>, 11 | ): string => { 12 | const generated = generateDocument(value as Opml.Document, options) 13 | 14 | if (!generated) { 15 | throw new Error(locales.invalidInputOpml) 16 | } 17 | 18 | return generateXml(builder, generated, options) 19 | } 20 | -------------------------------------------------------------------------------- /src/namespaces/yt/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { generateCdataString, isObject, trimObject } from '../../../common/utils.js' 3 | import type { YtNs } from '../common/types.js' 4 | 5 | export const generateItem: GenerateUtil = (item) => { 6 | if (!isObject(item)) { 7 | return 8 | } 9 | 10 | const value = { 11 | 'yt:videoId': generateCdataString(item.videoId), 12 | 'yt:channelId': generateCdataString(item.channelId), 13 | } 14 | 15 | return trimObject(value) 16 | } 17 | 18 | export const generateFeed: GenerateUtil = (feed) => { 19 | if (!isObject(feed)) { 20 | return 21 | } 22 | 23 | const value = { 24 | 'yt:channelId': generateCdataString(feed.channelId), 25 | 'yt:playlistId': generateCdataString(feed.playlistId), 26 | } 27 | 28 | return trimObject(value) 29 | } 30 | -------------------------------------------------------------------------------- /compatibility/bundler/esm/index.ts: -------------------------------------------------------------------------------- 1 | import type { RssFeed } from 'feedsmith' 2 | import { generateRssFeed, parseFeed } from 'feedsmith' 3 | import type { Rss } from 'feedsmith/types' 4 | 5 | const rssXml = ` 6 | 7 | 8 | Test Feed 9 | https://example.com 10 | Test Description 11 | 12 | ` 13 | 14 | const result = parseFeed(rssXml) 15 | 16 | const feedData: Rss.Feed = { 17 | title: 'Generated Feed', 18 | link: 'https://example.com', 19 | description: 'Generated description', 20 | items: [], 21 | } 22 | 23 | const legacyFeedData: RssFeed = { 24 | title: 'Legacy Type', 25 | link: 'https://example.com', 26 | description: 'Legacy description', 27 | items: [], 28 | } 29 | 30 | const generatedRss = generateRssFeed(feedData) 31 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-cjs/index.ts: -------------------------------------------------------------------------------- 1 | import type { RssFeed } from 'feedsmith' 2 | import { generateRssFeed, parseFeed } from 'feedsmith' 3 | import type { Rss } from 'feedsmith/types' 4 | 5 | const rssXml = ` 6 | 7 | 8 | Test Feed 9 | https://example.com 10 | Test Description 11 | 12 | ` 13 | 14 | const result = parseFeed(rssXml) 15 | 16 | const feedData: Rss.Feed = { 17 | title: 'Generated Feed', 18 | link: 'https://example.com', 19 | description: 'Generated description', 20 | items: [], 21 | } 22 | 23 | const legacyFeedData: RssFeed = { 24 | title: 'Legacy Type', 25 | link: 'https://example.com', 26 | description: 'Legacy description', 27 | items: [], 28 | } 29 | 30 | const generatedRss = generateRssFeed(feedData) 31 | -------------------------------------------------------------------------------- /compatibility/typescript/modern-esm/index.ts: -------------------------------------------------------------------------------- 1 | import type { RssFeed } from 'feedsmith' 2 | import { generateRssFeed, parseFeed } from 'feedsmith' 3 | import type { Rss } from 'feedsmith/types' 4 | 5 | const rssXml = ` 6 | 7 | 8 | Test Feed 9 | https://example.com 10 | Test Description 11 | 12 | ` 13 | 14 | const result = parseFeed(rssXml) 15 | 16 | const feedData: Rss.Feed = { 17 | title: 'Generated Feed', 18 | link: 'https://example.com', 19 | description: 'Generated description', 20 | items: [], 21 | } 22 | 23 | const legacyFeedData: RssFeed = { 24 | title: 'Legacy Type', 25 | link: 'https://example.com', 26 | description: 'Legacy description', 27 | items: [], 28 | } 29 | 30 | const generatedRss = generateRssFeed(feedData) 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_call: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Bun 18 | uses: oven-sh/setup-bun@v2 19 | 20 | - name: Install dependencies 21 | run: bun install --frozen-lockfile --ignore-scripts 22 | 23 | - name: Run tests with coverage 24 | run: bun test --coverage --coverage-reporter=lcov 25 | 26 | - name: Upload coverage to Codecov 27 | if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') 28 | uses: codecov/codecov-action@v5 29 | with: 30 | token: ${{ secrets.CODECOV_TOKEN }} 31 | files: ./coverage/lcov.info 32 | -------------------------------------------------------------------------------- /src/namespaces/app/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike, GenerateUtil } from '../../../common/types.js' 2 | import { 3 | generateRfc3339Date, 4 | generateYesNoBoolean, 5 | isObject, 6 | trimObject, 7 | } from '../../../common/utils.js' 8 | import type { AppNs } from '../common/types.js' 9 | 10 | export const generateControl: GenerateUtil = (control) => { 11 | if (!isObject(control)) { 12 | return 13 | } 14 | 15 | const value = { 16 | 'app:draft': generateYesNoBoolean(control.draft), 17 | } 18 | 19 | return trimObject(value) 20 | } 21 | 22 | export const generateEntry: GenerateUtil> = (entry) => { 23 | if (!isObject(entry)) { 24 | return 25 | } 26 | 27 | const value = { 28 | 'app:edited': generateRfc3339Date(entry.edited), 29 | 'app:control': generateControl(entry.control), 30 | } 31 | 32 | return trimObject(value) 33 | } 34 | -------------------------------------------------------------------------------- /src/namespaces/blogchannel/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseSingularOf, 5 | parseString, 6 | retrieveText, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { BlogChannelNs } from '../common/types.js' 10 | 11 | export const retrieveFeed: ParsePartialUtil = (value) => { 12 | if (!isObject(value)) { 13 | return 14 | } 15 | 16 | const feed = { 17 | blogRoll: parseSingularOf(value['blogchannel:blogroll'], (value) => 18 | parseString(retrieveText(value)), 19 | ), 20 | blink: parseSingularOf(value['blogchannel:blink'], (value) => parseString(retrieveText(value))), 21 | mySubscriptions: parseSingularOf(value['blogchannel:mysubscriptions'], (value) => 22 | parseString(retrieveText(value)), 23 | ), 24 | } 25 | 26 | return trimObject(feed) 27 | } 28 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/cjs-package/index.cts: -------------------------------------------------------------------------------- 1 | import type { RssFeed } from 'feedsmith' 2 | import { generateRssFeed, parseFeed } from 'feedsmith' 3 | import type { Rss } from 'feedsmith/types' 4 | 5 | const rssXml = ` 6 | 7 | 8 | Test Feed 9 | https://example.com 10 | Test Description 11 | 12 | ` 13 | 14 | const result = parseFeed(rssXml) 15 | 16 | const feedData: Rss.Feed = { 17 | title: 'Generated Feed', 18 | link: 'https://example.com', 19 | description: 'Generated description', 20 | items: [], 21 | } 22 | 23 | const legacyFeedData: RssFeed = { 24 | title: 'Legacy Type', 25 | link: 'https://example.com', 26 | description: 'Legacy description', 27 | items: [], 28 | } 29 | 30 | const generatedRss = generateRssFeed(feedData) 31 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/cjs-package/index.mts: -------------------------------------------------------------------------------- 1 | import type { RssFeed } from 'feedsmith' 2 | import { generateRssFeed, parseFeed } from 'feedsmith' 3 | import type { Rss } from 'feedsmith/types' 4 | 5 | const rssXml = ` 6 | 7 | 8 | Test Feed 9 | https://example.com 10 | Test Description 11 | 12 | ` 13 | 14 | const result = parseFeed(rssXml) 15 | 16 | const feedData: Rss.Feed = { 17 | title: 'Generated Feed', 18 | link: 'https://example.com', 19 | description: 'Generated description', 20 | items: [], 21 | } 22 | 23 | const legacyFeedData: RssFeed = { 24 | title: 'Legacy Type', 25 | link: 'https://example.com', 26 | description: 'Legacy description', 27 | items: [], 28 | } 29 | 30 | const generatedRss = generateRssFeed(feedData) 31 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/esm-package/index.cts: -------------------------------------------------------------------------------- 1 | import type { RssFeed } from 'feedsmith' 2 | import { generateRssFeed, parseFeed } from 'feedsmith' 3 | import type { Rss } from 'feedsmith/types' 4 | 5 | const rssXml = ` 6 | 7 | 8 | Test Feed 9 | https://example.com 10 | Test Description 11 | 12 | ` 13 | 14 | const result = parseFeed(rssXml) 15 | 16 | const feedData: Rss.Feed = { 17 | title: 'Generated Feed', 18 | link: 'https://example.com', 19 | description: 'Generated description', 20 | items: [], 21 | } 22 | 23 | const legacyFeedData: RssFeed = { 24 | title: 'Legacy Type', 25 | link: 'https://example.com', 26 | description: 'Legacy description', 27 | items: [], 28 | } 29 | 30 | const generatedRss = generateRssFeed(feedData) 31 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/esm-package/index.mts: -------------------------------------------------------------------------------- 1 | import type { RssFeed } from 'feedsmith' 2 | import { generateRssFeed, parseFeed } from 'feedsmith' 3 | import type { Rss } from 'feedsmith/types' 4 | 5 | const rssXml = ` 6 | 7 | 8 | Test Feed 9 | https://example.com 10 | Test Description 11 | 12 | ` 13 | 14 | const result = parseFeed(rssXml) 15 | 16 | const feedData: Rss.Feed = { 17 | title: 'Generated Feed', 18 | link: 'https://example.com', 19 | description: 'Generated description', 20 | items: [], 21 | } 22 | 23 | const legacyFeedData: RssFeed = { 24 | title: 'Legacy Type', 25 | link: 'https://example.com', 26 | description: 'Legacy description', 27 | items: [], 28 | } 29 | 30 | const generatedRss = generateRssFeed(feedData) 31 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/mixed-package/index.cts: -------------------------------------------------------------------------------- 1 | import type { RssFeed } from 'feedsmith' 2 | import { generateRssFeed, parseFeed } from 'feedsmith' 3 | import type { Rss } from 'feedsmith/types' 4 | 5 | const rssXml = ` 6 | 7 | 8 | Test Feed 9 | https://example.com 10 | Test Description 11 | 12 | ` 13 | 14 | const result = parseFeed(rssXml) 15 | 16 | const feedData: Rss.Feed = { 17 | title: 'Generated Feed', 18 | link: 'https://example.com', 19 | description: 'Generated description', 20 | items: [], 21 | } 22 | 23 | const legacyFeedData: RssFeed = { 24 | title: 'Legacy Type', 25 | link: 'https://example.com', 26 | description: 'Legacy description', 27 | items: [], 28 | } 29 | 30 | const generatedRss = generateRssFeed(feedData) 31 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/mixed-package/index.mts: -------------------------------------------------------------------------------- 1 | import type { RssFeed } from 'feedsmith' 2 | import { generateRssFeed, parseFeed } from 'feedsmith' 3 | import type { Rss } from 'feedsmith/types' 4 | 5 | const rssXml = ` 6 | 7 | 8 | Test Feed 9 | https://example.com 10 | Test Description 11 | 12 | ` 13 | 14 | const result = parseFeed(rssXml) 15 | 16 | const feedData: Rss.Feed = { 17 | title: 'Generated Feed', 18 | link: 'https://example.com', 19 | description: 'Generated description', 20 | items: [], 21 | } 22 | 23 | const legacyFeedData: RssFeed = { 24 | title: 'Legacy Type', 25 | link: 'https://example.com', 26 | description: 'Legacy description', 27 | items: [], 28 | } 29 | 30 | const generatedRss = generateRssFeed(feedData) 31 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/mixed-package/index.ts: -------------------------------------------------------------------------------- 1 | import type { RssFeed } from 'feedsmith' 2 | import { generateRssFeed, parseFeed } from 'feedsmith' 3 | import type { Rss } from 'feedsmith/types' 4 | 5 | const rssXml = ` 6 | 7 | 8 | Test Feed 9 | https://example.com 10 | Test Description 11 | 12 | ` 13 | 14 | const result = parseFeed(rssXml) 15 | 16 | const feedData: Rss.Feed = { 17 | title: 'Generated Feed', 18 | link: 'https://example.com', 19 | description: 'Generated description', 20 | items: [], 21 | } 22 | 23 | const legacyFeedData: RssFeed = { 24 | title: 'Legacy Type', 25 | link: 'https://example.com', 26 | description: 'Legacy description', 27 | items: [], 28 | } 29 | 30 | const generatedRss = generateRssFeed(feedData) 31 | -------------------------------------------------------------------------------- /compatibility/typescript/legacy-cjs/index.ts: -------------------------------------------------------------------------------- 1 | const { generateRssFeed, parseFeed } = require('feedsmith') 2 | 3 | import type { RssFeed } from 'feedsmith' 4 | import type { Rss } from 'feedsmith/types' 5 | 6 | const rssXml = ` 7 | 8 | 9 | Test Feed 10 | https://example.com 11 | Test Description 12 | 13 | ` 14 | 15 | const result = parseFeed(rssXml) 16 | 17 | const feedData: Rss.Feed = { 18 | title: 'Generated Feed', 19 | link: 'https://example.com', 20 | description: 'Generated description', 21 | items: [], 22 | } 23 | 24 | const legacyFeedData: RssFeed = { 25 | title: 'Legacy Type', 26 | link: 'https://example.com', 27 | description: 'Legacy description', 28 | items: [], 29 | } 30 | 31 | const generatedRss = generateRssFeed(feedData) 32 | -------------------------------------------------------------------------------- /src/namespaces/source/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace SourceNs { 3 | export type Account = { 4 | service: string 5 | value?: string 6 | } 7 | 8 | export type Likes = { 9 | server: string 10 | } 11 | 12 | export type Archive = { 13 | url: string 14 | startDay: string 15 | endDay?: string 16 | filename?: string 17 | } 18 | 19 | export type SubscriptionList = { 20 | url: string 21 | value?: string 22 | } 23 | 24 | export type Feed = { 25 | accounts?: Array 26 | likes?: Likes 27 | archive?: Archive 28 | subscriptionLists?: Array 29 | cloud?: string 30 | blogroll?: string 31 | self?: string 32 | } 33 | 34 | export type Item = { 35 | markdown?: string 36 | outlines?: Array 37 | localTime?: string 38 | linkFull?: string 39 | } 40 | } 41 | // #endregion reference 42 | -------------------------------------------------------------------------------- /src/namespaces/sy/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseDate, 5 | parseNumber, 6 | parseSingularOf, 7 | parseString, 8 | retrieveText, 9 | trimObject, 10 | } from '../../../common/utils.js' 11 | import type { SyNs } from '../common/types.js' 12 | 13 | export const retrieveFeed: ParsePartialUtil> = (value) => { 14 | if (!isObject(value)) { 15 | return 16 | } 17 | 18 | const feed = { 19 | updatePeriod: parseSingularOf(value['sy:updateperiod'], (value) => 20 | parseString(retrieveText(value)), 21 | ), 22 | updateFrequency: parseSingularOf(value['sy:updatefrequency'], (value) => 23 | parseNumber(retrieveText(value)), 24 | ), 25 | updateBase: parseSingularOf(value['sy:updatebase'], (value) => parseDate(retrieveText(value))), 26 | } 27 | 28 | return trimObject(feed) 29 | } 30 | -------------------------------------------------------------------------------- /compatibility/explicit-modules/mixed-package/README.md: -------------------------------------------------------------------------------- 1 | # Explicit Modules - Mixed Package 2 | 3 | This test validates that feedsmith works correctly in projects that use both .mts and .cts files simultaneously (dual-module pattern). 4 | 5 | ## Files 6 | 7 | - **esm-module.mts** - ESM version using feedsmith 8 | - **cjs-module.cts** - CJS version using feedsmith 9 | - **shared.ts** - Shared utilities (follows package.json type) 10 | 11 | ## Pattern 12 | 13 | This simulates packages that publish both ESM and CJS builds from the same source: 14 | 15 | ``` 16 | src/ 17 | esm-module.mts → dist/esm-module.mjs 18 | cjs-module.cts → dist/cjs-module.cjs 19 | shared.ts → dist/shared.js (follows package type) 20 | ``` 21 | 22 | ## Use Cases 23 | 24 | - Dual-module package authoring 25 | - Libraries that need to support both module systems 26 | - Gradual migration scenarios 27 | - Testing both export conditions simultaneously 28 | -------------------------------------------------------------------------------- /docs/generating/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | prev: Parsing › Examples 3 | next: Generating › Styling 4 | --- 5 | 6 | # Generating Feeds 7 | 8 | Create RSS, Atom, JSON Feed, and OPML files with full namespace support. 9 | 10 | ## Overview 11 | 12 | Feed generation is straightforward - provide the feed data and get back a properly formatted string: 13 | 14 | ```typescript 15 | import { 16 | generateRssFeed, 17 | generateAtomFeed, 18 | generateJsonFeed, 19 | generateOpml 20 | } from 'feedsmith' 21 | 22 | // Generate different formats 23 | const rss = generateRssFeed({ /* feed data */ }) 24 | const atom = generateAtomFeed({ /* feed data */ }) 25 | const json = generateJsonFeed({ /* feed data */ }) 26 | const opml = generateOpml({ /* opml data */ }) 27 | ``` 28 | 29 | ## Returned Values 30 | 31 | The generation functions return properly formatted feeds as XML or JSON. 32 | 33 | For detailed examples of input and output for each feed format, see the [Generating Examples](/generating/examples) page. 34 | -------------------------------------------------------------------------------- /docs/parsing/detecting.md: -------------------------------------------------------------------------------- 1 | --- 2 | prev: Parsing › Dates 3 | next: Parsing › Examples 4 | --- 5 | 6 | # Format Detection 7 | 8 | You can quickly detect the feed format without parsing it. 9 | 10 | ```typescript 11 | import { 12 | detectRssFeed, 13 | detectAtomFeed, 14 | detectRdfFeed, 15 | detectJsonFeed 16 | } from 'feedsmith' 17 | 18 | if (detectRssFeed(content)) { 19 | console.log('This is an RSS feed') 20 | } 21 | 22 | if (detectAtomFeed(content)) { 23 | console.log('This is an Atom feed') 24 | } 25 | 26 | if (detectRdfFeed(content)) { 27 | console.log('This is an RDF feed') 28 | } 29 | 30 | if (detectJsonFeed(content)) { 31 | console.log('This is a JSON feed') 32 | } 33 | ``` 34 | 35 | > [!WARNING] 36 | > Detect functions are designed to quickly identify the feed format by looking for its signature, such as the the root tag, version attribute or feed elements. They're accurate in most cases, but to be 100% certain that the feed is valid, parsing it is a more reliable approach. 37 | -------------------------------------------------------------------------------- /docs/reference/namespaces/rdf.md: -------------------------------------------------------------------------------- 1 | # RDF Reference 2 | 3 | Built-in namespace for RDF feeds exposing standard RDF metadata. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://www.w3.org/1999/02/22-rdf-syntax-ns#
SpecificationRDF/XML Syntax Specification
Prefix<rdf:*>
Available inRDF
Propertyrdf
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/rdf/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 37 | -------------------------------------------------------------------------------- /docs/reference/namespaces/app.md: -------------------------------------------------------------------------------- 1 | # Atom Publishing Protocol Reference 2 | 3 | Extends Atom feeds with elements for content management workflows. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://www.w3.org/2007/app
SpecificationThe Atom Publishing Protocol (RFC 5023)
Prefix<app:*>
Available inAtom
Propertyapp
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/app/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 37 | -------------------------------------------------------------------------------- /src/feeds/rss/parse/index.ts: -------------------------------------------------------------------------------- 1 | import { locales, namespacePrefixes, namespaceUris } from '../../../common/config.js' 2 | import type { DeepPartial, ParseOptions } from '../../../common/types.js' 3 | import { createNamespaceNormalizator } from '../../../common/utils.js' 4 | import { detectRssFeed } from '../../../index.js' 5 | import type { Rss } from '../common/types.js' 6 | import { parser } from './config.js' 7 | import { retrieveFeed } from './utils.js' 8 | 9 | export const parse = (value: unknown, options?: ParseOptions): DeepPartial> => { 10 | if (!detectRssFeed(value)) { 11 | throw new Error(locales.invalidFeedFormat) 12 | } 13 | 14 | const normalizeNamespaces = createNamespaceNormalizator(namespaceUris, namespacePrefixes) 15 | 16 | const object = parser.parse(value) 17 | const normalized = normalizeNamespaces(object) 18 | const parsed = retrieveFeed(normalized, options) 19 | 20 | if (!parsed) { 21 | throw new Error(locales.invalidFeedFormat) 22 | } 23 | 24 | return parsed 25 | } 26 | -------------------------------------------------------------------------------- /src/feeds/rdf/parse/index.ts: -------------------------------------------------------------------------------- 1 | import { locales, namespacePrefixes, namespaceUris } from '../../../common/config.js' 2 | import type { DeepPartial, ParseOptions } from '../../../common/types.js' 3 | import { createNamespaceNormalizator } from '../../../common/utils.js' 4 | import { detectRdfFeed } from '../../../index.js' 5 | import type { Rdf } from '../common/types.js' 6 | import { parser } from './config.js' 7 | import { retrieveFeed } from './utils.js' 8 | 9 | export const parse = (value: unknown, options?: ParseOptions): DeepPartial> => { 10 | if (!detectRdfFeed(value)) { 11 | throw new Error(locales.invalidFeedFormat) 12 | } 13 | 14 | const normalizeNamespaces = createNamespaceNormalizator(namespaceUris, namespacePrefixes, 'rdf') 15 | 16 | const object = parser.parse(value) 17 | const normalized = normalizeNamespaces(object) 18 | const parsed = retrieveFeed(normalized, options) 19 | 20 | if (!parsed) { 21 | throw new Error(locales.invalidFeedFormat) 22 | } 23 | 24 | return parsed 25 | } 26 | -------------------------------------------------------------------------------- /src/namespaces/app/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseDate, 5 | parseSingularOf, 6 | parseYesNoBoolean, 7 | retrieveText, 8 | trimObject, 9 | } from '../../../common/utils.js' 10 | import type { AppNs } from '../common/types.js' 11 | 12 | export const parseControl: ParsePartialUtil = (value) => { 13 | if (!isObject(value)) { 14 | return 15 | } 16 | 17 | const control = { 18 | draft: parseSingularOf(value['app:draft'], (value) => parseYesNoBoolean(retrieveText(value))), 19 | } 20 | 21 | return trimObject(control) 22 | } 23 | 24 | export const retrieveEntry: ParsePartialUtil> = (value) => { 25 | if (!isObject(value)) { 26 | return 27 | } 28 | 29 | const entry = { 30 | edited: parseSingularOf(value['app:edited'], (value) => parseDate(retrieveText(value))), 31 | control: parseSingularOf(value['app:control'], parseControl), 32 | } 33 | 34 | return trimObject(entry) 35 | } 36 | -------------------------------------------------------------------------------- /src/feeds/atom/parse/index.ts: -------------------------------------------------------------------------------- 1 | import { locales, namespacePrefixes, namespaceUris } from '../../../common/config.js' 2 | import type { DeepPartial, ParseOptions } from '../../../common/types.js' 3 | import { createNamespaceNormalizator } from '../../../common/utils.js' 4 | import { detectAtomFeed } from '../../../index.js' 5 | import type { Atom } from '../common/types.js' 6 | import { parser } from './config.js' 7 | import { retrieveFeed } from './utils.js' 8 | 9 | export const parse = (value: unknown, options?: ParseOptions): DeepPartial> => { 10 | if (!detectAtomFeed(value)) { 11 | throw new Error(locales.invalidFeedFormat) 12 | } 13 | 14 | const normalizeNamespaces = createNamespaceNormalizator(namespaceUris, namespacePrefixes, 'atom') 15 | 16 | const object = parser.parse(value) 17 | const normalized = normalizeNamespaces(object) 18 | const parsed = retrieveFeed(normalized, options) 19 | 20 | if (!parsed) { 21 | throw new Error(locales.invalidFeedFormat) 22 | } 23 | 24 | return parsed 25 | } 26 | -------------------------------------------------------------------------------- /docs/parsing/dates.md: -------------------------------------------------------------------------------- 1 | --- 2 | prev: Parsing › Namespaces 3 | next: Parsing › Detecting 4 | --- 5 | 6 | # Handling Dates 7 | 8 | Dates in feeds do not always follow a format defined in the specifications, or even any consistent format. Instead of attempting to parse all of them and risking errors, Feedsmith returns dates in their original string form. This method allows for the use of a preferred date parsing library, custom function, or the `Date` object directly. 9 | 10 | ### Common Issues 11 | 12 | - **RSS**: Should use RFC 2822 format, but many feeds use incorrect formats 13 | - **Atom**: ISO 8601/RFC 3339 format, generally more consistent but still varies 14 | - **Real-world problems**: 15 | - Missing timezone information 16 | - Invalid day/month combinations 17 | - Inconsistent formatting within the same feed 18 | - Localized date strings 19 | - Custom date formats 20 | 21 | > [!NOTE] 22 | > Automatic date parsing may be implemented in a future version of Feedsmith, with an option to preserve string behavior for backward compatibility. 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | lint: 9 | # Disable for dependabot PRs due https://github.com/dependabot/dependabot-core/issues/11705. 10 | if: github.actor != 'dependabot[bot]' 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - uses: oven-sh/setup-bun@v2 18 | 19 | - name: Install dependencies 20 | run: bun install --frozen-lockfile 21 | 22 | - name: Run Biome check 23 | run: bunx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true 24 | 25 | - name: Run TypeScript check 26 | run: bunx tsc --noEmit 27 | 28 | - name: Validate PR commits 29 | run: | 30 | bunx commitlint \ 31 | --from ${{ github.event.pull_request.base.sha }} \ 32 | --to ${{ github.event.pull_request.head.sha }} \ 33 | --config ./commitlint.json \ 34 | --verbose 35 | -------------------------------------------------------------------------------- /docs/reference/namespaces/yt.md: -------------------------------------------------------------------------------- 1 | # YouTube Namespace Reference 2 | 3 | The YouTube namespace provides YouTube-specific metadata for RSS feeds, enabling identification of YouTube videos and channels within RSS feeds. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://www.youtube.com/xml/schemas/2015
SpecificationYouTube RSS Extensions
Prefix<yt:*>
Available inAtom
Propertyyt
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/yt/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 37 | -------------------------------------------------------------------------------- /docs/reference/namespaces/pingback.md: -------------------------------------------------------------------------------- 1 | # Pingback Reference 2 | 3 | The Pingback namespace provides a mechanism for notifying websites when content references or links to them, enabling automatic trackback of linkages between web resources. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://madskills.com/public/xml/rss/module/pingback/
SpecificationPingback RSS Module (madskills.com)
Prefix<pingback:*>
Available inRSS, Atom
Propertypingback
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/pingback/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 37 | -------------------------------------------------------------------------------- /src/namespaces/feedpress/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseSingularOf, 5 | parseString, 6 | retrieveText, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { FeedPressNs } from '../common/types.js' 10 | 11 | export const retrieveFeed: ParsePartialUtil = (value) => { 12 | if (!isObject(value)) { 13 | return 14 | } 15 | 16 | const feed = { 17 | link: parseSingularOf(value['feedpress:link'], (value) => parseString(retrieveText(value))), 18 | newsletterId: parseSingularOf(value['feedpress:newsletterid'], (value) => 19 | parseString(retrieveText(value)), 20 | ), 21 | locale: parseSingularOf(value['feedpress:locale'], (value) => parseString(retrieveText(value))), 22 | podcastId: parseSingularOf(value['feedpress:podcastid'], (value) => 23 | parseString(retrieveText(value)), 24 | ), 25 | cssFile: parseSingularOf(value['feedpress:cssfile'], (value) => 26 | parseString(retrieveText(value)), 27 | ), 28 | } 29 | 30 | return trimObject(feed) 31 | } 32 | -------------------------------------------------------------------------------- /docs/reference/namespaces/feedpress.md: -------------------------------------------------------------------------------- 1 | # FeedPress Namespace Reference 2 | 3 | The FeedPress namespace provides elements for FeedPress-specific feed metadata, including podcast identifiers, newsletter identifiers, locale information, and custom CSS file references. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttps://feed.press/xmlns
SpecificationFeedPress Namespace Specification
Prefix<feedpress:*>
Available inRSS
Propertyfeedpress
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/feedpress/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 37 | -------------------------------------------------------------------------------- /docs/reference/namespaces/cc.md: -------------------------------------------------------------------------------- 1 | # ccREL Namespace Reference 2 | 3 | The Creative Commons Rights Expression Language (ccREL) enables RSS and Atom feeds to declare copyright licenses and additional permissions for feed content. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://creativecommons.org/ns#
SpecificationCreative Commons Rights Expression Language (ccREL)
Prefix<cc:*>
Available inRSS, Atom
Propertycc
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/cc/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 37 | -------------------------------------------------------------------------------- /src/namespaces/psc/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { generatePlainString, isObject, trimArray, trimObject } from '../../../common/utils.js' 3 | import type { PscNs } from '../common/types.js' 4 | 5 | export const generateChapter: GenerateUtil = (chapter) => { 6 | if (!isObject(chapter)) { 7 | return 8 | } 9 | 10 | const value = { 11 | '@start': generatePlainString(chapter.start), 12 | '@title': generatePlainString(chapter.title), 13 | '@href': generatePlainString(chapter.href), 14 | '@image': generatePlainString(chapter.image), 15 | } 16 | 17 | return trimObject(value) 18 | } 19 | 20 | export const generateChapters: GenerateUtil> = (chapters) => { 21 | const value = { 22 | 'psc:chapter': trimArray(chapters, generateChapter), 23 | } 24 | 25 | return trimObject(value) 26 | } 27 | 28 | export const generateItem: GenerateUtil = (item) => { 29 | if (!isObject(item)) { 30 | return 31 | } 32 | 33 | return trimObject({ 34 | 'psc:chapters': generateChapters(item.chapters), 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /docs/reference/namespaces/geo.md: -------------------------------------------------------------------------------- 1 | # W3C Basic Geo Reference 2 | 3 | The W3C Basic Geo (WGS84 lat/long) Vocabulary provides a simple way to represent geographic coordinates in RSS and Atom feeds using the WGS84 geodetic reference datum. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://www.w3.org/2003/01/geo/wgs84_pos#
SpecificationW3C Basic Geo Vocabulary
Prefix<geo:*>
Available inRSS, Atom
Propertygeo
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/geo/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Maciej Lamberski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/namespaces/yt/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseSingularOf, 5 | parseString, 6 | retrieveText, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { YtNs } from '../common/types.js' 10 | 11 | export const retrieveItem: ParsePartialUtil = (value) => { 12 | if (!isObject(value)) { 13 | return 14 | } 15 | 16 | const item = { 17 | videoId: parseSingularOf(value['yt:videoid'], (value) => parseString(retrieveText(value))), 18 | channelId: parseSingularOf(value['yt:channelid'], (value) => parseString(retrieveText(value))), 19 | } 20 | 21 | return trimObject(item) 22 | } 23 | 24 | export const retrieveFeed: ParsePartialUtil = (value) => { 25 | if (!isObject(value)) { 26 | return 27 | } 28 | 29 | const feed = { 30 | channelId: parseSingularOf(value['yt:channelid'], (value) => parseString(retrieveText(value))), 31 | playlistId: parseSingularOf(value['yt:playlistid'], (value) => 32 | parseString(retrieveText(value)), 33 | ), 34 | } 35 | 36 | return trimObject(feed) 37 | } 38 | -------------------------------------------------------------------------------- /docs/reference/namespaces/rawvoice.md: -------------------------------------------------------------------------------- 1 | # RawVoice Namespace Reference 2 | 3 | The RawVoice namespace provides elements for enhanced podcast and video content delivery, including live streaming, video formats, and episode metadata. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
Namespace URIhttps://blubrry.com/developer/rawvoice-rss
SpecificationRawVoice RSS Namespace Specification
Prefix<rawvoice:*>
Available in 22 | RSS 23 |
Propertyrawvoice
31 | 32 | ## Types 33 | 34 | <<< @/../src/namespaces/rawvoice/common/types.ts#reference 35 | 36 | ## Related 37 | 38 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 39 | -------------------------------------------------------------------------------- /src/feeds/rss/references/rss-090.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample Feed 6 | http://example.org/ 7 | For documentation <em>only</em> 8 | 9 | Example banner 10 | http://example.org/banner.png 11 | http://example.org/ 12 | 13 | 14 | First item title 15 | http://example.org/item/1 16 | Watch out for <span style="background: url(javascript:window.location='http://example.org/')"> nasty tricks</span> 17 | 18 | 19 | Second item title 20 | http://example.org/item/2 21 | Watch out for <span style="background: url(javascript:window.location='http://example.org/')"> nasty tricks</span> 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/namespaces/cc/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { generateCdataString, isObject, trimObject } from '../../../common/utils.js' 3 | import type { CcNs } from '../common/types.js' 4 | 5 | export const generateItemOrFeed: GenerateUtil = (data) => { 6 | if (!isObject(data)) { 7 | return 8 | } 9 | 10 | const value = { 11 | 'cc:license': generateCdataString(data.license), 12 | 'cc:morePermissions': generateCdataString(data.morePermissions), 13 | 'cc:attributionName': generateCdataString(data.attributionName), 14 | 'cc:attributionURL': generateCdataString(data.attributionURL), 15 | 'cc:useGuidelines': generateCdataString(data.useGuidelines), 16 | 'cc:permits': generateCdataString(data.permits), 17 | 'cc:requires': generateCdataString(data.requires), 18 | 'cc:prohibits': generateCdataString(data.prohibits), 19 | 'cc:jurisdiction': generateCdataString(data.jurisdiction), 20 | 'cc:legalcode': generateCdataString(data.legalcode), 21 | 'cc:deprecatedOn': generateCdataString(data.deprecatedOn), 22 | } 23 | 24 | return trimObject(value) 25 | } 26 | -------------------------------------------------------------------------------- /src/namespaces/pingback/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseSingularOf, 5 | parseString, 6 | retrieveRdfResourceOrText, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { PingbackNs } from '../common/types.js' 10 | 11 | export const retrieveItem: ParsePartialUtil = (value) => { 12 | if (!isObject(value)) { 13 | return 14 | } 15 | 16 | const item = { 17 | server: parseSingularOf(value['pingback:server'], (value) => 18 | retrieveRdfResourceOrText(value, parseString), 19 | ), 20 | target: parseSingularOf(value['pingback:target'], (value) => 21 | retrieveRdfResourceOrText(value, parseString), 22 | ), 23 | } 24 | 25 | return trimObject(item) 26 | } 27 | 28 | export const retrieveFeed: ParsePartialUtil = (value) => { 29 | if (!isObject(value)) { 30 | return 31 | } 32 | 33 | const feed = { 34 | to: parseSingularOf(value['pingback:to'], (value) => 35 | retrieveRdfResourceOrText(value, parseString), 36 | ), 37 | } 38 | 39 | return trimObject(feed) 40 | } 41 | -------------------------------------------------------------------------------- /src/namespaces/slash/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseCsvOf, 5 | parseNumber, 6 | parseSingularOf, 7 | parseString, 8 | retrieveText, 9 | trimObject, 10 | } from '../../../common/utils.js' 11 | import type { SlashNs } from '../common/types.js' 12 | 13 | export const parseHitParade: ParsePartialUtil = (value) => { 14 | return parseCsvOf(value, parseNumber) 15 | } 16 | 17 | export const retrieveItem: ParsePartialUtil = (value) => { 18 | if (!isObject(value)) { 19 | return 20 | } 21 | 22 | const item = { 23 | section: parseSingularOf(value['slash:section'], (value) => parseString(retrieveText(value))), 24 | department: parseSingularOf(value['slash:department'], (value) => 25 | parseString(retrieveText(value)), 26 | ), 27 | comments: parseSingularOf(value['slash:comments'], (value) => parseNumber(retrieveText(value))), 28 | hitParade: parseSingularOf(value['slash:hit_parade'], (value) => 29 | parseHitParade(retrieveText(value)), 30 | ), 31 | } 32 | 33 | return trimObject(item) 34 | } 35 | -------------------------------------------------------------------------------- /docs/reference/namespaces/blogchannel.md: -------------------------------------------------------------------------------- 1 | # blogChannel Namespace Reference 2 | 3 | The blogChannel namespace is an RSS 2.0 module for weblogging applications, providing metadata about blog-related content such as blogrolls, recommended links, and subscription lists. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://backend.userland.com/blogChannelModule
SpecificationblogChannel RSS Module
Prefix<blogChannel:*>
Available inRSS
PropertyblogChannel
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/blogchannel/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 37 | -------------------------------------------------------------------------------- /compatibility/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | FAILED=0 5 | 6 | run_test() { 7 | local output 8 | if output=$(eval "$2" 2>&1); then 9 | echo "✅ $1" 10 | else 11 | echo "❌ $1" 12 | echo "$output" 13 | FAILED=1 14 | fi 15 | } 16 | 17 | # TypeScript 18 | for type in modern-esm modern-cjs; do 19 | for config in node node16 nodenext bundler; do 20 | run_test "$type/$config" "bunx tsc --project typescript/$type/tsconfig.$config.json --noEmit" 21 | done 22 | done 23 | 24 | # Legacy CJS 25 | run_test "legacy-cjs" "bunx tsc --project typescript/legacy-cjs/tsconfig.json --noEmit" 26 | 27 | # Explicit modules 28 | for pkg in esm-package cjs-package mixed-package; do 29 | run_test "explicit/$pkg" "bunx tsc --project explicit-modules/$pkg/tsconfig.json --noEmit" 30 | done 31 | 32 | # JavaScript 33 | for type in esm cjs; do 34 | ext=$([ "$type" = "esm" ] && echo "mjs" || echo "cjs") 35 | run_test "javascript/$type" "node javascript/$type/index.js && node javascript/$type/index.$ext" 36 | done 37 | 38 | # Vite bundler 39 | for type in esm cjs; do 40 | run_test "vite/$type" "bunx vite build bundler/$type" 41 | done 42 | 43 | exit $FAILED 44 | -------------------------------------------------------------------------------- /docs/reference/namespaces/source.md: -------------------------------------------------------------------------------- 1 | # Source Namespace Reference 2 | 3 | The Source namespace provides elements for enhanced feed metadata, including social media accounts, subscription lists, blogrolls, and source content in various formats like Markdown and OPML outlines. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://source.scripting.com/
Specificationsource.scripting.com
Prefix<source:*>
Available inRSS
PropertysourceNs (due to conflict with RSS's source element)
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/source/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 37 | -------------------------------------------------------------------------------- /src/namespaces/psc/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseArrayOf, 5 | parseSingularOf, 6 | parseString, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { PscNs } from '../common/types.js' 10 | 11 | export const parseChapter: ParsePartialUtil = (value) => { 12 | if (!isObject(value)) { 13 | return 14 | } 15 | 16 | const chapter = { 17 | start: parseSingularOf(value['@start'], parseString), 18 | title: parseSingularOf(value['@title'], parseString), 19 | href: parseSingularOf(value['@href'], parseString), 20 | image: parseSingularOf(value['@image'], parseString), 21 | } 22 | 23 | return trimObject(chapter) 24 | } 25 | 26 | export const parseChapters: ParsePartialUtil> = (value) => { 27 | return parseArrayOf(value?.['psc:chapter'], parseChapter) 28 | } 29 | 30 | export const retrieveItem: ParsePartialUtil = (value) => { 31 | if (!isObject(value)) { 32 | return 33 | } 34 | 35 | const item = { 36 | chapters: parseSingularOf(value['psc:chapters'], parseChapters), 37 | } 38 | 39 | return trimObject(item) 40 | } 41 | -------------------------------------------------------------------------------- /docs/reference/namespaces/georss.md: -------------------------------------------------------------------------------- 1 | # GeoRSS Simple Namespace Reference 2 | 3 | The GeoRSS Simple namespace enables geographic tagging of RSS feeds and items, allowing publishers to associate location information with their content. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Namespace URIhttp://www.georss.org/georss
SpecificationGeoRSS Specification
Prefix<georss:*>
Available in 22 | RSS, 23 | Atom, 24 | RDF 25 |
Propertygeorss
33 | 34 | ## Types 35 | 36 | <<< @/../src/namespaces/georss/common/types.ts#reference 37 | 38 | ## Related 39 | 40 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 41 | -------------------------------------------------------------------------------- /docs/reference/namespaces/content.md: -------------------------------------------------------------------------------- 1 | # Content Namespace Reference 2 | 3 | The Content namespace allows RSS and RDF feeds to include full content alongside or instead of summaries. It provides a way to embed complete articles or posts within feed items. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Namespace URIhttp://purl.org/rss/1.0/modules/content/
SpecificationRSS 1.0 Content Module
Prefix<content:*>
Available in 22 | RSS, 23 | RDF 24 |
Propertycontent
32 | 33 | ## Types 34 | 35 | <<< @/../src/namespaces/content/common/types.ts#reference 36 | 37 | ## Related 38 | 39 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 40 | -------------------------------------------------------------------------------- /docs/reference/namespaces/admin.md: -------------------------------------------------------------------------------- 1 | # Administrative Reference 2 | 3 | The Administrative namespace (MVCB - Meta Vocabulary for Community Building) provides administrative metadata about RSS/RDF feeds, enabling better identification of feed generators and error reporting contacts. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://webns.net/mvcb/
SpecificationAdmin Module Specification
Prefix<admin:*>
Available inRSS, Atom, RDF
Propertyadmin
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/admin/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 37 | -------------------------------------------------------------------------------- /docs/reference/namespaces/media.md: -------------------------------------------------------------------------------- 1 | # Media RSS Namespace Reference 2 | 3 | The Media RSS namespace provides rich media metadata for RSS feeds, enabling comprehensive description of multimedia content including videos, images, and audio files. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Namespace URIhttp://search.yahoo.com/mrss/
SpecificationMedia RSS Specification
Prefix<media:*>
Available in 22 | RSS, 23 | Atom, 24 | RDF 25 |
Propertymedia
33 | 34 | ## Types 35 | 36 | <<< @/../src/namespaces/media/common/types.ts#reference 37 | 38 | ## Related 39 | 40 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 41 | -------------------------------------------------------------------------------- /src/feeds/rdf/references/rdf-09.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Mozilla Dot Org", 3 | "link": "http://www.mozilla.org", 4 | "description": "the Mozilla Organization web site", 5 | "image": { 6 | "title": "Mozilla", 7 | "link": "http://www.mozilla.org", 8 | "url": "http://www.mozilla.org/images/moz.gif" 9 | }, 10 | "items": [ 11 | { 12 | "title": "New Status Updates", 13 | "link": "http://www.mozilla.org/status/", 14 | "rdf": { "about": "http://example.org/item1" } 15 | }, 16 | { 17 | "title": "Bugzilla Reorganized", 18 | "link": "http://www.mozilla.org/bugs/", 19 | "rdf": { "about": "http://example.org/item2" } 20 | }, 21 | { 22 | "title": "Mozilla Party, 2.0!", 23 | "link": "http://www.mozilla.org/party/1999/", 24 | "rdf": { "about": "http://example.org/item3" } 25 | }, 26 | { 27 | "title": "Unix Platform Parity", 28 | "link": "http://www.mozilla.org/build/unix.html", 29 | "rdf": { "about": "http://example.org/item4" } 30 | }, 31 | { 32 | "title": "NPL 1.0M published", 33 | "link": "http://www.mozilla.org/NPL/NPL-1.0M.html", 34 | "rdf": { "about": "http://example.org/item5" } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /docs/reference/namespaces/wfw.md: -------------------------------------------------------------------------------- 1 | # Comment API Namespace Reference 2 | 3 | The Comment API namespace provides elements for linking to comment feeds and comment posting interfaces, enabling better integration between feeds and commenting systems. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Namespace URIhttp://wellformedweb.org/CommentAPI/
SpecificationWell Formed Web Comment API
Prefix<wfw:*>
Available in 22 | RSS, 23 | Atom, 24 | RDF 25 |
Propertywfw
33 | 34 | ## Types 35 | 36 | <<< @/../src/namespaces/wfw/common/types.ts#reference 37 | 38 | ## Related 39 | 40 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 41 | -------------------------------------------------------------------------------- /docs/reference/namespaces/trackback.md: -------------------------------------------------------------------------------- 1 | # Trackback Reference 2 | 3 | The Trackback namespace enables peer-to-peer communication between web sites that publish related content. In its simplest form, trackback is a means of sending a message that lets a site know you've published a link to one of its pages. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://madskills.com/public/xml/rss/module/trackback/
SpecificationTrackback Namespace for RSS
Prefix<trackback:*>
Available inRSS, Atom
Propertytrackback
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/trackback/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 37 | -------------------------------------------------------------------------------- /docs/reference/namespaces/slash.md: -------------------------------------------------------------------------------- 1 | # Slash Namespace Reference 2 | 3 | The Slash namespace provides metadata about user engagement, particularly comment counts. Originally created by Slashdot, it's now widely used to indicate discussion activity on feed items. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Namespace URIhttp://purl.org/rss/1.0/modules/slash/
SpecificationSlash Module
Prefix<slash:*>
Available in 22 | RSS, 23 | Atom, 24 | RDF 25 |
Propertyslash
33 | 34 | ## Types 35 | 36 | <<< @/../src/namespaces/slash/common/types.ts#reference 37 | 38 | ## Related 39 | 40 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 41 | -------------------------------------------------------------------------------- /docs/reference/namespaces/opensearch.md: -------------------------------------------------------------------------------- 1 | # OpenSearch Namespace Reference 2 | 3 | The OpenSearch namespace provides elements for communicating search metadata and pagination information in RSS and Atom feeds. It enables search engines and APIs to publish search results in standard syndication formats. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://a9.com/-/spec/opensearch/1.1/
SpecificationOpenSearch 1.1 Specification
Prefix<opensearch:*>
Available inRSS, Atom
Propertyopensearch
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/opensearch/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 37 | -------------------------------------------------------------------------------- /docs/reference/namespaces/arxiv.md: -------------------------------------------------------------------------------- 1 | # arXiv Reference 2 | 3 | arXiv is an extension namespace for the arXiv preprint repository API, providing metadata specific to scholarly papers in physics, mathematics, computer science, and related fields. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://arxiv.org/schemas/atom
SpecificationarXiv API User's Manual
Prefix<arxiv:*>
Available inAtom
Propertyarxiv
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/arxiv/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Dublin Core](/reference/namespaces/dc)** - Standard metadata for scholarly content 37 | - **[Dublin Core Terms](/reference/namespaces/dcterms)** - Extended metadata vocabulary 38 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 39 | -------------------------------------------------------------------------------- /docs/reference/namespaces/creativecommons.md: -------------------------------------------------------------------------------- 1 | # Creative Commons Namespace Reference 2 | 3 | The Creative Commons namespace provides elements for specifying the license under which the feed content is distributed. This allows content creators to clearly indicate their licensing terms using Creative Commons or other license URLs. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://backend.userland.com/creativeCommonsRssModule
SpecificationCreative Commons RSS Module Specification
Prefix<creativeCommons:*>
Available inRSS, Atom
PropertycreativeCommons
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/creativecommons/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 37 | -------------------------------------------------------------------------------- /docs/reference/namespaces/spotify.md: -------------------------------------------------------------------------------- 1 | # Spotify Namespace Reference 2 | 3 | The Spotify namespace provides podcast-specific metadata for Spotify's podcast platform, including episode limits and country targeting information. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttp://www.spotify.com/ns/rss
SpecificationSpotify Podcast Delivery Specification
Prefix<spotify:*>
Available inRSS
Propertyspotify
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/spotify/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[iTunes Namespace](/reference/namespaces/itunes)** - Traditional podcast metadata 37 | - **[Podcast Namespace](/reference/namespaces/podcast)** - Podcasting 2.0 features 38 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 39 | -------------------------------------------------------------------------------- /docs/reference/namespaces/itunes.md: -------------------------------------------------------------------------------- 1 | # iTunes Namespace Reference 2 | 3 | The iTunes namespace provides podcast-specific metadata for RSS and Atom feeds. This namespace is essential for podcast distribution through Apple Podcasts and other podcast platforms. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Namespace URIhttp://www.itunes.com/dtds/podcast-1.0.dtd
SpecificationApple Podcasts Requirements
Prefix<itunes:*>
Available in 22 | RSS, 23 | Atom 24 |
Propertyitunes
32 | 33 | ## Types 34 | 35 | <<< @/../src/namespaces/itunes/common/types.ts#reference 36 | 37 | ## Related 38 | 39 | - **[Podcast Namespace](/reference/namespaces/podcast)** - Podcasting 2.0 extensions 40 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 41 | -------------------------------------------------------------------------------- /docs/reference/namespaces/acast.md: -------------------------------------------------------------------------------- 1 | # Acast Namespace Reference 2 | 3 | The Acast namespace provides podcast-specific metadata for Acast's podcast hosting platform, including show and episode identifiers, encrypted settings, and network information. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttps://schema.acast.com/1.0/
SpecificationNo official documentation (inferred from live feeds)
Prefix<acast:*>
Available inRSS
Propertyacast
29 | 30 | ## Types 31 | 32 | <<< @/../src/namespaces/acast/common/types.ts#reference 33 | 34 | ## Related 35 | 36 | - **[Spotify Namespace](/reference/namespaces/spotify)** - Spotify podcast metadata 37 | - **[iTunes Namespace](/reference/namespaces/itunes)** - Traditional podcast metadata 38 | - **[Podcast Namespace](/reference/namespaces/podcast)** - Podcasting 2.0 features 39 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 40 | -------------------------------------------------------------------------------- /src/namespaces/rdf/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { generatePlainString, isObject, trimArray, trimObject } from '../../../common/utils.js' 3 | import type { RdfNs } from '../common/types.js' 4 | 5 | export const generateAbout: GenerateUtil = (about) => { 6 | if (!isObject(about)) { 7 | return 8 | } 9 | 10 | const value = { 11 | '@rdf:about': generatePlainString(about.about), 12 | } 13 | 14 | return trimObject(value) 15 | } 16 | 17 | /** @internal General RDF element kept for potential future use when all RDF data is needed. */ 18 | export const generateElement: GenerateUtil = (element) => { 19 | if (!isObject(element)) { 20 | return 21 | } 22 | 23 | const type = generatePlainString(element.type) 24 | const value = { 25 | '@rdf:about': generatePlainString(element.about), 26 | '@rdf:resource': generatePlainString(element.resource), 27 | '@rdf:ID': generatePlainString(element.id), 28 | '@rdf:nodeID': generatePlainString(element.nodeId), 29 | '@rdf:parseType': generatePlainString(element.parseType), 30 | '@rdf:datatype': generatePlainString(element.datatype), 31 | 'rdf:type': trimObject({ '@rdf:resource': type }), 32 | 'rdf:value': trimArray(element.value), 33 | } 34 | 35 | return trimObject(value) 36 | } 37 | -------------------------------------------------------------------------------- /benchmarks/cross-language/parsing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | FILES_DIR="$SCRIPT_DIR/../files" 7 | 8 | run_benchmark() { 9 | local feed_dir=$1 10 | local feed_format=$2 11 | local description=$3 12 | 13 | echo "" 14 | echo "⏳ Running: $description" 15 | 16 | hyperfine --warmup 3 --min-runs 10 \ 17 | --command-name 'feedsmith *' "bun run --cwd $SCRIPT_DIR/../.. $SCRIPT_DIR/parsing-feedsmith.ts $FILES_DIR/$feed_dir $feed_format" \ 18 | --command-name 'feedjira (ruby)' "ruby $SCRIPT_DIR/parsing-feedjira.rb $FILES_DIR/$feed_dir $feed_format" \ 19 | --command-name 'feedparser (python)' "python3 $SCRIPT_DIR/parsing-feedparser.py $FILES_DIR/$feed_dir $feed_format" \ 20 | --command-name 'gofeed (go)' "$SCRIPT_DIR/parsing-gofeed $FILES_DIR/$feed_dir $feed_format" \ 21 | --command-name 'simplepie (php)' "php $SCRIPT_DIR/parsing-simplepie.php $FILES_DIR/$feed_dir $feed_format" 22 | } 23 | 24 | run_benchmark "rss-small" "rss" "RSS feed parsing (100 files × 100KB–5MB)" 25 | run_benchmark "rss-big" "rss" "RSS feed parsing (10 files × 5MB–50MB)" 26 | run_benchmark "atom-small" "atom" "Atom feed parsing (100 files × 100KB–5MB)" 27 | run_benchmark "atom-big" "atom" "Atom feed parsing (10 files × 5MB–50MB)" 28 | run_benchmark "rdf" "rdf" "RDF feed parsing (100 files × 100KB–5MB)" 29 | -------------------------------------------------------------------------------- /src/feeds/rdf/references/rdf-09.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | Mozilla Dot Org 8 | http://www.mozilla.org 9 | the Mozilla Organization web site 10 | 11 | 12 | Mozilla 13 | http://www.mozilla.org/images/moz.gif 14 | http://www.mozilla.org 15 | 16 | 17 | New Status Updates 18 | http://www.mozilla.org/status/ 19 | 20 | 21 | Bugzilla Reorganized 22 | http://www.mozilla.org/bugs/ 23 | 24 | 25 | Mozilla Party, 2.0! 26 | http://www.mozilla.org/party/1999/ 27 | 28 | 29 | Unix Platform Parity 30 | http://www.mozilla.org/build/unix.html 31 | 32 | 33 | NPL 1.0M published 34 | http://www.mozilla.org/NPL/NPL-1.0M.html 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/reference/namespaces/psc.md: -------------------------------------------------------------------------------- 1 | # Podlove Simple Chapters Namespace Reference 2 | 3 | The Podlove Simple Chapters (PSC) namespace provides structured chapter information for podcasts and other media, allowing creators to define timed segments with titles, links, and images. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Namespace URIhttp://podlove.org/simple-chapters
SpecificationPodlove Simple Chapters
Prefix<psc:*>
Available in 22 | RSS, 23 | Atom 24 |
Propertypsc
32 | 33 | ## Structure 34 | 35 | <<< @/../src/namespaces/psc/common/types.ts#reference 36 | 37 | ## Related 38 | 39 | - **[iTunes Namespace](/reference/namespaces/itunes)** - Traditional podcast metadata 40 | - **[Podcast Namespace](/reference/namespaces/podcast)** - Modern Podcasting 2.0 features 41 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | branch: 7 | description: 'Branch to release' 8 | required: true 9 | default: 'main' 10 | type: choice 11 | options: 12 | - main 13 | - next 14 | 15 | jobs: 16 | test: 17 | uses: ./.github/workflows/test.yml 18 | 19 | compatibility: 20 | uses: ./.github/workflows/compatibility.yml 21 | 22 | release: 23 | needs: [test, compatibility] 24 | permissions: 25 | contents: write 26 | issues: write 27 | pull-requests: write 28 | id-token: write 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | with: 34 | ref: ${{ inputs.branch }} 35 | 36 | - name: Setup Node.js 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: 24 40 | registry-url: https://registry.npmjs.org 41 | 42 | - name: Setup Bun 43 | uses: oven-sh/setup-bun@v2 44 | 45 | - name: Install dependencies 46 | run: bun install --frozen-lockfile 47 | 48 | - name: Build package 49 | run: bun run build 50 | 51 | - name: Release and publish package 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | run: bunx semantic-release --extends kvalita/semantic-release 55 | -------------------------------------------------------------------------------- /src/namespaces/thr/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike, GenerateUtil } from '../../../common/types.js' 2 | import { 3 | generateNumber, 4 | generatePlainString, 5 | generateRfc3339Date, 6 | isObject, 7 | trimArray, 8 | trimObject, 9 | } from '../../../common/utils.js' 10 | import type { ThrNs } from '../common/types.js' 11 | 12 | export const generateInReplyTo: GenerateUtil = (inReplyTo) => { 13 | if (!isObject(inReplyTo)) { 14 | return 15 | } 16 | 17 | const value = { 18 | '@ref': generatePlainString(inReplyTo.ref), 19 | '@href': generatePlainString(inReplyTo.href), 20 | '@type': generatePlainString(inReplyTo.type), 21 | '@source': generatePlainString(inReplyTo.source), 22 | } 23 | 24 | return trimObject(value) 25 | } 26 | 27 | export const generateLink: GenerateUtil> = (link) => { 28 | if (!isObject(link)) { 29 | return 30 | } 31 | 32 | const value = { 33 | '@thr:count': generateNumber(link.count), 34 | '@thr:updated': generateRfc3339Date(link.updated), 35 | } 36 | 37 | return trimObject(value) 38 | } 39 | 40 | export const generateItem: GenerateUtil = (item) => { 41 | if (!isObject(item)) { 42 | return 43 | } 44 | 45 | const value = { 46 | 'thr:total': generateNumber(item.total), 47 | 'thr:in-reply-to': trimArray(item.inReplyTos, generateInReplyTo), 48 | } 49 | 50 | return trimObject(value) 51 | } 52 | -------------------------------------------------------------------------------- /docs/reference/namespaces/googleplay.md: -------------------------------------------------------------------------------- 1 | # Google Play Podcast Namespace Reference 2 | 3 | The Google Play Podcast namespace provides podcast-specific metadata for feed and episode information optimized for Google Play's podcast platform, including author details, content descriptions, and content policies. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Namespace URIhttps://www.google.com/schemas/play-podcasts/1.0/
SpecificationGoogle Play Podcast Namespace
Prefix<googleplay:*>
Available in 22 | RSS, 23 | Atom 24 |
Propertygoogleplay
32 | 33 | ## Structure 34 | 35 | <<< @/../src/namespaces/googleplay/common/types.ts#reference 36 | 37 | ## Related 38 | 39 | - **[iTunes Namespace](/reference/namespaces/itunes)** - Apple Podcasts metadata 40 | - **[Podcast Namespace](/reference/namespaces/podcast)** - Modern Podcasting 2.0 features 41 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works -------------------------------------------------------------------------------- /src/namespaces/thr/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseArrayOf, 5 | parseDate, 6 | parseNumber, 7 | parseSingularOf, 8 | parseString, 9 | retrieveText, 10 | trimObject, 11 | } from '../../../common/utils.js' 12 | import type { ThrNs } from '../common/types.js' 13 | 14 | export const parseInReplyTo: ParsePartialUtil = (value) => { 15 | if (!isObject(value)) { 16 | return 17 | } 18 | 19 | const inReplyTo = { 20 | ref: parseString(value['@ref']), 21 | href: parseString(value['@href']), 22 | type: parseString(value['@type']), 23 | source: parseString(value['@source']), 24 | } 25 | 26 | return trimObject(inReplyTo) 27 | } 28 | 29 | export const retrieveLink: ParsePartialUtil> = (value) => { 30 | if (!isObject(value)) { 31 | return 32 | } 33 | 34 | const link = { 35 | count: parseNumber(value['@thr:count']), 36 | updated: parseDate(value['@thr:updated']), 37 | } 38 | 39 | return trimObject(link) 40 | } 41 | 42 | export const retrieveItem: ParsePartialUtil = (value) => { 43 | if (!isObject(value)) { 44 | return 45 | } 46 | 47 | const item = { 48 | total: parseSingularOf(value['thr:total'], (value) => parseNumber(retrieveText(value))), 49 | inReplyTos: parseArrayOf(value['thr:in-reply-to'], parseInReplyTo), 50 | } 51 | 52 | return trimObject(item) 53 | } 54 | -------------------------------------------------------------------------------- /docs/reference/namespaces/thr.md: -------------------------------------------------------------------------------- 1 | # Atom Threading Namespace Reference 2 | 3 | The Atom Threading namespace provides elements for representing threaded discussions and comment relationships in Atom feeds, enabling proper conversation threading. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Namespace URIhttp://purl.org/syndication/thread/1.0
SpecificationThreading Extensions
Prefix<thr:*>
Available in 22 | RSS, 23 | Atom 24 |
Propertythr
32 | 33 | ## Types 34 | 35 | > [!INFO] 36 | > `TDate` represents date fields in the type definitions. When **parsing**, dates are returned as strings in their original format (see [Parsing › Handling Dates](/parsing/dates) for more details). When **generating**, dates should be provided as JavaScript `Date` objects. 37 | 38 | <<< @/../src/namespaces/thr/common/types.ts#reference 39 | 40 | ## Related 41 | 42 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 43 | -------------------------------------------------------------------------------- /src/namespaces/opensearch/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { 3 | generateNumber, 4 | generatePlainString, 5 | isObject, 6 | trimArray, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { OpenSearchNs } from '../common/types.js' 10 | 11 | export const generateQuery: GenerateUtil = (query) => { 12 | if (!isObject(query)) { 13 | return 14 | } 15 | 16 | const value = { 17 | '@role': generatePlainString(query.role), 18 | '@searchTerms': generatePlainString(query.searchTerms), 19 | '@count': generateNumber(query.count), 20 | '@startIndex': generateNumber(query.startIndex), 21 | '@startPage': generateNumber(query.startPage), 22 | '@language': generatePlainString(query.language), 23 | '@inputEncoding': generatePlainString(query.inputEncoding), 24 | '@outputEncoding': generatePlainString(query.outputEncoding), 25 | } 26 | 27 | return trimObject(value) 28 | } 29 | 30 | export const generateFeed: GenerateUtil = (feed) => { 31 | if (!isObject(feed)) { 32 | return 33 | } 34 | 35 | const value = { 36 | 'opensearch:totalResults': generateNumber(feed.totalResults), 37 | 'opensearch:startIndex': generateNumber(feed.startIndex), 38 | 'opensearch:itemsPerPage': generateNumber(feed.itemsPerPage), 39 | 'opensearch:Query': trimArray(feed.queries, generateQuery), 40 | } 41 | 42 | return trimObject(value) 43 | } 44 | -------------------------------------------------------------------------------- /src/opml/references/directory.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tech Blog Directory 5 | Mon, 22 Jan 2024 11:22:45 GMT 6 | Fri, 16 Feb 2024 16:48:32 GMT 7 | Anna Smith 8 | anna@example.com 9 | 1 10 | 100 11 | 460 12 | 380 13 | 960 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/namespaces/arxiv/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { 3 | generateCdataString, 4 | generatePlainString, 5 | isObject, 6 | trimObject, 7 | } from '../../../common/utils.js' 8 | import type { ArxivNs } from '../common/types.js' 9 | 10 | export const generatePrimaryCategory: GenerateUtil = (primaryCategory) => { 11 | if (!isObject(primaryCategory)) { 12 | return 13 | } 14 | 15 | const value = { 16 | '@term': generatePlainString(primaryCategory.term), 17 | '@scheme': generatePlainString(primaryCategory.scheme), 18 | '@label': generatePlainString(primaryCategory.label), 19 | } 20 | 21 | return trimObject(value) 22 | } 23 | 24 | export const generateAuthor: GenerateUtil = (author) => { 25 | if (!isObject(author)) { 26 | return 27 | } 28 | 29 | const value = { 30 | 'arxiv:affiliation': generateCdataString(author.affiliation), 31 | } 32 | 33 | return trimObject(value) 34 | } 35 | 36 | export const generateEntry: GenerateUtil = (entry) => { 37 | if (!isObject(entry)) { 38 | return 39 | } 40 | 41 | const value = { 42 | 'arxiv:comment': generateCdataString(entry.comment), 43 | 'arxiv:journal_ref': generateCdataString(entry.journalRef), 44 | 'arxiv:doi': generateCdataString(entry.doi), 45 | 'arxiv:primary_category': generatePrimaryCategory(entry.primaryCategory), 46 | } 47 | 48 | return trimObject(value) 49 | } 50 | -------------------------------------------------------------------------------- /src/namespaces/content/generate/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { generateItem } from './utils.js' 3 | 4 | describe('generateItem', () => { 5 | it('should generate content with CDATA for HTML content', () => { 6 | const value = { 7 | encoded: '

Full HTML content here

', 8 | } 9 | const expected = { 10 | 'content:encoded': { '#cdata': '

Full HTML content here

' }, 11 | } 12 | 13 | expect(generateItem(value)).toEqual(expected) 14 | }) 15 | 16 | it('should generate content without CDATA for simple text', () => { 17 | const value = { 18 | encoded: 'Simple text content without HTML', 19 | } 20 | const expected = { 21 | 'content:encoded': 'Simple text content without HTML', 22 | } 23 | 24 | expect(generateItem(value)).toEqual(expected) 25 | }) 26 | 27 | it('should generate content with CDATA for text containing ampersands', () => { 28 | const value = { 29 | encoded: 'Text with & ampersand characters', 30 | } 31 | const expected = { 32 | 'content:encoded': { '#cdata': 'Text with & ampersand characters' }, 33 | } 34 | 35 | expect(generateItem(value)).toEqual(expected) 36 | }) 37 | 38 | it('should handle empty object', () => { 39 | const value = {} 40 | 41 | expect(generateItem(value)).toBeUndefined() 42 | }) 43 | 44 | it('should handle undefined input', () => { 45 | expect(generateItem(undefined)).toBeUndefined() 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/namespaces/sy/generate/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { generateFeed } from './utils.js' 3 | 4 | describe('generateFeed', () => { 5 | it('should generate valid feed object with all properties', () => { 6 | const value = { 7 | updatePeriod: 'hourly', 8 | updateFrequency: 2, 9 | updateBase: new Date('2023-01-01T00:00:00Z'), 10 | } 11 | const expected = { 12 | 'sy:updatePeriod': 'hourly', 13 | 'sy:updateFrequency': 2, 14 | 'sy:updateBase': '2023-01-01T00:00:00.000Z', 15 | } 16 | 17 | expect(generateFeed(value)).toEqual(expected) 18 | }) 19 | 20 | it('should generate feed with minimal properties', () => { 21 | const value = { 22 | updatePeriod: 'daily', 23 | } 24 | const expected = { 25 | 'sy:updatePeriod': 'daily', 26 | } 27 | 28 | expect(generateFeed(value)).toEqual(expected) 29 | }) 30 | 31 | it('should handle object with only undefined/empty properties', () => { 32 | const value = { 33 | updatePeriod: undefined, 34 | updateFrequency: undefined, 35 | updateBase: undefined, 36 | } 37 | 38 | expect(generateFeed(value)).toBeUndefined() 39 | }) 40 | 41 | it('should handle empty object', () => { 42 | const value = {} 43 | 44 | expect(generateFeed(value)).toBeUndefined() 45 | }) 46 | 47 | it('should handle non-object inputs gracefully', () => { 48 | expect(generateFeed(undefined)).toBeUndefined() 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/feeds/json/common/types.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike } from '../../../common/types.js' 2 | 3 | // #region reference 4 | export namespace Json { 5 | export type Author = { 6 | name?: string 7 | url?: string 8 | avatar?: string 9 | } 10 | 11 | export type Attachment = { 12 | url: string 13 | mime_type: string 14 | title?: string 15 | size_in_bytes?: number 16 | duration_in_seconds?: number 17 | } 18 | 19 | export type Item = { 20 | id: string 21 | url?: string 22 | external_url?: string 23 | title?: string 24 | content_html?: string 25 | content_text?: string 26 | summary?: string 27 | image?: string 28 | banner_image?: string 29 | date_published?: TDate 30 | date_modified?: TDate 31 | tags?: Array 32 | authors?: Array 33 | language?: string 34 | attachments?: Array 35 | } & ({ content_html: string } | { content_text: string }) 36 | 37 | export type Hub = { 38 | type: string 39 | url: string 40 | } 41 | 42 | export type Feed = { 43 | title: string 44 | home_page_url?: string 45 | feed_url?: string 46 | description?: string 47 | user_comment?: string 48 | next_url?: string 49 | icon?: string 50 | favicon?: string 51 | language?: string 52 | expired?: boolean 53 | hubs?: Array 54 | authors?: Array 55 | items: Array> 56 | } 57 | } 58 | // #endregion reference 59 | -------------------------------------------------------------------------------- /docs/reference/namespaces/sy.md: -------------------------------------------------------------------------------- 1 | # Syndication Namespace Reference 2 | 3 | The Syndication namespace provides information about the frequency and timing of feed updates. It helps aggregators understand how often to check for new content. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Namespace URIhttp://purl.org/rss/1.0/modules/syndication/
SpecificationSyndication Module
Prefix<sy:*>
Available in 22 | RSS, 23 | Atom, 24 | RDF 25 |
Propertysy
33 | 34 | ## Types 35 | 36 | > [!INFO] 37 | > `TDate` represents date fields in the type definitions. When **parsing**, dates are returned as strings in their original format (see [Parsing › Handling Dates](/parsing/dates) for more details). When **generating**, dates should be provided as JavaScript `Date` objects. 38 | 39 | <<< @/../src/namespaces/sy/common/types.ts#reference 40 | 41 | ## Related 42 | 43 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 44 | -------------------------------------------------------------------------------- /docs/reference/index.md: -------------------------------------------------------------------------------- 1 | # Universal Functions 2 | 3 | Universal functions that work across all feed formats. 4 | 5 | ## Functions 6 | 7 | ### `parseFeed()` 8 | 9 | Universal parser that automatically detects feed format and parses accordingly. 10 | 11 | #### Parameters 12 | 13 | | Parameter | Type | Description | 14 | |-----------|------|-------------| 15 | | `content` | `string` | The feed content to parse (XML or JSON) | 16 | | `options` | `object` | Optional parsing settings | 17 | 18 | #### Options 19 | 20 | | Option | Type | Default | Description | 21 | |--------|------|---------|-------------| 22 | | `maxItems` | `number` | - | Limit the number of items/entries parsed. Use `0` to skip items entirely, useful when only feed metadata is needed | 23 | 24 | #### Returns 25 | `object` - Object containing: 26 | - `format: 'rss' | 'atom' | 'rdf' | 'json'` - Detected feed format 27 | - `feed: object` - Parsed feed with all fields optional and dates as strings 28 | 29 | #### Example 30 | ```typescript 31 | import { parseFeed } from 'feedsmith' 32 | 33 | const { format, feed } = parseFeed(feedContent) 34 | 35 | // Limit number of items parsed 36 | const { format, feed } = parseFeed(feedContent, { maxItems: 5 }) 37 | 38 | // Parse only feed metadata (skip all items) 39 | const { format, feed } = parseFeed(feedContent, { maxItems: 0 }) 40 | ``` 41 | 42 | > [!IMPORTANT] 43 | > The universal parser uses detection functions to identify feed formats. For maximum reliability when you know the format in advance, use format-specific parsers. -------------------------------------------------------------------------------- /docs/reference/namespaces/podcast.md: -------------------------------------------------------------------------------- 1 | # Podcast Index Namespace Reference 2 | 3 | The Podcast Index namespace implements the Podcasting 2.0 specification, providing advanced features for modern podcasting including transcripts, chapters, value streaming, and enhanced metadata. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Namespace URIhttps://podcastindex.org/namespace/1.0
SpecificationPodcast Namespace 2.0
Prefix<podcast:*>
Available inRSS
Propertypodcast
29 | 30 | ## Types 31 | 32 | > [!INFO] 33 | > `TDate` represents date fields in the type definitions. When **parsing**, dates are returned as strings in their original format (see [Parsing › Handling Dates](/parsing/dates) for more details). When **generating**, dates should be provided as JavaScript `Date` objects. 34 | 35 | <<< @/../src/namespaces/podcast/common/types.ts#reference 36 | 37 | ## Related 38 | 39 | - **[iTunes Namespace](/reference/namespaces/itunes)** - Traditional podcast metadata 40 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 41 | -------------------------------------------------------------------------------- /src/opml/references/places.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Places 5 | Mon, 15 Jan 2024 10:45:22 GMT 6 | Sat, 20 Jan 2024 14:32:18 GMT 7 | Anna Smith 8 | https://annasmith.com 9 | 1,2,5,8,11,14 10 | 1 11 | 200 12 | 300 13 | 600 14 | 500 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/opml/references/script.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | workspace.examples.generateTestData 5 | Fri, 12 Jan 2024 18:30:45 GMT 6 | Sun, 18 Feb 2024 09:15:22 GMT 7 | Anna Smith 8 | anna@example.com 9 | 1,2,4 10 | 1 11 | 70 12 | 40 13 | 310 14 | 470 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/namespaces/rawvoice/common/types.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike } from '../../../common/types.js' 2 | 3 | // #region reference 4 | export namespace RawVoiceNs { 5 | export type Rating = { 6 | value?: string 7 | tv?: string 8 | movie?: string 9 | } 10 | 11 | export type LiveStream = { 12 | url?: string 13 | schedule?: TDate 14 | duration?: string 15 | type?: string 16 | } 17 | 18 | export type Poster = { 19 | url?: string 20 | } 21 | 22 | export type AlternateEnclosure = { 23 | src?: string 24 | type?: string 25 | length?: number 26 | } 27 | 28 | export type Subscribe = Record 29 | 30 | export type Metamark = { 31 | type?: string 32 | link?: string 33 | position?: number 34 | duration?: number 35 | value?: string 36 | } 37 | 38 | export type Donate = { 39 | href: string 40 | value?: string 41 | } 42 | 43 | export type Feed = { 44 | rating?: Rating 45 | liveEmbed?: string 46 | flashLiveStream?: LiveStream 47 | httpLiveStream?: LiveStream 48 | shoutcastLiveStream?: LiveStream 49 | liveStream?: LiveStream 50 | location?: string 51 | frequency?: string 52 | mycast?: boolean 53 | subscribe?: Subscribe 54 | donate?: Donate 55 | } 56 | 57 | export type Item = { 58 | poster?: Poster 59 | isHd?: boolean 60 | embed?: string 61 | webm?: AlternateEnclosure 62 | mp4?: AlternateEnclosure 63 | metamarks?: Array 64 | } 65 | } 66 | // #endregion reference 67 | -------------------------------------------------------------------------------- /src/namespaces/itunes/common/types.ts: -------------------------------------------------------------------------------- 1 | // #region reference 2 | export namespace ItunesNs { 3 | export type Category = { 4 | text: string 5 | categories?: Array 6 | } 7 | 8 | export type Owner = { 9 | name?: string 10 | email?: string 11 | } 12 | 13 | export type Item = { 14 | duration?: number 15 | image?: string 16 | explicit?: boolean 17 | author?: string 18 | title?: string 19 | episode?: number 20 | season?: number 21 | episodeType?: string 22 | block?: boolean 23 | /** @deprecated Use standard RSS description instead. No longer used by Apple Podcasts. */ 24 | summary?: string 25 | /** @deprecated No longer used by Apple Podcasts. */ 26 | subtitle?: string 27 | /** @deprecated No longer used for search in Apple Podcasts. */ 28 | keywords?: Array 29 | } 30 | 31 | export type Feed = { 32 | image?: string 33 | categories?: Array 34 | explicit?: boolean 35 | author?: string 36 | title?: string 37 | type?: string 38 | newFeedUrl?: string 39 | block?: boolean 40 | complete?: boolean 41 | applePodcastsVerify?: string 42 | /** @deprecated Use standard RSS description instead. No longer used by Apple Podcasts. */ 43 | summary?: string 44 | /** @deprecated No longer used by Apple Podcasts. */ 45 | subtitle?: string 46 | /** @deprecated No longer used for search in Apple Podcasts. */ 47 | keywords?: Array 48 | /** @deprecated No longer supported by Apple Podcasts. */ 49 | owner?: Owner 50 | } 51 | } 52 | // #endregion reference 53 | -------------------------------------------------------------------------------- /docs/reference/namespaces/atom.md: -------------------------------------------------------------------------------- 1 | # Atom Namespace Reference 2 | 3 | The Atom namespace allows RSS and RDF feeds to include Atom-specific elements, providing richer metadata and linking capabilities. This namespace provides partial Atom elements that can be embedded within other feed formats. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Namespace URIhttp://www.w3.org/2005/Atom
SpecificationRFC 4287 - Atom Syndication Format
Prefix<atom:*>
Available in 22 | RSS, 23 | RDF 24 |
Propertyatom
32 | 33 | ## Types 34 | 35 | > [!INFO] 36 | > `TDate` represents date fields in the type definitions. When **parsing**, dates are returned as strings in their original format (see [Parsing › Handling Dates](/parsing/dates) for more details). When **generating**, dates should be provided as JavaScript `Date` objects. 37 | 38 | <<< @/../src/feeds/atom/common/types.ts#reference 39 | 40 | ## Related 41 | 42 | - **[Atom Feed Fields](/reference/feeds/atom)** - Complete Atom feed specification 43 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 44 | -------------------------------------------------------------------------------- /src/feeds/rss/references/rss-091.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sample Feed", 3 | "link": "http://example.org/", 4 | "description": "For documentation only", 5 | "language": "en", 6 | "copyright": "Copyright 2004, Mark Pilgrim", 7 | "managingEditor": "editor@example.org", 8 | "webMaster": "webmaster@example.org", 9 | "pubDate": "Sat, 19 Mar 1988 07:15:00 GMT", 10 | "lastBuildDate": "Sat, 19 Mar 1988 07:15:00 GMT", 11 | "image": { 12 | "url": "http://example.org/banner.png", 13 | "title": "Example banner", 14 | "link": "http://example.org/", 15 | "description": "Quos placeat quod ea temporibus ratione", 16 | "height": 15, 17 | "width": 80 18 | }, 19 | "rating": "(PICS-1.1 \"http://www.rsac.org/ratingsv01.html\" l by \"webmaster@example.com\" on \"2006.01.29T10:09-0800\" r (n 0 s 0 v 0 l 0))", 20 | "textInput": { 21 | "title": "Search", 22 | "description": "Search this site:", 23 | "name": "q", 24 | "link": "http://example.org/mt/mt-search.cgi" 25 | }, 26 | "skipHours": [0, 20, 21, 22, 23], 27 | "skipDays": ["Monday", "Wednesday", "Friday"], 28 | "items": [ 29 | { 30 | "title": "First item title", 31 | "link": "http://example.org/item/1", 32 | "description": "Watch out for nasty tricks" 33 | }, 34 | { 35 | "title": "Second item title", 36 | "link": "http://example.org/item/2", 37 | "description": "Watch out for nasty tricks" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /docs/reference/namespaces/dcterms.md: -------------------------------------------------------------------------------- 1 | # Dublin Core Terms Namespace Reference 2 | 3 | The Dublin Core Terms namespace provides extended metadata elements based on the Dublin Core Metadata Initiative, offering comprehensive resource description capabilities. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Namespace URIhttp://purl.org/dc/terms/
SpecificationDublin Core Terms
Prefix<dcterms:*>
Available in 22 | RSS, 23 | Atom, 24 | RDF 25 |
Propertydcterms
33 | 34 | ## Types 35 | 36 | > [!INFO] 37 | > `TDate` represents date fields in the type definitions. When **parsing**, dates are returned as strings in their original format (see [Parsing › Handling Dates](/parsing/dates) for more details). When **generating**, dates should be provided as JavaScript `Date` objects. 38 | 39 | <<< @/../src/namespaces/dcterms/common/types.ts#reference 40 | 41 | ## Related 42 | 43 | - **[Dublin Core Namespace](/reference/namespaces/dc)** - Basic Dublin Core elements 44 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 45 | -------------------------------------------------------------------------------- /docs/reference/namespaces/dc.md: -------------------------------------------------------------------------------- 1 | # Dublin Core Namespace Reference 2 | 3 | The Dublin Core namespace provides standardized metadata elements for describing digital resources. It offers a simple and effective way to add bibliographic information to feeds and items. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Namespace URIhttp://purl.org/dc/elements/1.1/
SpecificationDublin Core Metadata Terms
Prefix<dc:*>
Available in 22 | RSS, 23 | Atom, 24 | RDF 25 |
Propertydc
33 | 34 | ## Types 35 | 36 | > [!INFO] 37 | > `TDate` represents date fields in the type definitions. When **parsing**, dates are returned as strings in their original format (see [Parsing › Handling Dates](/parsing/dates) for more details). When **generating**, dates should be provided as JavaScript `Date` objects. 38 | 39 | <<< @/../src/namespaces/dc/common/types.ts#reference 40 | 41 | ## Related 42 | 43 | - **[Dublin Core Terms](/reference/namespaces/dcterms)** - Extended Dublin Core metadata 44 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 45 | -------------------------------------------------------------------------------- /docs/reference/namespaces/prism.md: -------------------------------------------------------------------------------- 1 | # PRISM Namespace Reference 2 | 3 | The PRISM (Publishing Requirements for Industry Standard Metadata) namespace provides comprehensive metadata elements for scholarly and academic publishing, including bibliographic information, page ranges, DOIs, and publication details. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
Namespace URIhttp://prismstandard.org/namespaces/basic/3.0/
SpecificationPRISM Specification
Prefix<prism:*>
Available in 22 | RSS 23 |
Propertyprism
31 | 32 | ## Types 33 | 34 | > [!INFO] 35 | > `TDate` represents date fields in the type definitions. When **parsing**, dates are returned as strings in their original format (see [Parsing › Handling Dates](/parsing/dates) for more details). When **generating**, dates should be provided as JavaScript `Date` objects. 36 | 37 | <<< @/../src/namespaces/prism/common/types.ts#reference 38 | 39 | ## Related 40 | 41 | - **[Dublin Core Namespace](/reference/namespaces/dc)** - Basic Dublin Core elements 42 | - **[Dublin Core Terms Namespace](/reference/namespaces/dcterms)** - Extended Dublin Core metadata 43 | - **[Parsing Namespaces](/parsing/namespaces)** - How namespace parsing works 44 | -------------------------------------------------------------------------------- /src/namespaces/arxiv/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseSingularOf, 5 | parseString, 6 | retrieveText, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { ArxivNs } from '../common/types.js' 10 | 11 | export const parsePrimaryCategory: ParsePartialUtil = (value) => { 12 | if (!isObject(value)) { 13 | return 14 | } 15 | 16 | const primaryCategory = { 17 | term: parseString(value['@term']), 18 | scheme: parseString(value['@scheme']), 19 | label: parseString(value['@label']), 20 | } 21 | 22 | return trimObject(primaryCategory) 23 | } 24 | 25 | export const retrieveAuthor: ParsePartialUtil = (value) => { 26 | if (!isObject(value)) { 27 | return 28 | } 29 | 30 | const author = { 31 | affiliation: parseSingularOf(value['arxiv:affiliation'], (value) => 32 | parseString(retrieveText(value)), 33 | ), 34 | } 35 | 36 | return trimObject(author) 37 | } 38 | 39 | export const retrieveEntry: ParsePartialUtil = (value) => { 40 | if (!isObject(value)) { 41 | return 42 | } 43 | 44 | const entry = { 45 | comment: parseSingularOf(value['arxiv:comment'], (value) => parseString(retrieveText(value))), 46 | journalRef: parseSingularOf(value['arxiv:journal_ref'], (value) => 47 | parseString(retrieveText(value)), 48 | ), 49 | doi: parseSingularOf(value['arxiv:doi'], (value) => parseString(retrieveText(value))), 50 | primaryCategory: parseSingularOf(value['arxiv:primary_category'], parsePrimaryCategory), 51 | } 52 | 53 | return trimObject(entry) 54 | } 55 | -------------------------------------------------------------------------------- /src/namespaces/opensearch/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseArrayOf, 5 | parseNumber, 6 | parseSingularOf, 7 | parseString, 8 | retrieveText, 9 | trimObject, 10 | } from '../../../common/utils.js' 11 | import type { OpenSearchNs } from '../common/types.js' 12 | 13 | export const parseQuery: ParsePartialUtil = (value) => { 14 | if (!isObject(value)) { 15 | return 16 | } 17 | 18 | const query = { 19 | role: parseString(value['@role']), 20 | searchTerms: parseString(value['@searchterms']), 21 | count: parseNumber(value['@count']), 22 | startIndex: parseNumber(value['@startindex']), 23 | startPage: parseNumber(value['@startpage']), 24 | language: parseString(value['@language']), 25 | inputEncoding: parseString(value['@inputencoding']), 26 | outputEncoding: parseString(value['@outputencoding']), 27 | } 28 | 29 | return trimObject(query) 30 | } 31 | 32 | export const retrieveFeed: ParsePartialUtil = (value) => { 33 | if (!isObject(value)) { 34 | return 35 | } 36 | 37 | const feed = { 38 | totalResults: parseSingularOf(value['opensearch:totalresults'], (value) => 39 | parseNumber(retrieveText(value)), 40 | ), 41 | startIndex: parseSingularOf(value['opensearch:startindex'], (value) => 42 | parseNumber(retrieveText(value)), 43 | ), 44 | itemsPerPage: parseSingularOf(value['opensearch:itemsperpage'], (value) => 45 | parseNumber(retrieveText(value)), 46 | ), 47 | queries: parseArrayOf(value['opensearch:query'], parseQuery), 48 | } 49 | 50 | return trimObject(feed) 51 | } 52 | -------------------------------------------------------------------------------- /src/feeds/rdf/references/rdf-10.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "XML.com", 3 | "link": "http://xml.com/pub", 4 | "description": "XML.com features a rich mix of information and services\n for the XML community.", 5 | "image": { 6 | "title": "XML.com", 7 | "link": "http://www.xml.com", 8 | "url": "http://xml.com/universal/images/xml_tiny.gif", 9 | "rdf": { 10 | "about": "http://xml.com/universal/images/xml_tiny.gif" 11 | } 12 | }, 13 | "items": [ 14 | { 15 | "title": "Processing Inclusions with XSLT", 16 | "link": "http://xml.com/pub/2000/08/09/xslt/xslt.html", 17 | "description": "Processing document inclusions with general XML tools can be\n problematic. This article proposes a way of preserving inclusion\n information through SAX-based processing.", 18 | "rdf": { 19 | "about": "http://xml.com/pub/2000/08/09/xslt/xslt.html" 20 | } 21 | }, 22 | { 23 | "title": "Putting RDF to Work", 24 | "link": "http://xml.com/pub/2000/08/09/rdfdb/index.html", 25 | "description": "Tool and API support for the Resource Description Framework\n is slowly coming of age. Edd Dumbill takes a look at RDFDB,\n one of the most exciting new RDF toolkits.", 26 | "rdf": { 27 | "about": "http://xml.com/pub/2000/08/09/rdfdb/index.html" 28 | } 29 | } 30 | ], 31 | "textInput": { 32 | "title": "Search XML.com", 33 | "description": "Search XML.com's XML collection", 34 | "name": "s", 35 | "link": "http://search.xml.com", 36 | "rdf": { 37 | "about": "http://search.xml.com" 38 | } 39 | }, 40 | "rdf": { 41 | "about": "http://www.xml.com/xml/news.rss" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/namespaces/rdf/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseArrayOf, 5 | parseSingularOf, 6 | parseString, 7 | retrieveRdfResourceOrText, 8 | retrieveText, 9 | trimObject, 10 | } from '../../../common/utils.js' 11 | import type { RdfNs } from '../common/types.js' 12 | 13 | export const retrieveAbout: ParsePartialUtil = (value) => { 14 | if (!isObject(value)) { 15 | return 16 | } 17 | 18 | const about = { 19 | about: parseString(value['@about']) ?? parseString(value['@rdf:about']), 20 | } 21 | 22 | return trimObject(about) 23 | } 24 | 25 | /** @internal General RDF element kept for potential future use when all RDF data is needed. */ 26 | export const retrieveElement: ParsePartialUtil = (value) => { 27 | if (!isObject(value)) { 28 | return 29 | } 30 | 31 | const element = { 32 | about: parseString(value['@about']) ?? parseString(value['@rdf:about']), 33 | resource: parseString(value['@resource']) ?? parseString(value['@rdf:resource']), 34 | id: parseString(value['@id']) ?? parseString(value['@rdf:id']), 35 | nodeId: parseString(value['@nodeid']) ?? parseString(value['@rdf:nodeid']), 36 | parseType: parseString(value['@parsetype']) ?? parseString(value['@rdf:parsetype']), 37 | datatype: parseString(value['@datatype']) ?? parseString(value['@rdf:datatype']), 38 | type: parseSingularOf(value.type ?? value['rdf:type'], (value) => 39 | retrieveRdfResourceOrText(value, parseString), 40 | ), 41 | value: parseArrayOf(value.value ?? value['rdf:value'], (value) => retrieveText(value)), 42 | } 43 | 44 | return trimObject(element) 45 | } 46 | -------------------------------------------------------------------------------- /src/feeds/rss/parse/config.ts: -------------------------------------------------------------------------------- 1 | import { XMLParser } from 'fast-xml-parser' 2 | import { parserConfig } from '../../../common/config.js' 3 | 4 | export const stopNodes = [ 5 | 'rss.channel.title', 6 | 'rss.channel.link', 7 | 'rss.channel.description', 8 | 'rss.channel.language', 9 | 'rss.channel.copyright', 10 | 'rss.channel.managingeditor', 11 | 'rss.channel.webmaster', 12 | 'rss.channel.pubdate', 13 | 'rss.channel.lastbuilddate', 14 | 'rss.channel.author', 15 | 'rss.channel.category', 16 | 'rss.channel.generator', 17 | 'rss.channel.docs', 18 | 'rss.channel.cloud', 19 | 'rss.channel.ttl', 20 | 'rss.channel.image.description', 21 | 'rss.channel.image.height', 22 | 'rss.channel.image.link', 23 | 'rss.channel.image.title', 24 | 'rss.channel.image.url', 25 | 'rss.channel.image.width', 26 | 'rss.channel.rating', 27 | 'rss.channel.textinput.title', 28 | 'rss.channel.textinput.description', 29 | 'rss.channel.textinput.name', 30 | 'rss.channel.textinput.link', 31 | 'rss.channel.skiphours.hour', 32 | 'rss.channel.skipdays.day', 33 | 'rss.channel.item.title', 34 | 'rss.channel.item.link', 35 | 'rss.channel.item.description', 36 | // INFO: Added support for nested *.name under author to support cases as 37 | // described here: https://github.com/macieklamberski/feedsmith/issues/22. 38 | 'rss.channel.item.author.name', 39 | 'rss.channel.item.category', 40 | 'rss.channel.item.comments', 41 | 'rss.channel.item.enclosure', 42 | 'rss.channel.item.guid', 43 | 'rss.channel.item.pubdate', 44 | 'rss.channel.item.source', 45 | // TODO: What about the namespaces? 46 | ] 47 | 48 | export const parser = new XMLParser({ 49 | ...parserConfig, 50 | stopNodes, 51 | }) 52 | -------------------------------------------------------------------------------- /src/opml/common/types.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike, ExtraFields, ParseOptions } from '../../common/types.js' 2 | 3 | export type MainOptions
= ReadonlyArray> = ParseOptions & { 4 | extraOutlineAttributes?: A 5 | } 6 | 7 | // #region reference 8 | export namespace Opml { 9 | export type Outline< 10 | TDate extends DateLike, 11 | A extends ReadonlyArray = ReadonlyArray, 12 | > = { 13 | text: string 14 | type?: string 15 | isComment?: boolean 16 | isBreakpoint?: boolean 17 | created?: TDate 18 | category?: string 19 | description?: string 20 | xmlUrl?: string 21 | htmlUrl?: string 22 | language?: string 23 | title?: string 24 | version?: string 25 | url?: string 26 | outlines?: Array> 27 | } & ExtraFields 28 | 29 | export type Head = { 30 | title?: string 31 | dateCreated?: TDate 32 | dateModified?: TDate 33 | ownerName?: string 34 | ownerEmail?: string 35 | ownerId?: string 36 | docs?: string 37 | expansionState?: Array 38 | vertScrollState?: number 39 | windowTop?: number 40 | windowLeft?: number 41 | windowBottom?: number 42 | windowRight?: number 43 | } 44 | 45 | export type Body< 46 | TDate extends DateLike, 47 | A extends ReadonlyArray = ReadonlyArray, 48 | > = { 49 | outlines?: Array> 50 | } 51 | 52 | export type Document< 53 | TDate extends DateLike, 54 | A extends ReadonlyArray = ReadonlyArray, 55 | > = { 56 | head?: Head 57 | body?: Body 58 | } 59 | } 60 | // #endregion reference 61 | -------------------------------------------------------------------------------- /src/opml/references/directory.json: -------------------------------------------------------------------------------- 1 | { 2 | "head": { 3 | "title": "Tech Blog Directory", 4 | "dateCreated": "Mon, 22 Jan 2024 11:22:45 GMT", 5 | "dateModified": "Fri, 16 Feb 2024 16:48:32 GMT", 6 | "ownerName": "Anna Smith", 7 | "ownerEmail": "anna@example.com", 8 | "vertScrollState": 1, 9 | "windowTop": 100, 10 | "windowLeft": 460, 11 | "windowBottom": 380, 12 | "windowRight": 960 13 | }, 14 | "body": { 15 | "outlines": [ 16 | { 17 | "text": "TechCrunch Top 50 Startups", 18 | "created": "Fri, 16 Feb 2024 16:48:12 GMT", 19 | "type": "link", 20 | "url": "http://tech.example.com/blogs/startups50.opml" 21 | }, 22 | { 23 | "text": "WebDevCon 2024 Speakers", 24 | "created": "Tue, 13 Feb 2024 14:22:18 GMT", 25 | "type": "link", 26 | "url": "http://static.webdevcon.org/2024/speakers.opml" 27 | }, 28 | { 29 | "text": "Sarah Johnson's directory of Tech podcasts", 30 | "type": "link", 31 | "url": "http://sarahjohnson.com/tech-podcasts.opml" 32 | }, 33 | { 34 | "text": "Mark Davis's Developer Tools directory", 35 | "type": "link", 36 | "url": "http://homepage.example.com/markdavis/tools/devToolsDirectory.opml" 37 | }, 38 | { 39 | "text": "DevNews", 40 | "created": "Thu, 25 Jan 2024 09:45:30 GMT", 41 | "type": "link", 42 | "url": "http://dev.news.example.com/index.opml" 43 | }, 44 | { 45 | "text": "React Community archive", 46 | "created": "Mon, 05 Feb 2024 12:18:45 GMT", 47 | "type": "link", 48 | "url": "http://react-community.opml.org/index.opml" 49 | } 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/namespaces/slash/generate/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { generateItem } from './utils.js' 3 | 4 | describe('generateItem', () => { 5 | it('should generate valid item object with all properties', () => { 6 | const value = { 7 | section: 'Technology', 8 | department: 'News', 9 | comments: 42, 10 | hitParade: [1, 2, 3, 4, 5], 11 | } 12 | const expected = { 13 | 'slash:section': 'Technology', 14 | 'slash:department': 'News', 15 | 'slash:comments': 42, 16 | 'slash:hit_parade': '1,2,3,4,5', 17 | } 18 | 19 | expect(generateItem(value)).toEqual(expected) 20 | }) 21 | 22 | it('should generate item with minimal properties', () => { 23 | const value = { 24 | comments: 10, 25 | } 26 | const expected = { 27 | 'slash:comments': 10, 28 | } 29 | 30 | expect(generateItem(value)).toEqual(expected) 31 | }) 32 | 33 | it('should handle empty hit_parade array', () => { 34 | const value = { 35 | comments: 5, 36 | hitParade: [], 37 | } 38 | const expected = { 39 | 'slash:comments': 5, 40 | } 41 | 42 | expect(generateItem(value)).toEqual(expected) 43 | }) 44 | 45 | it('should handle object with only undefined/empty properties', () => { 46 | const value = { 47 | section: undefined, 48 | department: undefined, 49 | comments: undefined, 50 | hitParade: undefined, 51 | } 52 | 53 | expect(generateItem(value)).toBeUndefined() 54 | }) 55 | 56 | it('should handle empty object', () => { 57 | const value = {} 58 | 59 | expect(generateItem(value)).toBeUndefined() 60 | }) 61 | 62 | it('should handle non-object inputs gracefully', () => { 63 | expect(generateItem(undefined)).toBeUndefined() 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/opml/references/script.json: -------------------------------------------------------------------------------- 1 | { 2 | "head": { 3 | "title": "workspace.examples.generateTestData", 4 | "dateCreated": "Fri, 12 Jan 2024 18:30:45 GMT", 5 | "dateModified": "Sun, 18 Feb 2024 09:15:22 GMT", 6 | "ownerName": "Anna Smith", 7 | "ownerEmail": "anna@example.com", 8 | "expansionState": [1, 2, 4], 9 | "vertScrollState": 1, 10 | "windowTop": 70, 11 | "windowLeft": 40, 12 | "windowBottom": 310, 13 | "windowRight": 470 14 | }, 15 | "body": { 16 | "outlines": [ 17 | { 18 | "text": "Changes", 19 | "isComment": false, 20 | "outlines": [ 21 | { 22 | "text": "18/02/2024; 9:15:22 AM by AS", 23 | "outlines": [ 24 | { 25 | "text": "Change 'output' to 'export'." 26 | } 27 | ] 28 | }, 29 | { 30 | "text": "25/01/2024; 3:22:15 PM by AS", 31 | "isComment": true, 32 | "outlines": [ 33 | { 34 | "text": "Create test data by generating JSON files with random user information." 35 | } 36 | ] 37 | } 38 | ] 39 | }, 40 | { 41 | "text": "on createUserData (filename, count)", 42 | "outlines": [ 43 | { 44 | "text": "directory.ensurePath (filename)", 45 | "isBreakpoint": true 46 | }, 47 | { 48 | "text": "file.writeJsonFile (filename, string.generateRandomUsers (count))" 49 | } 50 | ] 51 | }, 52 | { 53 | "text": "local (outputDir = user.app.preferences.dataFolder + 'test\\\\users\\\\')" 54 | }, 55 | { 56 | "text": "for i = 1 to 10", 57 | "outlines": [ 58 | { 59 | "text": "createUserData (outputDir + 'batch' + i + '.json', random (50, 200))" 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/common/parse.ts: -------------------------------------------------------------------------------- 1 | import type { Atom } from '../feeds/atom/common/types.js' 2 | import { detect as detectAtomFeed } from '../feeds/atom/detect/index.js' 3 | import { parse as parseAtomFeed } from '../feeds/atom/parse/index.js' 4 | import type { Json } from '../feeds/json/common/types.js' 5 | import { detect as detectJsonFeed } from '../feeds/json/detect/index.js' 6 | import { parse as parseJsonFeed } from '../feeds/json/parse/index.js' 7 | import type { Rdf } from '../feeds/rdf/common/types.js' 8 | import { detect as detectRdfFeed } from '../feeds/rdf/detect/index.js' 9 | import { parse as parseRdfFeed } from '../feeds/rdf/parse/index.js' 10 | import type { Rss } from '../feeds/rss/common/types.js' 11 | import { detect as detectRssFeed } from '../feeds/rss/detect/index.js' 12 | import { parse as parseRssFeed } from '../feeds/rss/parse/index.js' 13 | import { locales } from './config.js' 14 | import type { DeepPartial, ParseOptions } from './types.js' 15 | import { parseJsonObject } from './utils.js' 16 | 17 | export type Parse = ( 18 | value: unknown, 19 | options?: ParseOptions, 20 | ) => 21 | | { format: 'rss'; feed: DeepPartial> } 22 | | { format: 'atom'; feed: DeepPartial> } 23 | | { format: 'rdf'; feed: DeepPartial> } 24 | | { format: 'json'; feed: DeepPartial> } 25 | 26 | export const parse: Parse = (value, options) => { 27 | if (detectRssFeed(value)) { 28 | return { format: 'rss', feed: parseRssFeed(value, options) } 29 | } 30 | 31 | if (detectAtomFeed(value)) { 32 | return { format: 'atom', feed: parseAtomFeed(value, options) } 33 | } 34 | 35 | if (detectRdfFeed(value)) { 36 | return { format: 'rdf', feed: parseRdfFeed(value, options) } 37 | } 38 | 39 | const json = parseJsonObject(value) 40 | 41 | if (detectJsonFeed(json)) { 42 | return { format: 'json', feed: parseJsonFeed(json, options) } 43 | } 44 | 45 | throw new Error(locales.unrecognizedFeedFormat) 46 | } 47 | -------------------------------------------------------------------------------- /src/namespaces/cc/parse/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ParsePartialUtil } from '../../../common/types.js' 2 | import { 3 | isObject, 4 | parseSingularOf, 5 | parseString, 6 | retrieveRdfResourceOrText, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { CcNs } from '../common/types.js' 10 | 11 | export const retrieveItemOrFeed: ParsePartialUtil = (value) => { 12 | if (!isObject(value)) { 13 | return 14 | } 15 | 16 | const result = { 17 | license: parseSingularOf(value['cc:license'], (value) => 18 | retrieveRdfResourceOrText(value, parseString), 19 | ), 20 | morePermissions: parseSingularOf(value['cc:morepermissions'], (value) => 21 | retrieveRdfResourceOrText(value, parseString), 22 | ), 23 | attributionName: parseSingularOf(value['cc:attributionname'], (value) => 24 | retrieveRdfResourceOrText(value, parseString), 25 | ), 26 | attributionURL: parseSingularOf(value['cc:attributionurl'], (value) => 27 | retrieveRdfResourceOrText(value, parseString), 28 | ), 29 | useGuidelines: parseSingularOf(value['cc:useguidelines'], (value) => 30 | retrieveRdfResourceOrText(value, parseString), 31 | ), 32 | permits: parseSingularOf(value['cc:permits'], (value) => 33 | retrieveRdfResourceOrText(value, parseString), 34 | ), 35 | requires: parseSingularOf(value['cc:requires'], (value) => 36 | retrieveRdfResourceOrText(value, parseString), 37 | ), 38 | prohibits: parseSingularOf(value['cc:prohibits'], (value) => 39 | retrieveRdfResourceOrText(value, parseString), 40 | ), 41 | jurisdiction: parseSingularOf(value['cc:jurisdiction'], (value) => 42 | retrieveRdfResourceOrText(value, parseString), 43 | ), 44 | legalcode: parseSingularOf(value['cc:legalcode'], (value) => 45 | retrieveRdfResourceOrText(value, parseString), 46 | ), 47 | deprecatedOn: parseSingularOf(value['cc:deprecatedon'], (value) => 48 | retrieveRdfResourceOrText(value, parseString), 49 | ), 50 | } 51 | 52 | return trimObject(result) 53 | } 54 | -------------------------------------------------------------------------------- /src/namespaces/acast/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { 3 | generateCdataString, 4 | generatePlainString, 5 | generateTextOrCdataString, 6 | isObject, 7 | trimObject, 8 | } from '../../../common/utils.js' 9 | import type { AcastNs } from '../common/types.js' 10 | 11 | export const generateSignature: GenerateUtil = (signature) => { 12 | if (!isObject(signature)) { 13 | return 14 | } 15 | 16 | const value = { 17 | '@key': generatePlainString(signature.key), 18 | '@algorithm': generatePlainString(signature.algorithm), 19 | ...generateTextOrCdataString(signature.value), 20 | } 21 | 22 | return trimObject(value) 23 | } 24 | 25 | export const generateNetwork: GenerateUtil = (network) => { 26 | if (!isObject(network)) { 27 | return 28 | } 29 | 30 | const value = { 31 | '@id': generatePlainString(network.id), 32 | '@slug': generatePlainString(network.slug), 33 | ...generateTextOrCdataString(network.value), 34 | } 35 | 36 | return trimObject(value) 37 | } 38 | 39 | export const generateFeed: GenerateUtil = (feed) => { 40 | if (!isObject(feed)) { 41 | return 42 | } 43 | 44 | const value = { 45 | 'acast:showId': generateCdataString(feed.showId), 46 | 'acast:showUrl': generateCdataString(feed.showUrl), 47 | 'acast:signature': generateSignature(feed.signature), 48 | 'acast:settings': generateCdataString(feed.settings), 49 | 'acast:network': generateNetwork(feed.network), 50 | 'acast:importedFeed': generateCdataString(feed.importedFeed), 51 | } 52 | 53 | return trimObject(value) 54 | } 55 | 56 | export const generateItem: GenerateUtil = (item) => { 57 | if (!isObject(item)) { 58 | return 59 | } 60 | 61 | const value = { 62 | 'acast:episodeId': generateCdataString(item.episodeId), 63 | 'acast:showId': generateCdataString(item.showId), 64 | 'acast:episodeUrl': generateCdataString(item.episodeUrl), 65 | 'acast:settings': generateCdataString(item.settings), 66 | } 67 | 68 | return trimObject(value) 69 | } 70 | -------------------------------------------------------------------------------- /src/feeds/rdf/references/rdf-10.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | XML.com 8 | http://xml.com/pub 9 | 10 | XML.com features a rich mix of information and services 11 | for the XML community. 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | XML.com 24 | http://www.xml.com 25 | http://xml.com/universal/images/xml_tiny.gif 26 | 27 | 28 | Processing Inclusions with XSLT 29 | http://xml.com/pub/2000/08/09/xslt/xslt.html 30 | 31 | Processing document inclusions with general XML tools can be 32 | problematic. This article proposes a way of preserving inclusion 33 | information through SAX-based processing. 34 | 35 | 36 | 37 | Putting RDF to Work 38 | http://xml.com/pub/2000/08/09/rdfdb/index.html 39 | 40 | Tool and API support for the Resource Description Framework 41 | is slowly coming of age. Edd Dumbill takes a look at RDFDB, 42 | one of the most exciting new RDF toolkits. 43 | 44 | 45 | 46 | Search XML.com 47 | Search XML.com's XML collection 48 | s 49 | http://search.xml.com 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/namespaces/googleplay/generate/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateUtil } from '../../../common/types.js' 2 | import { 3 | generateCdataString, 4 | generatePlainString, 5 | generateYesNoBoolean, 6 | isObject, 7 | trimArray, 8 | trimObject, 9 | } from '../../../common/utils.js' 10 | import type { GooglePlayNs } from '../common/types.js' 11 | 12 | const generateImage: GenerateUtil = (image) => { 13 | if (!isObject(image)) { 14 | return 15 | } 16 | 17 | const value = { 18 | '@href': generatePlainString(image.href), 19 | } 20 | 21 | return trimObject(value) 22 | } 23 | 24 | const generateCategory: GenerateUtil = (category) => { 25 | const value = { 26 | '@text': generatePlainString(category), 27 | } 28 | 29 | return trimObject(value) 30 | } 31 | 32 | const generateExplicit: GenerateUtil = (explicit) => { 33 | if (explicit === 'clean') { 34 | return explicit 35 | } 36 | 37 | return generateYesNoBoolean(explicit) 38 | } 39 | 40 | export const generateItem: GenerateUtil = (item) => { 41 | if (!isObject(item)) { 42 | return 43 | } 44 | 45 | return trimObject({ 46 | 'googleplay:author': generatePlainString(item.author), 47 | 'googleplay:description': generateCdataString(item.description), 48 | 'googleplay:explicit': generateExplicit(item.explicit), 49 | 'googleplay:block': generateYesNoBoolean(item.block), 50 | 'googleplay:image': generateImage(item.image), 51 | }) 52 | } 53 | 54 | export const generateFeed: GenerateUtil = (feed) => { 55 | if (!isObject(feed)) { 56 | return 57 | } 58 | 59 | return trimObject({ 60 | 'googleplay:author': generatePlainString(feed.author), 61 | 'googleplay:description': generateCdataString(feed.description), 62 | 'googleplay:explicit': generateExplicit(feed.explicit), 63 | 'googleplay:block': generateYesNoBoolean(feed.block), 64 | 'googleplay:image': generateImage(feed.image), 65 | 'googleplay:new-feed-url': generatePlainString(feed.newFeedUrl), 66 | 'googleplay:email': generatePlainString(feed.email), 67 | 'googleplay:category': trimArray(feed.categories, generateCategory), 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /src/feeds/rdf/common/types.ts: -------------------------------------------------------------------------------- 1 | import type { DateLike } from '../../../common/types.js' 2 | import type { AdminNs } from '../../../namespaces/admin/common/types.js' 3 | import type { AtomNs } from '../../../namespaces/atom/common/types.js' 4 | import type { ContentNs } from '../../../namespaces/content/common/types.js' 5 | import type { DcNs } from '../../../namespaces/dc/common/types.js' 6 | import type { DcTermsNs } from '../../../namespaces/dcterms/common/types.js' 7 | import type { GeoRssNs } from '../../../namespaces/georss/common/types.js' 8 | import type { MediaNs } from '../../../namespaces/media/common/types.js' 9 | import type { RdfNs } from '../../../namespaces/rdf/common/types.js' 10 | import type { SlashNs } from '../../../namespaces/slash/common/types.js' 11 | import type { SyNs } from '../../../namespaces/sy/common/types.js' 12 | import type { WfwNs } from '../../../namespaces/wfw/common/types.js' 13 | 14 | // #region reference 15 | export namespace Rdf { 16 | export type Image = { 17 | title: string 18 | link: string 19 | url?: string 20 | rdf?: RdfNs.About 21 | } 22 | 23 | export type TextInput = { 24 | title: string 25 | description: string 26 | name: string 27 | link: string 28 | rdf?: RdfNs.About 29 | } 30 | 31 | export type Item = { 32 | title: string 33 | link: string 34 | description?: string 35 | rdf?: RdfNs.About 36 | atom?: AtomNs.Entry 37 | dc?: DcNs.ItemOrFeed 38 | content?: ContentNs.Item 39 | slash?: SlashNs.Item 40 | media?: MediaNs.ItemOrFeed 41 | georss?: GeoRssNs.ItemOrFeed 42 | dcterms?: DcTermsNs.ItemOrFeed 43 | wfw?: WfwNs.Item 44 | } 45 | 46 | export type Feed = { 47 | title: string 48 | link: string 49 | description: string 50 | image?: Image 51 | items?: Array> 52 | textInput?: TextInput 53 | rdf?: RdfNs.About 54 | atom?: AtomNs.Feed 55 | dc?: DcNs.ItemOrFeed 56 | sy?: SyNs.Feed 57 | media?: MediaNs.ItemOrFeed 58 | georss?: GeoRssNs.ItemOrFeed 59 | dcterms?: DcTermsNs.ItemOrFeed 60 | admin?: AdminNs.Feed 61 | } 62 | } 63 | // #endregion reference 64 | -------------------------------------------------------------------------------- /src/feeds/atom/parse/config.ts: -------------------------------------------------------------------------------- 1 | import { XMLParser } from 'fast-xml-parser' 2 | import { parserConfig } from '../../../common/config.js' 3 | 4 | export const stopNodes = [ 5 | 'feed.author.name', 6 | 'feed.author.uri', 7 | 'feed.author.url', // Atom 0.3. 8 | 'feed.author.email', 9 | 'feed.category', 10 | 'feed.contributor.name', 11 | 'feed.contributor.uri', 12 | 'feed.contributor.url', // Atom 0.3. 13 | 'feed.contributor.email', 14 | 'feed.generator', 15 | 'feed.icon', 16 | 'feed.id', 17 | 'feed.link', 18 | 'feed.logo', 19 | 'feed.rights', 20 | 'feed.subtitle', 21 | 'feed.tagline', // Atom 0.3. 22 | 'feed.title', 23 | 'feed.updated', 24 | 'feed.modified', // Atom 0.3. 25 | 'feed.entry.author.name', 26 | 'feed.entry.author.uri', 27 | 'feed.entry.author.url', // Atom 0.3. 28 | 'feed.entry.author.email', 29 | 'feed.entry.category', 30 | 'feed.entry.content', 31 | 'feed.entry.contributor.name', 32 | 'feed.entry.contributor.uri', 33 | 'feed.entry.contributor.url', // Atom 0.3. 34 | 'feed.entry.contributor.email', 35 | 'feed.entry.id', 36 | 'feed.entry.link', 37 | 'feed.entry.published', 38 | 'feed.entry.issued', // Atom 0.3. 39 | 'feed.entry.created', // Atom 0.3. 40 | 'feed.entry.rights', 41 | 'feed.entry.source.author.name', 42 | 'feed.entry.source.author.uri', 43 | 'feed.entry.source.author.url', // Atom 0.3. 44 | 'feed.entry.source.author.email', 45 | 'feed.entry.source.category', 46 | 'feed.entry.source.contributor.name', 47 | 'feed.entry.source.contributor.uri', 48 | 'feed.entry.source.contributor.url', // Atom 0.3. 49 | 'feed.entry.source.contributor.email', 50 | 'feed.entry.source.generator', 51 | 'feed.entry.source.icon', 52 | 'feed.entry.source.id', 53 | 'feed.entry.source.link', 54 | 'feed.entry.source.logo', 55 | 'feed.entry.source.rights', 56 | 'feed.entry.source.subtitle', 57 | 'feed.entry.source.title,', 58 | 'feed.entry.source.updated', 59 | 'feed.entry.source.modified', // Atom 0.3. 60 | 'feed.entry.summary', 61 | 'feed.entry.title', 62 | 'feed.entry.updated', 63 | 'feed.entry.modified', // Atom 0.3. 64 | // TODO: What about the namespaces? 65 | ] 66 | 67 | export const parser = new XMLParser({ 68 | ...parserConfig, 69 | stopNodes, 70 | }) 71 | -------------------------------------------------------------------------------- /src/feeds/rss/references/rss-091.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample Feed 6 | http://example.org/ 7 | For documentation <em>only</em> 8 | en 9 | Copyright 2004, Mark Pilgrim 10 | editor@example.org 11 | webmaster@example.org 12 | Sat, 19 Mar 1988 07:15:00 GMT 13 | Sat, 19 Mar 1988 07:15:00 GMT 14 | (PICS-1.1 "http://www.rsac.org/ratingsv01.html" l by "webmaster@example.com" on "2006.01.29T10:09-0800" r (n 0 s 0 v 0 l 0)) 15 | 16 | Example banner 17 | http://example.org/banner.png 18 | http://example.org/ 19 | 80 20 | 15 21 | Quos placeat quod ea temporibus ratione 22 | 23 | 24 | Search 25 | Search this site: 26 | q 27 | http://example.org/mt/mt-search.cgi 28 | 29 | 30 | 0 31 | 20 32 | 21 33 | 22 34 | 23 35 | 36 | 37 | Monday 38 | Wednesday 39 | Friday 40 | 41 | 42 | First item title 43 | http://example.org/item/1 44 | Watch out for <span style="background: url(javascript:window.location='http://example.org/')"> nasty tricks</span> 45 | 46 | 47 | Second item title 48 | http://example.org/item/2 49 | Watch out for <span style="background: url(javascript:window.location='http://example.org/')"> nasty tricks</span> 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/opml/references/places.json: -------------------------------------------------------------------------------- 1 | { 2 | "head": { 3 | "title": "Places", 4 | "dateCreated": "Mon, 15 Jan 2024 10:45:22 GMT", 5 | "dateModified": "Sat, 20 Jan 2024 14:32:18 GMT", 6 | "ownerName": "Anna Smith", 7 | "ownerId": "https://annasmith.com", 8 | "expansionState": [1, 2, 5, 8, 11, 14], 9 | "vertScrollState": 1, 10 | "windowTop": 200, 11 | "windowLeft": 300, 12 | "windowBottom": 600, 13 | "windowRight": 500 14 | }, 15 | "body": { 16 | "outlines": [ 17 | { 18 | "text": "Countries I've visited", 19 | "outlines": [ 20 | { 21 | "text": "France", 22 | "outlines": [ 23 | { 24 | "text": "Paris" 25 | }, 26 | { 27 | "text": "Lyon" 28 | } 29 | ] 30 | }, 31 | { 32 | "text": "Italy", 33 | "outlines": [ 34 | { 35 | "text": "Rome" 36 | }, 37 | { 38 | "text": "Venice" 39 | }, 40 | { 41 | "text": "Florence" 42 | }, 43 | { 44 | "text": "Milan" 45 | } 46 | ] 47 | }, 48 | { 49 | "text": "Germany", 50 | "outlines": [ 51 | { 52 | "text": "Berlin" 53 | }, 54 | { 55 | "text": "Munich" 56 | } 57 | ] 58 | }, 59 | { 60 | "text": "Spain", 61 | "outlines": [ 62 | { 63 | "text": "Barcelona" 64 | } 65 | ] 66 | }, 67 | { 68 | "text": "Portugal", 69 | "type": "include", 70 | "url": "https://annasmith.com/portugal.opml" 71 | }, 72 | { 73 | "text": "United Kingdom", 74 | "outlines": [ 75 | { 76 | "text": "London" 77 | }, 78 | { 79 | "text": "Manchester" 80 | }, 81 | { 82 | "text": "Edinburgh" 83 | } 84 | ] 85 | } 86 | ] 87 | } 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feedsmith", 3 | "description": "Fast, all‑in‑one feed parser and generator for RSS, Atom, RDF, and JSON Feed, with support for Podcast, iTunes, Dublin Core, and OPML files.", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/macieklamberski/feedsmith.git" 7 | }, 8 | "homepage": "https://feedsmith.dev", 9 | "bugs": { 10 | "url": "https://github.com/macieklamberski/feedsmith/issues" 11 | }, 12 | "license": "MIT", 13 | "author": "Maciej Lamberski", 14 | "sideEffects": false, 15 | "keywords": [ 16 | "rss to json", 17 | "rss reader", 18 | "rss parser", 19 | "rss generator", 20 | "opml parser", 21 | "opml to json", 22 | "rss podcast", 23 | "feed parser", 24 | "feed generator", 25 | "atom", 26 | "rss", 27 | "rdf", 28 | "jsonfeed", 29 | "opml" 30 | ], 31 | "type": "module", 32 | "main": "./dist/index.cjs", 33 | "types": "./dist/index.d.cts", 34 | "typesVersions": { 35 | "*": { 36 | "types": [ 37 | "./dist/types.d.ts" 38 | ] 39 | } 40 | }, 41 | "exports": { 42 | ".": { 43 | "import": { 44 | "types": "./dist/index.d.ts", 45 | "default": "./dist/index.js" 46 | }, 47 | "require": { 48 | "types": "./dist/index.d.cts", 49 | "default": "./dist/index.cjs" 50 | } 51 | }, 52 | "./types": { 53 | "import": { 54 | "types": "./dist/types.d.ts", 55 | "default": "./dist/types.js" 56 | }, 57 | "require": { 58 | "types": "./dist/types.d.cts", 59 | "default": "./dist/types.cjs" 60 | } 61 | }, 62 | "./package.json": "./package.json" 63 | }, 64 | "files": [ 65 | "dist" 66 | ], 67 | "scripts": { 68 | "prepare": "lefthook install", 69 | "build": "tsdown src/index.ts src/types.ts --format cjs,esm --dts --clean --unbundle --no-fixed-extension", 70 | "docs:dev": "vitepress dev docs", 71 | "docs:build": "vitepress build docs", 72 | "docs:preview": "vitepress preview docs" 73 | }, 74 | "dependencies": { 75 | "entities": "^7.0.0", 76 | "fast-xml-parser": "^5.3.3" 77 | }, 78 | "devDependencies": { 79 | "@types/bun": "^1.3.5", 80 | "kvalita": "^1.8.0", 81 | "tsdown": "^0.17.4", 82 | "vitepress": "^1.6.4", 83 | "vitepress-plugin-llms": "^1.9.3" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /compatibility/README.md: -------------------------------------------------------------------------------- 1 | # Compatibility 2 | 3 | Test suite validating correct module resolution and type definitions across all common consumer environments. 4 | 5 | ## Purpose 6 | 7 | This test suite validates that feedsmith's dual ESM/CJS package exports work correctly across: 8 | - Different TypeScript configurations (`moduleResolution`, `module` settings) 9 | - Different package contexts (`"type": "module"` vs `"type": "commonjs"`) 10 | - Pure JavaScript runtime (without TypeScript) 11 | - Build tools (Vite) 12 | 13 | ## Quick Start 14 | 15 | ```bash 16 | # 1. Install dependencies 17 | bun install 18 | 19 | # 2. Run all tests 20 | ./test.sh 21 | ``` 22 | 23 | ## Test Coverage: 16 Scenarios 24 | 25 | ### TypeScript - 9 scenarios 26 | 27 | **modern-esm** (`"type": "module"`) - 4 configs 28 | - `moduleResolution: "node"` (legacy) 29 | - `moduleResolution: "node16"` 30 | - `moduleResolution: "nodenext"` 31 | - `moduleResolution: "bundler"` 32 | 33 | **modern-cjs** (`"type": "commonjs"`) - 4 configs 34 | - `module: "commonjs"` + `moduleResolution: "node"` 35 | - `module: "node16"` + `moduleResolution: "node16"` 36 | - `module: "nodenext"` + `moduleResolution: "nodenext"` 37 | - `module: "esnext"` + `moduleResolution: "bundler"` 38 | 39 | **legacy-cjs** (`"type": "commonjs"`) - 1 config 40 | - `module: "commonjs"` + `moduleResolution: "node"` with `require()` syntax 41 | 42 | ### Explicit Module Extensions - 3 scenarios 43 | 44 | - **esm-package**: `.mts` and `.cts` files in ESM package context 45 | - **cjs-package**: `.mts` and `.cts` files in CJS package context 46 | - **mixed-package**: `.ts`, `.mts`, and `.cts` coexisting 47 | 48 | ### JavaScript Runtime - 2 scenarios 49 | 50 | - **esm**: runs both `index.js` (follows package type) and `index.mjs` (explicit ESM) 51 | - **cjs**: runs both `index.js` (follows package type) and `index.cjs` (explicit CJS) 52 | 53 | ### Bundler - 2 scenarios 54 | 55 | - **Vite ESM**: TypeScript entry (`index.ts`) with `import` syntax 56 | - **Vite CJS**: CommonJS entry (`index.cjs`) with `require()` syntax 57 | 58 | ## Real-World Coverage 59 | 60 | These scenarios cover common consumer setups: 61 | - **NestJS, Express, Next.js**: `modern-cjs` configs 62 | - **Modern ESM projects**: `modern-esm` configs 63 | - **Vite, webpack, Rollup**: `bundler` configs 64 | - **Pure JavaScript**: `javascript` configs 65 | - **Dual-module packages**: `explicit` configs 66 | --------------------------------------------------------------------------------