Skip to content

Commit 7f9909c

Browse files
Merge pull request #29 from macieklamberski/feat/singular-fields
feat: Make sure that singular fields are treated as such if multiple present in XML
2 parents 9cc0867 + 2e2fb83 commit 7f9909c

27 files changed

Lines changed: 2745 additions & 1093 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![npm version](https://img.shields.io/npm/v/feedsmith.svg)](https://www.npmjs.com/package/feedsmith)
55
[![license](https://img.shields.io/npm/l/feedsmith.svg)](https://github.com/macieklamberski/feedsmith/blob/main/LICENSE)
66

7-
Modern JavaScript utility for parsing and generating JSON Feed, Atom, RSS, and RDF feeds, with support for popular namespaces. It provides both universal and format-specific parsers that maintain the original feed structure while offering helpful normalization.
7+
Robust and fast JavaScript parser for RSS, Atom, JSON Feed, and RDF feeds, with support for popular namespaces. It provides both universal and format-specific parsers that maintain the original feed structure while offering helpful normalization.
88

99
Feedsmith maintains the original feed structure in a clean, object-oriented format. It intelligently normalizes legacy elements, providing you with complete access to all feed data without compromising simplicity.
1010

@@ -421,7 +421,7 @@ For a quick overview, here are the results of parsing RSS, Atom, and RDF feeds u
421421

422422
## FAQ
423423

424-
### Why should I use Feedsmith instead of alternative modules?
424+
### Why should I use Feedsmith instead of alternative packages?
425425

426426
As stated in the overview section, the key advantage of Feedsmith is that it preserves the original feed structure exactly as provided in each specific feed format.
427427

benchmarks/bun.lock

Lines changed: 1041 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

benchmarks/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
"feed-read": "^0.0.1",
1111
"feedme": "^2.0.2",
1212
"feedparser": "^2.2.10",
13+
"podcast-feed-parser": "^1.0.4",
1314
"rss-parser": "^3.13.0",
1415
"tinybench": "^4.0.1"
1516
},
1617
"devDependencies": {
1718
"@types/benchmark": "^2.1.5",
18-
"@types/bun": "^1.2.9",
19+
"@types/bun": "^1.2.10",
1920
"@types/feedme": "^2.0.4",
2021
"@types/feedparser": "^2.2.8",
2122
"typescript": "^5.8.3"

benchmarks/parsing.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { parseFeed as rowanmanningFeedParser } from '@rowanmanning/feed-parser'
55
import { rssParse as ulisesgasconRssFeedParser } from '@ulisesgascon/rss-feed-parser'
66
import FeedMeJs from 'feedme'
77
import FeedParser from 'feedparser'
8+
import { getPodcastFromFeed as podcastFeedParser } from 'podcast-feed-parser'
89
import RssParser from 'rss-parser'
910
import { parseAtomFeed, parseJsonFeed, parseRdfFeed, parseRssFeed } from '../src/index'
1011
import { loadFeedFiles, runBenchmarks, runTest } from './utils'
@@ -59,62 +60,67 @@ const atomBigFiles = loadFeedFiles('feeds/atom-big', 10)
5960
const atomSmallFiles = loadFeedFiles('feeds/atom-small', 50)
6061
const rdfFiles = loadFeedFiles('feeds/rdf', 50)
6162

62-
await runBenchmarks('RSS feed parsing (10 files × 5MB50MB)', {
63+
await runBenchmarks('RSS feed parsing (10 files × 5MB50MB)', {
6364
'rss-parser': () => runTest(rssBigFiles, rssParser),
6465
'@gaphub/feed': () => runTest(rssBigFiles, gaphubFeedParser),
6566
'@rowanmanning/feed-parser': () => runTest(rssBigFiles, rowanmanningFeedParser),
6667
'feedme.js': () => runTest(rssBigFiles, feedMeJs),
6768
'@extractus/feed-extractor': () => runTest(rssBigFiles, extractFromXml),
6869
'@ulisesgascon/rss-feed-parser': () => runTest(rssBigFiles, ulisesgasconRssFeedParser),
6970
feedparser: () => runTest(rssBigFiles, feedParser),
71+
'podcast-feed-parser': () => runTest(rssBigFiles, podcastFeedParser),
7072
'feedsmith *': () => runTest(rssBigFiles, parseRssFeed),
7173
})
7274

73-
await runBenchmarks('RSS feed parsing (50 files × 100KB5MB)', {
75+
await runBenchmarks('RSS feed parsing (50 files × 100KB5MB)', {
7476
'rss-parser': () => runTest(rssSmallFiles, rssParser),
7577
'@gaphub/feed': () => runTest(rssSmallFiles, gaphubFeedParser),
7678
'@rowanmanning/feed-parser': () => runTest(rssSmallFiles, rowanmanningFeedParser),
7779
'feedme.js': () => runTest(rssSmallFiles, feedMeJs),
7880
'@extractus/feed-extractor': () => runTest(rssSmallFiles, extractFromXml),
7981
'@ulisesgascon/rss-feed-parser': () => runTest(rssSmallFiles, ulisesgasconRssFeedParser),
8082
feedparser: () => runTest(rssSmallFiles, feedParser),
83+
'podcast-feed-parser': () => runTest(rssSmallFiles, podcastFeedParser),
8184
'feedsmith *': () => runTest(rssSmallFiles, parseRssFeed),
8285
})
8386

84-
await runBenchmarks('Atom feed parsing (10 files × 5MB50MB)', {
87+
await runBenchmarks('Atom feed parsing (10 files × 5MB50MB)', {
8588
'rss-parser': () => runTest(atomBigFiles, rssParser),
8689
'@gaphub/feed': () => runTest(atomBigFiles, gaphubFeedParser),
8790
'@rowanmanning/feed-parser': () => runTest(atomBigFiles, rowanmanningFeedParser),
8891
'feedme.js': () => runTest(atomBigFiles, feedMeJs),
8992
'@extractus/feed-extractor': () => runTest(atomBigFiles, extractFromXml),
9093
// @ulisesgascon/rss-feed-parser — does not support Atom feeds.
9194
feedparser: () => runTest(atomBigFiles, feedParser),
95+
// podcast-feed-parser — does not support Atom feeds.
9296
'feedsmith *': () => runTest(atomBigFiles, parseAtomFeed),
9397
})
9498

95-
await runBenchmarks('Atom feed parsing (50 files × 100KB5MB)', {
99+
await runBenchmarks('Atom feed parsing (50 files × 100KB5MB)', {
96100
'rss-parser': () => runTest(atomSmallFiles, rssParser),
97101
'@gaphub/feed': () => runTest(atomSmallFiles, gaphubFeedParser),
98102
'@rowanmanning/feed-parser': () => runTest(atomSmallFiles, rowanmanningFeedParser),
99103
'feedme.js': () => runTest(atomSmallFiles, feedMeJs),
100104
'@extractus/feed-extractor': () => runTest(atomSmallFiles, extractFromXml),
101105
// @ulisesgascon/rss-feed-parser — does not support Atom feeds.
102106
feedparser: () => runTest(atomSmallFiles, feedParser),
107+
// podcast-feed-parser — does not support Atom feeds.
103108
'feedsmith *': () => runTest(atomSmallFiles, parseAtomFeed),
104109
})
105110

106-
await runBenchmarks('RDF feed parsing (50 files × 100KB5MB)', {
111+
await runBenchmarks('RDF feed parsing (50 files × 100KB5MB)', {
107112
'rss-parser': () => runTest(rdfFiles, rssParser),
108113
'@gaphub/feed': () => runTest(rdfFiles, gaphubFeedParser),
109114
'@rowanmanning/feed-parser': () => runTest(rdfFiles, rowanmanningFeedParser),
110115
'feedme.js': () => runTest(rdfFiles, feedMeJs),
111116
'@extractus/feed-extractor': () => runTest(rdfFiles, extractFromXml),
112117
// @ulisesgascon/rss-feed-parser — does not support RDF feeds.
113118
feedparser: () => runTest(rdfFiles, feedParser),
119+
// podcast-feed-parser — does not support RDF feeds.
114120
'feedsmith *': () => runTest(rdfFiles, parseRdfFeed),
115121
})
116122

117-
await runBenchmarks('JSON feed parsing (50 files × 100KB5MB)', {
123+
await runBenchmarks('JSON feed parsing (50 files × 100KB5MB)', {
118124
// '@extractus/feed-extractor' — does not properly parse and always returns empty feed.
119125
'feedsmith *': () => runTest(jsonFilesObjects, parseJsonFeed),
120126
})

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "feedsmith",
3-
"description": "Modern JavaScript utility for parsing and generating JSON Feed, Atom, RSS, and RDF feeds, with support for Dublin Core, Syndication, iTunes, and many other popular namespaces.",
3+
"description": "Robust and fast parser for RSS, Atom, JSON Feed, and RDF feeds, with support for iTunes, Dublin Core, and many other popular namespaces.",
44
"repository": {
55
"type": "git",
66
"url": "https://github.com/macieklamberski/feedsmith.git"

src/common/utils.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
parseBoolean,
1313
parseNumber,
1414
parseSingular,
15+
parseSingularOf,
1516
parseString,
1617
retrieveText,
1718
stripCdata,
@@ -903,6 +904,64 @@ describe('parseSingular', () => {
903904
})
904905
})
905906

907+
describe('parseSingularOf', () => {
908+
it('should apply parse function to the first element of an array', () => {
909+
const parseToString: ParseFunction<string> = (value) => {
910+
return typeof value === 'number' || typeof value === 'string' ? String(value) : undefined
911+
}
912+
913+
expect(parseSingularOf([1, 2, 3], parseToString)).toEqual('1')
914+
expect(parseSingularOf(['a', 'b', 'c'], parseToString)).toEqual('a')
915+
expect(parseSingularOf([42, 'text'], parseString)).toEqual('42')
916+
})
917+
918+
it('should apply parse function to non-array values', () => {
919+
expect(parseSingularOf(42, parseString)).toEqual('42')
920+
expect(parseSingularOf('123', parseNumber)).toEqual(123)
921+
expect(parseSingularOf('true', parseBoolean)).toEqual(true)
922+
})
923+
924+
it('should return undefined when the parse function returns undefined', () => {
925+
expect(parseSingularOf({}, parseNumber)).toBeUndefined()
926+
expect(parseSingularOf('not-a-number', parseNumber)).toBeUndefined()
927+
expect(parseSingularOf([{}, 'string'], parseNumber)).toBeUndefined()
928+
})
929+
930+
it('should handle empty arrays', () => {
931+
expect(parseSingularOf([], parseString)).toBeUndefined()
932+
})
933+
934+
it('should handle arrays with undefined or null first elements', () => {
935+
expect(parseSingularOf([undefined, 1, 2], parseNumber)).toBeUndefined()
936+
expect(parseSingularOf([null, 1, 2], parseNumber)).toBeUndefined()
937+
})
938+
939+
it('should preserve the type returned by the parse function', () => {
940+
const numberResult = parseSingularOf<number>('42', parseNumber)
941+
const stringResult = parseSingularOf<string>(42, parseString)
942+
const booleanResult = parseSingularOf<boolean>('true', parseBoolean)
943+
944+
expect(typeof numberResult).toEqual('number')
945+
expect(typeof stringResult).toEqual('string')
946+
expect(typeof booleanResult).toEqual('boolean')
947+
})
948+
949+
it('should work with custom parse functions', () => {
950+
const parseUpperCase: ParseFunction<string> = (value) => {
951+
return typeof value === 'string' ? value.toUpperCase() : undefined
952+
}
953+
954+
expect(parseSingularOf('hello', parseUpperCase)).toEqual('HELLO')
955+
expect(parseSingularOf(['hello', 'world'], parseUpperCase)).toEqual('HELLO')
956+
expect(parseSingularOf(123, parseUpperCase)).toBeUndefined()
957+
})
958+
959+
it('should handle null and undefined input values', () => {
960+
expect(parseSingularOf(null, parseString)).toBeUndefined()
961+
expect(parseSingularOf(undefined, parseNumber)).toBeUndefined()
962+
})
963+
})
964+
906965
describe('parseArray', () => {
907966
it('should handle arrays', () => {
908967
const value1 = [] as Array<string>

src/common/utils.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,6 @@ export const parseBoolean: ParseFunction<boolean> = (value) => {
138138
}
139139
}
140140

141-
export const parseSingular = <T>(value: T | Array<T>): T => {
142-
return Array.isArray(value) ? value[0] : value
143-
}
144-
145141
export const parseArray: ParseFunction<Array<Unreliable>> = (value) => {
146142
if (Array.isArray(value)) {
147143
return value
@@ -190,6 +186,14 @@ export const parseArrayOf = <R>(
190186
}
191187
}
192188

189+
export const parseSingular = <T>(value: T | Array<T>): T => {
190+
return Array.isArray(value) ? value[0] : value
191+
}
192+
193+
export const parseSingularOf = <R>(value: Unreliable, parse: ParseFunction<R>): R | undefined => {
194+
return parse(parseSingular(value))
195+
}
196+
193197
export const createNamespaceGetter = (
194198
value: Record<string, Unreliable>,
195199
prefix: string | undefined,
@@ -211,3 +215,13 @@ export const createCaseInsensitiveGetter = (value: Record<string, unknown>) => {
211215
return originalKey ? value[originalKey] : undefined
212216
}
213217
}
218+
219+
// TODO: Write tests.
220+
export const parseTextString: ParseFunction<string> = (value) => {
221+
return parseString(retrieveText(value))
222+
}
223+
224+
// TODO: Write tests.
225+
export const parseTextNumber: ParseFunction<number> = (value) => {
226+
return parseNumber(retrieveText(value))
227+
}

0 commit comments

Comments
 (0)