Skip to content

Commit 5707c07

Browse files
committed
Allow trailing feed metadata after items
1 parent 9a07163 commit 5707c07

4 files changed

Lines changed: 88 additions & 4 deletions

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,16 @@ You can also check out this nice [working implementation](https://github.com/scr
113113

114114
- `addmeta` - Set to `false` to override Feedparser's default behavior, which
115115
is to add the feed's `meta` information to each article.
116+
Feed metadata is available as soon as Feedparser has enough information to
117+
emit the first article. While bad practice and borderline pathological, feeds
118+
can legally include additional channel metadata after articles, so the `meta`
119+
object may be enriched until the stream ends. If you need complete metadata,
120+
also handle the `meta` event and keep the emitted object until the stream ends.
121+
If you only need the metadata available when each article streams, you can
122+
use `item.meta` as usual.
116123

117124
- `feedurl` - The url (string) of the feed. FeedParser is very good at
118-
resolving relative urls in feeds, including those embedded in HTML content
125+
resolving relative urls in feeds, including those embedded in HTML content
119126
fields. But some feeds use relative urls without declaring the `xml:base`
120127
attribute any place in the feed. This is perfectly valid, but we don't know
121128
the feed's url before we start parsing the feed and trying to resolve those

lib/feedparser.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -363,11 +363,10 @@ FeedParser.prototype.handleCloseTag = function (el) {
363363
}
364364
if (this.meta.author && !item.author) item.author = this.meta.author;
365365
this.push(item);
366-
} else if (!this.meta.title && // We haven't yet parsed all the metadata
367-
(node['#name'] === 'channel' ||
366+
} else if (node['#name'] === 'channel' ||
368367
node['#name'] === 'feed' ||
369368
(node['#local'] === 'channel' && (node['#prefix'] === '' || node['#type'] === 'rdf')) ||
370-
(node['#local'] === 'feed' && (node['#prefix'] === '' || node['#type'] === 'atom')))) {
369+
(node['#local'] === 'feed' && (node['#prefix'] === '' || node['#type'] === 'atom'))) {
371370
_.assign(this.meta, this.handleMeta(n, this.meta['#type'], this.options));
372371
if (!this._emitted_meta) {
373372
this.emit('meta', this.meta);
@@ -503,6 +502,8 @@ FeedParser.prototype.handleMeta = function handleMeta (node, type, options) {
503502
Object.keys(node).forEach((name) => {
504503
var el = node[name];
505504

505+
if (name === 'item' || name === 'entry' || name.match(/:(item|entry)$/)) return;
506+
506507
if (normalize) {
507508
switch (name) {
508509
case ('title'):
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0"?>
2+
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
3+
<channel>
4+
<title>Trailing Meta Feed</title>
5+
<link>https://example.com/</link>
6+
<description>Feed metadata can appear after items.</description>
7+
<item>
8+
<title>First item</title>
9+
<link>https://example.com/items/1</link>
10+
<guid>https://example.com/items/1</guid>
11+
</item>
12+
<itunes:category text="Music"/>
13+
</channel>
14+
</rss>

test/trailing-meta.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
describe('trailing metadata', function () {
2+
3+
var feed = __dirname + '/feeds/rss-with-trailing-meta.xml';
4+
5+
it('should include channel metadata that appears after items in final meta', function (done) {
6+
var feedparser = new FeedParser();
7+
var items = [];
8+
var meta;
9+
var metaEvents = 0;
10+
11+
fs.createReadStream(feed).pipe(feedparser)
12+
.on('error', function (err) {
13+
assert.ifError(err);
14+
done(err);
15+
})
16+
.on('meta', function (_meta) {
17+
meta = _meta;
18+
metaEvents++;
19+
})
20+
.on('readable', function () {
21+
var item;
22+
while ((item = this.read()) !== null) {
23+
items.push(item);
24+
}
25+
})
26+
.on('end', function () {
27+
assert.strictEqual(metaEvents, 1);
28+
assert.deepStrictEqual(feedparser.meta.categories, ['Music']);
29+
assert.strictEqual(meta, feedparser.meta);
30+
assert.strictEqual(items.length, 1);
31+
assert.strictEqual(items[0].meta, feedparser.meta);
32+
assert.deepStrictEqual(items[0].meta.categories, ['Music']);
33+
assert.strictEqual(feedparser.meta['rss:item'], undefined);
34+
done();
35+
});
36+
});
37+
38+
it('should skip items in final native meta when normalize is false', function (done) {
39+
var feedparser = new FeedParser({ normalize: false });
40+
41+
fs.createReadStream(feed).pipe(feedparser)
42+
.on('error', function (err) {
43+
assert.ifError(err);
44+
done(err);
45+
})
46+
.on('readable', function () {
47+
var item;
48+
while ((item = this.read()) !== null) {
49+
assert(item);
50+
}
51+
})
52+
.on('end', function () {
53+
assert.strictEqual(feedparser.meta.title, undefined);
54+
assert.strictEqual(feedparser.meta['rss:title']['#'], 'Trailing Meta Feed');
55+
assert.strictEqual(feedparser.meta['itunes:category']['@'].text, 'Music');
56+
assert.strictEqual(feedparser.meta['rss:item'], undefined);
57+
assert.strictEqual(feedparser.meta.item, undefined);
58+
done();
59+
});
60+
});
61+
62+
});

0 commit comments

Comments
 (0)