Skip to content

Commit 6fe139f

Browse files
authored
Merge pull request #2 from sudame/feature/support_RSS1.0
RSS1.0に対応する
2 parents cf3bb97 + 2e87746 commit 6fe139f

13 files changed

+798
-4
lines changed

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ A dart package for parsing RSS and Atom feed.
77

88
### Features
99

10-
- [x] RSS
10+
- [x] RSS 2.0
1111
- [x] Atom
1212
- [x] Namespaces
1313
- [x] Media RSS
1414
- [x] Dublin Core
15+
- [x] RSS 1.0
1516

1617
### Installing
1718

@@ -29,8 +30,9 @@ import 'package:webfeed/webfeed.dart';
2930

3031
To parse string into `RssFeed` object use:
3132
```
32-
var rssFeed = new RssFeed.parse(xmlString); // for parsing RSS feed
33+
var rssFeed = new RssFeed.parse(xmlString); // for parsing RSS 2.0 feed
3334
var atomFeed = new AtomFeed.parse(xmlString); // for parsing Atom feed
35+
var rss1Feed = new RssFeed.parse(xmlString); // for parsing RSS 1.0 feed
3436
```
3537

3638
### Preview
@@ -105,6 +107,26 @@ item.rights
105107
item.media
106108
```
107109

110+
**RSS 1.0**
111+
```
112+
feed.title
113+
feed.description
114+
feed.link
115+
feed.items
116+
feed.image
117+
feed.updatePeriod
118+
feed.updateFrequency
119+
feed.updateBase
120+
feed.dc
121+
122+
RssItem item = feed.items.first;
123+
item.title
124+
item.description
125+
item.link
126+
item.dc
127+
item.content
128+
```
129+
108130
## License
109131

110132
WebFeed is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details

analysis_options.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
analyzer:
22
language:
3-
enablePreviewDart2: true
4-
strong-mode: true
53
errors:
64
unused_import: error
75
unused_local_variable: error

lib/domain/dublin_core/dublin_core.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class DublinCore {
66
final String description;
77
final String creator;
88
final String subject;
9+
final List<String> subjects;
910
final String publisher;
1011
final String contributor;
1112
final String date;
@@ -23,6 +24,7 @@ class DublinCore {
2324
this.description,
2425
this.creator,
2526
this.subject,
27+
this.subjects,
2628
this.publisher,
2729
this.contributor,
2830
this.date,
@@ -45,6 +47,9 @@ class DublinCore {
4547
description: findElementOrNull(element, "dc:description")?.text,
4648
creator: findElementOrNull(element, "dc:creator")?.text,
4749
subject: findElementOrNull(element, "dc:subject")?.text,
50+
subjects: findAllDirectElementsOrNull(element, 'dc:subject')
51+
.map((subjectElement) => subjectElement.text)
52+
.toList(),
4853
publisher: findElementOrNull(element, "dc:publisher")?.text,
4954
contributor: findElementOrNull(element, "dc:contributor")?.text,
5055
date: findElementOrNull(element, "dc:date")?.text,

lib/domain/rss1_feed.dart

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import 'dart:core';
2+
3+
import 'package:webfeed/domain/dublin_core/dublin_core.dart';
4+
import 'package:webfeed/domain/rss1_item.dart';
5+
import 'package:webfeed/util/helpers.dart';
6+
import 'package:xml/xml.dart';
7+
8+
enum UpdatePeriod {
9+
Hourly,
10+
Daily,
11+
Weekly,
12+
Monthly,
13+
Yearly,
14+
}
15+
16+
class Rss1Feed {
17+
final String title;
18+
final String description;
19+
final String link;
20+
final String image;
21+
final List<Rss1Item> items;
22+
final UpdatePeriod updatePeriod;
23+
final int updateFrequency;
24+
final DateTime updateBase;
25+
final DublinCore dc;
26+
27+
Rss1Feed({
28+
this.title,
29+
this.description,
30+
this.link,
31+
this.items,
32+
this.image,
33+
this.updatePeriod,
34+
this.updateFrequency,
35+
this.updateBase,
36+
this.dc,
37+
});
38+
39+
static UpdatePeriod _parseUpdatePeriod(String updatePeriodString) {
40+
switch (updatePeriodString) {
41+
case 'hourly':
42+
return UpdatePeriod.Hourly;
43+
case 'daily':
44+
return UpdatePeriod.Daily;
45+
case 'weekly':
46+
return UpdatePeriod.Weekly;
47+
case 'monthly':
48+
return UpdatePeriod.Monthly;
49+
case 'yearly':
50+
return UpdatePeriod.Yearly;
51+
default:
52+
return null;
53+
}
54+
}
55+
56+
factory Rss1Feed.parse(String xmlString) {
57+
var document = parse(xmlString);
58+
XmlElement rdfElement;
59+
try {
60+
rdfElement = document.findAllElements("rdf:RDF").first;
61+
} on StateError {
62+
throw ArgumentError("channel not found");
63+
}
64+
65+
return Rss1Feed(
66+
title: findElementOrNull(rdfElement, "title")?.text,
67+
link: findElementOrNull(rdfElement, "link")?.text,
68+
description: findElementOrNull(rdfElement, "description")?.text,
69+
items: rdfElement.findElements("item").map((element) {
70+
return Rss1Item.parse(element);
71+
}).toList(),
72+
image:
73+
findElementOrNull(rdfElement, 'image')?.getAttribute('rdf:resource'),
74+
updatePeriod: _parseUpdatePeriod(
75+
findElementOrNull(rdfElement, 'sy:updatePeriod')?.text),
76+
updateFrequency:
77+
parseInt(findElementOrNull(rdfElement, 'sy:updateFrequency')?.text),
78+
updateBase:
79+
parseDateTime(findElementOrNull(rdfElement, 'sy:updateBase')?.text),
80+
dc: DublinCore.parse(rdfElement.findElements('channel').first),
81+
);
82+
}
83+
}

lib/domain/rss1_item.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import 'package:webfeed/domain/rss_content.dart';
2+
import 'package:webfeed/util/helpers.dart';
3+
import 'package:xml/xml.dart';
4+
5+
import 'dublin_core/dublin_core.dart';
6+
7+
class Rss1Item {
8+
final String title;
9+
final String description;
10+
final String link;
11+
final DublinCore dc;
12+
final RssContent content;
13+
14+
Rss1Item({
15+
this.title,
16+
this.description,
17+
this.link,
18+
this.dc,
19+
this.content,
20+
});
21+
22+
static DateTime _dateTimeBuilder(String dateTimeStringOrNull) {
23+
if (dateTimeStringOrNull == null) {
24+
return null;
25+
}
26+
return DateTime.parse(dateTimeStringOrNull);
27+
}
28+
29+
factory Rss1Item.parse(XmlElement element) {
30+
return Rss1Item(
31+
title: findElementOrNull(element, "title")?.text,
32+
description: findElementOrNull(element, "description")?.text,
33+
link: findElementOrNull(element, "link")?.text,
34+
dc: DublinCore.parse(element),
35+
content: RssContent.parse(findElementOrNull(element, "content:encoded")),
36+
);
37+
}
38+
}

lib/domain/rss_content.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,15 @@ class RssContent {
2727
});
2828
return RssContent(content, images);
2929
}
30+
31+
@override
32+
bool operator ==(Object other) =>
33+
identical(this, other) ||
34+
other is RssContent &&
35+
runtimeType == other.runtimeType &&
36+
value == other.value &&
37+
images == other.images;
38+
39+
@override
40+
int get hashCode => value.hashCode ^ images.hashCode;
3041
}

lib/util/helpers.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,12 @@ bool parseBoolLiteral(XmlElement element, String tagName) {
2626
return ["yes", "true"].contains(v);
2727
}
2828

29+
DateTime parseDateTime(String dateTimeString) {
30+
if (dateTimeString == null) return null;
31+
return DateTime.parse(dateTimeString);
32+
}
33+
34+
int parseInt(String intString) {
35+
if (intString == null) return null;
36+
return int.parse(intString);
37+
}

test/rss1_test.dart

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import 'dart:core';
2+
import 'dart:io';
3+
4+
import 'package:test/test.dart';
5+
import 'package:webfeed/domain/rss1_feed.dart';
6+
7+
void main() {
8+
test('parse basic RSS 1.0', () {
9+
final xmlString = File('test/xml/RSS1-basic.xml').readAsStringSync();
10+
final feed = new Rss1Feed.parse(xmlString);
11+
12+
expect(feed.title, 'XML.com');
13+
expect(feed.link, 'http://xml.com/pub');
14+
expect(
15+
feed.description,
16+
'XML.com features a rich mix of information and services for the XML community.',
17+
);
18+
expect(feed.image, 'http://xml.com/universal/images/xml_tiny.gif');
19+
20+
expect(feed.items.length, 2);
21+
22+
final firstItem = feed.items.first;
23+
expect(firstItem.title, 'Processing Inclusions with XSLT');
24+
expect(firstItem.link, 'http://xml.com/pub/2000/08/09/xslt/xslt.html');
25+
expect(firstItem.description,
26+
'Processing document inclusions with general XML tools can be problematic. This article proposes a way of preserving inclusion information through SAX-based processing.');
27+
});
28+
29+
test('parse RSS1 with syndication module', () {
30+
final xmlString =
31+
File('test/xml/RSS1-with-syndication-module.xml').readAsStringSync();
32+
final feed = new Rss1Feed.parse(xmlString);
33+
34+
expect(feed.title, 'Meerkat');
35+
expect(feed.link, 'http://meerkat.oreillynet.com');
36+
expect(feed.description, 'Meerkat: An Open Wire Service');
37+
38+
expect(feed.updatePeriod, UpdatePeriod.Hourly);
39+
expect(feed.updateFrequency, 2);
40+
expect(feed.updateBase, DateTime.parse('2000-01-01T12:00+00:00'));
41+
});
42+
43+
test('parse RSS1 with dublin core module', () {
44+
final xmlString =
45+
File('test/xml/RSS1-with-dublin-core-module.xml').readAsStringSync();
46+
final feed = new Rss1Feed.parse(xmlString);
47+
48+
expect(feed.title, 'Meerkat');
49+
expect(feed.link, 'http://meerkat.oreillynet.com');
50+
expect(feed.description, 'Meerkat: An Open Wire Service');
51+
52+
expect(feed.dc.publisher, 'The O\'Reilly Network');
53+
expect(feed.dc.creator, 'Rael Dornfest (mailto:rael@oreilly.com)');
54+
expect(feed.dc.rights, 'Copyright © 2000 O\'Reilly & Associates, Inc.');
55+
expect(feed.dc.date, '2000-01-01T12:00+00:00');
56+
57+
final firstItem = feed.items.first;
58+
expect(
59+
firstItem.dc.description,
60+
'XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.',
61+
);
62+
expect(firstItem.dc.publisher, 'The O\'Reilly Network');
63+
expect(
64+
firstItem.dc.creator,
65+
'Simon St.Laurent (mailto:simonstl@simonstl.com)',
66+
);
67+
expect(
68+
firstItem.dc.rights, 'Copyright © 2000 O\'Reilly & Associates, Inc.');
69+
expect(firstItem.dc.subject, 'XML');
70+
});
71+
72+
test('parse RSS1 with content module', () {
73+
final xmlString =
74+
File('test/xml/RSS1-with-content-module.xml').readAsStringSync();
75+
final feed = new Rss1Feed.parse(xmlString);
76+
77+
expect(feed.title, 'Example Feed');
78+
expect(feed.link, 'http://www.example.org');
79+
expect(feed.description, 'Simply for the purpose of demonstration.');
80+
81+
final firstItem = feed.items.first;
82+
expect(
83+
firstItem.content.value,
84+
'<p>What a <em>beautiful</em> day!</p>',
85+
);
86+
expect(
87+
firstItem.content.images,
88+
Iterable.empty(),
89+
);
90+
});
91+
92+
// Japanese Social Bookmark Service "Hatena Bookmark" is still using RSS1.0!
93+
// As I don't know english service using RSS 1.0, I use Japanese service for test case.
94+
test("parse production RSS1.0", () {
95+
var xmlString =
96+
new File("test/xml/RSS1-production_hatena.xml").readAsStringSync();
97+
98+
var feed = new Rss1Feed.parse(xmlString);
99+
100+
expect(feed.title, 'sampleのはてなブックマーク');
101+
expect(feed.link, 'https://b.hatena.ne.jp/sample/bookmark');
102+
expect(feed.description, 'sampleのはてなブックマーク (17)');
103+
104+
expect(feed.items.length, 17);
105+
106+
final firstItem = feed.items.first;
107+
108+
expect(firstItem.description, '');
109+
expect(firstItem.title, 'はてなスタッフのブックマーク拝見! - 営業マン編「仕事の様々なシーンでフル活用」');
110+
expect(firstItem.link, 'http://b.hatena.ne.jp/guide/staff_bookmark_03');
111+
expect(firstItem.dc.creator, 'sample');
112+
expect(firstItem.dc.date, '2009-04-10T09:44:20Z');
113+
expect(firstItem.dc.subject, 'はてな');
114+
expect(firstItem.dc.subjects[0], 'はてな');
115+
expect(firstItem.dc.subjects[1], 'インタビュー');
116+
expect(firstItem.dc.subjects[2], 'はてなブックマーク');
117+
});
118+
}

0 commit comments

Comments
 (0)