UNPKG

13.3 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.normalizeURL = exports.chunk = exports.lineSeparatedURLsToSitemapOptions = exports.ReadlineStream = exports.mergeStreams = exports.validateSMIOptions = void 0;
4/*!
5 * Sitemap
6 * Copyright(c) 2011 Eugene Kalinin
7 * MIT Licensed
8 */
9const fs_1 = require("fs");
10const stream_1 = require("stream");
11const readline_1 = require("readline");
12const url_1 = require("url");
13const types_1 = require("./types");
14const errors_1 = require("./errors");
15const types_2 = require("./types");
16function validate(subject, name, url, level) {
17 Object.keys(subject).forEach((key) => {
18 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
19 // @ts-ignore
20 const val = subject[key];
21 if (types_2.validators[key] && !types_2.validators[key].test(val)) {
22 if (level === types_1.ErrorLevel.THROW) {
23 throw new errors_1.InvalidAttrValue(key, val, types_2.validators[key]);
24 }
25 else {
26 console.warn(`${url}: ${name} key ${key} has invalid value: ${val}`);
27 }
28 }
29 });
30}
31function handleError(error, level) {
32 if (level === types_1.ErrorLevel.THROW) {
33 throw error;
34 }
35 else if (level === types_1.ErrorLevel.WARN) {
36 console.warn(error.name, error.message);
37 }
38}
39/**
40 * Verifies all data passed in will comply with sitemap spec.
41 * @param conf Options to validate
42 * @param level logging level
43 * @param errorHandler error handling func
44 */
45function validateSMIOptions(conf, level = types_1.ErrorLevel.WARN, errorHandler = handleError) {
46 if (!conf) {
47 throw new errors_1.NoConfigError();
48 }
49 if (level === types_1.ErrorLevel.SILENT) {
50 return conf;
51 }
52 const { url, changefreq, priority, news, video } = conf;
53 if (!url) {
54 errorHandler(new errors_1.NoURLError(), level);
55 }
56 if (changefreq) {
57 if (!types_1.isValidChangeFreq(changefreq)) {
58 errorHandler(new errors_1.ChangeFreqInvalidError(url, changefreq), level);
59 }
60 }
61 if (priority) {
62 if (!(priority >= 0.0 && priority <= 1.0)) {
63 errorHandler(new errors_1.PriorityInvalidError(url, priority), level);
64 }
65 }
66 if (news) {
67 if (news.access &&
68 news.access !== 'Registration' &&
69 news.access !== 'Subscription') {
70 errorHandler(new errors_1.InvalidNewsAccessValue(url, news.access), level);
71 }
72 if (!news.publication ||
73 !news.publication.name ||
74 !news.publication.language ||
75 !news.publication_date ||
76 !news.title) {
77 errorHandler(new errors_1.InvalidNewsFormat(url), level);
78 }
79 validate(news, 'news', url, level);
80 validate(news.publication, 'publication', url, level);
81 }
82 if (video) {
83 video.forEach((vid) => {
84 var _a;
85 if (vid.duration !== undefined) {
86 if (vid.duration < 0 || vid.duration > 28800) {
87 errorHandler(new errors_1.InvalidVideoDuration(url, vid.duration), level);
88 }
89 }
90 if (vid.rating !== undefined && (vid.rating < 0 || vid.rating > 5)) {
91 errorHandler(new errors_1.InvalidVideoRating(url, vid.title, vid.rating), level);
92 }
93 if (typeof vid !== 'object' ||
94 !vid.thumbnail_loc ||
95 !vid.title ||
96 !vid.description) {
97 // has to be an object and include required categories https://support.google.com/webmasters/answer/80471?hl=en&ref_topic=4581190
98 errorHandler(new errors_1.InvalidVideoFormat(url), level);
99 }
100 if (vid.title.length > 100) {
101 errorHandler(new errors_1.InvalidVideoTitle(url, vid.title.length), level);
102 }
103 if (vid.description.length > 2048) {
104 errorHandler(new errors_1.InvalidVideoDescription(url, vid.description.length), level);
105 }
106 if (vid.view_count !== undefined && vid.view_count < 0) {
107 errorHandler(new errors_1.InvalidVideoViewCount(url, vid.view_count), level);
108 }
109 if (vid.tag.length > 32) {
110 errorHandler(new errors_1.InvalidVideoTagCount(url, vid.tag.length), level);
111 }
112 if (vid.category !== undefined && ((_a = vid.category) === null || _a === void 0 ? void 0 : _a.length) > 256) {
113 errorHandler(new errors_1.InvalidVideoCategory(url, vid.category.length), level);
114 }
115 if (vid.family_friendly !== undefined &&
116 !types_1.isValidYesNo(vid.family_friendly)) {
117 errorHandler(new errors_1.InvalidVideoFamilyFriendly(url, vid.family_friendly), level);
118 }
119 if (vid.restriction) {
120 if (!types_2.validators.restriction.test(vid.restriction)) {
121 errorHandler(new errors_1.InvalidVideoRestriction(url, vid.restriction), level);
122 }
123 if (!vid['restriction:relationship'] ||
124 !types_1.isAllowDeny(vid['restriction:relationship'])) {
125 errorHandler(new errors_1.InvalidVideoRestrictionRelationship(url, vid['restriction:relationship']), level);
126 }
127 }
128 // TODO price element should be unbounded
129 if ((vid.price === '' && vid['price:type'] === undefined) ||
130 (vid['price:type'] !== undefined && !types_1.isPriceType(vid['price:type']))) {
131 errorHandler(new errors_1.InvalidVideoPriceType(url, vid['price:type'], vid.price), level);
132 }
133 if (vid['price:resolution'] !== undefined &&
134 !types_1.isResolution(vid['price:resolution'])) {
135 errorHandler(new errors_1.InvalidVideoResolution(url, vid['price:resolution']), level);
136 }
137 if (vid['price:currency'] !== undefined &&
138 !types_2.validators['price:currency'].test(vid['price:currency'])) {
139 errorHandler(new errors_1.InvalidVideoPriceCurrency(url, vid['price:currency']), level);
140 }
141 validate(vid, 'video', url, level);
142 });
143 }
144 return conf;
145}
146exports.validateSMIOptions = validateSMIOptions;
147/**
148 * Combines multiple streams into one
149 * @param streams the streams to combine
150 */
151function mergeStreams(streams) {
152 let pass = new stream_1.PassThrough();
153 let waiting = streams.length;
154 for (const stream of streams) {
155 pass = stream.pipe(pass, { end: false });
156 stream.once('end', () => --waiting === 0 && pass.emit('end'));
157 }
158 return pass;
159}
160exports.mergeStreams = mergeStreams;
161/**
162 * Wraps node's ReadLine in a stream
163 */
164class ReadlineStream extends stream_1.Readable {
165 constructor(options) {
166 if (options.autoDestroy === undefined) {
167 options.autoDestroy = true;
168 }
169 options.objectMode = true;
170 super(options);
171 this._source = readline_1.createInterface({
172 input: options.input,
173 terminal: false,
174 crlfDelay: Infinity,
175 });
176 // Every time there's data, push it into the internal buffer.
177 this._source.on('line', (chunk) => {
178 // If push() returns false, then stop reading from source.
179 if (!this.push(chunk))
180 this._source.pause();
181 });
182 // When the source ends, push the EOF-signaling `null` chunk.
183 this._source.on('close', () => {
184 this.push(null);
185 });
186 }
187 // _read() will be called when the stream wants to pull more data in.
188 // The advisory size argument is ignored in this case.
189 _read(size) {
190 this._source.resume();
191 }
192}
193exports.ReadlineStream = ReadlineStream;
194/**
195 * Takes a stream likely from fs.createReadStream('./path') and returns a stream
196 * of sitemap items
197 * @param stream a stream of line separated urls.
198 * @param opts.isJSON is the stream line separated JSON. leave undefined to guess
199 */
200function lineSeparatedURLsToSitemapOptions(stream, { isJSON } = {}) {
201 return new ReadlineStream({ input: stream }).pipe(new stream_1.Transform({
202 objectMode: true,
203 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
204 transform: (line, encoding, cb) => {
205 if (isJSON || (isJSON === undefined && line[0] === '{')) {
206 cb(null, JSON.parse(line));
207 }
208 else {
209 cb(null, line);
210 }
211 },
212 }));
213}
214exports.lineSeparatedURLsToSitemapOptions = lineSeparatedURLsToSitemapOptions;
215/**
216 * Based on lodash's implementation of chunk.
217 *
218 * Copyright JS Foundation and other contributors <https://js.foundation/>
219 *
220 * Based on Underscore.js, copyright Jeremy Ashkenas,
221 * DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
222 *
223 * This software consists of voluntary contributions made by many
224 * individuals. For exact contribution history, see the revision history
225 * available at https://github.com/lodash/lodash
226 */
227/* eslint-disable @typescript-eslint/no-explicit-any */
228function chunk(array, size = 1) {
229 size = Math.max(Math.trunc(size), 0);
230 const length = array ? array.length : 0;
231 if (!length || size < 1) {
232 return [];
233 }
234 const result = Array(Math.ceil(length / size));
235 let index = 0, resIndex = 0;
236 while (index < length) {
237 result[resIndex++] = array.slice(index, (index += size));
238 }
239 return result;
240}
241exports.chunk = chunk;
242function boolToYESNO(bool) {
243 if (bool === undefined) {
244 return bool;
245 }
246 if (typeof bool === 'boolean') {
247 return bool ? types_1.EnumYesNo.yes : types_1.EnumYesNo.no;
248 }
249 return bool;
250}
251/**
252 * Converts the passed in sitemap entry into one capable of being consumed by SitemapItem
253 * @param {string | SitemapItemLoose} elem the string or object to be converted
254 * @param {string} hostname
255 * @returns SitemapItemOptions a strict sitemap item option
256 */
257function normalizeURL(elem, hostname, lastmodDateOnly = false) {
258 // SitemapItem
259 // create object with url property
260 let smi = {
261 img: [],
262 video: [],
263 links: [],
264 url: '',
265 };
266 let smiLoose;
267 if (typeof elem === 'string') {
268 smi.url = elem;
269 smiLoose = { url: elem };
270 }
271 else {
272 smiLoose = elem;
273 }
274 smi.url = new url_1.URL(smiLoose.url, hostname).toString();
275 let img = [];
276 if (smiLoose.img) {
277 if (typeof smiLoose.img === 'string') {
278 // string -> array of objects
279 smiLoose.img = [{ url: smiLoose.img }];
280 }
281 else if (!Array.isArray(smiLoose.img)) {
282 // object -> array of objects
283 smiLoose.img = [smiLoose.img];
284 }
285 img = smiLoose.img.map((el) => (typeof el === 'string' ? { url: el } : el));
286 }
287 // prepend hostname to all image urls
288 smi.img = img.map((el) => ({
289 ...el,
290 url: new url_1.URL(el.url, hostname).toString(),
291 }));
292 let links = [];
293 if (smiLoose.links) {
294 links = smiLoose.links;
295 }
296 smi.links = links.map((link) => {
297 return { ...link, url: new url_1.URL(link.url, hostname).toString() };
298 });
299 if (smiLoose.video) {
300 if (!Array.isArray(smiLoose.video)) {
301 // make it an array
302 smiLoose.video = [smiLoose.video];
303 }
304 smi.video = smiLoose.video.map((video) => {
305 const nv = {
306 ...video,
307 family_friendly: boolToYESNO(video.family_friendly),
308 live: boolToYESNO(video.live),
309 requires_subscription: boolToYESNO(video.requires_subscription),
310 tag: [],
311 rating: undefined,
312 };
313 if (video.tag !== undefined) {
314 nv.tag = !Array.isArray(video.tag) ? [video.tag] : video.tag;
315 }
316 if (video.rating !== undefined) {
317 if (typeof video.rating === 'string') {
318 nv.rating = parseFloat(video.rating);
319 }
320 else {
321 nv.rating = video.rating;
322 }
323 }
324 if (typeof video.view_count === 'string') {
325 nv.view_count = parseInt(video.view_count, 10);
326 }
327 else if (typeof video.view_count === 'number') {
328 nv.view_count = video.view_count;
329 }
330 return nv;
331 });
332 }
333 // If given a file to use for last modified date
334 if (smiLoose.lastmodfile) {
335 const { mtime } = fs_1.statSync(smiLoose.lastmodfile);
336 smi.lastmod = new Date(mtime).toISOString();
337 // The date of last modification (YYYY-MM-DD)
338 }
339 else if (smiLoose.lastmodISO) {
340 smi.lastmod = new Date(smiLoose.lastmodISO).toISOString();
341 }
342 else if (smiLoose.lastmod) {
343 smi.lastmod = new Date(smiLoose.lastmod).toISOString();
344 }
345 if (lastmodDateOnly && smi.lastmod) {
346 smi.lastmod = smi.lastmod.slice(0, 10);
347 }
348 delete smiLoose.lastmodfile;
349 delete smiLoose.lastmodISO;
350 smi = { ...smiLoose, ...smi };
351 return smi;
352}
353exports.normalizeURL = normalizeURL;