Skip to content

Commit 2a7d443

Browse files
authored
Merge pull request #298 from danmactough/add-types
Add TypeScript type definitions
2 parents d7c08b8 + 3892eb2 commit 2a7d443

9 files changed

Lines changed: 403 additions & 24 deletions

File tree

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
**/*.ts

index.d.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/// <reference types="node" />
2+
3+
import stream = require("stream");
4+
5+
export = FeedParser;
6+
7+
declare class FeedParser extends stream.Transform {
8+
constructor(options?: FeedParser.Options);
9+
meta: FeedParser.Meta;
10+
options: FeedParser.Options;
11+
12+
read(): FeedParser.Item | null;
13+
resumeSaxError(): void;
14+
15+
on(event: 'meta', listener: (meta: FeedParser.Meta) => void): this;
16+
on(event: 'readable', listener: (this: FeedParser) => void): this;
17+
on(event: 'error', listener: (error: Error) => void): this;
18+
on(event: string, listener: (...args: any[]) => void): this;
19+
20+
addListener(event: 'meta', listener: (meta: FeedParser.Meta) => void): this;
21+
addListener(event: 'readable', listener: (this: FeedParser) => void): this;
22+
addListener(event: 'error', listener: (error: Error) => void): this;
23+
addListener(event: string, listener: (...args: any[]) => void): this;
24+
25+
once(event: 'meta', listener: (meta: FeedParser.Meta) => void): this;
26+
once(event: 'readable', listener: (this: FeedParser) => void): this;
27+
once(event: 'error', listener: (error: Error) => void): this;
28+
once(event: string, listener: (...args: any[]) => void): this;
29+
}
30+
31+
declare namespace FeedParser {
32+
type Type = "atom" | "rss" | "rdf";
33+
34+
interface Options {
35+
strict?: boolean;
36+
normalize?: boolean;
37+
addmeta?: boolean;
38+
feedurl?: string;
39+
resume_saxerror?: boolean;
40+
MAX_BUFFER_LENGTH?: number;
41+
}
42+
43+
interface Image {
44+
url: string;
45+
title: string;
46+
}
47+
48+
interface Meta {
49+
"#ns": Array<{ [key: string]: string }>;
50+
"#type": Type;
51+
"#version": string;
52+
"@": { [key: string]: any };
53+
title: string;
54+
description: string;
55+
date: Date | null;
56+
pubdate: Date | null;
57+
link: string;
58+
xmlurl: string;
59+
author: string;
60+
language: string;
61+
image: Image;
62+
favicon: string;
63+
copyright: string;
64+
generator: string;
65+
categories: string[];
66+
[key: string]: any;
67+
}
68+
69+
interface Enclosure {
70+
url: string;
71+
type?: string;
72+
length?: string;
73+
}
74+
75+
interface Source {
76+
title: string;
77+
url: string;
78+
}
79+
80+
interface Item {
81+
title: string;
82+
description: string;
83+
summary: string;
84+
date: Date | null;
85+
pubdate: Date | null;
86+
link: string;
87+
origlink: string;
88+
author: string;
89+
guid: string;
90+
comments: string;
91+
image: { url: string };
92+
categories: string[];
93+
source: Source;
94+
enclosures: Enclosure[];
95+
meta: Meta;
96+
[key: string]: any;
97+
}
98+
}

lib/feedparser/index.js

Lines changed: 114 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ var sax = require('sax')
5555
* - generator {String}
5656
* - categories {Array}
5757
*
58-
* @param {Object} options
59-
* @api public
58+
* @this {FeedParserInstance}
59+
* @param {import('../../index').Options} [options]
6060
*/
6161
function FeedParser (options) {
6262
if (!(this instanceof FeedParser)) return new FeedParser(options);
@@ -72,9 +72,13 @@ function FeedParser (options) {
7272
if (!('normalize' in this.options)) this.options.normalize = true;
7373
if (!('addmeta' in this.options)) this.options.addmeta = true;
7474
if (!('resume_saxerror' in this.options)) this.options.resume_saxerror = true;
75+
// MAX_BUFFER_LENGTH is not part of the public API of sax, but we need to be
76+
// able to handle nodes that are larger than the 64K default
7577
if ('MAX_BUFFER_LENGTH' in this.options) {
78+
// @ts-expect-error - private API of sax
7679
sax.MAX_BUFFER_LENGTH = this.options.MAX_BUFFER_LENGTH; // set to Infinity to have unlimited buffers
7780
} else {
81+
// @ts-expect-error
7882
sax.MAX_BUFFER_LENGTH = 16 * 1024 * 1024; // 16M versus the 64K default
7983
}
8084
if (this.options.feedurl) this.xmlbase.unshift({ '#name': 'xml', '#': this.options.feedurl});
@@ -96,6 +100,7 @@ util.inherits(FeedParser, TransformStream);
96100
*
97101
* Initializes the class-variables
98102
*/
103+
/** @this {FeedParserInstance} */
99104
FeedParser.prototype.init = function (){
100105
this.meta = {
101106
'#ns': [],
@@ -113,6 +118,7 @@ FeedParser.prototype.init = function (){
113118
this.errors = [];
114119
};
115120

121+
/** @this {FeedParserInstance} */
116122
FeedParser.prototype.handleEnd = function (){
117123
// We made it to the end without throwing, but let's make sure we were actually
118124
// parsing a feed
@@ -123,26 +129,33 @@ FeedParser.prototype.handleEnd = function (){
123129
this.push(null);
124130
};
125131

132+
/** @this {FeedParserInstance} */
126133
FeedParser.prototype.handleSaxError = function (e) {
127134
this.emit('error', e);
128135
if (this.options.resume_saxerror) {
129136
this.resumeSaxError();
130137
}
131138
};
132139

140+
/** @this {FeedParserInstance} */
133141
FeedParser.prototype.resumeSaxError = function () {
134142
if (this.stream._parser) {
135143
this.stream._parser.error = null;
136144
this.stream._parser.resume();
137145
}
138146
};
139147

148+
/** @this {FeedParserInstance} */
140149
FeedParser.prototype.handleError = function (e){
141150
this.emit('error', e);
142151
};
143152

144153
// parses the xml declaration, which looks like:
145154
// <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
155+
/**
156+
* @this {FeedParserInstance}
157+
* @param {SaxProcessingInstruction} node
158+
*/
146159
FeedParser.prototype.handleProcessingInstruction = function (node) {
147160
if (node.name === 'xml') {
148161
this.meta['#xml'] = node.body.trim().split(/\s+/).reduce(function (map, attr) {
@@ -155,6 +168,10 @@ FeedParser.prototype.handleProcessingInstruction = function (node) {
155168
}
156169
};
157170

171+
/**
172+
* @this {FeedParserInstance}
173+
* @param {import('sax').QualifiedTag} node
174+
*/
158175
FeedParser.prototype.handleOpenTag = function (node){
159176
var n = {};
160177
n['#name'] = node.name; // Avoid namespace collissions later...
@@ -204,6 +221,7 @@ FeedParser.prototype.handleOpenTag = function (node){
204221
this.stack.unshift(n);
205222
};
206223

224+
/** @this {FeedParserInstance} */
207225
FeedParser.prototype.handleCloseTag = function (el){
208226
var node = {
209227
'#name': el,
@@ -355,6 +373,10 @@ FeedParser.prototype.handleCloseTag = function (el){
355373
}
356374
};
357375

376+
/**
377+
* @this {FeedParserInstance}
378+
* @param {string} text
379+
*/
358380
FeedParser.prototype.handleText = function (text){
359381
if (this.in_xhtml) {
360382
this.xhtml['#'] += text;
@@ -369,6 +391,12 @@ FeedParser.prototype.handleText = function (text){
369391
}
370392
};
371393

394+
/**
395+
* @this {FeedParserInstance}
396+
* @param {Object.<string, import('sax').QualifiedAttribute>} attrs
397+
* @param {string} el
398+
* @returns {Object.<string, string>}
399+
*/
372400
FeedParser.prototype.handleAttributes = function handleAttributes (attrs, el) {
373401
/*
374402
* Using the sax.js option { xmlns: true }
@@ -382,14 +410,14 @@ FeedParser.prototype.handleAttributes = function handleAttributes (attrs, el) {
382410
*/
383411

384412
var basepath = ''
385-
, simplifiedAttributes = {}
413+
, simplifiedAttributes = /** @type {Object.<string, string>} */ ({})
386414
;
387415

388416
if (this.xmlbase && this.xmlbase.length) {
389417
basepath = this.xmlbase[0]['#'];
390418
}
391419

392-
Object.keys(attrs).forEach(function(key){
420+
Object.keys(attrs).forEach(/** @this {FeedParserInstance} */ function(key){
393421
var attr = attrs[key]
394422
, ns = {}
395423
, prefix = ''
@@ -425,6 +453,13 @@ FeedParser.prototype.handleAttributes = function handleAttributes (attrs, el) {
425453
return simplifiedAttributes;
426454
};
427455

456+
/**
457+
* @this {FeedParserInstance}
458+
* @param {ParsedNode} node
459+
* @param {import('../../index').Type} type
460+
* @param {import('../../index').Options} options
461+
* @returns {Object}
462+
*/
428463
FeedParser.prototype.handleMeta = function handleMeta (node, type, options) {
429464
if (!type || !node) return {};
430465

@@ -772,6 +807,13 @@ FeedParser.prototype.handleMeta = function handleMeta (node, type, options) {
772807
return meta;
773808
};
774809

810+
/**
811+
* @this {FeedParserInstance}
812+
* @param {ParsedNode} node
813+
* @param {import('../../index').Type} type
814+
* @param {import('../../index').Options} options
815+
* @returns {Object}
816+
*/
775817
FeedParser.prototype.handleItem = function handleItem (node, type, options){
776818
if (!type || !node) return {};
777819

@@ -830,7 +872,7 @@ FeedParser.prototype.handleItem = function handleItem (node, type, options){
830872
if (link['@']['rel'] == 'self' && !item.link) item.link = link['@']['href'];
831873
if (link['@']['rel'] == 'replies') item.comments = link['@']['href'];
832874
if (link['@']['rel'] == 'enclosure') {
833-
enclosure = {};
875+
enclosure = /** @type {import('../../index').Enclosure} */ ({});
834876
enclosure.url = link['@']['href'];
835877
enclosure.type = _.get(link['@'], 'type');
836878
enclosure.length = _.get(link['@'], 'length');
@@ -853,7 +895,7 @@ FeedParser.prototype.handleItem = function handleItem (node, type, options){
853895
if (el['@']['rel'] == 'self' && !item.link) item.link = el['@']['href'];
854896
if (el['@']['rel'] == 'replies') item.comments = el['@']['href'];
855897
if (el['@']['rel'] == 'enclosure') {
856-
enclosure = {};
898+
enclosure = /** @type {import('../../index').Enclosure} */ ({});
857899
enclosure.url = el['@']['href'];
858900
enclosure.type = _.get(el['@'], 'type');
859901
enclosure.length = _.get(el['@'], 'length');
@@ -932,7 +974,7 @@ FeedParser.prototype.handleItem = function handleItem (node, type, options){
932974
case('enclosure'):
933975
if (Array.isArray(el)) {
934976
el.forEach(function (enc){
935-
enclosure = {};
977+
enclosure = /** @type {import('../../index').Enclosure} */ ({});
936978
enclosure.url = _.get(enc['@'], 'url');
937979
enclosure.type = _.get(enc['@'], 'type');
938980
enclosure.length = _.get(enc['@'], 'length');
@@ -943,7 +985,7 @@ FeedParser.prototype.handleItem = function handleItem (node, type, options){
943985
}
944986
});
945987
} else {
946-
enclosure = {};
988+
enclosure = /** @type {import('../../index').Enclosure} */ ({});
947989
enclosure.url = _.get(el['@'], 'url');
948990
enclosure.type = _.get(el['@'], 'type');
949991
enclosure.length = _.get(el['@'], 'length');
@@ -958,7 +1000,7 @@ FeedParser.prototype.handleItem = function handleItem (node, type, options){
9581000
var optionalAttributes = ['bitrate', 'framerate', 'samplingrate', 'duration', 'height', 'width'];
9591001
if (Array.isArray(el)) {
9601002
el.forEach(function (enc){
961-
enclosure = {};
1003+
enclosure = /** @type {import('../../index').Enclosure} */ ({});
9621004
enclosure.url = _.get(enc['@'], 'url');
9631005
enclosure.type = _.get(enc['@'], 'type') || _.get(enc['@'], 'medium');
9641006
enclosure.length = _.get(enc['@'], 'filesize');
@@ -976,7 +1018,7 @@ FeedParser.prototype.handleItem = function handleItem (node, type, options){
9761018
}
9771019
});
9781020
} else {
979-
enclosure = {};
1021+
enclosure = /** @type {import('../../index').Enclosure} */ ({});
9801022
enclosure.url = _.get(el['@'], 'url');
9811023
enclosure.type = _.get(el['@'], 'type') || _.get(el['@'], 'medium');
9821024
enclosure.length = _.get(el['@'], 'filesize');
@@ -1112,6 +1154,7 @@ FeedParser.prototype.handleItem = function handleItem (node, type, options){
11121154
};
11131155

11141156
// Naive Stream API
1157+
/** @this {FeedParserInstance} */
11151158
FeedParser.prototype._transform = function (data, encoding, done) {
11161159
try {
11171160
this.stream.write(data);
@@ -1123,6 +1166,7 @@ FeedParser.prototype._transform = function (data, encoding, done) {
11231166
}
11241167
};
11251168

1169+
/** @this {FeedParserInstance} */
11261170
FeedParser.prototype._flush = function (done) {
11271171
try {
11281172
this.stream.end();
@@ -1133,4 +1177,64 @@ FeedParser.prototype._flush = function (done) {
11331177
}
11341178
};
11351179

1180+
/**
1181+
* @typedef {Object} ParsedNode
1182+
* The internal accumulator object that handleOpenTag builds and pushes onto
1183+
* this.stack. Keys accumulate as child elements are parsed. String keys
1184+
* '#name', '#prefix', '#local', '#uri' hold element namespace info; '@' holds
1185+
* simplified attributes; '#' holds text content. Named keys hold child element
1186+
* values which may be strings, nested ParsedNodes, or arrays of either.
1187+
*/
1188+
1189+
/**
1190+
* @typedef {Object} XmlBaseEntry
1191+
* An entry in the this.xmlbase stack. The '#name' key holds the element name
1192+
* that established the base URL; the '#' key holds the xml:base URL value.
1193+
*/
1194+
1195+
/**
1196+
* @typedef {Object} SaxProcessingInstruction
1197+
* Payload of the sax 'processinginstruction' event.
1198+
* @property {string} name - Processing instruction target, e.g. "xml"
1199+
* @property {string} body - The rest of the processing instruction content
1200+
*/
1201+
1202+
/**
1203+
* @typedef {Object} AddressParserResult
1204+
* Shape of each item returned by the addressparser module.
1205+
* @property {string} [name]
1206+
* @property {string} [address]
1207+
*/
1208+
1209+
/**
1210+
* @typedef {Object} FeedParserState
1211+
* Instance properties set up by FeedParser.prototype.init and the constructor.
1212+
* @property {Object} meta - Parsed feed metadata; shape evolves during parsing
1213+
* @property {import('../../index').Options} options
1214+
* @property {Object.<string, string>} _namespaces
1215+
* @property {boolean} _emitted_meta
1216+
* @property {Array.<ParsedNode>} stack
1217+
* @property {Array.<XmlBaseEntry>} xmlbase
1218+
* @property {boolean} in_xhtml
1219+
* @property {Object} xhtml
1220+
* @property {Error[]} errors
1221+
* @property {import('sax').SAXStream} stream - The underlying sax stream
1222+
* @property {function(): void} init
1223+
* @property {function(): void} handleEnd
1224+
* @property {function(Error): void} handleSaxError
1225+
* @property {function(): void} resumeSaxError
1226+
* @property {function(Error): void} handleError
1227+
* @property {function(SaxProcessingInstruction): void} handleProcessingInstruction
1228+
* @property {function(import('sax').QualifiedTag): void} handleOpenTag
1229+
* @property {function(string): void} handleCloseTag
1230+
* @property {function(string): void} handleText
1231+
* @property {function(Object.<string, import('sax').QualifiedAttribute>, string): Object.<string, string>} handleAttributes
1232+
* @property {function(ParsedNode, import('../../index').Type, import('../../index').Options): Object} handleMeta
1233+
* @property {function(ParsedNode, import('../../index').Type, import('../../index').Options): Object} handleItem
1234+
*/
1235+
1236+
/**
1237+
* @typedef {import('readable-stream').Transform & FeedParserState} FeedParserInstance
1238+
*/
1239+
11361240
exports = module.exports = FeedParser;

0 commit comments

Comments
 (0)