@stati/core 1.10.3 → 1.11.1
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.
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +52 -4
- package/dist/core/invalidate.d.ts.map +1 -1
- package/dist/core/invalidate.js +3 -110
- package/dist/core/utils/glob-patterns.d.ts +40 -0
- package/dist/core/utils/glob-patterns.d.ts.map +1 -0
- package/dist/core/utils/glob-patterns.js +127 -0
- package/dist/core/utils/index.d.ts +1 -0
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/rss/generator.d.ts +26 -0
- package/dist/rss/generator.d.ts.map +1 -0
- package/dist/rss/generator.js +331 -0
- package/dist/rss/index.d.ts +8 -0
- package/dist/rss/index.d.ts.map +1 -0
- package/dist/rss/index.js +6 -0
- package/dist/rss/utils/index.d.ts +6 -0
- package/dist/rss/utils/index.d.ts.map +1 -0
- package/dist/rss/utils/index.js +5 -0
- package/dist/rss/utils/pattern-matching.d.ts +33 -0
- package/dist/rss/utils/pattern-matching.d.ts.map +1 -0
- package/dist/rss/utils/pattern-matching.js +87 -0
- package/dist/rss/validation.d.ts +30 -0
- package/dist/rss/validation.d.ts.map +1 -0
- package/dist/rss/validation.js +124 -0
- package/dist/seo/sitemap.d.ts.map +1 -1
- package/dist/seo/sitemap.js +3 -52
- package/dist/types/config.d.ts +2 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/rss.d.ts +247 -0
- package/dist/types/rss.d.ts.map +1 -0
- package/dist/types/rss.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RSS feed generation utilities for Stati
|
|
3
|
+
* @module rss/generator
|
|
4
|
+
*/
|
|
5
|
+
import { escapeHtml } from '../seo/utils/index.js';
|
|
6
|
+
import { filterByPatterns } from './utils/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Formats a date for RSS pubDate field (RFC 822)
|
|
9
|
+
* @param date - Date to format
|
|
10
|
+
* @returns RFC 822 formatted date string
|
|
11
|
+
*/
|
|
12
|
+
function formatRSSDate(date) {
|
|
13
|
+
return date.toUTCString();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Converts a date string or Date object to RFC 822 format
|
|
17
|
+
* @param dateInput - Date string or Date object
|
|
18
|
+
* @returns RFC 822 formatted date string or undefined if invalid
|
|
19
|
+
*/
|
|
20
|
+
function parseAndFormatDate(dateInput) {
|
|
21
|
+
if (!dateInput)
|
|
22
|
+
return undefined;
|
|
23
|
+
let date;
|
|
24
|
+
if (typeof dateInput === 'string') {
|
|
25
|
+
date = new Date(dateInput);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
date = dateInput;
|
|
29
|
+
}
|
|
30
|
+
if (isNaN(date.getTime())) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return formatRSSDate(date);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Filters pages based on feed configuration
|
|
37
|
+
* @param pages - All pages
|
|
38
|
+
* @param feedConfig - Feed configuration
|
|
39
|
+
* @returns Filtered pages
|
|
40
|
+
*/
|
|
41
|
+
function filterPages(pages, feedConfig) {
|
|
42
|
+
const options = {};
|
|
43
|
+
if (feedConfig.contentPatterns) {
|
|
44
|
+
options.includePatterns = feedConfig.contentPatterns;
|
|
45
|
+
}
|
|
46
|
+
if (feedConfig.excludePatterns) {
|
|
47
|
+
options.excludePatterns = feedConfig.excludePatterns;
|
|
48
|
+
}
|
|
49
|
+
let filtered = filterByPatterns(pages, (page) => page.sourcePath, options);
|
|
50
|
+
// Apply custom filter function if provided
|
|
51
|
+
if (feedConfig.filter) {
|
|
52
|
+
filtered = filtered.filter(feedConfig.filter);
|
|
53
|
+
}
|
|
54
|
+
return filtered;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Helper to get date value from page for sorting
|
|
58
|
+
* @param page - Page to get date from
|
|
59
|
+
* @returns Date value or undefined
|
|
60
|
+
*/
|
|
61
|
+
function getPageDate(page) {
|
|
62
|
+
return page.frontMatter.publishedAt || page.frontMatter.date;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Sorts pages based on feed configuration
|
|
66
|
+
* @param pages - Pages to sort
|
|
67
|
+
* @param feedConfig - Feed configuration
|
|
68
|
+
* @returns Sorted pages
|
|
69
|
+
*/
|
|
70
|
+
function sortPages(pages, feedConfig) {
|
|
71
|
+
const sorted = [...pages];
|
|
72
|
+
const sortBy = feedConfig.sortBy || 'date-desc';
|
|
73
|
+
if (sortBy === 'custom' && feedConfig.sortFn) {
|
|
74
|
+
return sorted.sort(feedConfig.sortFn);
|
|
75
|
+
}
|
|
76
|
+
return sorted.sort((a, b) => {
|
|
77
|
+
switch (sortBy) {
|
|
78
|
+
case 'date-desc':
|
|
79
|
+
case 'date-asc': {
|
|
80
|
+
const dateA = getPageDate(a);
|
|
81
|
+
const dateB = getPageDate(b);
|
|
82
|
+
if (!dateA && !dateB)
|
|
83
|
+
return 0;
|
|
84
|
+
if (!dateA)
|
|
85
|
+
return 1;
|
|
86
|
+
if (!dateB)
|
|
87
|
+
return -1;
|
|
88
|
+
const comparison = new Date(dateA).getTime() - new Date(dateB).getTime();
|
|
89
|
+
return sortBy === 'date-asc' ? comparison : -comparison;
|
|
90
|
+
}
|
|
91
|
+
case 'title-asc':
|
|
92
|
+
case 'title-desc': {
|
|
93
|
+
const titleA = (a.frontMatter.title || '').toLowerCase();
|
|
94
|
+
const titleB = (b.frontMatter.title || '').toLowerCase();
|
|
95
|
+
const comparison = titleA.localeCompare(titleB);
|
|
96
|
+
return sortBy === 'title-asc' ? comparison : -comparison;
|
|
97
|
+
}
|
|
98
|
+
default:
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Type guard to ensure we have a valid record-like object
|
|
105
|
+
* @param value - Value to check
|
|
106
|
+
* @returns True if value is a non-null object
|
|
107
|
+
*/
|
|
108
|
+
function isValidRecord(value) {
|
|
109
|
+
return value !== null && value !== undefined && typeof value === 'object';
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Gets a value from page using field mapping with improved type safety
|
|
113
|
+
* @param page - Page to extract value from
|
|
114
|
+
* @param mapping - Field mapping (property name or function)
|
|
115
|
+
* @param defaultProp - Default property name to use
|
|
116
|
+
* @param logger - Logger instance for warnings
|
|
117
|
+
* @returns Extracted value or undefined
|
|
118
|
+
*/
|
|
119
|
+
function getFieldValue(page, mapping, defaultProp, logger) {
|
|
120
|
+
if (typeof mapping === 'function') {
|
|
121
|
+
try {
|
|
122
|
+
return mapping(page);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// Log error but don't crash - return undefined for invalid mappings
|
|
126
|
+
if (logger) {
|
|
127
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
128
|
+
logger.warning(`Field mapping function failed for page ${page.slug}: ${errorMessage}`);
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const prop = mapping || defaultProp;
|
|
134
|
+
if (!prop)
|
|
135
|
+
return undefined;
|
|
136
|
+
// Type-safe access to frontMatter properties using type guard
|
|
137
|
+
// FrontMatter interface allows index signature for custom properties
|
|
138
|
+
if (!isValidRecord(page.frontMatter)) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const value = page.frontMatter[prop];
|
|
142
|
+
// Return value with proper type checking
|
|
143
|
+
return value !== undefined && value !== null ? value : undefined;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Builds the absolute URL for a page
|
|
147
|
+
* @param page - Page to build URL for
|
|
148
|
+
* @param config - Stati configuration
|
|
149
|
+
* @returns Absolute URL
|
|
150
|
+
*/
|
|
151
|
+
function buildPageUrl(page, config) {
|
|
152
|
+
const baseUrl = config.site.baseUrl.replace(/\/$/, '');
|
|
153
|
+
const pageUrl = page.url.startsWith('/') ? page.url : '/' + page.url;
|
|
154
|
+
return baseUrl + pageUrl;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Generates RSS XML for a single feed
|
|
158
|
+
* @param pages - All pages in the site
|
|
159
|
+
* @param config - Stati configuration
|
|
160
|
+
* @param feedConfig - Feed configuration
|
|
161
|
+
* @param logger - Logger instance for warnings (optional)
|
|
162
|
+
* @returns RSS generation result
|
|
163
|
+
*/
|
|
164
|
+
export function generateRSSFeed(pages, config, feedConfig, logger) {
|
|
165
|
+
// Filter and sort pages
|
|
166
|
+
let feedPages = sortPages(filterPages(pages, feedConfig), feedConfig);
|
|
167
|
+
// Limit to maxItems if specified
|
|
168
|
+
if (feedConfig.maxItems && feedConfig.maxItems > 0) {
|
|
169
|
+
feedPages = feedPages.slice(0, feedConfig.maxItems);
|
|
170
|
+
}
|
|
171
|
+
// Build feed metadata
|
|
172
|
+
const feedLink = feedConfig.link || config.site.baseUrl;
|
|
173
|
+
const feedTitle = feedConfig.title;
|
|
174
|
+
const feedDescription = feedConfig.description;
|
|
175
|
+
// Start building XML
|
|
176
|
+
const xmlLines = [];
|
|
177
|
+
xmlLines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
178
|
+
// Build namespace attributes
|
|
179
|
+
const namespaces = feedConfig.namespaces || {};
|
|
180
|
+
const nsAttrs = Object.entries(namespaces)
|
|
181
|
+
.map(([prefix, uri]) => `xmlns:${prefix}="${escapeHtml(uri)}"`)
|
|
182
|
+
.join(' ');
|
|
183
|
+
xmlLines.push(`<rss version="2.0"${nsAttrs ? ' ' + nsAttrs : ''}>`);
|
|
184
|
+
xmlLines.push(' <channel>');
|
|
185
|
+
xmlLines.push(` <title>${escapeHtml(feedTitle)}</title>`);
|
|
186
|
+
xmlLines.push(` <link>${escapeHtml(feedLink)}</link>`);
|
|
187
|
+
xmlLines.push(` <description>${escapeHtml(feedDescription)}</description>`);
|
|
188
|
+
// Optional channel elements
|
|
189
|
+
if (feedConfig.language) {
|
|
190
|
+
xmlLines.push(` <language>${escapeHtml(feedConfig.language)}</language>`);
|
|
191
|
+
}
|
|
192
|
+
if (feedConfig.copyright) {
|
|
193
|
+
xmlLines.push(` <copyright>${escapeHtml(feedConfig.copyright)}</copyright>`);
|
|
194
|
+
}
|
|
195
|
+
if (feedConfig.managingEditor) {
|
|
196
|
+
xmlLines.push(` <managingEditor>${escapeHtml(feedConfig.managingEditor)}</managingEditor>`);
|
|
197
|
+
}
|
|
198
|
+
if (feedConfig.webMaster) {
|
|
199
|
+
xmlLines.push(` <webMaster>${escapeHtml(feedConfig.webMaster)}</webMaster>`);
|
|
200
|
+
}
|
|
201
|
+
// Publication date and last build date (use same timestamp for consistency)
|
|
202
|
+
const buildDate = new Date();
|
|
203
|
+
xmlLines.push(` <pubDate>${formatRSSDate(buildDate)}</pubDate>`);
|
|
204
|
+
// Last build date
|
|
205
|
+
xmlLines.push(` <lastBuildDate>${formatRSSDate(buildDate)}</lastBuildDate>`);
|
|
206
|
+
if (feedConfig.category) {
|
|
207
|
+
xmlLines.push(` <category>${escapeHtml(feedConfig.category)}</category>`);
|
|
208
|
+
}
|
|
209
|
+
// Generator
|
|
210
|
+
xmlLines.push(` <generator>Stati Static Site Generator</generator>`);
|
|
211
|
+
if (feedConfig.ttl) {
|
|
212
|
+
xmlLines.push(` <ttl>${feedConfig.ttl}</ttl>`);
|
|
213
|
+
}
|
|
214
|
+
// Image
|
|
215
|
+
if (feedConfig.image) {
|
|
216
|
+
xmlLines.push(` <image>`);
|
|
217
|
+
xmlLines.push(` <url>${escapeHtml(feedConfig.image.url)}</url>`);
|
|
218
|
+
xmlLines.push(` <title>${escapeHtml(feedConfig.image.title)}</title>`);
|
|
219
|
+
xmlLines.push(` <link>${escapeHtml(feedConfig.image.link)}</link>`);
|
|
220
|
+
if (feedConfig.image.width) {
|
|
221
|
+
xmlLines.push(` <width>${feedConfig.image.width}</width>`);
|
|
222
|
+
}
|
|
223
|
+
if (feedConfig.image.height) {
|
|
224
|
+
xmlLines.push(` <height>${feedConfig.image.height}</height>`);
|
|
225
|
+
}
|
|
226
|
+
xmlLines.push(` </image>`);
|
|
227
|
+
}
|
|
228
|
+
// Add items
|
|
229
|
+
const itemMapping = feedConfig.itemMapping || {};
|
|
230
|
+
for (const page of feedPages) {
|
|
231
|
+
xmlLines.push(` <item>`);
|
|
232
|
+
// Title
|
|
233
|
+
const title = getFieldValue(page, itemMapping.title, 'title', logger);
|
|
234
|
+
if (title) {
|
|
235
|
+
xmlLines.push(` <title>${escapeHtml(title)}</title>`);
|
|
236
|
+
}
|
|
237
|
+
// Link
|
|
238
|
+
let link;
|
|
239
|
+
if (itemMapping.link) {
|
|
240
|
+
link = itemMapping.link(page, config);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
link = buildPageUrl(page, config);
|
|
244
|
+
}
|
|
245
|
+
xmlLines.push(` <link>${escapeHtml(link)}</link>`);
|
|
246
|
+
// Description (or full content)
|
|
247
|
+
if (itemMapping.includeContent && page.content) {
|
|
248
|
+
xmlLines.push(` <description><![CDATA[${page.content}]]></description>`);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const description = getFieldValue(page, itemMapping.description, 'description', logger);
|
|
252
|
+
if (description) {
|
|
253
|
+
xmlLines.push(` <description>${escapeHtml(description)}</description>`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Pub date
|
|
257
|
+
const pubDate = getFieldValue(page, itemMapping.pubDate, 'publishedAt', logger) ||
|
|
258
|
+
getFieldValue(page, itemMapping.pubDate, 'date', logger);
|
|
259
|
+
const formattedPubDate = parseAndFormatDate(pubDate);
|
|
260
|
+
if (formattedPubDate) {
|
|
261
|
+
xmlLines.push(` <pubDate>${formattedPubDate}</pubDate>`);
|
|
262
|
+
}
|
|
263
|
+
// Author
|
|
264
|
+
const author = getFieldValue(page, itemMapping.author, 'author', logger);
|
|
265
|
+
if (author) {
|
|
266
|
+
xmlLines.push(` <author>${escapeHtml(author)}</author>`);
|
|
267
|
+
}
|
|
268
|
+
// Category/categories
|
|
269
|
+
const category = getFieldValue(page, itemMapping.category, 'tags', logger);
|
|
270
|
+
if (category) {
|
|
271
|
+
const categories = Array.isArray(category) ? category : [category];
|
|
272
|
+
for (const cat of categories) {
|
|
273
|
+
xmlLines.push(` <category>${escapeHtml(cat)}</category>`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// GUID
|
|
277
|
+
let guid;
|
|
278
|
+
if (itemMapping.guid) {
|
|
279
|
+
guid = itemMapping.guid(page, config);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
guid = link;
|
|
283
|
+
}
|
|
284
|
+
xmlLines.push(` <guid isPermaLink="true">${escapeHtml(guid)}</guid>`);
|
|
285
|
+
// Enclosure
|
|
286
|
+
if (feedConfig.enclosure) {
|
|
287
|
+
const enclosure = feedConfig.enclosure(page);
|
|
288
|
+
if (enclosure) {
|
|
289
|
+
xmlLines.push(` <enclosure url="${escapeHtml(enclosure.url)}" length="${enclosure.length}" type="${escapeHtml(enclosure.type)}" />`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Custom elements
|
|
293
|
+
if (feedConfig.customItemElements) {
|
|
294
|
+
const customElements = feedConfig.customItemElements(page);
|
|
295
|
+
for (const [key, value] of Object.entries(customElements)) {
|
|
296
|
+
if (value !== undefined && value !== null) {
|
|
297
|
+
xmlLines.push(` <${key}>${escapeHtml(String(value))}</${key}>`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
xmlLines.push(` </item>`);
|
|
302
|
+
}
|
|
303
|
+
xmlLines.push(' </channel>');
|
|
304
|
+
xmlLines.push('</rss>');
|
|
305
|
+
const xml = xmlLines.join('\n');
|
|
306
|
+
return {
|
|
307
|
+
filename: feedConfig.filename,
|
|
308
|
+
itemCount: feedPages.length,
|
|
309
|
+
sizeInBytes: Buffer.byteLength(xml, 'utf8'),
|
|
310
|
+
xml,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Generates all RSS feeds configured in the site
|
|
315
|
+
* @param pages - All pages in the site
|
|
316
|
+
* @param config - Stati configuration
|
|
317
|
+
* @param logger - Logger instance for warnings (optional)
|
|
318
|
+
* @returns Array of RSS generation results
|
|
319
|
+
*/
|
|
320
|
+
export function generateRSSFeeds(pages, config, logger) {
|
|
321
|
+
const rssConfig = config.rss;
|
|
322
|
+
if (!rssConfig || !rssConfig.enabled || !rssConfig.feeds) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
const results = [];
|
|
326
|
+
for (const feedConfig of rssConfig.feeds) {
|
|
327
|
+
const result = generateRSSFeed(pages, config, feedConfig, logger);
|
|
328
|
+
results.push(result);
|
|
329
|
+
}
|
|
330
|
+
return results;
|
|
331
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RSS feed generation module
|
|
3
|
+
* @module rss
|
|
4
|
+
*/
|
|
5
|
+
export { generateRSSFeed, generateRSSFeeds } from './generator.js';
|
|
6
|
+
export { validateRSSConfig, validateRSSFeedConfig } from './validation.js';
|
|
7
|
+
export type { RSSValidationResult } from './validation.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rss/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC3E,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/rss/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared pattern matching utilities for Stati
|
|
3
|
+
* Provides glob pattern matching functionality used across RSS, sitemap, and other modules
|
|
4
|
+
* @module utils/pattern-matching
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Checks if a path matches any of the given glob patterns
|
|
8
|
+
* @param path - Path to check (will be normalized to forward slashes)
|
|
9
|
+
* @param patterns - Array of glob patterns
|
|
10
|
+
* @param allowPrefix - If true, also match if path starts with pattern (for non-glob patterns)
|
|
11
|
+
* @returns true if path matches any pattern
|
|
12
|
+
*/
|
|
13
|
+
export declare function matchesAnyPattern(path: string, patterns: string[], allowPrefix?: boolean): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a path matches any of the given glob patterns (URL-based)
|
|
16
|
+
* This is a convenience wrapper for URL-based matching
|
|
17
|
+
* @param url - URL to check
|
|
18
|
+
* @param patterns - Array of glob patterns
|
|
19
|
+
* @returns true if URL matches any pattern
|
|
20
|
+
*/
|
|
21
|
+
export declare function urlMatchesAnyPattern(url: string, patterns: string[]): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Filters an array of items based on include and exclude patterns
|
|
24
|
+
* @param items - Items to filter
|
|
25
|
+
* @param getPath - Function to extract path from item
|
|
26
|
+
* @param options - Filter options
|
|
27
|
+
* @returns Filtered items
|
|
28
|
+
*/
|
|
29
|
+
export declare function filterByPatterns<T>(items: T[], getPath: (item: T) => string, options?: {
|
|
30
|
+
includePatterns?: string[];
|
|
31
|
+
excludePatterns?: string[];
|
|
32
|
+
}): T[];
|
|
33
|
+
//# sourceMappingURL=pattern-matching.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pattern-matching.d.ts","sourceRoot":"","sources":["../../../src/rss/utils/pattern-matching.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,WAAW,UAAO,GAAG,OAAO,CAgD/F;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAE7E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,KAAK,EAAE,CAAC,EAAE,EACV,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,EAC5B,OAAO,GAAE;IACP,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB,GACL,CAAC,EAAE,CAkBL"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared pattern matching utilities for Stati
|
|
3
|
+
* Provides glob pattern matching functionality used across RSS, sitemap, and other modules
|
|
4
|
+
* @module utils/pattern-matching
|
|
5
|
+
*/
|
|
6
|
+
import { globToRegex } from '../../core/utils/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Checks if a path matches any of the given glob patterns
|
|
9
|
+
* @param path - Path to check (will be normalized to forward slashes)
|
|
10
|
+
* @param patterns - Array of glob patterns
|
|
11
|
+
* @param allowPrefix - If true, also match if path starts with pattern (for non-glob patterns)
|
|
12
|
+
* @returns true if path matches any pattern
|
|
13
|
+
*/
|
|
14
|
+
export function matchesAnyPattern(path, patterns, allowPrefix = true) {
|
|
15
|
+
if (patterns.length === 0) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
// Normalize path to use forward slashes
|
|
19
|
+
const normalizedPath = path.replace(/\\/g, '/');
|
|
20
|
+
for (const pattern of patterns) {
|
|
21
|
+
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
22
|
+
// Check if pattern is a glob pattern
|
|
23
|
+
if (normalizedPattern.includes('*') || normalizedPattern.includes('?')) {
|
|
24
|
+
const regex = globToRegex(normalizedPattern);
|
|
25
|
+
if (regex.test(normalizedPath)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// For non-glob patterns, check exact match first
|
|
31
|
+
if (normalizedPath === normalizedPattern) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
// For prefix matching with path boundaries
|
|
35
|
+
if (allowPrefix) {
|
|
36
|
+
// Special case: root pattern "/" should ONLY match "/" exactly
|
|
37
|
+
if (normalizedPattern === '/') {
|
|
38
|
+
continue; // Already checked exact match above, skip prefix matching
|
|
39
|
+
}
|
|
40
|
+
// For patterns ending with "/", they are explicitly prefix patterns
|
|
41
|
+
if (normalizedPattern.endsWith('/')) {
|
|
42
|
+
if (normalizedPath.startsWith(normalizedPattern)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// For other patterns, match if:
|
|
48
|
+
// 1. Path starts with pattern followed by "/"
|
|
49
|
+
// This ensures "/api" matches "/api/foo" but not "/apitest"
|
|
50
|
+
if (normalizedPath.startsWith(normalizedPattern + '/')) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Checks if a path matches any of the given glob patterns (URL-based)
|
|
61
|
+
* This is a convenience wrapper for URL-based matching
|
|
62
|
+
* @param url - URL to check
|
|
63
|
+
* @param patterns - Array of glob patterns
|
|
64
|
+
* @returns true if URL matches any pattern
|
|
65
|
+
*/
|
|
66
|
+
export function urlMatchesAnyPattern(url, patterns) {
|
|
67
|
+
return matchesAnyPattern(url, patterns, true);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Filters an array of items based on include and exclude patterns
|
|
71
|
+
* @param items - Items to filter
|
|
72
|
+
* @param getPath - Function to extract path from item
|
|
73
|
+
* @param options - Filter options
|
|
74
|
+
* @returns Filtered items
|
|
75
|
+
*/
|
|
76
|
+
export function filterByPatterns(items, getPath, options = {}) {
|
|
77
|
+
let filtered = [...items];
|
|
78
|
+
// Apply include patterns first (if specified)
|
|
79
|
+
if (options.includePatterns && options.includePatterns.length > 0) {
|
|
80
|
+
filtered = filtered.filter((item) => matchesAnyPattern(getPath(item), options.includePatterns, true));
|
|
81
|
+
}
|
|
82
|
+
// Apply exclude patterns
|
|
83
|
+
if (options.excludePatterns && options.excludePatterns.length > 0) {
|
|
84
|
+
filtered = filtered.filter((item) => !matchesAnyPattern(getPath(item), options.excludePatterns, true));
|
|
85
|
+
}
|
|
86
|
+
return filtered;
|
|
87
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RSS feed validation utilities
|
|
3
|
+
* @module rss/validation
|
|
4
|
+
*/
|
|
5
|
+
import type { RSSFeedConfig, RSSConfig } from '../types/rss.js';
|
|
6
|
+
/**
|
|
7
|
+
* Validation result for RSS configuration
|
|
8
|
+
*/
|
|
9
|
+
export interface RSSValidationResult {
|
|
10
|
+
/** Whether validation passed */
|
|
11
|
+
valid: boolean;
|
|
12
|
+
/** Array of validation errors (empty if valid) */
|
|
13
|
+
errors: string[];
|
|
14
|
+
/** Array of validation warnings (non-critical issues) */
|
|
15
|
+
warnings: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Validates a single RSS feed configuration
|
|
19
|
+
* @param feedConfig - Feed configuration to validate
|
|
20
|
+
* @param feedIndex - Index of feed in config array (for error messages)
|
|
21
|
+
* @returns Validation result
|
|
22
|
+
*/
|
|
23
|
+
export declare function validateRSSFeedConfig(feedConfig: RSSFeedConfig, feedIndex?: number): RSSValidationResult;
|
|
24
|
+
/**
|
|
25
|
+
* Validates RSS configuration
|
|
26
|
+
* @param rssConfig - RSS configuration to validate
|
|
27
|
+
* @returns Validation result
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateRSSConfig(rssConfig: RSSConfig | undefined): RSSValidationResult;
|
|
30
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/rss/validation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,gCAAgC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,yDAAyD;IACzD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,aAAa,EACzB,SAAS,SAAI,GACZ,mBAAmB,CA0FrB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,GAAG,SAAS,GAAG,mBAAmB,CAuCvF"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RSS feed validation utilities
|
|
3
|
+
* @module rss/validation
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validates a single RSS feed configuration
|
|
7
|
+
* @param feedConfig - Feed configuration to validate
|
|
8
|
+
* @param feedIndex - Index of feed in config array (for error messages)
|
|
9
|
+
* @returns Validation result
|
|
10
|
+
*/
|
|
11
|
+
export function validateRSSFeedConfig(feedConfig, feedIndex = 0) {
|
|
12
|
+
const errors = [];
|
|
13
|
+
const warnings = [];
|
|
14
|
+
// Required fields
|
|
15
|
+
if (!feedConfig.filename) {
|
|
16
|
+
errors.push(`Feed ${feedIndex}: 'filename' is required`);
|
|
17
|
+
}
|
|
18
|
+
else if (!feedConfig.filename.endsWith('.xml')) {
|
|
19
|
+
warnings.push(`Feed ${feedIndex}: filename '${feedConfig.filename}' should end with .xml`);
|
|
20
|
+
}
|
|
21
|
+
if (!feedConfig.title || feedConfig.title.trim() === '') {
|
|
22
|
+
errors.push(`Feed ${feedIndex}: 'title' is required and cannot be empty`);
|
|
23
|
+
}
|
|
24
|
+
if (!feedConfig.description || feedConfig.description.trim() === '') {
|
|
25
|
+
errors.push(`Feed ${feedIndex}: 'description' is required and cannot be empty`);
|
|
26
|
+
}
|
|
27
|
+
// Validate optional fields
|
|
28
|
+
if (feedConfig.ttl !== undefined) {
|
|
29
|
+
if (typeof feedConfig.ttl !== 'number' || feedConfig.ttl < 0) {
|
|
30
|
+
errors.push(`Feed ${feedIndex}: 'ttl' must be a non-negative number`);
|
|
31
|
+
}
|
|
32
|
+
else if (feedConfig.ttl === 0) {
|
|
33
|
+
warnings.push(`Feed ${feedIndex}: ttl is 0, which means feed should not be cached`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (feedConfig.maxItems !== undefined) {
|
|
37
|
+
if (typeof feedConfig.maxItems !== 'number' || feedConfig.maxItems < 1) {
|
|
38
|
+
errors.push(`Feed ${feedIndex}: 'maxItems' must be a positive number`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Validate sort configuration
|
|
42
|
+
if (feedConfig.sortBy === 'custom' && !feedConfig.sortFn) {
|
|
43
|
+
errors.push(`Feed ${feedIndex}: 'sortFn' is required when sortBy is 'custom'`);
|
|
44
|
+
}
|
|
45
|
+
const validSortOptions = ['date-desc', 'date-asc', 'title-asc', 'title-desc', 'custom'];
|
|
46
|
+
if (feedConfig.sortBy && !validSortOptions.includes(feedConfig.sortBy)) {
|
|
47
|
+
errors.push(`Feed ${feedIndex}: 'sortBy' must be one of: ${validSortOptions.join(', ')}`);
|
|
48
|
+
}
|
|
49
|
+
// Validate image configuration
|
|
50
|
+
if (feedConfig.image) {
|
|
51
|
+
if (!feedConfig.image.url) {
|
|
52
|
+
errors.push(`Feed ${feedIndex}: image.url is required when image is specified`);
|
|
53
|
+
}
|
|
54
|
+
if (!feedConfig.image.title) {
|
|
55
|
+
errors.push(`Feed ${feedIndex}: image.title is required when image is specified`);
|
|
56
|
+
}
|
|
57
|
+
if (!feedConfig.image.link) {
|
|
58
|
+
errors.push(`Feed ${feedIndex}: image.link is required when image is specified`);
|
|
59
|
+
}
|
|
60
|
+
if (feedConfig.image.width !== undefined && feedConfig.image.width > 144) {
|
|
61
|
+
warnings.push(`Feed ${feedIndex}: image width ${feedConfig.image.width} exceeds recommended maximum of 144 pixels`);
|
|
62
|
+
}
|
|
63
|
+
if (feedConfig.image.height !== undefined && feedConfig.image.height > 400) {
|
|
64
|
+
warnings.push(`Feed ${feedIndex}: image height ${feedConfig.image.height} exceeds recommended maximum of 400 pixels`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Validate email formats
|
|
68
|
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+/;
|
|
69
|
+
if (feedConfig.managingEditor && !emailPattern.test(feedConfig.managingEditor)) {
|
|
70
|
+
warnings.push(`Feed ${feedIndex}: managingEditor should start with a valid email address (format: 'email@example.com (Name)')`);
|
|
71
|
+
}
|
|
72
|
+
if (feedConfig.webMaster && !emailPattern.test(feedConfig.webMaster)) {
|
|
73
|
+
warnings.push(`Feed ${feedIndex}: webMaster should start with a valid email address (format: 'email@example.com (Name)')`);
|
|
74
|
+
}
|
|
75
|
+
// Validate patterns
|
|
76
|
+
if (feedConfig.contentPatterns && feedConfig.contentPatterns.length === 0) {
|
|
77
|
+
warnings.push(`Feed ${feedIndex}: contentPatterns is empty - feed may not include any content`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
valid: errors.length === 0,
|
|
81
|
+
errors,
|
|
82
|
+
warnings,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Validates RSS configuration
|
|
87
|
+
* @param rssConfig - RSS configuration to validate
|
|
88
|
+
* @returns Validation result
|
|
89
|
+
*/
|
|
90
|
+
export function validateRSSConfig(rssConfig) {
|
|
91
|
+
const errors = [];
|
|
92
|
+
const warnings = [];
|
|
93
|
+
if (!rssConfig) {
|
|
94
|
+
return { valid: true, errors: [], warnings: ['RSS configuration is not defined'] };
|
|
95
|
+
}
|
|
96
|
+
if (!rssConfig.enabled) {
|
|
97
|
+
return { valid: true, errors: [], warnings: ['RSS feed generation is disabled'] };
|
|
98
|
+
}
|
|
99
|
+
if (!rssConfig.feeds || rssConfig.feeds.length === 0) {
|
|
100
|
+
errors.push('At least one feed configuration is required when RSS is enabled');
|
|
101
|
+
return { valid: false, errors, warnings };
|
|
102
|
+
}
|
|
103
|
+
// Validate each feed
|
|
104
|
+
const feedFilenames = new Set();
|
|
105
|
+
rssConfig.feeds.forEach((feedConfig, index) => {
|
|
106
|
+
const result = validateRSSFeedConfig(feedConfig, index);
|
|
107
|
+
errors.push(...result.errors);
|
|
108
|
+
warnings.push(...result.warnings);
|
|
109
|
+
// Check for duplicate filenames
|
|
110
|
+
if (feedConfig.filename) {
|
|
111
|
+
if (feedFilenames.has(feedConfig.filename)) {
|
|
112
|
+
errors.push(`Duplicate filename '${feedConfig.filename}' found in feed ${index}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
feedFilenames.add(feedConfig.filename);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
valid: errors.length === 0,
|
|
121
|
+
errors,
|
|
122
|
+
warnings,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/seo/sitemap.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAEb,uBAAuB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/seo/sitemap.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAEb,uBAAuB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAoGtD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,aAAa,EAAE,aAAa,GAC3B,YAAY,GAAG,IAAI,CA4ErB;AA2BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAUlE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAatF;AAoBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,EAAE,EAClB,MAAM,EAAE,WAAW,EACnB,aAAa,EAAE,aAAa,GAC3B,uBAAuB,CA8CzB"}
|