@chrisburnell/eleventy-cache-webmentions 2.1.12 → 2.2.2

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.
@@ -1 +1 @@
1
- {"cachedAt":1761155512715,"type":"json","metadata":{}}
1
+ {"cachedAt":1761203061369,"type":"json","metadata":{}}
@@ -0,0 +1,677 @@
1
+ const { AssetCache } = require("@11ty/eleventy-fetch");
2
+ const { styleText } = require("node:util");
3
+ const sanitizeHTML = require("sanitize-html");
4
+
5
+ /**
6
+ * @typedef {sanitizeHTML.IOptions} AllowedHTML
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} OptionsDefaults
11
+ * @prop {boolean} refresh
12
+ * @prop {string} duration
13
+ * @prop {string} uniqueKey
14
+ * @prop {string} [cacheDirectory]
15
+ * @prop {AllowedHTML} allowedHTML
16
+ * @prop {string[]} allowlist
17
+ * @prop {string[]} blocklist
18
+ * @prop {Object<string, string>} urlReplacements
19
+ * @prop {number} maximumHtmlLength
20
+ * @prop {string} maximumHtmlText
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} OptionsUserInput
25
+ * @prop {string} domain
26
+ * @prop {string} feed
27
+ * @prop {string} key
28
+ */
29
+
30
+ /**
31
+ * @typedef {OptionsDefaults & OptionsUserInput} Options
32
+ */
33
+
34
+ /**
35
+ * @typedef {Object} Webmention
36
+ * @prop {string} [source]
37
+ * @prop {string} [url]
38
+ * @prop {string} [target]
39
+ * @prop {string} [published]
40
+ * @prop {string} [contentSanitized]
41
+ * @prop {Object} [content]
42
+ * @prop {string} [content.html]
43
+ * @prop {string} [content.value]
44
+ * @prop {Object} [data]
45
+ * @prop {string} [data.title]
46
+ * @prop {string} [data.url]
47
+ * @prop {string} [data.published]
48
+ * @prop {string} [data.content]
49
+ * @prop {string} [type]
50
+ * @prop {Object} [activity]
51
+ * @prop {string} [activity.type]
52
+ * @prop {boolean} [verified]
53
+ * @prop {string} ["wm-property"]
54
+ * @prop {string} ["wm-received"]
55
+ * @prop {string} ["wm-source"]
56
+ * @prop {string} ["wm-target"]
57
+ * @prop {string} ["verified_date"]
58
+ */
59
+
60
+ /**
61
+ * @typedef {"bookmark-of"|"like-of"|"repost-of"|"mention-of"|"in-reply-to"} WebmentionType
62
+ */
63
+
64
+ /**
65
+ * @type {OptionsDefaults}
66
+ */
67
+ const defaults = {
68
+ refresh: false,
69
+ duration: "1d",
70
+ uniqueKey: "webmentions",
71
+ cacheDirectory: undefined,
72
+ allowedHTML: {
73
+ allowedTags: ["a", "b", "em", "i", "strong"],
74
+ allowedAttributes: {
75
+ a: ["href"],
76
+ },
77
+ },
78
+ allowlist: [],
79
+ blocklist: [],
80
+ urlReplacements: {},
81
+ maximumHtmlLength: 1000,
82
+ maximumHtmlText: "mentioned this in",
83
+ };
84
+
85
+ /**
86
+ * @param {string} url
87
+ * @param {string} domain
88
+ * @returns {string}
89
+ */
90
+ const absoluteURL = (url, domain) => {
91
+ try {
92
+ return new URL(url, domain).toString();
93
+ } catch (e) {
94
+ console.error(
95
+ `Trying to convert ${styleText(
96
+ "bold",
97
+ url,
98
+ )} to be an absolute url with base ${styleText(
99
+ "bold",
100
+ domain,
101
+ )} and failed.`,
102
+ );
103
+ return url;
104
+ }
105
+ };
106
+
107
+ /**
108
+ * @param {string} url
109
+ * @returns {string}
110
+ */
111
+ const baseURL = (url) => {
112
+ let hashSplit = url.split("#");
113
+ let queryparamSplit = hashSplit[0].split("?");
114
+ return queryparamSplit[0];
115
+ };
116
+
117
+ /**
118
+ * @param {string} url
119
+ * @param {Object<string, string>} [urlReplacements]
120
+ * @returns {string}
121
+ */
122
+ const fixURL = (url, urlReplacements) => {
123
+ return Object.entries(urlReplacements).reduce(
124
+ (accumulator, [key, value]) => {
125
+ const regex = new RegExp(key, "g");
126
+ return accumulator.replace(regex, value);
127
+ },
128
+ url,
129
+ );
130
+ };
131
+
132
+ /**
133
+ * @param {string} url
134
+ * @returns {string}
135
+ */
136
+ const hostname = (url) => {
137
+ if (typeof url === "string" && url.includes("//")) {
138
+ const urlObject = new URL(url);
139
+ return urlObject.hostname;
140
+ }
141
+ return url;
142
+ };
143
+
144
+ /**
145
+ * @param {string|number|Date} date
146
+ * @returns {number}
147
+ */
148
+ const epoch = (date) => {
149
+ return new Date(date).getTime();
150
+ };
151
+
152
+ /**
153
+ * @param {Webmention[]} webmentions
154
+ * @returns {Webmention[]}
155
+ */
156
+ const removeDuplicates = (webmentions) => {
157
+ return [
158
+ ...webmentions
159
+ .reduce((map, webmention) => {
160
+ const key =
161
+ webmention === null || webmention === undefined
162
+ ? webmention
163
+ : getSource(webmention);
164
+ if (!map.has(key)) {
165
+ map.set(key, webmention);
166
+ }
167
+ return map;
168
+ }, new Map())
169
+ .values(),
170
+ ];
171
+ };
172
+
173
+ /**
174
+ * @param {Webmention} webmention
175
+ * @returns {string|undefined}
176
+ */
177
+ const getPublished = (webmention) => {
178
+ return (
179
+ webmention?.["data"]?.["published"] ||
180
+ webmention["published"] ||
181
+ webmention["wm-received"] ||
182
+ webmention["verified_date"]
183
+ );
184
+ };
185
+
186
+ /**
187
+ * @param {Webmention} webmention
188
+ * @returns {string|undefined}
189
+ */
190
+ const getReceived = (webmention) => {
191
+ return (
192
+ webmention["wm-received"] ||
193
+ webmention["verified_date"] ||
194
+ webmention["published"] ||
195
+ webmention?.["data"]?.["published"]
196
+ );
197
+ };
198
+
199
+ /**
200
+ * @param {Webmention} webmention
201
+ * @returns {string}
202
+ */
203
+ const getContent = (webmention) => {
204
+ return (
205
+ webmention?.["contentSanitized"] ||
206
+ webmention?.["content"]?.["html"] ||
207
+ webmention?.["content"]?.["value"] ||
208
+ webmention?.["content"] ||
209
+ webmention?.["data"]?.["content"] ||
210
+ ""
211
+ );
212
+ };
213
+
214
+ /**
215
+ * @param {Webmention} webmention
216
+ * @returns {string|undefined}
217
+ */
218
+ const getSource = (webmention) => {
219
+ return (
220
+ webmention["wm-source"] ||
221
+ webmention["source"] ||
222
+ webmention?.["data"]?.["url"] ||
223
+ webmention["url"]
224
+ );
225
+ };
226
+
227
+ /**
228
+ * @param {Webmention} webmention
229
+ * @returns {string|undefined}
230
+ */
231
+ const getURL = (webmention) => {
232
+ return (
233
+ webmention?.["data"]?.["url"] ||
234
+ webmention["url"] ||
235
+ webmention["wm-source"] ||
236
+ webmention["source"]
237
+ );
238
+ };
239
+
240
+ /**
241
+ * @param {Webmention} webmention
242
+ * @returns {string|undefined}
243
+ */
244
+ const getTarget = (webmention) => {
245
+ return webmention["wm-target"] || webmention["target"];
246
+ };
247
+
248
+ /**
249
+ * @param {Webmention} webmention
250
+ * @returns {string|undefined}
251
+ */
252
+ const getType = (webmention) => {
253
+ return (
254
+ webmention["wm-property"] ||
255
+ webmention?.["activity"]?.["type"] ||
256
+ webmention["type"]
257
+ );
258
+ };
259
+
260
+ /**
261
+ * @param {Webmention[]} webmentions
262
+ * @param {WebmentionType|WebmentionType[]} types
263
+ * @returns {Webmention[]}
264
+ */
265
+ const getByTypes = (webmentions, types) => {
266
+ return webmentions.filter((webmention) => {
267
+ if (typeof types === "string") {
268
+ return types === getType(webmention);
269
+ }
270
+ return types.includes(getType(webmention));
271
+ });
272
+ };
273
+
274
+ /**
275
+ * @param {Webmention[]} webmentions
276
+ * @param {string[]} blocklist
277
+ * @returns {Webmention[]}
278
+ */
279
+ const processBlocklist = (webmentions, blocklist) => {
280
+ return webmentions.filter((webmention) => {
281
+ let url = getSource(webmention);
282
+ let source = getSource(webmention);
283
+ for (let blocklistURL of blocklist) {
284
+ if (
285
+ url.includes(blocklistURL.replace(/\/?$/, "/")) ||
286
+ source.includes(blocklistURL.replace(/\/?$/, "/"))
287
+ ) {
288
+ return false;
289
+ }
290
+ }
291
+ return true;
292
+ });
293
+ };
294
+
295
+ /**
296
+ * @param {Webmention[]} webmentions
297
+ * @param {string[]} allowlist
298
+ * @returns {Webmention[]}
299
+ */
300
+ const processAllowlist = (webmentions, allowlist) => {
301
+ return webmentions.filter((webmention) => {
302
+ let url = getSource(webmention);
303
+ let source = getSource(webmention);
304
+ for (let allowlistURL of allowlist) {
305
+ if (
306
+ url.includes(allowlistURL.replace(/\/?$/, "/")) ||
307
+ source.includes(allowlistURL.replace(/\/?$/, "/"))
308
+ ) {
309
+ return true;
310
+ }
311
+ }
312
+ return false;
313
+ });
314
+ };
315
+
316
+ /**
317
+ * @param {Options} options
318
+ * @param {Webmention[]} webmentions
319
+ * @param {string} url
320
+ * @returns {Promise<{found: number, webmentions: Webmention[]}>}
321
+ */
322
+ const fetchWebmentions = async (options, webmentions, url) => {
323
+ return await fetch(url)
324
+ .then(async (response) => {
325
+ if (!response.ok) {
326
+ return Promise.reject(response);
327
+ }
328
+
329
+ const feed = await response.json();
330
+
331
+ if (!(options.key in feed)) {
332
+ console.log(
333
+ `${styleText("grey", `[${hostname(options.domain)}]`)} ${
334
+ options.key
335
+ } was not found as a key in the response from ${styleText(
336
+ "bold",
337
+ hostname(options.feed),
338
+ )}!`,
339
+ );
340
+ return Promise.reject(response);
341
+ }
342
+
343
+ // Combine newly-fetched Webmentions with cached Webmentions
344
+ webmentions = feed[options.key].concat(webmentions);
345
+ // Remove duplicates by source URL
346
+ webmentions = removeDuplicates(webmentions);
347
+ // Process the blocklist, if it has any entries
348
+ if (options.blocklist.length) {
349
+ webmentions = processBlocklist(webmentions, options.blocklist);
350
+ }
351
+ // Process the allowlist, if it has any entries
352
+ if (options.allowlist.length) {
353
+ webmentions = processAllowlist(webmentions, options.allowlist);
354
+ }
355
+ // Sort webmentions by received date for getting most recent Webmention on subsequent requests
356
+ webmentions = webmentions.sort((a, b) => {
357
+ return epoch(getReceived(b)) - epoch(getReceived(a));
358
+ });
359
+
360
+ return {
361
+ found: feed[options.key].length,
362
+ webmentions: webmentions,
363
+ };
364
+ })
365
+ .catch((error) => {
366
+ console.warn(
367
+ `${styleText(
368
+ "grey",
369
+ `[${hostname(options.domain)}]`,
370
+ )} Something went wrong with your Webmention request to ${styleText(
371
+ "bold",
372
+ hostname(options.feed),
373
+ )}!`,
374
+ );
375
+ console.warn(error instanceof Error ? error.message : error);
376
+
377
+ return {
378
+ found: 0,
379
+ webmentions: webmentions,
380
+ };
381
+ });
382
+ };
383
+
384
+ /**
385
+ * @param {Options} options
386
+ * @returns {Promise<Webmention[]>}
387
+ */
388
+ const retrieveWebmentions = async (options) => {
389
+ if (!options.domain) {
390
+ throw new Error(
391
+ "`domain` is a required field when attempting to retrieve Webmentions. See https://www.npmjs.com/package/@chrisburnell/eleventy-cache-webmentions#installation for more information.",
392
+ );
393
+ }
394
+
395
+ if (!options.feed) {
396
+ throw new Error(
397
+ "`feed` is a required field when attempting to retrieve Webmentions. See https://www.npmjs.com/package/@chrisburnell/eleventy-cache-webmentions#installation for more information.",
398
+ );
399
+ }
400
+
401
+ if (!options.key) {
402
+ throw new Error(
403
+ "`key` is a required field when attempting to retrieve Webmentions. See https://www.npmjs.com/package/@chrisburnell/eleventy-cache-webmentions#installation for more information.",
404
+ );
405
+ }
406
+
407
+ let asset = new AssetCache(
408
+ options.uniqueKey || `webmentions-${hostname(options.domain)}`,
409
+ options.cacheDirectory,
410
+ );
411
+
412
+ let webmentions = [];
413
+
414
+ // Unless specifically getting fresh Webmentions, if there is a cached file
415
+ // at all, grab its contents now
416
+ if (asset.isCacheValid("9001y") && !options.refresh) {
417
+ webmentions = await asset.getCachedValue();
418
+ }
419
+
420
+ // Get the number of cached Webmentions for diffing against fetched
421
+ // Webmentions later
422
+ const webmentionsCachedLength = webmentions.length;
423
+
424
+ // If there is a cached file but it is outside of expiry, fetch fresh
425
+ // results since the most recent Webmention
426
+ if (!asset.isCacheValid(options.refresh ? "0s" : options.duration)) {
427
+ const performanceStart = process.hrtime();
428
+ // Get the received date of the most recent Webmention, if it exists
429
+ const since = webmentions.length ? getReceived(webmentions[0]) : false;
430
+ // Build the URL for the fetch request
431
+ const url = `${options.feed}${
432
+ since
433
+ ? `${options.feed.includes("?") ? "&" : "?"}since=${since}`
434
+ : ""
435
+ }`;
436
+
437
+ // If using webmention.io, loop through pages until no results found
438
+ if (url.includes("https://webmention.io")) {
439
+ const urlObject = new URL(url);
440
+ const perPage =
441
+ Number(urlObject.searchParams.get("per-page")) || 1000;
442
+ urlObject.searchParams.delete("per-page");
443
+ // Start on page 0, to increment per subsequent request
444
+ let page = 0;
445
+ // Loop until a break condition is hit
446
+ while (true) {
447
+ const urlPaginated =
448
+ urlObject.href + `&per-page=${perPage}&page=${page}`;
449
+ const fetched = await fetchWebmentions(
450
+ options,
451
+ webmentions,
452
+ urlPaginated,
453
+ );
454
+
455
+ // An error occurred during fetching paged results → break
456
+ if (!fetched && !fetched.found && !fetched.webmentions) {
457
+ break;
458
+ }
459
+
460
+ // Page has no Webmentions → break
461
+ if (fetched.found === 0) {
462
+ break;
463
+ }
464
+
465
+ webmentions = fetched.webmentions;
466
+
467
+ // If there are less Webmentions found than should be in each
468
+ // page → break
469
+ if (fetched.found < perPage) {
470
+ break;
471
+ }
472
+
473
+ // Increment page
474
+ page += 1;
475
+ // Throttle next request
476
+ await new Promise((resolve) => setTimeout(resolve, 1000));
477
+ }
478
+ } else {
479
+ const fetched = await fetchWebmentions(options, webmentions, url);
480
+ webmentions = fetched.webmentions;
481
+ }
482
+
483
+ // Process the blocklist, if it has any entries
484
+ if (options.blocklist.length) {
485
+ webmentions = processBlocklist(webmentions, options.blocklist);
486
+ }
487
+
488
+ // Process the allowlist, if it has any entries
489
+ if (options.allowlist.length) {
490
+ webmentions = processAllowlist(webmentions, options.allowlist);
491
+ }
492
+
493
+ await asset.save(webmentions, "json");
494
+
495
+ const performance = process.hrtime(performanceStart);
496
+
497
+ // Add a console message with the number of fetched and processed Webmentions, if any
498
+ if (webmentionsCachedLength < webmentions.length) {
499
+ console.log(
500
+ `${styleText(
501
+ "grey",
502
+ `[${hostname(options.domain)}]`,
503
+ )} ${styleText(
504
+ "bold",
505
+ String(webmentions.length - webmentionsCachedLength),
506
+ )} new Webmentions fetched into cache in ${styleText(
507
+ "bold",
508
+ (performance[0] + performance[1] / 1e9).toFixed(3) +
509
+ " seconds",
510
+ )}.`,
511
+ );
512
+ }
513
+ }
514
+
515
+ return webmentions;
516
+ };
517
+
518
+ /** @type {Webmention[]} */
519
+ const WEBMENTIONS = {};
520
+
521
+ /**
522
+ * @param {Options} options
523
+ * @returns {Promise<Object<string, Webmention[]>>}
524
+ */
525
+ const webmentionsByURL = async (options) => {
526
+ if (Object.keys(WEBMENTIONS).length) {
527
+ return WEBMENTIONS;
528
+ }
529
+
530
+ let rawWebmentions = await retrieveWebmentions(options);
531
+
532
+ // Fix local URLs based on urlReplacements and sort Webmentions into groups
533
+ // by target base URL
534
+ rawWebmentions.forEach((webmention) => {
535
+ let url = baseURL(
536
+ fixURL(
537
+ getTarget(webmention).replace(/\/?$/, "/"),
538
+ options.urlReplacements,
539
+ ),
540
+ );
541
+
542
+ if (!WEBMENTIONS[url]) {
543
+ WEBMENTIONS[url] = [];
544
+ }
545
+
546
+ WEBMENTIONS[url].push(webmention);
547
+ });
548
+
549
+ return WEBMENTIONS;
550
+ };
551
+
552
+ /**
553
+ * @param {Options} options
554
+ * @param {string} url
555
+ * @param {WebmentionType[]|WebmentionType} [types=[]]
556
+ * @returns {Promise<Webmention[]>}
557
+ */
558
+ const getWebmentions = async (options, url, types = []) => {
559
+ const webmentions = await webmentionsByURL(options);
560
+ url = absoluteURL(url, options.domain);
561
+
562
+ if (!url || !webmentions || !webmentions[url]) {
563
+ return [];
564
+ }
565
+
566
+ return (
567
+ webmentions[url]
568
+ // Filter webmentions by allowed response post types
569
+ .filter((entry) => {
570
+ return typeof types === "object" && Object.keys(types).length
571
+ ? types.includes(getType(entry))
572
+ : typeof types === "string"
573
+ ? types === getType(entry)
574
+ : true;
575
+ })
576
+ // Sanitize content of webmentions against HTML limit
577
+ .map((entry) => {
578
+ const html = getContent(entry);
579
+
580
+ if (html.length) {
581
+ entry.contentSanitized = sanitizeHTML(
582
+ html,
583
+ options.allowedHTML,
584
+ );
585
+ if (html.length > options.maximumHtmlLength) {
586
+ entry.contentSanitized = `${
587
+ options.maximumHtmlText
588
+ } <a href="${getSource(entry)}">${getSource(
589
+ entry,
590
+ )}</a>`;
591
+ }
592
+ }
593
+
594
+ return entry;
595
+ })
596
+ // Sort by published
597
+ .sort((a, b) => {
598
+ return epoch(getPublished(a)) - epoch(getPublished(b));
599
+ })
600
+ );
601
+ };
602
+
603
+ /**
604
+ * @param {Object} eleventyConfig
605
+ * @param {Options} [options={}]
606
+ */
607
+ const eleventyCacheWebmentions = (eleventyConfig, options = {}) => {
608
+ options = Object.assign(defaults, options);
609
+
610
+ const byURL = webmentionsByURL(options);
611
+ const all = Object.values(byURL).reduce(
612
+ (array, webmentions) => [...array, ...webmentions],
613
+ [],
614
+ );
615
+
616
+ // Global Data
617
+ eleventyConfig.addGlobalData("webmentionsDefaults", defaults);
618
+ eleventyConfig.addGlobalData("webmentionsOptions", options);
619
+ eleventyConfig.addGlobalData("webmentionsByURL", byURL);
620
+ eleventyConfig.addGlobalData("webmentionsByUrl", byURL);
621
+ eleventyConfig.addGlobalData("webmentionsAll", all);
622
+
623
+ // Liquid Filters
624
+ eleventyConfig.addLiquidFilter("getWebmentionsByType", getByTypes);
625
+ eleventyConfig.addLiquidFilter("getWebmentionsByTypes", getByTypes);
626
+ eleventyConfig.addLiquidFilter("getWebmentionPublished", getPublished);
627
+ eleventyConfig.addLiquidFilter("getWebmentionReceived", getReceived);
628
+ eleventyConfig.addLiquidFilter("getWebmentionContent", getContent);
629
+ eleventyConfig.addLiquidFilter("getWebmentionSource", getSource);
630
+ eleventyConfig.addLiquidFilter("getWebmentionURL", getURL);
631
+ eleventyConfig.addLiquidFilter("getWebmentionTarget", getTarget);
632
+ eleventyConfig.addLiquidFilter("getWebmentionType", getType);
633
+
634
+ // Nunjucks Filters
635
+ eleventyConfig.addNunjucksFilter("getWebmentionsByType", getByTypes);
636
+ eleventyConfig.addNunjucksFilter("getWebmentionsByTypes", getByTypes);
637
+ eleventyConfig.addNunjucksFilter("getWebmentionPublished", getPublished);
638
+ eleventyConfig.addNunjucksFilter("getWebmentionReceived", getReceived);
639
+ eleventyConfig.addNunjucksFilter("getWebmentionContent", getContent);
640
+ eleventyConfig.addNunjucksFilter("getWebmentionSource", getSource);
641
+ eleventyConfig.addNunjucksFilter("getWebmentionURL", getURL);
642
+ eleventyConfig.addNunjucksFilter("getWebmentionTarget", getTarget);
643
+ eleventyConfig.addNunjucksFilter("getWebmentionType", getType);
644
+ };
645
+
646
+ module.exports = eleventyCacheWebmentions;
647
+ module.exports.defaults = defaults;
648
+ module.exports.getPublished = getPublished;
649
+ module.exports.getWebmentionPublished = getPublished;
650
+ module.exports.getReceived = getReceived;
651
+ module.exports.getWebmentionReceived = getReceived;
652
+ module.exports.getContent = getContent;
653
+ module.exports.getWebmentionContent = getContent;
654
+ module.exports.getSource = getSource;
655
+ module.exports.getWebmentionSource = getSource;
656
+ module.exports.getURL = getURL;
657
+ module.exports.getWebmentionURL = getURL;
658
+ module.exports.getTarget = getTarget;
659
+ module.exports.getWebmentionTarget = getTarget;
660
+ module.exports.getType = getType;
661
+ module.exports.getWebmentionType = getType;
662
+ module.exports.getByTypes = getByTypes;
663
+ module.exports.getByType = getByTypes;
664
+ module.exports.getWebmentionsByTypes = getByTypes;
665
+ module.exports.getWebmentionsByType = getByTypes;
666
+ module.exports.processBlocklist = processBlocklist;
667
+ module.exports.processWebmentionBlocklist = processBlocklist;
668
+ module.exports.processWebmentionsBlocklist = processBlocklist;
669
+ module.exports.processAllowlist = processAllowlist;
670
+ module.exports.processWebmentionAllowlist = processAllowlist;
671
+ module.exports.processWebmentionsAllowlist = processAllowlist;
672
+ module.exports.fetchWebmentions = fetchWebmentions;
673
+ module.exports.retrieveWebmentions = retrieveWebmentions;
674
+ module.exports.webmentionsByURL = webmentionsByURL;
675
+ module.exports.webmentionsByUrl = webmentionsByURL;
676
+ module.exports.filteredWebmentions = webmentionsByURL;
677
+ module.exports.getWebmentions = getWebmentions;
@@ -1,16 +1,76 @@
1
- const sanitizeHTML = require("sanitize-html");
2
- const { AssetCache } = require("@11ty/eleventy-fetch");
3
- const { styleText } = require("node:util");
1
+ import { AssetCache } from "@11ty/eleventy-fetch";
2
+ import { styleText } from "node:util";
3
+ import sanitizeHTML from "sanitize-html";
4
4
 
