Skip to content

Commit

Permalink
Merge pull request #2 from sudame/feature/support_RSS1.0
Browse files Browse the repository at this point in the history
RSS1.0に対応する
  • Loading branch information
sudame authored Mar 9, 2020
2 parents cf3bb97 + 2e87746 commit 6fe139f
Show file tree
Hide file tree
Showing 13 changed files with 798 additions and 4 deletions.
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ A dart package for parsing RSS and Atom feed.

### Features

- [x] RSS
- [x] RSS 2.0
- [x] Atom
- [x] Namespaces
- [x] Media RSS
- [x] Dublin Core
- [x] RSS 1.0

### Installing

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

To parse string into `RssFeed` object use:
```
var rssFeed = new RssFeed.parse(xmlString); // for parsing RSS feed
var rssFeed = new RssFeed.parse(xmlString); // for parsing RSS 2.0 feed
var atomFeed = new AtomFeed.parse(xmlString); // for parsing Atom feed
var rss1Feed = new RssFeed.parse(xmlString); // for parsing RSS 1.0 feed
```

### Preview
Expand Down Expand Up @@ -105,6 +107,26 @@ item.rights
item.media
```

**RSS 1.0**
```
feed.title
feed.description
feed.link
feed.items
feed.image
feed.updatePeriod
feed.updateFrequency
feed.updateBase
feed.dc
RssItem item = feed.items.first;
item.title
item.description
item.link
item.dc
item.content
```

## License

WebFeed is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
2 changes: 0 additions & 2 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
analyzer:
language:
enablePreviewDart2: true
strong-mode: true
errors:
unused_import: error
unused_local_variable: error
Expand Down
5 changes: 5 additions & 0 deletions lib/domain/dublin_core/dublin_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class DublinCore {
final String description;
final String creator;
final String subject;
final List<String> subjects;
final String publisher;
final String contributor;
final String date;
Expand All @@ -23,6 +24,7 @@ class DublinCore {
this.description,
this.creator,
this.subject,
this.subjects,
this.publisher,
this.contributor,
this.date,
Expand All @@ -45,6 +47,9 @@ class DublinCore {
description: findElementOrNull(element, "dc:description")?.text,
creator: findElementOrNull(element, "dc:creator")?.text,
subject: findElementOrNull(element, "dc:subject")?.text,
subjects: findAllDirectElementsOrNull(element, 'dc:subject')
.map((subjectElement) => subjectElement.text)
.toList(),
publisher: findElementOrNull(element, "dc:publisher")?.text,
contributor: findElementOrNull(element, "dc:contributor")?.text,
date: findElementOrNull(element, "dc:date")?.text,
Expand Down
83 changes: 83 additions & 0 deletions lib/domain/rss1_feed.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'dart:core';

import 'package:webfeed/domain/dublin_core/dublin_core.dart';
import 'package:webfeed/domain/rss1_item.dart';
import 'package:webfeed/util/helpers.dart';
import 'package:xml/xml.dart';

enum UpdatePeriod {
Hourly,
Daily,
Weekly,
Monthly,
Yearly,
}

class Rss1Feed {
final String title;
final String description;
final String link;
final String image;
final List<Rss1Item> items;
final UpdatePeriod updatePeriod;
final int updateFrequency;
final DateTime updateBase;
final DublinCore dc;

Rss1Feed({
this.title,
this.description,
this.link,
this.items,
this.image,
this.updatePeriod,
this.updateFrequency,
this.updateBase,
this.dc,
});

static UpdatePeriod _parseUpdatePeriod(String updatePeriodString) {
switch (updatePeriodString) {
case 'hourly':
return UpdatePeriod.Hourly;
case 'daily':
return UpdatePeriod.Daily;
case 'weekly':
return UpdatePeriod.Weekly;
case 'monthly':
return UpdatePeriod.Monthly;
case 'yearly':
return UpdatePeriod.Yearly;
default:
return null;
}
}

factory Rss1Feed.parse(String xmlString) {
var document = parse(xmlString);
XmlElement rdfElement;
try {
rdfElement = document.findAllElements("rdf:RDF").first;
} on StateError {
throw ArgumentError("channel not found");
}

return Rss1Feed(
title: findElementOrNull(rdfElement, "title")?.text,
link: findElementOrNull(rdfElement, "link")?.text,
description: findElementOrNull(rdfElement, "description")?.text,
items: rdfElement.findElements("item").map((element) {
return Rss1Item.parse(element);
}).toList(),
image:
findElementOrNull(rdfElement, 'image')?.getAttribute('rdf:resource'),
updatePeriod: _parseUpdatePeriod(
findElementOrNull(rdfElement, 'sy:updatePeriod')?.text),
updateFrequency:
parseInt(findElementOrNull(rdfElement, 'sy:updateFrequency')?.text),
updateBase:
parseDateTime(findElementOrNull(rdfElement, 'sy:updateBase')?.text),
dc: DublinCore.parse(rdfElement.findElements('channel').first),
);
}
}
38 changes: 38 additions & 0 deletions lib/domain/rss1_item.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:webfeed/domain/rss_content.dart';
import 'package:webfeed/util/helpers.dart';
import 'package:xml/xml.dart';

import 'dublin_core/dublin_core.dart';

class Rss1Item {
final String title;
final String description;
final String link;
final DublinCore dc;
final RssContent content;

Rss1Item({
this.title,
this.description,
this.link,
this.dc,
this.content,
});

static DateTime _dateTimeBuilder(String dateTimeStringOrNull) {
if (dateTimeStringOrNull == null) {
return null;
}
return DateTime.parse(dateTimeStringOrNull);
}

factory Rss1Item.parse(XmlElement element) {
return Rss1Item(
title: findElementOrNull(element, "title")?.text,
description: findElementOrNull(element, "description")?.text,
link: findElementOrNull(element, "link")?.text,
dc: DublinCore.parse(element),
content: RssContent.parse(findElementOrNull(element, "content:encoded")),
);
}
}
11 changes: 11 additions & 0 deletions lib/domain/rss_content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,15 @@ class RssContent {
});
return RssContent(content, images);
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RssContent &&
runtimeType == other.runtimeType &&
value == other.value &&
images == other.images;

@override
int get hashCode => value.hashCode ^ images.hashCode;
}
9 changes: 9 additions & 0 deletions lib/util/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,12 @@ bool parseBoolLiteral(XmlElement element, String tagName) {
return ["yes", "true"].contains(v);
}

DateTime parseDateTime(String dateTimeString) {
if (dateTimeString == null) return null;
return DateTime.parse(dateTimeString);
}

int parseInt(String intString) {
if (intString == null) return null;
return int.parse(intString);
}
118 changes: 118 additions & 0 deletions test/rss1_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import 'dart:core';
import 'dart:io';

import 'package:test/test.dart';
import 'package:webfeed/domain/rss1_feed.dart';

void main() {
test('parse basic RSS 1.0', () {
final xmlString = File('test/xml/RSS1-basic.xml').readAsStringSync();
final feed = new Rss1Feed.parse(xmlString);

expect(feed.title, 'XML.com');
expect(feed.link, 'http://xml.com/pub');
expect(
feed.description,
'XML.com features a rich mix of information and services for the XML community.',
);
expect(feed.image, 'http://xml.com/universal/images/xml_tiny.gif');

expect(feed.items.length, 2);

final firstItem = feed.items.first;
expect(firstItem.title, 'Processing Inclusions with XSLT');
expect(firstItem.link, 'http://xml.com/pub/2000/08/09/xslt/xslt.html');
expect(firstItem.description,
'Processing document inclusions with general XML tools can be problematic. This article proposes a way of preserving inclusion information through SAX-based processing.');
});

test('parse RSS1 with syndication module', () {
final xmlString =
File('test/xml/RSS1-with-syndication-module.xml').readAsStringSync();
final feed = new Rss1Feed.parse(xmlString);

expect(feed.title, 'Meerkat');
expect(feed.link, 'http://meerkat.oreillynet.com');
expect(feed.description, 'Meerkat: An Open Wire Service');

expect(feed.updatePeriod, UpdatePeriod.Hourly);
expect(feed.updateFrequency, 2);
expect(feed.updateBase, DateTime.parse('2000-01-01T12:00+00:00'));
});

test('parse RSS1 with dublin core module', () {
final xmlString =
File('test/xml/RSS1-with-dublin-core-module.xml').readAsStringSync();
final feed = new Rss1Feed.parse(xmlString);

expect(feed.title, 'Meerkat');
expect(feed.link, 'http://meerkat.oreillynet.com');
expect(feed.description, 'Meerkat: An Open Wire Service');

expect(feed.dc.publisher, 'The O\'Reilly Network');
expect(feed.dc.creator, 'Rael Dornfest (mailto:[email protected])');
expect(feed.dc.rights, 'Copyright © 2000 O\'Reilly & Associates, Inc.');
expect(feed.dc.date, '2000-01-01T12:00+00:00');

final firstItem = feed.items.first;
expect(
firstItem.dc.description,
'XML is placing increasingly heavy loads on the existing technical infrastructure of the Internet.',
);
expect(firstItem.dc.publisher, 'The O\'Reilly Network');
expect(
firstItem.dc.creator,
'Simon St.Laurent (mailto:[email protected])',
);
expect(
firstItem.dc.rights, 'Copyright © 2000 O\'Reilly & Associates, Inc.');
expect(firstItem.dc.subject, 'XML');
});

test('parse RSS1 with content module', () {
final xmlString =
File('test/xml/RSS1-with-content-module.xml').readAsStringSync();
final feed = new Rss1Feed.parse(xmlString);

expect(feed.title, 'Example Feed');
expect(feed.link, 'http://www.example.org');
expect(feed.description, 'Simply for the purpose of demonstration.');

final firstItem = feed.items.first;
expect(
firstItem.content.value,
'<p>What a <em>beautiful</em> day!</p>',
);
expect(
firstItem.content.images,
Iterable.empty(),
);
});

// Japanese Social Bookmark Service "Hatena Bookmark" is still using RSS1.0!
// As I don't know english service using RSS 1.0, I use Japanese service for test case.
test("parse production RSS1.0", () {
var xmlString =
new File("test/xml/RSS1-production_hatena.xml").readAsStringSync();

var feed = new Rss1Feed.parse(xmlString);

expect(feed.title, 'sampleのはてなブックマーク');
expect(feed.link, 'https://b.hatena.ne.jp/sample/bookmark');
expect(feed.description, 'sampleのはてなブックマーク (17)');

expect(feed.items.length, 17);

final firstItem = feed.items.first;

expect(firstItem.description, '');
expect(firstItem.title, 'はてなスタッフのブックマーク拝見! - 営業マン編「仕事の様々なシーンでフル活用」');
expect(firstItem.link, 'http://b.hatena.ne.jp/guide/staff_bookmark_03');
expect(firstItem.dc.creator, 'sample');
expect(firstItem.dc.date, '2009-04-10T09:44:20Z');
expect(firstItem.dc.subject, 'はてな');
expect(firstItem.dc.subjects[0], 'はてな');
expect(firstItem.dc.subjects[1], 'インタビュー');
expect(firstItem.dc.subjects[2], 'はてなブックマーク');
});
}
Loading

0 comments on commit 6fe139f

Please sign in to comment.