1
2
3
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
|
package org.terst.feedreader.opml
import android.util.Xml
import org.terst.feedreader.data.FeedEntity
import org.xmlpull.v1.XmlPullParser
import java.io.InputStream
import java.io.OutputStream
data class OpmlFeed(val title: String, val xmlUrl: String, val htmlUrl: String?, val folder: String? = null)
/**
* OPML import/export. Folder nesting (<outline> containing other <outline>
* elements instead of an xmlUrl) is captured as a single folder name per
* feed -- the nearest enclosing container outline's title/text. Nested
* folders-within-folders collapse to just the innermost name; this app
* groups by one folder level, not a full category tree.
*/
object Opml {
fun parse(input: InputStream): List<OpmlFeed> {
val parser = Xml.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
parser.setInput(input, null)
val feeds = mutableListOf<OpmlFeed>()
// One entry pushed per <outline> START_TAG, popped on its END_TAG -- a folder
// container's entry holds its name, a feed (or any other) outline's is null.
// The current folder is whichever named entry is nearest the top of the stack.
val outlineStack = mutableListOf<String?>()
var eventType = parser.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
when (eventType) {
XmlPullParser.START_TAG -> if (parser.name.equals("outline", ignoreCase = true)) {
val xmlUrl = parser.getAttributeValue(null, "xmlUrl")
if (xmlUrl != null) {
val title = parser.getAttributeValue(null, "title")
?: parser.getAttributeValue(null, "text")
?: xmlUrl
val htmlUrl = parser.getAttributeValue(null, "htmlUrl")
val folder = outlineStack.filterNotNull().lastOrNull()
feeds += OpmlFeed(title, xmlUrl, htmlUrl, folder)
outlineStack += null
} else {
val folderName = parser.getAttributeValue(null, "text")
?: parser.getAttributeValue(null, "title")
outlineStack += folderName
}
}
XmlPullParser.END_TAG -> if (parser.name.equals("outline", ignoreCase = true) && outlineStack.isNotEmpty()) {
outlineStack.removeAt(outlineStack.size - 1)
}
}
eventType = parser.next()
}
return feeds
}
fun write(feeds: List<FeedEntity>, output: OutputStream) {
val serializer = Xml.newSerializer()
serializer.setOutput(output, "UTF-8")
serializer.startDocument("UTF-8", true)
serializer.startTag(null, "opml")
serializer.attribute(null, "version", "2.0")
serializer.startTag(null, "head")
serializer.startTag(null, "title")
serializer.text("Feeds subscriptions")
serializer.endTag(null, "title")
serializer.endTag(null, "head")
serializer.startTag(null, "body")
fun writeFeedOutline(feed: FeedEntity) {
serializer.startTag(null, "outline")
serializer.attribute(null, "text", feed.title)
serializer.attribute(null, "title", feed.title)
serializer.attribute(null, "type", "rss")
serializer.attribute(null, "xmlUrl", feed.id)
feed.siteUrl?.let { serializer.attribute(null, "htmlUrl", it) }
serializer.endTag(null, "outline")
}
val (grouped, ungrouped) = feeds.partition { it.folder != null }
for ((folder, feedsInFolder) in grouped.groupBy { it.folder }) {
serializer.startTag(null, "outline")
serializer.attribute(null, "text", folder)
serializer.attribute(null, "title", folder)
for (feed in feedsInFolder) writeFeedOutline(feed)
serializer.endTag(null, "outline")
}
for (feed in ungrouped) writeFeedOutline(feed)
serializer.endTag(null, "body")
serializer.endTag(null, "opml")
serializer.endDocument()
serializer.flush()
}
}
|