5
5
  /**
6
- * @type {Object}
6
+ * @typedef {sanitizeHTML.IOptions} AllowedHTML
7
7
  */
8
- const defaults = {
8
+
9
+ /**
10
+ * @typedef {Object} OptionsDefaults
11
+ * @prop {boolean} refresh
12
+ * @prop {string} duration
13
+ * @prop {string} uniqueKey
14
+ * @prop {string} [cacheDirectory]
15
+ * @prop {AllowedHTML} allowedHTML
16
+ * @prop {string[]} allowlist
17
+ * @prop {string[]} blocklist
18
+ * @prop {Object<string, string>} urlReplacements
19
+ * @prop {number} maximumHtmlLength
20
+ * @prop {string} maximumHtmlText
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} OptionsUserInput
25
+ * @prop {string} domain
26
+ * @prop {string} feed
27
+ * @prop {string} key
28
+ */
29
+
30
+ /**
31
+ * @typedef {OptionsDefaults & OptionsUserInput} Options
32
+ */
33
+
34
+ /**
35
+ * @typedef {Object} Webmention
36
+ * @prop {string} [source]
37
+ * @prop {string} [url]
38
+ * @prop {string} [target]
39
+ * @prop {string} [published]
40
+ * @prop {string} [contentSanitized]
41
+ * @prop {Object} [content]
42
+ * @prop {string} [content.html]
43
+ * @prop {string} [content.value]
44
+ * @prop {Object} [data]
45
+ * @prop {string} [data.title]
46
+ * @prop {string} [data.url]
47
+ * @prop {string} [data.published]
48
+ * @prop {string} [data.content]
49
+ * @prop {string} [type]
50
+ * @prop {Object} [activity]
51
+ * @prop {string} [activity.type]
52
+ * @prop {boolean} [verified]
53
+ * @prop {string} ["wm-property"]
54
+ * @prop {string} ["wm-received"]
55
+ * @prop {string} ["wm-source"]
56
+ * @prop {string} ["wm-target"]
57
+ * @prop {string} ["verified_date"]
58
+ */
59
+
60
+ /**
61
+ * @typedef {"bookmark-of"|"like-of"|"repost-of"|"mention-of"|"in-reply-to"} WebmentionType
62
+ */
63
+
64
+ /**
65
+ * @type {OptionsDefaults}
66
+ */
67
+ export const defaults = {
9
68
  refresh: false,
10
69
  duration: "1d",
11
70
  uniqueKey: "webmentions",
71
+ cacheDirectory: undefined,
12
72
  allowedHTML: {
13
- allowedTags: ["b", "i", "em", "strong", "a"],
73
+ allowedTags: ["a", "b", "em", "i", "strong"],
14
74
  allowedAttributes: {
15
75
  a: ["href"],
16
76
  },
@@ -48,7 +108,7 @@ const absoluteURL = (url, domain) => {
48
108
  * @param {string} url
49
109
  * @returns {string}
50
110
  */
51
- const baseUrl = (url) => {
111
+ const baseURL = (url) => {
52
112
  let hashSplit = url.split("#");
53
113
  let queryparamSplit = hashSplit[0].split("?");
54
114
  return queryparamSplit[0];
@@ -56,10 +116,10 @@ const baseUrl = (url) => {
56
116
 
57
117
  /**
58
118
  * @param {string} url
59
- * @param {Object} urlReplacements
119
+ * @param {Object<string, string>} [urlReplacements]
60
120
  * @returns {string}
61
121
  */
62
- const fixUrl = (url, urlReplacements) => {
122
+ const fixURL = (url, urlReplacements) => {
63
123
  return Object.entries(urlReplacements).reduce(
64
124
  (accumulator, [key, value]) => {
65
125
  const regex = new RegExp(key, "g");
@@ -90,10 +150,31 @@ const epoch = (date) => {
90
150
  };
91
151
 
92
152
  /**
93
- * @param {Object} date
153
+ * @param {Webmention[]} webmentions
154
+ * @returns {Webmention[]}
155
+ */
156
+ const removeDuplicates = (webmentions) => {
157
+ return [
158
+ ...webmentions
159
+ .reduce((map, webmention) => {
160
+ const key =
161
+ webmention === null || webmention === undefined
162
+ ? webmention
163
+ : getSource(webmention);
164
+ if (!map.has(key)) {
165
+ map.set(key, webmention);
166
+ }
167
+ return map;
168
+ }, new Map())
169
+ .values(),
170
+ ];
171
+ };
172
+
173
+ /**
174
+ * @param {Webmention} webmention
94
175
  * @returns {string|undefined}
95
176
  */
96
- const getPublished = (webmention) => {
177
+ export const getPublished = (webmention) => {
97
178
  return (
98
179
  webmention?.["data"]?.["published"] ||
99
180
  webmention["published"] ||
@@ -101,12 +182,13 @@ const getPublished = (webmention) => {
101
182
  webmention["verified_date"]
102
183
  );
103
184
  };
185
+ export const getWebmentionPublished = getPublished;
104
186
 
105
187
  /**
106
- * @param {Object} webmention
188
+ * @param {Webmention} webmention
107
189
  * @returns {string|undefined}
108
190
  */
109
- const getReceived = (webmention) => {
191
+ export const getReceived = (webmention) => {
110
192
  return (
111
193
  webmention["wm-received"] ||
112
194
  webmention["verified_date"] ||
@@ -114,12 +196,13 @@ const getReceived = (webmention) => {
114
196
  webmention?.["data"]?.["published"]
115
197
  );
116
198
  };
199
+ export const getWebmentionReceived = getReceived;
117
200
 
118
201
  /**
119
- * @param {Object} webmention
202
+ * @param {Webmention} webmention
120
203
  * @returns {string}
121
204
  */
122
- const getContent = (webmention) => {
205
+ export const getContent = (webmention) => {
123
206
  return (
124
207
  webmention?.["contentSanitized"] ||
125
208
  webmention?.["content"]?.["html"] ||
@@ -129,12 +212,13 @@ const getContent = (webmention) => {
129
212
  ""
130
213
  );
131
214
  };
215
+ export const getWebmentionContent = getContent;
132
216
 
133
217
  /**
134
- * @param {Object} webmention
135
- * @returns {string}
218
+ * @param {Webmention} webmention
219
+ * @returns {string|undefined}
136
220
  */
137
- const getSource = (webmention) => {
221
+ export const getSource = (webmention) => {
138
222
  return (
139
223
  webmention["wm-source"] ||
140
224
  webmention["source"] ||
@@ -142,12 +226,13 @@ const getSource = (webmention) => {
142
226
  webmention["url"]
143
227
  );
144
228
  };
229
+ export const getWebmentionSource = getSource;
145
230
 
146
231
  /**
147
- * @param {Object} webmention
148
- * @returns {string}
232
+ * @param {Webmention} webmention
233
+ * @returns {string|undefined}
149
234
  */
150
- const getURL = (webmention) => {
235
+ export const getURL = (webmention) => {
151
236
  return (
152
237
  webmention?.["data"]?.["url"] ||
153
238
  webmention["url"] ||
@@ -155,76 +240,53 @@ const getURL = (webmention) => {
155
240
  webmention["source"]
156
241
  );
157
242
  };
243
+ export const getWebmentionURL = getURL;
158
244
 
159
245
  /**
160
- * @param {Object} webmention
161
- * @returns {string}
246
+ * @param {Webmention} webmention
247
+ * @returns {string|undefined}
162
248
  */
163
- const getTarget = (webmention) => {
249
+ export const getTarget = (webmention) => {
164
250
  return webmention["wm-target"] || webmention["target"];
165
251
  };
252
+ export const getWebmentionTarget = getTarget;
166
253
 
167
254
  /**
168
- * @param {Object} webmention
255
+ * @param {Webmention} webmention
169
256
  * @returns {string|undefined}
170
257
  */
171
- const getType = (webmention) => {
258
+ export const getType = (webmention) => {
172
259
  return (
173
260
  webmention["wm-property"] ||
174
261
  webmention?.["activity"]?.["type"] ||
175
262
  webmention["type"]
176
263
  );
177
264
  };
265
+ export const getWebmentionType = getType;
178
266
 
179
267
  /**
180
- * @param {Object[]} webmentions
181
- * @param {string} type
182
- * @returns {Object[]}
183
- */
184
- const getByType = (webmentions, type) => {
185
- return webmentions.filter((webmention) => {
186
- return type === getType(webmention);
187
- });
188
- };
189
-
190
- /**
191
- * @param {Object[]} webmentions
192
- * @param {string[]} types
193
- * @returns {Object[]}
268
+ * @param {Webmention[]} webmentions
269
+ * @param {WebmentionType|WebmentionType[]} types
270
+ * @returns {Webmention[]}
194
271
  */
195
- const getByTypes = (webmentions, types) => {
272
+ export const getByTypes = (webmentions, types) => {
196
273
  return webmentions.filter((webmention) => {
274
+ if (typeof types === "string") {
275
+ return types === getType(webmention);
276
+ }
197
277
  return types.includes(getType(webmention));
198
278
  });
199
279
  };
280
+ export const getByType = getByTypes;
281
+ export const getWebmentionsByTypes = getByTypes;
282
+ export const getWebmentionsByType = getByTypes;
200
283
 
201
284
  /**
202
- * @param {Object[]} webmentions
203
- * @returns {Object[]}
204
- */
205
- const removeDuplicates = (webmentions) => {
206
- return [
207
- ...webmentions
208
- .reduce((map, webmention) => {
209
- const key =
210
- webmention === null || webmention === undefined
211
- ? webmention
212
- : getSource(webmention);
213
- if (!map.has(key)) {
214
- map.set(key, webmention);
215
- }
216
- return map;
217
- }, new Map())
218
- .values(),
219
- ];
220
- };
221
-
222
- /**
223
- * @param {Object[]} webmentions
285
+ * @param {Webmention[]} webmentions
224
286
  * @param {string[]} blocklist
225
- * @returns {Object[]}
287
+ * @returns {Webmention[]}
226
288
  */
227
- const processBlocklist = (webmentions, blocklist) => {
289
+ export const processBlocklist = (webmentions, blocklist) => {
228
290
  return webmentions.filter((webmention) => {
229
291
  let url = getSource(webmention);
230
292
  let source = getSource(webmention);
@@ -239,13 +301,15 @@ const processBlocklist = (webmentions, blocklist) => {
239
301
  return true;
240
302
  });
241
303
  };
304
+ export const processWebmentionBlocklist = processBlocklist;
305
+ export const processWebmentionsBlocklist = processBlocklist;
242
306
 
243
307
  /**
244
- * @param {Object[]} webmentions
308
+ * @param {Webmention[]} webmentions
245
309
  * @param {string[]} allowlist
246
- * @returns {Object[]}
310
+ * @returns {Webmention[]}
247
311
  */
248
- const processAllowlist = (webmentions, allowlist) => {
312
+ export const processAllowlist = (webmentions, allowlist) => {
249
313
  return webmentions.filter((webmention) => {
250
314
  let url = getSource(webmention);
251
315
  let source = getSource(webmention);
@@ -260,14 +324,16 @@ const processAllowlist = (webmentions, allowlist) => {
260
324
  return false;
261
325
  });
262
326
  };
327
+ export const processWebmentionAllowlist = processAllowlist;
328
+ export const processWebmentionsAllowlist = processAllowlist;
263
329
 
264
330
  /**
265
- * @param {Object} options
266
- * @param {Object[]} webmentions
331
+ * @param {Options} options
332
+ * @param {Webmention[]} webmentions
267
333
  * @param {string} url
268
- * @returns {Promise<{found:number,filtered:Object[]}>}
334
+ * @returns {Promise<{found: number, webmentions: Webmention[]}>}
269
335
  */
270
- const performFetch = async (options, webmentions, url) => {
336
+ export const fetchWebmentions = async (options, webmentions, url) => {
271
337
  return await fetch(url)
272
338
  .then(async (response) => {
273
339
  if (!response.ok) {
@@ -276,7 +342,7 @@ const performFetch = async (options, webmentions, url) => {
276
342
 
277
343
  const feed = await response.json();
278
344
 
279
- if (!options.key in feed) {
345
+ if (!(options.key in feed)) {
280
346
  console.log(
281
347
  `${styleText("grey", `[${hostname(options.domain)}]`)} ${
282
348
  options.key
@@ -307,7 +373,7 @@ const performFetch = async (options, webmentions, url) => {
307
373
 
308
374
  return {
309
375
  found: feed[options.key].length,
310
- filtered: webmentions,
376
+ webmentions: webmentions,
311
377
  };
312
378
  })
313
379
  .catch((error) => {
@@ -324,16 +390,16 @@ const performFetch = async (options, webmentions, url) => {
324
390
 
325
391
  return {
326
392
  found: 0,
327
- filtered: webmentions,
393
+ webmentions: webmentions,
328
394
  };
329
395
  });
330
396
  };
331
397
 
332
398
  /**
333
- * @param {Object} options
334
- * @returns {Promise<Object[]>}
399
+ * @param {Options} options
400
+ * @returns {Promise<Webmention[]>}
335
401
  */
336
- const fetchWebmentions = async (options) => {
402
+ export const retrieveWebmentions = async (options) => {
337
403
  if (!options.domain) {
338
404
  throw new Error(
339
405
  "`domain` is a required field when attempting to retrieve Webmentions. See https://www.npmjs.com/package/@chrisburnell/eleventy-cache-webmentions#installation for more information.",
@@ -354,7 +420,7 @@ const fetchWebmentions = async (options) => {
354
420
 
355
421
  let asset = new AssetCache(
356
422
  options.uniqueKey || `webmentions-${hostname(options.domain)}`,
357
- options.directory,
423
+ options.cacheDirectory,
358
424
  );
359
425
 
360
426
  let webmentions = [];
@@ -394,14 +460,14 @@ const fetchWebmentions = async (options) => {
394
460
  while (true) {
395
461
  const urlPaginated =
396
462
  urlObject.href + `&per-page=${perPage}&page=${page}`;
397
- const fetched = await performFetch(
463
+ const fetched = await fetchWebmentions(
398
464
  options,
399
465
  webmentions,
400
466
  urlPaginated,
401
467
  );
402
468
 
403
469
  // An error occurred during fetching paged results → break
404
- if (!fetched && !fetched.found && !fetched.filtered) {
470
+ if (!fetched && !fetched.found && !fetched.webmentions) {
405
471
  break;
406
472
  }
407
473
 
@@ -410,7 +476,7 @@ const fetchWebmentions = async (options) => {
410
476
  break;
411
477
  }
412
478
 
413
- webmentions = fetched.filtered;
479
+ webmentions = fetched.webmentions;
414
480
 
415
481
  // If there are less Webmentions found than should be in each
416
482
  // page → break
@@ -424,8 +490,8 @@ const fetchWebmentions = async (options) => {
424
490
  await new Promise((resolve) => setTimeout(resolve, 1000));
425
491
  }
426
492
  } else {
427
- const fetched = await performFetch(options, webmentions, url);
428
- webmentions = fetched.filtered;
493
+ const fetched = await fetchWebmentions(options, webmentions, url);
494
+ webmentions = fetched.webmentions;
429
495
  }
430
496
 
431
497
  // Process the blocklist, if it has any entries
@@ -463,47 +529,50 @@ const fetchWebmentions = async (options) => {
463
529
  return webmentions;
464
530
  };
465
531
 
466
- let filtered = {};
532
+ /** @type {Webmention[]} */
533
+ const WEBMENTIONS = {};
467
534
 
468
535
  /**
469
- * @param {Object} options
470
- * @returns {Promise<Object<string, Object[]>>}
536
+ * @param {Options} options
537
+ * @returns {Promise<Object<string, Webmention[]>>}
471
538
  */
472
- const filteredWebmentions = async (options) => {
473
- if (Object.entries(filtered).length) {
474
- return filtered;
539
+ export const webmentionsByURL = async (options) => {
540
+ if (Object.keys(WEBMENTIONS).length) {
541
+ return WEBMENTIONS;
475
542
  }
476
543
 
477
- let rawWebmentions = await fetchWebmentions(options);
544
+ let rawWebmentions = await retrieveWebmentions(options);
478
545
 
479
546
  // Fix local URLs based on urlReplacements and sort Webmentions into groups
480
547
  // by target base URL
481
548
  rawWebmentions.forEach((webmention) => {
482
- let url = baseUrl(
483
- fixUrl(
549
+ let url = baseURL(
550
+ fixURL(
484
551
  getTarget(webmention).replace(/\/?$/, "/"),
485
552
  options.urlReplacements,
486
553
  ),
487
554
  );
488
555
 
489
- if (!filtered[url]) {
490
- filtered[url] = [];
556
+ if (!WEBMENTIONS[url]) {
557
+ WEBMENTIONS[url] = [];
491
558
  }
492
559
 
493
- filtered[url].push(webmention);
560
+ WEBMENTIONS[url].push(webmention);
494
561
  });
495
562
 
496
- return filtered;
563
+ return WEBMENTIONS;
497
564
  };
565
+ export const webmentionsByUrl = webmentionsByURL;
566
+ export const filteredWebmentions = webmentionsByURL;
498
567
 
499
568
  /**
500
- * @param {Object} options
569
+ * @param {Options} options
501
570
  * @param {string} url
502
- * @param {string[]|Object} [types={}]
503
- * @returns {Promise<Object[]>}
571
+ * @param {WebmentionType[]|WebmentionType} [types=[]]
572
+ * @returns {Promise<Webmention[]>}
504
573
  */
505
- const getWebmentions = async (options, url, types = {}) => {
506
- const webmentions = await filteredWebmentions(options);
574
+ export const getWebmentions = async (options, url, types = []) => {
575
+ const webmentions = await webmentionsByURL(options);
507
576
  url = absoluteURL(url, options.domain);
508
577
 
509
578
  if (!url || !webmentions || !webmentions[url]) {
@@ -516,6 +585,8 @@ const getWebmentions = async (options, url, types = {}) => {
516
585
  .filter((entry) => {
517
586
  return typeof types === "object" && Object.keys(types).length
518
587
  ? types.includes(getType(entry))
588
+ : typeof types === "string"
589
+ ? types === getType(entry)
519
590
  : true;
520
591
  })
521
592
  // Sanitize content of webmentions against HTML limit
@@ -547,27 +618,26 @@ const getWebmentions = async (options, url, types = {}) => {
547
618
 
548
619
  /**
549
620
  * @param {Object} eleventyConfig
550
- * @param {Object} [options={}]
621
+ * @param {Options} [options={}]
551
622
  */
552
- const eleventyCacheWebmentions = (eleventyConfig, options = {}) => {
623
+ export const eleventyCacheWebmentions = (eleventyConfig, options = {}) => {
553
624
  options = Object.assign(defaults, options);
554
625
 
626
+ const byURL = webmentionsByURL(options);
627
+ const all = Object.values(byURL).reduce(
628
+ (array, webmentions) => [...array, ...webmentions],
629
+ [],
630
+ );
631
+
555
632
  // Global Data
556
633
  eleventyConfig.addGlobalData("webmentionsDefaults", defaults);
557
634
  eleventyConfig.addGlobalData("webmentionsOptions", options);
558
- const filtered = async () => await filteredWebmentions(options);
559
- eleventyConfig.addGlobalData("webmentionsByUrl", filtered);
560
- const unfiltered = async () =>
561
- await filteredWebmentions(options).then((filtered) =>
562
- Object.values(filtered).reduce(
563
- (array, webmentions) => [...array, ...webmentions],
564
- [],
565
- ),
566
- );
567
- eleventyConfig.addGlobalData("webmentionsAll", unfiltered);
635
+ eleventyConfig.addGlobalData("webmentionsByURL", byURL);
636
+ eleventyConfig.addGlobalData("webmentionsByUrl", byURL);
637
+ eleventyConfig.addGlobalData("webmentionsAll", all);
568
638
 
569
639
  // Liquid Filters
570
- eleventyConfig.addLiquidFilter("getWebmentionsByType", getByType);
640
+ eleventyConfig.addLiquidFilter("getWebmentionsByType", getByTypes);
571
641
  eleventyConfig.addLiquidFilter("getWebmentionsByTypes", getByTypes);
572
642
  eleventyConfig.addLiquidFilter("getWebmentionPublished", getPublished);
573
643
  eleventyConfig.addLiquidFilter("getWebmentionReceived", getReceived);
@@ -578,7 +648,7 @@ const eleventyCacheWebmentions = (eleventyConfig, options = {}) => {
578
648
  eleventyConfig.addLiquidFilter("getWebmentionType", getType);
579
649
 
580
650
  // Nunjucks Filters
581
- eleventyConfig.addNunjucksFilter("getWebmentionsByType", getByType);
651
+ eleventyConfig.addNunjucksFilter("getWebmentionsByType", getByTypes);
582
652
  eleventyConfig.addNunjucksFilter("getWebmentionsByTypes", getByTypes);
583
653
  eleventyConfig.addNunjucksFilter("getWebmentionPublished", getPublished);
584
654
  eleventyConfig.addNunjucksFilter("getWebmentionReceived", getReceived);
@@ -589,28 +659,4 @@ const eleventyCacheWebmentions = (eleventyConfig, options = {}) => {
589
659
  eleventyConfig.addNunjucksFilter("getWebmentionType", getType);
590
660
  };
591
661
 
592
- module.exports = eleventyCacheWebmentions;
593
- module.exports.defaults = defaults;
594
- module.exports.filteredWebmentions = filteredWebmentions;
595
- module.exports.webmentionsByUrl = filteredWebmentions;
596
- module.exports.fetchWebmentions = fetchWebmentions;
597
- module.exports.performFetch = performFetch;
598
- module.exports.getWebmentions = getWebmentions;
599
- module.exports.getByType = getByType;
600
- module.exports.getWebmentionsByType = getByType;
601
- module.exports.getByTypes = getByTypes;
602
- module.exports.getWebmentionsByTypes = getByTypes;
603
- module.exports.getPublished = getPublished;
604
- module.exports.getWebmentionPublished = getPublished;
605
- module.exports.getReceived = getReceived;
606
- module.exports.getWebmentionReceived = getReceived;
607
- module.exports.getContent = getContent;
608
- module.exports.getWebmentionContent = getContent;
609
- module.exports.getSource = getSource;
610
- module.exports.getWebmentionSource = getSource;
611
- module.exports.getURL = getURL;
612
- module.exports.getWebmentionURL = getURL;
613
- module.exports.getTarget = getTarget;
614
- module.exports.getWebmentionTarget = getTarget;
615
- module.exports.getType = getType;
616
- module.exports.getWebmentionType = getType;
662
+ export default eleventyCacheWebmentions;
@@ -0,0 +1 @@
1
+ const{AssetCache}=require("@11ty/eleventy-fetch"),{styleText}=require("node:util"),sanitizeHTML=require("sanitize-html"),defaults={refresh:!1,duration:"1d",uniqueKey:"webmentions",cacheDirectory:void 0,allowedHTML:{allowedTags:["a","b","em","i","strong"],allowedAttributes:{a:["href"]}},allowlist:[],blocklist:[],urlReplacements:{},maximumHtmlLength:1e3,maximumHtmlText:"mentioned this in"},absoluteURL=(url,domain)=>{try{return new URL(url,domain).toString()}catch{return console.error(`Trying to convert ${styleText("bold",url)} to be an absolute url with base ${styleText("bold",domain)} and failed.`),url}},baseURL=url=>url.split("#")[0].split("?")[0],fixURL=(url,urlReplacements)=>Object.entries(urlReplacements).reduce((accumulator,[key,value])=>{const regex=new RegExp(key,"g");return accumulator.replace(regex,value)},url),hostname=url=>typeof url=="string"&&url.includes("//")?new URL(url).hostname:url,epoch=date=>new Date(date).getTime(),removeDuplicates=webmentions=>[...webmentions.reduce((map,webmention)=>{const key=webmention==null?webmention:getSource(webmention);return map.has(key)||map.set(key,webmention),map},new Map).values()],getPublished=webmention=>webmention?.data?.published||webmention.published||webmention["wm-received"]||webmention.verified_date,getReceived=webmention=>webmention["wm-received"]||webmention.verified_date||webmention.published||webmention?.data?.published,getContent=webmention=>webmention?.contentSanitized||webmention?.content?.html||webmention?.content?.value||webmention?.content||webmention?.data?.content||"",getSource=webmention=>webmention["wm-source"]||webmention.source||webmention?.data?.url||webmention.url,getURL=webmention=>webmention?.data?.url||webmention.url||webmention["wm-source"]||webmention.source,getTarget=webmention=>webmention["wm-target"]||webmention.target,getType=webmention=>webmention["wm-property"]||webmention?.activity?.type||webmention.type,getByTypes=(webmentions,types)=>webmentions.filter(webmention=>typeof types=="string"?types===getType(webmention):types.includes(getType(webmention))),processBlocklist=(webmentions,blocklist)=>webmentions.filter(webmention=>{let url=getSource(webmention),source=getSource(webmention);for(let blocklistURL of blocklist)if(url.includes(blocklistURL.replace(/\/?$/,"/"))||source.includes(blocklistURL.replace(/\/?$/,"/")))return!1;return!0}),processAllowlist=(webmentions,allowlist)=>webmentions.filter(webmention=>{let url=getSource(webmention),source=getSource(webmention);for(let allowlistURL of allowlist)if(url.includes(allowlistURL.replace(/\/?$/,"/"))||source.includes(allowlistURL.replace(/\/?$/,"/")))return!0;return!1}),fetchWebmentions=async(options,webmentions,url)=>await fetch(url).then(async response=>{if(!response.ok)return Promise.reject(response);const feed=await response.json();return options.key in feed?(webmentions=feed[options.key].concat(webmentions),webmentions=removeDuplicates(webmentions),options.blocklist.length&&(webmentions=processBlocklist(webmentions,options.blocklist)),options.allowlist.length&&(webmentions=processAllowlist(webmentions,options.allowlist)),webmentions=webmentions.sort((a,b)=>epoch(getReceived(b))-epoch(getReceived(a))),{found:feed[options.key].length,webmentions}):(console.log(`${styleText("grey",`[${hostname(options.domain)}]`)} ${options.key} was not found as a key in the response from ${styleText("bold",hostname(options.feed))}!`),Promise.reject(response))}).catch(error=>(console.warn(`${styleText("grey",`[${hostname(options.domain)}]`)} Something went wrong with your Webmention request to ${styleText("bold",hostname(options.feed))}!`),console.warn(error instanceof Error?error.message:error),{found:0,webmentions})),retrieveWebmentions=async options=>{if(!options.domain)throw new Error("`domain` is a required field when attempting to retrieve Webmentions. See https://www.npmjs.com/package/@chrisburnell/eleventy-cache-webmentions#installation for more information.");if(!options.feed)throw new Error("`feed` is a required field when attempting to retrieve Webmentions. See https://www.npmjs.com/package/@chrisburnell/eleventy-cache-webmentions#installation for more information.");if(!options.key)throw new Error("`key` is a required field when attempting to retrieve Webmentions. See https://www.npmjs.com/package/@chrisburnell/eleventy-cache-webmentions#installation for more information.");let asset=new AssetCache(options.uniqueKey||`webmentions-${hostname(options.domain)}`,options.cacheDirectory),webmentions=[];asset.isCacheValid("9001y")&&!options.refresh&&(webmentions=await asset.getCachedValue());const webmentionsCachedLength=webmentions.length;if(!asset.isCacheValid(options.refresh?"0s":options.duration)){const performanceStart=process.hrtime(),since=webmentions.length?getReceived(webmentions[0]):!1,url=`${options.feed}${since?`${options.feed.includes("?")?"&":"?"}since=${since}`:""}`;if(url.includes("https://webmention.io")){const urlObject=new URL(url),perPage=Number(urlObject.searchParams.get("per-page"))||1e3;urlObject.searchParams.delete("per-page");let page=0;for(;;){const urlPaginated=urlObject.href+`&per-page=${perPage}&page=${page}`,fetched=await fetchWebmentions(options,webmentions,urlPaginated);if(!fetched&&!fetched.found&&!fetched.webmentions||fetched.found===0||(webmentions=fetched.webmentions,fetched.found<perPage))break;page+=1,await new Promise(resolve=>setTimeout(resolve,1e3))}}else webmentions=(await fetchWebmentions(options,webmentions,url)).webmentions;options.blocklist.length&&(webmentions=processBlocklist(webmentions,options.blocklist)),options.allowlist.length&&(webmentions=processAllowlist(webmentions,options.allowlist)),await asset.save(webmentions,"json");const performance=process.hrtime(performanceStart);webmentionsCachedLength<webmentions.length&&console.log(`${styleText("grey",`[${hostname(options.domain)}]`)} ${styleText("bold",String(webmentions.length-webmentionsCachedLength))} new Webmentions fetched into cache in ${styleText("bold",(performance[0]+performance[1]/1e9).toFixed(3)+" seconds")}.`)}return webmentions},WEBMENTIONS={},webmentionsByURL=async options=>(Object.keys(WEBMENTIONS).length||(await retrieveWebmentions(options)).forEach(webmention=>{let url=baseURL(fixURL(getTarget(webmention).replace(/\/?$/,"/"),options.urlReplacements));WEBMENTIONS[url]||(WEBMENTIONS[url]=[]),WEBMENTIONS[url].push(webmention)}),WEBMENTIONS),getWebmentions=async(options,url,types=[])=>{const webmentions=await webmentionsByURL(options);return url=absoluteURL(url,options.domain),!url||!webmentions||!webmentions[url]?[]:webmentions[url].filter(entry=>typeof types=="object"&&Object.keys(types).length?types.includes(getType(entry)):typeof types=="string"?types===getType(entry):!0).map(entry=>{const html=getContent(entry);return html.length&&(entry.contentSanitized=sanitizeHTML(html,options.allowedHTML),html.length>options.maximumHtmlLength&&(entry.contentSanitized=`${options.maximumHtmlText} <a href="${getSource(entry)}">${getSource(entry)}</a>`)),entry}).sort((a,b)=>epoch(getPublished(a))-epoch(getPublished(b)))},eleventyCacheWebmentions=(eleventyConfig,options={})=>{options=Object.assign(defaults,options);const byURL=webmentionsByURL(options),all=Object.values(byURL).reduce((array,webmentions)=>[...array,...webmentions],[]);eleventyConfig.addGlobalData("webmentionsDefaults",defaults),eleventyConfig.addGlobalData("webmentionsOptions",options),eleventyConfig.addGlobalData("webmentionsByURL",byURL),eleventyConfig.addGlobalData("webmentionsByUrl",byURL),eleventyConfig.addGlobalData("webmentionsAll",all),eleventyConfig.addLiquidFilter("getWebmentionsByType",getByTypes),eleventyConfig.addLiquidFilter("getWebmentionsByTypes",getByTypes),eleventyConfig.addLiquidFilter("getWebmentionPublished",getPublished),eleventyConfig.addLiquidFilter("getWebmentionReceived",getReceived),eleventyConfig.addLiquidFilter("getWebmentionContent",getContent),eleventyConfig.addLiquidFilter("getWebmentionSource",getSource),eleventyConfig.addLiquidFilter("getWebmentionURL",getURL),eleventyConfig.addLiquidFilter("getWebmentionTarget",getTarget),eleventyConfig.addLiquidFilter("getWebmentionType",getType),eleventyConfig.addNunjucksFilter("getWebmentionsByType",getByTypes),eleventyConfig.addNunjucksFilter("getWebmentionsByTypes",getByTypes),eleventyConfig.addNunjucksFilter("getWebmentionPublished",getPublished),eleventyConfig.addNunjucksFilter("getWebmentionReceived",getReceived),eleventyConfig.addNunjucksFilter("getWebmentionContent",getContent),eleventyConfig.addNunjucksFilter("getWebmentionSource",getSource),eleventyConfig.addNunjucksFilter("getWebmentionURL",getURL),eleventyConfig.addNunjucksFilter("getWebmentionTarget",getTarget),eleventyConfig.addNunjucksFilter("getWebmentionType",getType)};module.exports=eleventyCacheWebmentions,module.exports.defaults=defaults,module.exports.getPublished=getPublished,module.exports.getWebmentionPublished=getPublished,module.exports.getReceived=getReceived,module.exports.getWebmentionReceived=getReceived,module.exports.getContent=getContent,module.exports.getWebmentionContent=getContent,module.exports.getSource=getSource,module.exports.getWebmentionSource=getSource,module.exports.getURL=getURL,module.exports.getWebmentionURL=getURL,module.exports.getTarget=getTarget,module.exports.getWebmentionTarget=getTarget,module.exports.getType=getType,module.exports.getWebmentionType=getType,module.exports.getByTypes=getByTypes,module.exports.getByType=getByTypes,module.exports.getWebmentionsByTypes=getByTypes,module.exports.getWebmentionsByType=getByTypes,module.exports.processBlocklist=processBlocklist,module.exports.processWebmentionBlocklist=processBlocklist,module.exports.processWebmentionsBlocklist=processBlocklist,module.exports.processAllowlist=processAllowlist,module.exports.processWebmentionAllowlist=processAllowlist,module.exports.processWebmentionsAllowlist=processAllowlist,module.exports.fetchWebmentions=fetchWebmentions,module.exports.retrieveWebmentions=retrieveWebmentions,module.exports.webmentionsByURL=webmentionsByURL,module.exports.webmentionsByUrl=webmentionsByURL,module.exports.filteredWebmentions=webmentionsByURL,module.exports.getWebmentions=getWebmentions;
@@ -0,0 +1 @@
1
+ import{AssetCache}from"@11ty/eleventy-fetch";import{styleText}from"node:util";import sanitizeHTML from"sanitize-html";export const defaults={refresh:!1,duration:"1d",uniqueKey:"webmentions",cacheDirectory:void 0,allowedHTML:{allowedTags:["a","b","em","i","strong"],allowedAttributes:{a:["href"]}},allowlist:[],blocklist:[],urlReplacements:{},maximumHtmlLength:1e3,maximumHtmlText:"mentioned this in"};const absoluteURL=(url,domain)=>{try{return new URL(url,domain).toString()}catch{return console.error(`Trying to convert ${styleText("bold",url)} to be an absolute url with base ${styleText("bold",domain)} and failed.`),url}},baseURL=url=>url.split("#")[0].split("?")[0],fixURL=(url,urlReplacements)=>Object.entries(urlReplacements).reduce((accumulator,[key,value])=>{const regex=new RegExp(key,"g");return accumulator.replace(regex,value)},url),hostname=url=>typeof url=="string"&&url.includes("//")?new URL(url).hostname:url,epoch=date=>new Date(date).getTime(),removeDuplicates=webmentions=>[...webmentions.reduce((map,webmention)=>{const key=webmention==null?webmention:getSource(webmention);return map.has(key)||map.set(key,webmention),map},new Map).values()];export const getPublished=webmention=>webmention?.data?.published||webmention.published||webmention["wm-received"]||webmention.verified_date,getWebmentionPublished=getPublished,getReceived=webmention=>webmention["wm-received"]||webmention.verified_date||webmention.published||webmention?.data?.published,getWebmentionReceived=getReceived,getContent=webmention=>webmention?.contentSanitized||webmention?.content?.html||webmention?.content?.value||webmention?.content||webmention?.data?.content||"",getWebmentionContent=getContent,getSource=webmention=>webmention["wm-source"]||webmention.source||webmention?.data?.url||webmention.url,getWebmentionSource=getSource,getURL=webmention=>webmention?.data?.url||webmention.url||webmention["wm-source"]||webmention.source,getWebmentionURL=getURL,getTarget=webmention=>webmention["wm-target"]||webmention.target,getWebmentionTarget=getTarget,getType=webmention=>webmention["wm-property"]||webmention?.activity?.type||webmention.type,getWebmentionType=getType,getByTypes=(webmentions,types)=>webmentions.filter(webmention=>typeof types=="string"?types===getType(webmention):types.includes(getType(webmention))),getByType=getByTypes,getWebmentionsByTypes=getByTypes,getWebmentionsByType=getByTypes,processBlocklist=(webmentions,blocklist)=>webmentions.filter(webmention=>{let url=getSource(webmention),source=getSource(webmention);for(let blocklistURL of blocklist)if(url.includes(blocklistURL.replace(/\/?$/,"/"))||source.includes(blocklistURL.replace(/\/?$/,"/")))return!1;return!0}),processWebmentionBlocklist=processBlocklist,processWebmentionsBlocklist=processBlocklist,processAllowlist=(webmentions,allowlist)=>webmentions.filter(webmention=>{let url=getSource(webmention),source=getSource(webmention);for(let allowlistURL of allowlist)if(url.includes(allowlistURL.replace(/\/?$/,"/"))||source.includes(allowlistURL.replace(/\/?$/,"/")))return!0;return!1}),processWebmentionAllowlist=processAllowlist,processWebmentionsAllowlist=processAllowlist,fetchWebmentions=async(options,webmentions,url)=>await fetch(url).then(async response=>{if(!response.ok)return Promise.reject(response);const feed=await response.json();return options.key in feed?(webmentions=feed[options.key].concat(webmentions),webmentions=removeDuplicates(webmentions),options.blocklist.length&&(webmentions=processBlocklist(webmentions,options.blocklist)),options.allowlist.length&&(webmentions=processAllowlist(webmentions,options.allowlist)),webmentions=webmentions.sort((a,b)=>epoch(getReceived(b))-epoch(getReceived(a))),{found:feed[options.key].length,webmentions}):(console.log(`${styleText("grey",`[${hostname(options.domain)}]`)} ${options.key} was not found as a key in the response from ${styleText("bold",hostname(options.feed))}!`),Promise.reject(response))}).catch(error=>(console.warn(`${styleText("grey",`[${hostname(options.domain)}]`)} Something went wrong with your Webmention request to ${styleText("bold",hostname(options.feed))}!`),console.warn(error instanceof Error?error.message:error),{found:0,webmentions})),retrieveWebmentions=async options=>{if(!options.domain)throw new Error("`domain` is a required field when attempting to retrieve Webmentions. See https://www.npmjs.com/package/@chrisburnell/eleventy-cache-webmentions#installation for more information.");if(!options.feed)throw new Error("`feed` is a required field when attempting to retrieve Webmentions. See https://www.npmjs.com/package/@chrisburnell/eleventy-cache-webmentions#installation for more information.");if(!options.key)throw new Error("`key` is a required field when attempting to retrieve Webmentions. See https://www.npmjs.com/package/@chrisburnell/eleventy-cache-webmentions#installation for more information.");let asset=new AssetCache(options.uniqueKey||`webmentions-${hostname(options.domain)}`,options.cacheDirectory),webmentions=[];asset.isCacheValid("9001y")&&!options.refresh&&(webmentions=await asset.getCachedValue());const webmentionsCachedLength=webmentions.length;if(!asset.isCacheValid(options.refresh?"0s":options.duration)){const performanceStart=process.hrtime(),since=webmentions.length?getReceived(webmentions[0]):!1,url=`${options.feed}${since?`${options.feed.includes("?")?"&":"?"}since=${since}`:""}`;if(url.includes("https://webmention.io")){const urlObject=new URL(url),perPage=Number(urlObject.searchParams.get("per-page"))||1e3;urlObject.searchParams.delete("per-page");let page=0;for(;;){const urlPaginated=urlObject.href+`&per-page=${perPage}&page=${page}`,fetched=await fetchWebmentions(options,webmentions,urlPaginated);if(!fetched&&!fetched.found&&!fetched.webmentions||fetched.found===0||(webmentions=fetched.webmentions,fetched.found<perPage))break;page+=1,await new Promise(resolve=>setTimeout(resolve,1e3))}}else webmentions=(await fetchWebmentions(options,webmentions,url)).webmentions;options.blocklist.length&&(webmentions=processBlocklist(webmentions,options.blocklist)),options.allowlist.length&&(webmentions=processAllowlist(webmentions,options.allowlist)),await asset.save(webmentions,"json");const performance=process.hrtime(performanceStart);webmentionsCachedLength<webmentions.length&&console.log(`${styleText("grey",`[${hostname(options.domain)}]`)} ${styleText("bold",String(webmentions.length-webmentionsCachedLength))} new Webmentions fetched into cache in ${styleText("bold",(performance[0]+performance[1]/1e9).toFixed(3)+" seconds")}.`)}return webmentions};const WEBMENTIONS={};export const webmentionsByURL=async options=>(Object.keys(WEBMENTIONS).length||(await retrieveWebmentions(options)).forEach(webmention=>{let url=baseURL(fixURL(getTarget(webmention).replace(/\/?$/,"/"),options.urlReplacements));WEBMENTIONS[url]||(WEBMENTIONS[url]=[]),WEBMENTIONS[url].push(webmention)}),WEBMENTIONS),webmentionsByUrl=webmentionsByURL,filteredWebmentions=webmentionsByURL,getWebmentions=async(options,url,types=[])=>{const webmentions=await webmentionsByURL(options);return url=absoluteURL(url,options.domain),!url||!webmentions||!webmentions[url]?[]:webmentions[url].filter(entry=>typeof types=="object"&&Object.keys(types).length?types.includes(getType(entry)):typeof types=="string"?types===getType(entry):!0).map(entry=>{const html=getContent(entry);return html.length&&(entry.contentSanitized=sanitizeHTML(html,options.allowedHTML),html.length>options.maximumHtmlLength&&(entry.contentSanitized=`${options.maximumHtmlText} <a href="${getSource(entry)}">${getSource(entry)}</a>`)),entry}).sort((a,b)=>epoch(getPublished(a))-epoch(getPublished(b)))},eleventyCacheWebmentions=(eleventyConfig,options={})=>{options=Object.assign(defaults,options);const byURL=webmentionsByURL(options),all=Object.values(byURL).reduce((array,webmentions)=>[...array,...webmentions],[]);eleventyConfig.addGlobalData("webmentionsDefaults",defaults),eleventyConfig.addGlobalData("webmentionsOptions",options),eleventyConfig.addGlobalData("webmentionsByURL",byURL),eleventyConfig.addGlobalData("webmentionsByUrl",byURL),eleventyConfig.addGlobalData("webmentionsAll",all),eleventyConfig.addLiquidFilter("getWebmentionsByType",getByTypes),eleventyConfig.addLiquidFilter("getWebmentionsByTypes",getByTypes),eleventyConfig.addLiquidFilter("getWebmentionPublished",getPublished),eleventyConfig.addLiquidFilter("getWebmentionReceived",getReceived),eleventyConfig.addLiquidFilter("getWebmentionContent",getContent),eleventyConfig.addLiquidFilter("getWebmentionSource",getSource),eleventyConfig.addLiquidFilter("getWebmentionURL",getURL),eleventyConfig.addLiquidFilter("getWebmentionTarget",getTarget),eleventyConfig.addLiquidFilter("getWebmentionType",getType),eleventyConfig.addNunjucksFilter("getWebmentionsByType",getByTypes),eleventyConfig.addNunjucksFilter("getWebmentionsByTypes",getByTypes),eleventyConfig.addNunjucksFilter("getWebmentionPublished",getPublished),eleventyConfig.addNunjucksFilter("getWebmentionReceived",getReceived),eleventyConfig.addNunjucksFilter("getWebmentionContent",getContent),eleventyConfig.addNunjucksFilter("getWebmentionSource",getSource),eleventyConfig.addNunjucksFilter("getWebmentionURL",getURL),eleventyConfig.addNunjucksFilter("getWebmentionTarget",getTarget),eleventyConfig.addNunjucksFilter("getWebmentionType",getType)};export default eleventyCacheWebmentions;
package/eslint.config.js CHANGED
@@ -1,16 +1,36 @@
1
- module.exports = [
1
+ import js from "@eslint/js";
2
+
3
+ export default [
4
+ js.configs.recommended,
2
5
  {
3
- files: ["**/*.js"],
6
+ files: ["src/**/*.js"],
4
7
  languageOptions: {
5
- ecmaVersion: 12,
6
- sourceType: "module",
8
+ ecmaVersion: "latest",
7
9
  },
8
- languageOptions: {
9
- globals: {
10
- browser: true,
11
- commonjs: true,
12
- es2021: true,
13
- },
10
+ rules: {
11
+ "no-process-env": 0,
12
+ "jsdoc/check-access": "warn",
13
+ "jsdoc/check-alignment": "warn",
14
+ "jsdoc/check-indentation": "warn",
15
+ "jsdoc/check-param-names": "warn",
16
+ "jsdoc/check-tag-names": "warn",
17
+ "jsdoc/check-types": "warn",
18
+ "jsdoc/implements-on-classes": "warn",
19
+ "jsdoc/no-undefined-types": "warn",
20
+ "jsdoc/require-jsdoc": [
21
+ "warn",
22
+ {
23
+ require: {
24
+ FunctionDeclaration: true,
25
+ MethodDefinition: true,
26
+ ClassDeclaration: true,
27
+ },
28
+ },
29
+ ],
30
+ "jsdoc/require-param": "warn",
31
+ "jsdoc/require-param-type": "warn",
32
+ "jsdoc/require-returns": "warn",
33
+ "jsdoc/require-returns-type": "warn",
14
34
  },
15
35
  },
16
36
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrisburnell/eleventy-cache-webmentions",
3
- "version": "2.1.12",
3
+ "version": "2.2.2",
4
4
  "description": "Cache webmentions using eleventy-fetch and make them available to use in collections, layouts, pages, etc. in Eleventy.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,8 +37,15 @@
37
37
  "access": "public",
38
38
  "provenance": true
39
39
  },
40
- "main": "eleventy-cache-webmentions.js",
40
+ "main": "eleventy-cache-webmentions.cjs",
41
+ "exports": {
42
+ ".": {
43
+ "require": "./eleventy-cache-webmentions.cjs",
44
+ "import": "./eleventy-cache-webmentions.js"
45
+ }
46
+ },
41
47
  "scripts": {
48
+ "build": "esbuild eleventy-cache-webmentions.js --minify-whitespace --minify-syntax --outfile=eleventy-cache-webmentions.min.js && esbuild eleventy-cache-webmentions.cjs --minify-whitespace --minify-syntax --outfile=eleventy-cache-webmentions.min.cjs",
42
49
  "lint": "eslint eleventy-cache-webmentions.js",
43
50
  "test": "node --test"
44
51
  },
@@ -58,7 +65,9 @@
58
65
  "sanitize-html": "^2.17.0"
59
66
  },
60
67
  "devDependencies": {
68
+ "esbuild": "^0.25.11",
61
69
  "eslint": "^9.38.0",
62
70
  "nock": "^14.0.10"
63
- }
71
+ },
72
+ "type": "module"
64
73
  }