@tmlmobilidade/rss 20260331.1620.53 → 20260406.1442.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ import { type RssRawItem } from '../../types/feed.types.js';
2
+ export declare function rssFeedItem(item: RssRawItem): string;
@@ -0,0 +1,30 @@
1
+ /* * */
2
+ import { buildContentEncoded } from '../../utils/build-content-encoded.js';
3
+ import { escapeXml } from '../../utils/escape-xml.js';
4
+ import { normalizeImageInputs } from '../../utils/normalize-image-inputs.js';
5
+ /* * */
6
+ export function rssFeedItem(item) {
7
+ //
8
+ //
9
+ // A. Transform Data
10
+ const enclosuresXml = normalizeImageInputs(item.images ?? []).map((image) => {
11
+ const lengthAttr = image.length != null ? ` length="${String(image.length)}"` : '';
12
+ const typeAttr = image.type ? ` type="${escapeXml(image.type)}"` : '';
13
+ return `<enclosure url="${escapeXml(image.url)}"${lengthAttr}${typeAttr} />`;
14
+ });
15
+ //
16
+ // B. Return Result
17
+ return [
18
+ '<item>',
19
+ `<title>${escapeXml(item.title || '')}</title>`,
20
+ `<link>${escapeXml(item.link || '')}</link>`,
21
+ `<guid isPermaLink="true">${escapeXml(item.link || '')}</guid>`,
22
+ item.publishDate ? `<pubDate>${escapeXml(item.publishDate)}</pubDate>` : '',
23
+ `<description>${escapeXml(item.description)}</description>`,
24
+ buildContentEncoded(item),
25
+ enclosuresXml.length ? enclosuresXml.join('\n') : '',
26
+ '</item>',
27
+ ].filter(Boolean).join('\n');
28
+ //
29
+ }
30
+ /* * */
@@ -2,6 +2,7 @@ interface RssChannelOptions {
2
2
  channelCopyright: string;
3
3
  channelDescription: string;
4
4
  channelDocs: string;
5
+ channelFeedSelfUrl?: string;
5
6
  channelGenerator: string;
6
7
  channelLanguage: string;
7
8
  channelLastBuildDate: string;
@@ -9,5 +10,5 @@ interface RssChannelOptions {
9
10
  channelPubDate: string;
10
11
  channelTitle: string;
11
12
  }
12
- export declare function rssFeedXml(itemsXml: string, channelOptions: RssChannelOptions): string;
13
+ export declare function rssFeed(itemsXml: string, channelOptions: RssChannelOptions): string;
13
14
  export {};
@@ -1,11 +1,17 @@
1
1
  /* * */
2
- import { escapeXml } from './escape-xml.js';
3
- export function rssFeedXml(itemsXml, channelOptions) {
2
+ import { escapeXml } from '../../utils/escape-xml.js';
3
+ /* * */
4
+ export function rssFeed(itemsXml, channelOptions) {
5
+ //
6
+ //
7
+ // A. Setup Variables
8
+ const rssOpenTag = '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">';
9
+ //
10
+ // B. Return Result
4
11
  return [
5
12
  '<?xml version="1.0" encoding="UTF-8"?>',
6
- '<rss version="2.0">',
13
+ rssOpenTag,
7
14
  '<channel>',
8
- `<title>${escapeXml(channelOptions.channelTitle)}</title>`,
9
15
  `<link>${escapeXml(channelOptions.channelLink)}</link>`,
10
16
  `<description>${escapeXml(channelOptions.channelDescription)}</description>`,
11
17
  `<language>${escapeXml(channelOptions.channelLanguage)}</language>`,
@@ -14,9 +20,13 @@ export function rssFeedXml(itemsXml, channelOptions) {
14
20
  `<docs>${escapeXml(channelOptions.channelDocs)}</docs>`,
15
21
  `<pubDate>${escapeXml(channelOptions.channelPubDate)}</pubDate>`,
16
22
  `<copyright>${escapeXml(channelOptions.channelCopyright)}</copyright>`,
23
+ channelOptions.channelFeedSelfUrl
24
+ ? `<atom:link href="${escapeXml(channelOptions.channelFeedSelfUrl)}" rel="self" type="application/rss+xml" />`
25
+ : '',
17
26
  itemsXml,
18
27
  '</channel>',
19
28
  '</rss>',
20
- ].join('\n');
29
+ ].filter(Boolean).join('\n');
30
+ //
21
31
  }
22
32
  /* * */
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- import { type CreateRssFeedOptions, type RssRawItem } from './types/index.js';
1
+ import type { CreateRssFeedOptions, RssRawItem } from './types/index.js';
2
+ export type { RssRawImageInput } from './types/index.js';
2
3
  export declare function createRssFeed(rawItems: RssRawItem[], options: CreateRssFeedOptions): string;
package/dist/index.js CHANGED
@@ -1,23 +1,28 @@
1
1
  /* * */
2
- import { rssFeedXml, rssItemXml } from './utils/index.js';
2
+ import { rssFeedItem } from './components/RSSFeedItem/index.js';
3
+ import { rssFeed } from './components/RssXML/index.js';
3
4
  /* * */
4
5
  export function createRssFeed(rawItems, options) {
5
6
  //
6
7
  //
7
8
  // A. Create XML items
8
- const itemsXml = rawItems.map(item => rssItemXml({
9
+ const itemsXml = rawItems.map(item => rssFeedItem({
10
+ contentHtml: item.contentHtml,
9
11
  description: item.summary || item.description || '',
12
+ images: item.images,
10
13
  link: item.link || '',
14
+ linkLabel: item.linkLabel,
11
15
  publishDate: item.publishDate || item.publish_start_date || item.created_at ? new Date(item.publishDate || item.publish_start_date || item.created_at).toUTCString() : undefined,
12
16
  title: item.title || '',
13
17
  })).join('\n');
14
18
  //
15
19
  // B. Create and return XML feed
16
20
  const now = new Date().toUTCString();
17
- return rssFeedXml(itemsXml, {
21
+ return rssFeed(itemsXml, {
18
22
  channelCopyright: options.copyright,
19
23
  channelDescription: options.description,
20
24
  channelDocs: 'https://www.rssboard.org/rss-specification',
25
+ channelFeedSelfUrl: options.feedSelfUrl,
21
26
  channelGenerator: '@tmlmobilidade/rss',
22
27
  channelLanguage: 'pt-pt',
23
28
  channelLastBuildDate: now,
@@ -1,10 +1,17 @@
1
+ export interface RssRawImageInput {
2
+ alt?: null | string;
3
+ length?: null | number;
4
+ type?: null | string;
5
+ url: string;
6
+ }
1
7
  export interface RssRawItem {
2
8
  _id?: string;
9
+ contentHtml?: null | string;
3
10
  created_at?: number | string;
4
11
  description?: null | string;
5
- id?: string;
6
- info_url?: null | string;
12
+ images?: Array<RssRawImageInput>;
7
13
  link?: null | string;
14
+ linkLabel?: null | string;
8
15
  publish_start_date?: null | number;
9
16
  publishDate?: null | string;
10
17
  slug?: null | string;
@@ -14,6 +21,7 @@ export interface RssRawItem {
14
21
  export interface CreateRssFeedOptions {
15
22
  copyright: string;
16
23
  description: string;
24
+ feedSelfUrl?: string;
17
25
  link: string;
18
26
  title: string;
19
27
  }
@@ -1 +1,3 @@
1
+ /* * */
1
2
  export {};
3
+ /* * */
@@ -1 +1,2 @@
1
1
  export * from './feed.types.js';
2
+ export * from './normalized-rss-image.js';
@@ -1 +1,2 @@
1
1
  export * from './feed.types.js';
2
+ export * from './normalized-rss-image.js';
@@ -0,0 +1,6 @@
1
+ export interface NormalizedRssImage {
2
+ alt?: string;
3
+ length?: number;
4
+ type?: string;
5
+ url: string;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { RssRawItem } from '../types/feed.types.js';
2
+ export declare function buildContentEncoded(item: RssRawItem): string;
@@ -0,0 +1,37 @@
1
+ /* * */
2
+ import { escapeXml } from './escape-xml.js';
3
+ import { normalizeImageInputs } from './normalize-image-inputs.js';
4
+ /* * */
5
+ export function buildContentEncoded(item) {
6
+ //
7
+ //
8
+ // A. Setup Variables
9
+ const parts = [];
10
+ const bodyHtml = escapeXml(item.description).replace(/\n/g, '<br />');
11
+ //
12
+ // B. Transform Data
13
+ const cdataSafe = (fragment) => {
14
+ return fragment.replace(/\]\]>/g, ']]]]><![CDATA[>');
15
+ };
16
+ if (item.contentHtml) {
17
+ return `<content:encoded><![CDATA[${cdataSafe(item.contentHtml)}]]></content:encoded>`;
18
+ }
19
+ if (item.description) {
20
+ parts.push(`<div>${bodyHtml}</div>`);
21
+ }
22
+ for (const image of normalizeImageInputs(item.images ?? [])) {
23
+ const alt = image.alt ? escapeXml(image.alt) : '';
24
+ parts.push(`<p>`
25
+ + `<img src="${escapeXml(image.url)}" alt="${alt}" />`
26
+ + `</p>`);
27
+ }
28
+ if (item.link) {
29
+ parts.push(`<p>`
30
+ + `<a href="${escapeXml(item.link)}">${escapeXml(item.linkLabel ?? 'Ler artigo completo')}</a>`
31
+ + `</p>`);
32
+ }
33
+ //
34
+ // C. Return Result
35
+ return `<content:encoded><![CDATA[${cdataSafe(parts.join('\n'))}]]></content:encoded>`;
36
+ //
37
+ }
@@ -0,0 +1,3 @@
1
+ import { type RssRawImageInput } from '../types/feed.types.js';
2
+ import { NormalizedRssImage } from '../types/normalized-rss-image.js';
3
+ export declare function normalizeImageInputs(images: RssRawImageInput[]): NormalizedRssImage[];
@@ -0,0 +1,27 @@
1
+ /* * */
2
+ /* * */
3
+ export function normalizeImageInputs(images) {
4
+ //
5
+ //
6
+ // A. Setup Variables
7
+ const normalizedImages = [];
8
+ //
9
+ // B. Transform Data
10
+ for (const image of images ?? []) {
11
+ const url = image.url?.trim() ?? '';
12
+ const normalizedImage = { url };
13
+ if (!url)
14
+ continue;
15
+ if (image.alt)
16
+ normalizedImage.alt = image.alt;
17
+ if (image.type)
18
+ normalizedImage.type = image.type;
19
+ if (image.length != null && Number.isFinite(image.length))
20
+ normalizedImage.length = Math.floor(image.length);
21
+ normalizedImages.push(normalizedImage);
22
+ }
23
+ //
24
+ // C. Return Result
25
+ return normalizedImages;
26
+ //
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/rss",
3
- "version": "20260331.1620.53",
3
+ "version": "20260406.1442.3",
4
4
  "author": {
5
5
  "email": "iso@tmlmobilidade.pt",
6
6
  "name": "TML-ISO"
@@ -1,3 +0,0 @@
1
- export * from './escape-xml.js';
2
- export * from './rss-feed-xml.js';
3
- export * from './rss-item-xml.js';
@@ -1,3 +0,0 @@
1
- export * from './escape-xml.js';
2
- export * from './rss-feed-xml.js';
3
- export * from './rss-item-xml.js';
@@ -1,2 +0,0 @@
1
- import { type RssRawItem } from '../types/feed.types.js';
2
- export declare function rssItemXml(item: RssRawItem): string;
@@ -1,15 +0,0 @@
1
- /* * */
2
- import { escapeXml } from './escape-xml.js';
3
- /* * */
4
- export function rssItemXml(item) {
5
- return [
6
- '<item>',
7
- `<title>${escapeXml(item.title)}</title>`,
8
- `<link>${escapeXml(item.link || '')}</link>`,
9
- `<guid isPermaLink="true">${escapeXml(item.link || '')}</guid>`,
10
- item.publishDate ? `<pubDate>${escapeXml(item.publishDate)}</pubDate>` : '',
11
- `<description>${escapeXml(item.description)}</description>`,
12
- '</item>',
13
- ].filter(Boolean).join('\n');
14
- }
15
- /* * */