Skip to content

WIP: v1.0.0 #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
"extends": [
"react-app",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"settings": {
"react": {
"version": "999.999.999"
}
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"module": "dist/pulse-feed-parser.esm.js",
"devDependencies": {
"@size-limit/preset-small-lib": "^4.5.5",
"eslint-plugin-prettier": "^3.1.4",
"husky": "^4.2.5",
"prettier": "^2.0.5",
"size-limit": "^4.5.5",
Expand Down
11 changes: 9 additions & 2 deletions src/Adapter/AtomFeedAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Enclosure, Feed, Image, Item, Person } from '../types/Feed';
import { AtomEntry, AtomFeed } from '../types/Atom';
import {
Enclosure,
Feed,
Image,
Item,
Person,
AtomEntry,
AtomFeed,
} from '../types';
import { parsePerson } from '../utils/parsePerson';

// DefaultAtomTranslator converts an atom.Feed struct
Expand Down
11 changes: 9 additions & 2 deletions src/Adapter/RSSFeedAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { RSSFeed, RSSItem } from '../types/RSS';
import { Enclosure, Feed, Image, Item, Person } from '../types/Feed';
import {
Enclosure,
Feed,
Image,
Item,
Person,
RSSFeed,
RSSItem,
} from '../types';
import { parsePerson } from '../utils/parsePerson';

export class RSSFeedAdapter {
Expand Down
36 changes: 18 additions & 18 deletions src/Extensions/DublinCoreExtension.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
type DCExtension = {
title: Maybe<string>
creator: Maybe<string>
author: Maybe<string>
subject: Maybe<string>
description: Maybe<string>
publisher: Maybe<string>
contributor: Maybe<string>
date: Maybe<string>
type: Maybe<string>
format: Maybe<string>
identifier: Maybe<string>
source: Maybe<string>
language: Maybe<string>
relation: Maybe<string>
coverage: Maybe<string>
rights: Maybe<string>
}
export type DCExtension = {
title: Maybe<string>;
creator: Maybe<string>;
author: Maybe<string>;
subject: Maybe<string>;
description: Maybe<string>;
publisher: Maybe<string>;
contributor: Maybe<string>;
date: Maybe<string>;
type: Maybe<string>;
format: Maybe<string>;
identifier: Maybe<string>;
source: Maybe<string>;
language: Maybe<string>;
relation: Maybe<string>;
coverage: Maybe<string>;
rights: Maybe<string>;
};
70 changes: 55 additions & 15 deletions src/Parser.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,55 @@
import { Feed } from './types/Feed';
import { Feed } from './types';
import { FeedType, XmlFeedTypeDetector } from './XmlFeedTypeDetector';
import { RSSParser } from './Parsers/RSSParser';
import { AtomParser } from './Parsers/AtomParser';
import { RSSFeedAdapter } from './Adapter/RSSFeedAdapter';
import { AtomFeedAdapter } from './Adapter/AtomFeedAdapter';
import { NetworkError } from './Errors/NetworkError';
import { FeedTypeError } from './Errors/FeedTypeError';
// @ts-ignore
import { version } from '../package.json';

export const DEFAULT_FETCH_HEADERS = {
'User-Agent': 'PulseRSS/1.0',
export type PFPOptions = {
/**
* Enables HTML content sanitization.
* Default sanitization rules will strip unwanted tags, attributes, comments
* and empty paragraphs. You can change this behavior with a function.
*/
sanitization?: boolean;
/**
* Options that will be passed to fetch() while parsing feeds from URLs.
* Default options contain a User-Agent string specific to PFP.
*/
fetchOptions?: RequestInit;
};

const DEFAULT_OPTIONS = {
sanitization: true,
fetchOptions: {
headers: {
'User-Agent': `pulse-feed-parser/${version}`,
},
},
};

/**
* Parser Factory
* Pulse Feed Parser Factory
*/
export class Parser {
fetchOptions: RequestInit;
options: PFPOptions;

constructor(
{ fetchOptions }: { fetchOptions: RequestInit } = { fetchOptions: {} }
) {
this.fetchOptions = fetchOptions;
/**
* Changed options will be merged with the defaults.
*/
constructor(options?: PFPOptions) {
this.options = options ? mergeOptions(options) : DEFAULT_OPTIONS;
}

/**
* Try to parse a feed from the given URL.
*/
public async parseURL(url: string): Promise<Feed> {
const response = await fetch(url, {
...this.fetchOptions,
headers: { ...DEFAULT_FETCH_HEADERS, ...this.fetchOptions?.headers },
});
const response = await fetch(url, this.options.fetchOptions);

if (response.status < 200 || response.status >= 300) {
throw new NetworkError(`The feed is unreachable`, response.status);
Expand All @@ -41,17 +63,35 @@ export class Parser {
return this.parseDocument(doc);
}

/**
* Parse a feed from the given XML document.
*/
public parseDocument(doc: Document): Feed {
const { sanitization } = this.options;
const type = XmlFeedTypeDetector.detect(doc);

if (type === FeedType.RSS) {
return RSSFeedAdapter.adapt(new RSSParser(doc).parse());
return RSSFeedAdapter.adapt(new RSSParser({ sanitization }).parse(doc));
}

if (type === FeedType.Atom) {
return AtomFeedAdapter.adapt(new AtomParser(doc).parse());
return AtomFeedAdapter.adapt(new AtomParser({ sanitization }).parse(doc));
}

throw new FeedTypeError('Unknown feed type');
}
}

/**
* Merge provided options with the defaults.
*/
const mergeOptions = (options: PFPOptions) => ({
...options,
fetchOptions: {
...options.fetchOptions,
headers: {
...DEFAULT_OPTIONS.fetchOptions.headers,
...options.fetchOptions?.headers,
},
},
});
34 changes: 16 additions & 18 deletions src/Parsers/AtomParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {
AtomLink,
AtomPerson,
AtomSource,
} from '../types/Atom';
IParser,
} from '../types';
import {
getExtensionName,
isExtension,
parseExtension,
} from '../utils/extensions';
import { BaseParser, ParserOptions } from './BaseParser';

// Atom elements which contain URIs
// https://tools.ietf.org/html/rfc4287
Expand All @@ -36,17 +38,14 @@ import {
/**
* Parser for Atom feeds
*/
export class AtomParser {
export class AtomParser extends BaseParser implements IParser {
private readonly entry: AtomEntry;
private readonly source: AtomSource;
private readonly person: AtomPerson;
private feed: AtomFeed;
private document: Document;
// private baseURL: Maybe<string>;

constructor(document: Document) {
this.document = document;
// this.baseURL = null;
constructor(options?: ParserOptions) {
super(options);

this.feed = {
id: null,
Expand Down Expand Up @@ -100,19 +99,14 @@ export class AtomParser {
this.person = { email: null, name: null, uri: null };
}

public parse(): AtomFeed {
const root = this.document.firstElementChild;
public parse(doc: Document): AtomFeed {
const root = doc.firstElementChild;

if (root === null) {
throw new Error('No root node');
}

// this.baseURL = root.getAttributeNS('xml', 'base');

const walker = window.document.createTreeWalker(
root,
NodeFilter.SHOW_ELEMENT
);
const walker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
walker.firstChild();
this.parseRoot(walker);

Expand Down Expand Up @@ -141,7 +135,8 @@ export class AtomParser {
...this.feed.extensions[ext][prop],
extension,
];
} if (tagName === 'title') {
}
if (tagName === 'title') {
this.feed.title = this.parseText(walker.currentNode as Element);
} else if (tagName === 'id') {
this.feed.id = this.parseText(walker.currentNode as Element);
Expand Down Expand Up @@ -217,7 +212,10 @@ export class AtomParser {
entry.extensions[ext][prop] = [];
}

entry.extensions[ext][prop] = [...entry.extensions[ext][prop], extension];
entry.extensions[ext][prop] = [
...entry.extensions[ext][prop],
extension,
];
} else if (tagName === 'title') {
entry.title = this.parseText(walker.currentNode as Element);
} else if (tagName === 'id') {
Expand Down Expand Up @@ -395,7 +393,7 @@ export class AtomParser {

// If type="xhtml", then this element contains inline xhtml, wrapped in a div element.
if (type === 'xhtml') {
return node.firstElementChild!.textContent!.trim()
return node.firstElementChild!.textContent!.trim();
}

return null;
Expand Down
15 changes: 15 additions & 0 deletions src/Parsers/BaseParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type ParserOptions = {
sanitization?: boolean;
};

const DEFAULT_OPTIONS = {
sanitization: true,
};

export abstract class BaseParser {
protected options: ParserOptions;

protected constructor(options?: ParserOptions) {
this.options = { ...DEFAULT_OPTIONS, ...options };
}
}
12 changes: 12 additions & 0 deletions src/Parsers/JSONParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @ts-nocheck
import { BaseParser, ParserOptions } from './BaseParser';
import { IParser } from '../types';

/**
* @todo
*/
export class JSONParser extends BaseParser implements IParser {
constructor(options: ParserOptions) {
super(options);
}
}
10 changes: 0 additions & 10 deletions src/Parsers/JsonParser.ts

This file was deleted.

Loading