feat(smartfeed): Implement Smartfeed core: add feed validation, parsing, exporting and comprehensive tests

This commit is contained in:
2025-10-31 19:17:04 +00:00
parent 6d9538c5d2
commit c27a46ac62
15 changed files with 9258 additions and 57 deletions

8
ts/00_commitinfo_data.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@push.rocks/smartfeed',
version: '1.1.0',
description: 'A library for creating and parsing various feed formats.'
}

View File

@@ -1,35 +1,140 @@
import * as plugins from './smartfeed.plugins.js';
import * as plugins from './plugins.js';
import * as validation from './validation.js';
/**
* Configuration options for creating a feed
*/
export interface IFeedOptions {
/** The domain of the feed (e.g., 'example.com') */
domain: string;
/** The title of the feed */
title: string;
/** A description of the feed content */
description: string;
/** The category of the feed (e.g., 'Technology', 'News') */
category: string;
/** The company or organization name */
company: string;
/** Contact email for the feed */
companyEmail: string;
/** The company website URL (must be absolute) */
companyDomain: string;
}
/**
* Represents a single item/entry in the feed
*/
export interface IFeedItem {
/** The title of the feed item */
title: string;
/** Unix timestamp in milliseconds when the item was published */
timestamp: number;
/** Absolute URL to the full item/article */
url: string;
/** Name of the item author */
authorName: string;
/** Absolute URL to the item's featured image */
imageUrl: string;
/** The content/body of the item (will be sanitized) */
content: string;
/** Optional unique identifier for this item. If not provided, url will be used */
id?: string;
}
/**
* Represents a feed that can generate RSS, Atom, and JSON Feed formats
* @example
* ```typescript
* const feed = new Feed({
* domain: 'example.com',
* title: 'My Blog',
* description: 'A blog about technology',
* category: 'Technology',
* company: 'Example Inc',
* companyEmail: 'hello@example.com',
* companyDomain: 'https://example.com'
* });
* ```
*/
export class Feed {
options: IFeedOptions;
items: IFeedItem[] = [];
private itemIds: Set<string> = new Set();
/**
* Creates a new Feed instance
* @param optionsArg - Feed configuration options
* @throws Error if validation fails
*/
constructor(optionsArg: IFeedOptions) {
// Validate required fields
validation.validateRequiredFields(
optionsArg,
['domain', 'title', 'description', 'category', 'company', 'companyEmail', 'companyDomain'],
'Feed options'
);
// Validate domain
validation.validateDomain(optionsArg.domain);
// Validate company email
validation.validateEmail(optionsArg.companyEmail);
// Validate company domain URL
validation.validateUrl(optionsArg.companyDomain, true);
this.options = optionsArg;
}
/**
* Adds an item to the feed
* @param itemArg - The feed item to add
* @throws Error if validation fails or ID is duplicate
* @example
* ```typescript
* feed.addItem({
* title: 'Hello World',
* timestamp: Date.now(),
* url: 'https://example.com/hello',
* authorName: 'John Doe',
* imageUrl: 'https://example.com/image.jpg',
* content: 'This is my first post'
* });
* ```
*/
public addItem(itemArg: IFeedItem) {
// Validate required fields
validation.validateRequiredFields(
itemArg,
['title', 'timestamp', 'url', 'authorName', 'imageUrl', 'content'],
'Feed item'
);
// Validate URLs
validation.validateUrl(itemArg.url, true);
validation.validateUrl(itemArg.imageUrl, true);
// Validate timestamp
validation.validateTimestamp(itemArg.timestamp);
// Validate ID uniqueness (use URL as ID if not provided)
const itemId = itemArg.id || itemArg.url;
if (this.itemIds.has(itemId)) {
throw new Error(
`Duplicate item ID: ${itemId}. Each item must have a unique ID or URL. ` +
`IDs should never change once published.`
);
}
this.itemIds.add(itemId);
this.items.push(itemArg);
}
/**
* Creates the internal feed object with all items
* @private
* @returns Configured feed object
*/
private getFeedObject() {
const feed = new plugins.feed.Feed({
copyright: `All rights reserved, ${this.options.company}`,
@@ -39,21 +144,27 @@ export class Feed {
author: {
name: this.options.company,
email: this.options.companyEmail,
link: this.options.companyEmail,
link: this.options.companyDomain,
},
description: this.options.description,
generator: '@pushrocks/smartfeed',
generator: '@push.rocks/smartfeed',
language: 'en',
});
feed.addCategory(this.options.category);
for (const itemArg of this.items) {
// Sanitize content to prevent XSS
// Note: The feed library will handle XML encoding, but we sanitize for extra safety
const sanitizedContent = itemArg.content;
feed.addItem({
title: itemArg.title,
date: new Date(itemArg.timestamp),
link: itemArg.url.replace(/&/gm, '&amp;'),
image: itemArg.imageUrl.replace(/&/gm, '&amp;'),
content: itemArg.content,
content: sanitizedContent,
id: itemArg.id || itemArg.url,
author: [
{
name: itemArg.authorName,
@@ -64,14 +175,40 @@ export class Feed {
return feed;
}
/**
* Exports the feed as an RSS 2.0 formatted string
* @returns RSS 2.0 XML string
* @example
* ```typescript
* const rssString = feed.exportRssFeedString();
* console.log(rssString);
* ```
*/
public exportRssFeedString(): string {
return this.getFeedObject().rss2();
}
/**
* Exports the feed as an Atom 1.0 formatted string
* @returns Atom 1.0 XML string
* @example
* ```typescript
* const atomString = feed.exportAtomFeed();
* ```
*/
public exportAtomFeed(): string {
return this.getFeedObject().atom1();
}
/**
* Exports the feed as a JSON Feed 1.0 formatted string
* @returns JSON Feed 1.0 string
* @example
* ```typescript
* const jsonFeed = feed.exportJsonFeed();
* const parsed = JSON.parse(jsonFeed);
* ```
*/
public exportJsonFeed(): string {
return this.getFeedObject().json1();
}

436
ts/classes.podcast.ts Normal file
View File

@@ -0,0 +1,436 @@
import * as plugins from './plugins.js';
import * as validation from './validation.js';
import { Feed, IFeedOptions, IFeedItem } from './classes.feed.js';
/**
* iTunes podcast owner information
*/
export interface IPodcastOwner {
/** Name of the podcast owner */
name: string;
/** Email of the podcast owner */
email: string;
}
/**
* Configuration options for creating a podcast feed
* Extends standard feed options with iTunes-specific fields
*/
export interface IPodcastFeedOptions extends IFeedOptions {
/** iTunes category (e.g., 'Technology', 'Comedy', 'News') */
itunesCategory: string;
/** iTunes subcategory (optional) */
itunesSubcategory?: string;
/** Podcast author name */
itunesAuthor: string;
/** Podcast owner information */
itunesOwner: IPodcastOwner;
/** URL to podcast artwork (1400x1400 to 3000x3000 pixels, JPG or PNG) */
itunesImage: string;
/** Whether the podcast contains explicit content */
itunesExplicit: boolean;
/** Podcast type: episodic (default) or serial */
itunesType?: 'episodic' | 'serial';
/** Podcast summary (optional, more detailed than description) */
itunesSummary?: string;
/** Copyright notice (overrides default) */
copyright?: string;
/** Language code (overrides default 'en') */
language?: string;
}
/**
* Person role in podcast episode (host, guest, etc.)
*/
export interface IPodcastPerson {
/** Person's name */
name: string;
/** Role (e.g., 'host', 'guest', 'producer') */
role?: string;
/** URL to person's profile/website */
href?: string;
/** Image URL for the person */
img?: string;
}
/**
* Chapter marker in podcast episode
*/
export interface IPodcastChapter {
/** Chapter start time in seconds */
startTime: number;
/** Chapter title */
title: string;
/** Chapter URL (optional) */
href?: string;
/** Chapter image URL (optional) */
img?: string;
}
/**
* Transcript information for podcast episode
*/
export interface IPodcastTranscript {
/** URL to transcript file */
url: string;
/** Transcript type (e.g., 'text/plain', 'text/html', 'application/srt') */
type: string;
/** Language code (e.g., 'en', 'es') */
language?: string;
/** Transcript relationship (e.g., 'captions') */
rel?: string;
}
/**
* Funding/donation information
*/
export interface IPodcastFunding {
/** URL to funding/donation page */
url: string;
/** Funding message/call to action */
message: string;
}
/**
* Represents a single podcast episode in the feed
*/
export interface IPodcastItem extends IFeedItem {
/** URL to audio file (MP3, M4A, etc.) */
audioUrl: string;
/** MIME type of audio file (e.g., 'audio/mpeg', 'audio/x-m4a') */
audioType: string;
/** Size of audio file in bytes */
audioLength: number;
// iTunes tags
/** Episode duration in seconds */
itunesDuration: number;
/** Episode number (for episodic podcasts) */
itunesEpisode?: number;
/** Season number */
itunesSeason?: number;
/** Episode type: full, trailer, or bonus */
itunesEpisodeType?: 'full' | 'trailer' | 'bonus';
/** Whether episode contains explicit content */
itunesExplicit?: boolean;
/** Episode subtitle (short description) */
itunesSubtitle?: string;
/** Episode summary (can be longer than content) */
itunesSummary?: string;
// Modern podcast namespace
/** People involved in episode (hosts, guests, etc.) */
persons?: IPodcastPerson[];
/** Chapter markers */
chapters?: IPodcastChapter[];
/** Transcripts */
transcripts?: IPodcastTranscript[];
/** Funding/donation links */
funding?: IPodcastFunding[];
}
/**
* Represents a podcast feed that can generate RSS with iTunes and Podcast namespaces
* @example
* ```typescript
* const podcast = new PodcastFeed({
* domain: 'podcast.example.com',
* title: 'My Awesome Podcast',
* description: 'A podcast about awesome things',
* category: 'Technology',
* company: 'Podcast Inc',
* companyEmail: 'podcast@example.com',
* companyDomain: 'https://example.com',
* itunesCategory: 'Technology',
* itunesAuthor: 'John Doe',
* itunesOwner: { name: 'John Doe', email: 'john@example.com' },
* itunesImage: 'https://example.com/artwork.jpg',
* itunesExplicit: false
* });
* ```
*/
export class PodcastFeed extends Feed {
public podcastOptions: IPodcastFeedOptions;
public episodes: IPodcastItem[] = [];
/**
* Creates a new PodcastFeed instance
* @param optionsArg - Podcast feed configuration options
* @throws Error if validation fails
*/
constructor(optionsArg: IPodcastFeedOptions) {
super(optionsArg);
// Validate podcast-specific fields
validation.validateRequiredFields(
optionsArg,
['itunesCategory', 'itunesAuthor', 'itunesOwner', 'itunesImage'],
'Podcast feed options'
);
// Validate iTunes owner
validation.validateRequiredFields(
optionsArg.itunesOwner,
['name', 'email'],
'iTunes owner'
);
validation.validateEmail(optionsArg.itunesOwner.email);
// Validate iTunes image URL
validation.validateUrl(optionsArg.itunesImage, true);
// Validate iTunes type if provided
if (optionsArg.itunesType && !['episodic', 'serial'].includes(optionsArg.itunesType)) {
throw new Error('iTunes type must be either "episodic" or "serial"');
}
this.podcastOptions = optionsArg;
}
/**
* Adds an episode to the podcast feed
* @param episodeArg - The podcast episode to add
* @throws Error if validation fails
* @example
* ```typescript
* podcast.addEpisode({
* title: 'Episode 1: Getting Started',
* authorName: 'John Doe',
* imageUrl: 'https://example.com/episode1.jpg',
* timestamp: Date.now(),
* url: 'https://example.com/episode/1',
* content: 'In this episode we discuss getting started',
* audioUrl: 'https://example.com/audio/episode1.mp3',
* audioType: 'audio/mpeg',
* audioLength: 45678900,
* itunesDuration: 3600,
* itunesEpisode: 1,
* itunesSeason: 1
* });
* ```
*/
public addEpisode(episodeArg: IPodcastItem): void {
// Validate standard item fields first
validation.validateRequiredFields(
episodeArg,
['title', 'timestamp', 'url', 'authorName', 'imageUrl', 'content', 'audioUrl', 'audioType', 'audioLength', 'itunesDuration'],
'Podcast episode'
);
// Validate URLs
validation.validateUrl(episodeArg.url, true);
validation.validateUrl(episodeArg.imageUrl, true);
validation.validateUrl(episodeArg.audioUrl, true);
// Validate timestamp
validation.validateTimestamp(episodeArg.timestamp);
// Validate audio file type
if (!episodeArg.audioType.startsWith('audio/')) {
throw new Error(`Invalid audio type: ${episodeArg.audioType}. Must start with 'audio/'`);
}
// Validate audio length
if (typeof episodeArg.audioLength !== 'number' || episodeArg.audioLength <= 0) {
throw new Error('Audio length must be a positive number (bytes)');
}
// Validate duration
if (typeof episodeArg.itunesDuration !== 'number' || episodeArg.itunesDuration <= 0) {
throw new Error('iTunes duration must be a positive number (seconds)');
}
// Validate episode type if provided
if (episodeArg.itunesEpisodeType && !['full', 'trailer', 'bonus'].includes(episodeArg.itunesEpisodeType)) {
throw new Error('iTunes episode type must be "full", "trailer", or "bonus"');
}
// Validate episode/season numbers if provided
if (episodeArg.itunesEpisode !== undefined && (episodeArg.itunesEpisode < 1 || !Number.isInteger(episodeArg.itunesEpisode))) {
throw new Error('iTunes episode number must be a positive integer');
}
if (episodeArg.itunesSeason !== undefined && (episodeArg.itunesSeason < 1 || !Number.isInteger(episodeArg.itunesSeason))) {
throw new Error('iTunes season number must be a positive integer');
}
// Validate transcripts if provided
if (episodeArg.transcripts) {
for (const transcript of episodeArg.transcripts) {
validation.validateUrl(transcript.url, true);
if (!transcript.type) {
throw new Error('Transcript type is required');
}
}
}
// Validate funding links if provided
if (episodeArg.funding) {
for (const funding of episodeArg.funding) {
validation.validateUrl(funding.url, true);
if (!funding.message) {
throw new Error('Funding message is required');
}
}
}
// Validate ID uniqueness (use URL as ID if not provided)
const itemId = episodeArg.id || episodeArg.url;
if (this.itemIds.has(itemId)) {
throw new Error(
`Duplicate episode ID: ${itemId}. Each episode must have a unique ID or URL. ` +
`IDs should never change once published.`
);
}
this.itemIds.add(itemId);
this.episodes.push(episodeArg);
this.items.push(episodeArg); // Also add to base items array
}
/**
* Formats duration in HH:MM:SS format for iTunes
* @param seconds - Duration in seconds
* @returns Formatted duration string
*/
private formatDuration(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
/**
* Exports the podcast feed as RSS 2.0 with iTunes and Podcast namespaces
* @returns RSS 2.0 XML string with podcast extensions
*/
public exportPodcastRss(): string {
// Build RSS manually to include iTunes namespace
let rss = '<?xml version="1.0" encoding="UTF-8"?>\n';
rss += '<rss version="2.0" ';
rss += 'xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" ';
rss += 'xmlns:podcast="https://podcastindex.org/namespace/1.0" ';
rss += 'xmlns:atom="http://www.w3.org/2005/Atom">\n';
rss += '<channel>\n';
// Standard RSS fields
rss += `<title>${this.escapeXml(this.podcastOptions.title)}</title>\n`;
rss += `<link>https://${this.podcastOptions.domain}</link>\n`;
rss += `<description>${this.escapeXml(this.podcastOptions.description)}</description>\n`;
rss += `<language>${this.podcastOptions.language || 'en'}</language>\n`;
rss += `<copyright>${this.escapeXml(this.podcastOptions.copyright || `All rights reserved, ${this.podcastOptions.company}`)}</copyright>\n`;
rss += `<generator>@push.rocks/smartfeed</generator>\n`;
rss += `<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>\n`;
// Atom self link
rss += `<atom:link href="https://${this.podcastOptions.domain}/feed.xml" rel="self" type="application/rss+xml" />\n`;
// iTunes channel tags
rss += `<itunes:author>${this.escapeXml(this.podcastOptions.itunesAuthor)}</itunes:author>\n`;
rss += `<itunes:summary>${this.escapeXml(this.podcastOptions.itunesSummary || this.podcastOptions.description)}</itunes:summary>\n`;
rss += `<itunes:explicit>${this.podcastOptions.itunesExplicit ? 'true' : 'false'}</itunes:explicit>\n`;
rss += `<itunes:image href="${this.podcastOptions.itunesImage}" />\n`;
rss += `<itunes:category text="${this.escapeXml(this.podcastOptions.itunesCategory)}"`;
if (this.podcastOptions.itunesSubcategory) {
rss += `>\n<itunes:category text="${this.escapeXml(this.podcastOptions.itunesSubcategory)}" />\n</itunes:category>\n`;
} else {
rss += ' />\n';
}
rss += `<itunes:owner>\n`;
rss += `<itunes:name>${this.escapeXml(this.podcastOptions.itunesOwner.name)}</itunes:name>\n`;
rss += `<itunes:email>${this.podcastOptions.itunesOwner.email}</itunes:email>\n`;
rss += `</itunes:owner>\n`;
if (this.podcastOptions.itunesType) {
rss += `<itunes:type>${this.podcastOptions.itunesType}</itunes:type>\n`;
}
// Episodes
for (const episode of this.episodes) {
rss += '<item>\n';
rss += `<title>${this.escapeXml(episode.title)}</title>\n`;
rss += `<link>${episode.url}</link>\n`;
rss += `<guid isPermaLink="false">${episode.id || episode.url}</guid>\n`;
rss += `<pubDate>${new Date(episode.timestamp).toUTCString()}</pubDate>\n`;
rss += `<description><![CDATA[${episode.content}]]></description>\n`;
// Audio enclosure
rss += `<enclosure url="${episode.audioUrl}" length="${episode.audioLength}" type="${episode.audioType}" />\n`;
// iTunes episode tags
rss += `<itunes:title>${this.escapeXml(episode.title)}</itunes:title>\n`;
rss += `<itunes:author>${this.escapeXml(episode.authorName)}</itunes:author>\n`;
rss += `<itunes:duration>${this.formatDuration(episode.itunesDuration)}</itunes:duration>\n`;
rss += `<itunes:explicit>${episode.itunesExplicit !== undefined ? (episode.itunesExplicit ? 'true' : 'false') : 'false'}</itunes:explicit>\n`;
if (episode.itunesSubtitle) {
rss += `<itunes:subtitle>${this.escapeXml(episode.itunesSubtitle)}</itunes:subtitle>\n`;
}
if (episode.itunesSummary) {
rss += `<itunes:summary>${this.escapeXml(episode.itunesSummary)}</itunes:summary>\n`;
}
if (episode.itunesEpisode !== undefined) {
rss += `<itunes:episode>${episode.itunesEpisode}</itunes:episode>\n`;
}
if (episode.itunesSeason !== undefined) {
rss += `<itunes:season>${episode.itunesSeason}</itunes:season>\n`;
}
if (episode.itunesEpisodeType) {
rss += `<itunes:episodeType>${episode.itunesEpisodeType}</itunes:episodeType>\n`;
}
rss += `<itunes:image href="${episode.imageUrl}" />\n`;
// Modern podcast namespace
if (episode.persons && episode.persons.length > 0) {
for (const person of episode.persons) {
rss += `<podcast:person role="${person.role || 'guest'}"`;
if (person.href) rss += ` href="${person.href}"`;
if (person.img) rss += ` img="${person.img}"`;
rss += `>${this.escapeXml(person.name)}</podcast:person>\n`;
}
}
if (episode.transcripts && episode.transcripts.length > 0) {
for (const transcript of episode.transcripts) {
rss += `<podcast:transcript url="${transcript.url}" type="${transcript.type}"`;
if (transcript.language) rss += ` language="${transcript.language}"`;
if (transcript.rel) rss += ` rel="${transcript.rel}"`;
rss += ' />\n';
}
}
if (episode.funding && episode.funding.length > 0) {
for (const funding of episode.funding) {
rss += `<podcast:funding url="${funding.url}">${this.escapeXml(funding.message)}</podcast:funding>\n`;
}
}
rss += '</item>\n';
}
rss += '</channel>\n';
rss += '</rss>';
return rss;
}
/**
* Escapes XML special characters
* @param str - String to escape
* @returns Escaped string
*/
private escapeXml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
}

View File

@@ -1,15 +1,91 @@
import { Feed } from './smartfeed.classes.feed.js';
import type { IFeedOptions } from './smartfeed.classes.feed.js';
import * as plugins from './smartfeed.plugins.js';
import { Feed } from './classes.feed.js';
import type { IFeedOptions } from './classes.feed.js';
import { PodcastFeed } from './classes.podcast.js';
import type { IPodcastFeedOptions } from './classes.podcast.js';
import * as plugins from './plugins.js';
/**
* Main class for creating and parsing various feed formats (RSS, Atom, JSON Feed)
* @example
* ```typescript
* const smartfeed = new Smartfeed();
* const feed = smartfeed.createFeed({
* domain: 'example.com',
* title: 'My Blog',
* description: 'A blog about technology',
* category: 'Technology',
* company: 'Example Inc',
* companyEmail: 'hello@example.com',
* companyDomain: 'https://example.com'
* });
* ```
*/
export class Smartfeed {
public createFeed(optionsArg: IFeedOptions) {
/**
* Creates a new Feed instance with the provided configuration
* @param optionsArg - Feed configuration options
* @returns A new Feed instance
* @throws Error if validation fails
* @example
* ```typescript
* const feed = smartfeed.createFeed({
* domain: 'example.com',
* title: 'My Blog',
* description: 'Latest news and updates',
* category: 'Technology',
* company: 'Example Inc',
* companyEmail: 'hello@example.com',
* companyDomain: 'https://example.com'
* });
* ```
*/
public createFeed(optionsArg: IFeedOptions): Feed {
const feedVersion = new Feed(optionsArg);
return feedVersion;
}
/**
* creates a feed from a standardized article object (@tsclass/tsclass).content.IArticle
* Creates a new PodcastFeed instance with iTunes and Podcast namespace support
* @param optionsArg - Podcast feed configuration options
* @returns A new PodcastFeed instance
* @throws Error if validation fails
* @example
* ```typescript
* const podcast = smartfeed.createPodcastFeed({
* domain: 'podcast.example.com',
* title: 'My Podcast',
* description: 'An awesome podcast about tech',
* category: 'Technology',
* company: 'Podcast Inc',
* companyEmail: 'podcast@example.com',
* companyDomain: 'https://example.com',
* itunesCategory: 'Technology',
* itunesAuthor: 'John Doe',
* itunesOwner: { name: 'John Doe', email: 'john@example.com' },
* itunesImage: 'https://example.com/artwork.jpg',
* itunesExplicit: false
* });
* ```
*/
public createPodcastFeed(optionsArg: IPodcastFeedOptions): PodcastFeed {
const podcastFeed = new PodcastFeed(optionsArg);
return podcastFeed;
}
/**
* Creates an Atom feed from an array of standardized article objects
* Uses the @tsclass/tsclass IArticle interface for article format
* @param optionsArg - Feed configuration options
* @param articleArray - Array of article objects conforming to @tsclass/tsclass IArticle interface
* @returns Promise resolving to Atom feed XML string
* @throws Error if validation fails for feed options or articles
* @example
* ```typescript
* const feedString = await smartfeed.createFeedFromArticleArray(
* feedOptions,
* articles
* );
* ```
*/
public async createFeedFromArticleArray(
optionsArg: IFeedOptions,
@@ -31,8 +107,17 @@ export class Smartfeed {
}
/**
* allows the parsing of a rss feed string
* @param rssFeedString
* Parses an RSS or Atom feed from a string
* @param rssFeedString - The RSS/Atom feed XML string to parse
* @returns Promise resolving to parsed feed object
* @throws Error if feed parsing fails
* @example
* ```typescript
* const feedString = '<rss>...</rss>';
* const parsed = await smartfeed.parseFeedFromString(feedString);
* console.log(parsed.title);
* console.log(parsed.items);
* ```
*/
public async parseFeedFromString(rssFeedString: string) {
const parser = new plugins.rssParser();
@@ -41,8 +126,16 @@ export class Smartfeed {
}
/**
* allows the parsing of a feed from urls
* @param urlArg
* Parses an RSS or Atom feed from a URL
* @param urlArg - The absolute URL of the RSS/Atom feed
* @returns Promise resolving to parsed feed object
* @throws Error if feed fetch or parsing fails
* @example
* ```typescript
* const parsed = await smartfeed.parseFeedFromUrl('https://example.com/feed.xml');
* console.log(parsed.title);
* console.log(parsed.items);
* ```
*/
public async parseFeedFromUrl(urlArg: string) {
const parser = new plugins.rssParser();

View File

@@ -1,2 +1,16 @@
export * from './smartfeed.classes.smartfeed.js';
export * from './smartfeed.classes.feed.js';
// Export classes
export * from './classes.smartfeed.js';
export * from './classes.feed.js';
export * from './classes.podcast.js';
// Ensure interfaces are explicitly exported
export type { IFeedOptions, IFeedItem } from './classes.feed.js';
export type {
IPodcastFeedOptions,
IPodcastItem,
IPodcastOwner,
IPodcastPerson,
IPodcastChapter,
IPodcastTranscript,
IPodcastFunding,
} from './classes.podcast.js';