1- import { XMLValidator } from 'fast-xml-parser' ;
1+ import { XMLBuilder , XMLParser } from 'fast-xml-parser' ;
22import { createCanonicalURL , isValidURL } from './util.js' ;
33
44type GlobResult = Record < string , ( ) => Promise < { [ key : string ] : any } > > ;
@@ -100,15 +100,17 @@ export default async function getRSS(rssOptions: RSSOptions) {
100100/** Generate RSS 2.0 feed */
101101export async function generateRSS ( { rssOptions, items } : GenerateRSSArgs ) : Promise < string > {
102102 const { site } = rssOptions ;
103- let xml = `<?xml version="1.0" encoding="UTF-8"?>` ;
103+ const xmlOptions = { ignoreAttributes : false } ;
104+ const parser = new XMLParser ( xmlOptions ) ;
105+ const root : any = { '?xml' : { '@_version' : '1.0' , '@_encoding' : 'UTF-8' } } ;
104106 if ( typeof rssOptions . stylesheet === 'string' ) {
105- xml += `< ?xml-stylesheet href=" ${ rssOptions . stylesheet } " type="text/xsl"?>` ;
107+ root [ ' ?xml-stylesheet' ] = { '@_href' : rssOptions . stylesheet , '@_encoding' : 'UTF-8' } ;
106108 }
107- xml += `<rss version=" 2.0"` ;
109+ root . rss = { '@_version' : ' 2.0' } ;
108110 if ( items . find ( ( result ) => result . content ) ) {
109111 // the namespace to be added to the xmlns:content attribute to enable the <content> RSS feature
110112 const XMLContentNamespace = 'http://purl.org/rss/1.0/modules/content/' ;
111- xml += ` xmlns :content=" ${ XMLContentNamespace } "` ;
113+ root . rss [ '@_xmlns :content' ] = XMLContentNamespace ;
112114 // Ensure that the user hasn't tried to manually include the necessary namespace themselves
113115 if ( rssOptions . xmlns ?. content && rssOptions . xmlns . content === XMLContentNamespace ) {
114116 delete rssOptions . xmlns . content ;
@@ -118,56 +120,55 @@ export async function generateRSS({ rssOptions, items }: GenerateRSSArgs): Promi
118120 // xmlns
119121 if ( rssOptions . xmlns ) {
120122 for ( const [ k , v ] of Object . entries ( rssOptions . xmlns ) ) {
121- xml += ` xmlns :${ k } =" ${ v } "` ;
123+ root . rss [ `@_xmlns :${ k } ` ] = v ;
122124 }
123125 }
124- xml += `>` ;
125- xml += `<channel>` ;
126126
127127 // title, description, customData
128- xml += `<title><![CDATA[${ rssOptions . title } ]]></title>` ;
129- xml += `<description><![CDATA[${ rssOptions . description } ]]></description>` ;
130- xml += `<link>${ createCanonicalURL ( site ) . href } </link>` ;
131- if ( typeof rssOptions . customData === 'string' ) xml += rssOptions . customData ;
128+ root . rss . channel = {
129+ title : rssOptions . title ,
130+ description : rssOptions . description ,
131+ link : createCanonicalURL ( site ) . href ,
132+ } ;
133+ if ( typeof rssOptions . customData === 'string' )
134+ Object . assign (
135+ root . rss . channel ,
136+ parser . parse ( `<channel>${ rssOptions . customData } </channel>` ) . channel
137+ ) ;
132138 // items
133- for ( const result of items ) {
139+ root . rss . channel . item = items . map ( ( result ) => {
134140 validate ( result ) ;
135- xml += `<item>` ;
136- xml += `<title><![CDATA[${ result . title } ]]></title>` ;
137141 // If the item's link is already a valid URL, don't mess with it.
138142 const itemLink = isValidURL ( result . link )
139143 ? result . link
140144 : createCanonicalURL ( result . link , site ) . href ;
141- xml += `<link>${ itemLink } </link>` ;
142- xml += `<guid>${ itemLink } </guid>` ;
143- if ( result . description ) xml += `<description><![CDATA[${ result . description } ]]></description>` ;
145+ const item : any = {
146+ title : result . title ,
147+ link : itemLink ,
148+ guid : itemLink ,
149+ } ;
150+ if ( result . description ) {
151+ item . description = result . description ;
152+ }
144153 if ( result . pubDate ) {
145154 // note: this should be a Date, but if user provided a string or number, we can work with that, too.
146155 if ( typeof result . pubDate === 'number' || typeof result . pubDate === 'string' ) {
147156 result . pubDate = new Date ( result . pubDate ) ;
148157 } else if ( result . pubDate instanceof Date === false ) {
149158 throw new Error ( '[${filename}] rss.item().pubDate must be a Date' ) ;
150159 }
151- xml += `<pubDate> ${ result . pubDate . toUTCString ( ) } </pubDate>` ;
160+ item . pubDate = result . pubDate . toUTCString ( ) ;
152161 }
153162 // include the full content of the post if the user supplies it
154163 if ( typeof result . content === 'string' ) {
155- xml += `< content:encoded><![CDATA[ ${ result . content } ]]></content:encoded>` ;
164+ item [ ' content:encoded' ] = result . content ;
156165 }
157- if ( typeof result . customData === 'string' ) xml += result . customData ;
158- xml += `</item>` ;
159- }
160-
161- xml += `</channel></rss>` ;
162-
163- // validate user’s inputs to see if it’s valid XML
164- const isValid = XMLValidator . validate ( xml ) ;
165- if ( isValid !== true ) {
166- // If valid XML, isValid will be `true`. Otherwise, this will be an error object. Throw.
167- throw new Error ( isValid as any ) ;
168- }
166+ if ( typeof rssOptions . customData === 'string' )
167+ Object . assign ( item , parser . parse ( `<item>${ rssOptions . customData } </item>` ) . item ) ;
168+ return item ;
169+ } ) ;
169170
170- return xml ;
171+ return new XMLBuilder ( xmlOptions ) . build ( root ) ;
171172}
172173
173174const requiredFields = Object . freeze ( [ 'link' , 'title' ] ) ;
0 commit comments