UNPKG

5.72 kBJavaScriptView Raw
1
2// pickup - transform RSS or Atom XML to JSON
3
4exports = module.exports = Pickup
5
6var StringDecoder = require('string_decoder').StringDecoder
7var assert = require('assert')
8var attribute = require('./lib/attribute')
9var mappings = require('./lib/mappings')
10var os = require('os')
11var sax = require('sax')
12var stream = require('readable-stream')
13var util = require('util')
14
15function isString(obj) {
16 return typeof obj === 'string'
17}
18
19function OpenHandlers (t) {
20 this.channel = t.feedopen
21 this.feed = t.feedopen
22 this.item = t.entryopen
23 this.entry = t.entryopen
24 this.image = t.imageopen
25}
26
27function CloseHandlers (t) {
28 this.channel = t.feedclose
29 this.feed = t.feedclose
30 this.item = t.entryclose
31 this.entry = t.entryclose
32 this.image = t.imageclose
33}
34
35function Opts (trim, normalize, position) {
36 this.trim = trim
37 this.normalize = normalize
38 this.position = position
39}
40var saxOpts = new Opts(true, true, false)
41
42util.inherits(Pickup, stream.Transform)
43function Pickup (opts) {
44 if (!(this instanceof Pickup)) return new Pickup(opts)
45 stream.Transform.call(this, opts)
46 if (!Pickup.openHandlers) {
47 Pickup.openHandlers = new OpenHandlers(Pickup.prototype)
48 Pickup.closeHandlers = new CloseHandlers(Pickup.prototype)
49 }
50
51 this.decoder = new StringDecoder('utf8')
52 this.eventMode = opts && opts.eventMode
53 this.map = null
54 this.parser = sax.parser(true, saxOpts)
55 this.state = new State()
56
57 var me = this
58 var parser = this.parser
59 parser.ontext = function (t) {
60 var current = me.current()
61 var map = me.map
62 var state = me.state
63 var name = me.state.name
64 if (!current || !map) return
65 var key = state.image && name === 'url' ? 'image' : map[name]
66 if (!key) return
67 if (state.feed && key === 'link' && !!current.link) return
68 var prop = current[key]
69 var add = isString(prop) && isString(t) && t !== prop
70 if (prop && add) {
71 current[key] += t
72 } else {
73 current[key] = t
74 }
75 }
76 parser.oncdata = function (d) {
77 parser.ontext(d)
78 }
79 function handle (name, handlers) {
80 if (handlers.hasOwnProperty(name)) {
81 handlers[name].apply(me)
82 }
83 }
84 parser.onopentag = function (node) {
85 var name = node.name
86 me.state.name = name
87 me.map = mappings[name] || me.map
88 handle(name, Pickup.openHandlers)
89 var current = me.current()
90 if (current) {
91 var key = me.map[name]
92 if (key) {
93 var attributes = node.attributes
94 var keys = Object.keys(attributes)
95 if (keys.length) {
96 var kv = attribute(key, attributes)
97 if (kv) current[kv[0]] = kv[1]
98 }
99 }
100 }
101 }
102 parser.onclosetag = function (name) {
103 handle(name, Pickup.closeHandlers)
104 me.state.name = null
105 }
106}
107
108Pickup.prototype.current = function () {
109 return this.state.entry || this.state.feed
110}
111
112Pickup.prototype.objectMode = function () {
113 return this._readableState.objectMode
114}
115
116Pickup.prototype.feedopen = function () {
117 this.state.feed = new Feed()
118}
119
120Pickup.prototype.entryopen = function () {
121 this.state.entry = new Entry()
122}
123
124Pickup.prototype.imageopen = function () {
125 this.state.image = true
126}
127
128Pickup.prototype.entryclose = function () {
129 var entry = this.state.entry
130 if (!this.eventMode) {
131 if (this.objectMode()) {
132 this.push(entry)
133 } else {
134 this.push(JSON.stringify(entry) + os.EOL)
135 }
136 } else {
137 this.emit('entry', entry)
138 }
139 this.state.entry = null
140}
141
142Pickup.prototype.feedclose = function () {
143 var feed = this.state.feed
144 if (!this.eventMode) {
145 if (this.objectMode()) {
146 this.push(feed)
147 } else {
148 this.push(JSON.stringify(feed) + os.EOL)
149 }
150 } else {
151 this.emit('feed', feed)
152 }
153 this.state.feed = null
154}
155
156Pickup.prototype.imageclose = function () {
157 this.state.image = false
158}
159
160function free (parser) {
161 ['ontext', 'oncdata', 'onopentag', 'onclosetag']
162 .forEach(function (p) {
163 delete parser[p]
164 })
165}
166
167Pickup.prototype._flush = function (cb) {
168 free(this.parser)
169 this.parser.close()
170 this.parser = null
171 this.decoder = null
172 this.map = null
173 this.state.deinit()
174 this.state = null
175 cb()
176}
177
178Pickup.prototype._transform = function (chunk, enc, cb) {
179 var str = this.decoder.write(chunk)
180 var er = this.parser.write(str).error
181 this.parser.error = null
182 cb(er)
183}
184
185function Entry (
186 author
187, enclosure
188, duration
189, id
190, image
191, link
192, subtitle
193, summary
194, title
195, updated) {
196 this.author = author
197 this.enclosure = enclosure
198 this.duration = duration
199 this.id = id
200 this.image = image
201 this.link = link
202 this.subtitle = subtitle
203 this.summary = summary
204 this.title = title
205 this.updated = updated
206}
207
208function Feed (
209 author
210, copyright
211, id
212, image
213, language
214, link
215, payment
216, subtitle
217, summary
218, title
219, ttl
220, updated) {
221 this.author = author
222 this.copyright = copyright
223 this.id = id
224 this.image = image
225 this.language = language
226 this.link = link
227 this.payment = payment
228 this.subtitle = subtitle
229 this.summary = summary
230 this.title = title
231 this.ttl = ttl
232 this.updated = updated
233}
234
235function State (entry, feed, image, name) {
236 this.entry = entry
237 this.feed = feed
238 this.image = image
239 this.name = name
240}
241
242State.prototype.deinit = function () {
243 this.entry = null
244 this.feed = null
245 this.image = false
246 this.name = undefined // String()
247}
248
249function extend (origin, add) {
250 return util._extend(origin, add || Object.create(null)) }
251function entry (obj) {
252 return extend(new Entry(), obj) }
253function feed (obj) {
254 return extend(new Feed(), obj) }
255
256if (process.env.NODE_TEST) {
257 exports.entry = entry
258 exports.feed = feed
259 exports.EVENTS = [
260 'data'
261 , 'drain'
262 , 'readable'
263 , 'end'
264 , 'entry'
265 , 'error'
266 , 'feed'
267 , 'finish'
268 ]
269}